- {/* Greeting positioned above the composer */}
{greeting}
- {/* Composer - top edge fixed, expands downward only */}
@@ -373,7 +351,6 @@ const ClipboardChip: FC<{ text: string; onDismiss: () => void }> = ({ text, onDi
};
const Composer: FC = () => {
- // Document mention state (atoms persist across component remounts)
const [mentionedDocuments, setMentionedDocuments] = useAtom(mentionedDocumentsAtom);
const [showDocumentPopover, setShowDocumentPopover] = useState(false);
const [showPromptPicker, setShowPromptPicker] = useState(false);
@@ -385,9 +362,8 @@ const Composer: FC = () => {
const promptPickerRef = useRef
(null);
const { search_space_id, chat_id } = useParams();
const aui = useAui();
- // Gate the always-focused composer behaviour to desktop. On mobile,
- // programmatic focus pops the soft keyboard, which would be jarring
- // whenever a picker closes or the user navigates between threads.
+ // Desktop-only auto-focus; on mobile, programmatic focus would
+ // summon the soft keyboard on every picker close / thread switch.
const isDesktop = useMediaQuery("(min-width: 640px)");
const electronAPI = useElectronAPI();
@@ -408,7 +384,6 @@ const Composer: FC = () => {
const currentPlaceholder = COMPOSER_PLACEHOLDER;
- // Live collaboration state
const { data: currentUser } = useAtomValue(currentUserAtom);
const { data: members } = useAtomValue(membersAtom);
const threadId = useMemo(() => {
@@ -422,13 +397,11 @@ const Composer: FC = () => {
const respondingToUserId = sessionState?.respondingToUserId ?? null;
const isBlockedByOtherUser = isAiResponding && respondingToUserId !== currentUser?.id;
- // Sync comments for the entire thread via Zero (one subscription per thread)
+ // One Zero subscription per thread for comment sync.
useCommentsSync(threadId);
- // Batch-prefetch comments for all assistant messages so individual useComments
- // hooks never fire their own network requests (eliminates N+1 API calls).
- // Return a primitive string from the selector so useSyncExternalStore can
- // compare snapshots by value and avoid infinite re-render loops.
+ // Batch-prefetch assistant message comments to avoid N+1 fetches.
+ // Returns a primitive string so useSyncExternalStore can compare by value.
const assistantIdsKey = useAuiState(({ thread }) =>
thread.messages
.filter((m) => m.role === "assistant" && m.id?.startsWith("msg-"))
@@ -441,18 +414,9 @@ const Composer: FC = () => {
);
useBatchCommentsPreload(assistantDbMessageIds);
- // Always-focused composer (Claude-style). Runs as a reactive
- // invariant: whenever the composer is mounted and no transient
- // picker has taken over keyboard input, the editor should be the
- // focused element. This naturally restores focus after pickers
- // close, after the user switches threads, and on first mount —
- // replacing the previous one-shot ``hasAutoFocusedRef`` gate that
- // only worked on the welcome screen.
- //
- // Gated on ``isDesktop`` so we don't repeatedly summon the mobile
- // soft keyboard whenever any of the deps change. ``threadId`` is
- // read so the effect re-fires when the user switches between two
- // non-empty threads (where the Composer instance is reused).
+ // Always-focused composer: refocus whenever no picker has taken
+ // over input. ``threadId`` is in the deps so the effect re-fires
+ // on thread switch (Composer instance is reused).
useEffect(() => {
if (!isDesktop) return;
if (showDocumentPopover || showPromptPicker) return;
@@ -460,7 +424,7 @@ const Composer: FC = () => {
editorRef.current?.focus();
}, [isDesktop, showDocumentPopover, showPromptPicker, threadId]);
- // Close document picker when a slide-out panel (inbox, shared/private chats) opens
+ // Close document picker when a slide-out panel (inbox, etc.) opens.
useEffect(() => {
const handler = () => {
setShowDocumentPopover(false);
@@ -470,21 +434,12 @@ const Composer: FC = () => {
return () => window.removeEventListener(SLIDEOUT_PANEL_OPENED_EVENT, handler);
}, []);
- // Sync editor text with the assistant-ui composer runtime and
- // reconcile the chip atom from the editor's reported docs.
- //
- // The editor is the source of truth for which chips exist on
- // screen. Reconciling here covers every deletion path Plate can
- // produce (the explicit Backspace handler, the X-button,
- // Cmd+Backspace, range-select+Delete, cut, paste-over) without
- // needing per-keybinding plumbing. Without this, paths that bypass
- // ``onDocumentRemove`` left the atom carrying stale entries that
- // the picker would re-emit via ``initialSelectedDocuments`` and
- // resurface as chips on the next selection.
- //
- // The setter returns ``prev`` when the chip set is unchanged so
- // pure-text keystrokes don't churn the atom (Jotai compares by
- // reference for store change notifications).
+ // Sync editor text into assistant-ui's composer and mirror the chip
+ // atom from the editor's reported ``docs``. The editor is the
+ // single source of truth, so this catches every Plate deletion path
+ // (Backspace, X button, Cmd+Backspace, range-delete, cut,
+ // paste-over) without per-keybinding plumbing. The ``prev``
+ // short-circuit keeps pure-text keystrokes from churning the atom.
const handleEditorChange = useCallback(
(text: string, docs: MentionedDocument[]) => {
aui.composer().setText(text);
@@ -498,11 +453,9 @@ const Composer: FC = () => {
return docs.map((d) => ({
id: d.id,
title: d.title,
- // ``MentionedDocument.document_type`` is optional but
- // the atom shape requires a string. ``"UNKNOWN"`` is
- // the same sentinel ``getMentionDocKey`` and the
- // editor's match predicates already use, so the key
- // is stable across the round trip.
+ // Atom requires a string; ``"UNKNOWN"`` matches the
+ // sentinel ``getMentionDocKey`` and the editor's
+ // match predicates use.
document_type: d.document_type ?? "UNKNOWN",
kind: d.kind,
}));
@@ -511,13 +464,11 @@ const Composer: FC = () => {
[aui, setMentionedDocuments]
);
- // Open document picker when @ mention is triggered
const handleMentionTrigger = useCallback((query: string) => {
setShowDocumentPopover(true);
setMentionQuery(query);
}, []);
- // Close document picker and reset query
const handleMentionClose = useCallback(() => {
if (showDocumentPopover) {
setShowDocumentPopover(false);
@@ -525,13 +476,11 @@ const Composer: FC = () => {
}
}, [showDocumentPopover]);
- // Open action picker when / is triggered
const handleActionTrigger = useCallback((query: string) => {
setShowPromptPicker(true);
setActionQuery(query);
}, []);
- // Close action picker and reset query
const handleActionClose = useCallback(() => {
if (showPromptPicker) {
setShowPromptPicker(false);
@@ -575,7 +524,7 @@ const Composer: FC = () => {
[clipboardInitialText, electronAPI, aui]
);
- // Keyboard navigation for document/action picker (arrow keys, Enter, Escape)
+ // Arrow / Enter / Escape navigation for the active picker.
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (showPromptPicker) {
@@ -656,7 +605,7 @@ const Composer: FC = () => {
(docId: number, docType?: string) => {
setMentionedDocuments((prev) => {
if (!docType) {
- // Defensive fallback: keep UI in sync even when chip type is unavailable.
+ // Fallback when chip type is unavailable.
return prev.filter((doc) => doc.id !== docId);
}
const removedKey = getMentionDocKey({ id: docId, document_type: docType });
@@ -674,16 +623,12 @@ const Composer: FC = () => {
const key = getMentionDocKey(mention);
if (editorDocKeys.has(key)) continue;
editorRef.current?.insertMentionChip(mention);
- // Track within the loop so duplicates in the same batch
- // (defensive — the picker shouldn't produce them today)
- // can't slip through as double-inserted chips.
+ // Track within the loop so a duplicate-in-batch can't double-insert.
editorDocKeys.add(key);
}
- // Atom is reconciled by the editor's ``onChange`` after each
- // ``insertMentionChip`` (see ``handleEditorChange``); writing
- // here would be a second, divergent write path — exactly the
- // shape that let stale entries resurface in the past.
+ // Atom is reconciled by ``handleEditorChange`` via the editor's
+ // onChange — no second write path here.
setMentionQuery("");
}, []);
@@ -1315,12 +1260,7 @@ const ComposerAction: FC = ({ isBlockedByOtherUser = false
);
};
-/**
- * Friendly tool name for display in the chat UI. Delegates to the
- * shared map in ``contracts/enums/toolIcons`` so unix-style identifiers
- * (``rm``, ``ls``, ``grep`` …) and snake_cased function names render as
- * plain English (e.g. "Delete file", "List files", "Search in files").
- */
+/** Friendly tool name (delegates to ``getToolDisplayName``). */
function formatToolName(name: string): string {
return getToolDisplayName(name);
}
From 0884b63406135a817a7134a76f3a70f62f314d39 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Tue, 12 May 2026 23:25:33 +0530
Subject: [PATCH 8/9] chore: ran linting
---
surfsense_web/atoms/chat/mentioned-documents.atom.ts | 4 +---
.../components/new-chat/document-mention-picker.tsx | 7 ++-----
2 files changed, 3 insertions(+), 8 deletions(-)
diff --git a/surfsense_web/atoms/chat/mentioned-documents.atom.ts b/surfsense_web/atoms/chat/mentioned-documents.atom.ts
index eafdaf87e..9163960f4 100644
--- a/surfsense_web/atoms/chat/mentioned-documents.atom.ts
+++ b/surfsense_web/atoms/chat/mentioned-documents.atom.ts
@@ -97,9 +97,7 @@ export const mentionedDocumentIdsAtom = atom((get) => {
surfsense_doc_ids: docs
.filter((doc) => doc.document_type === "SURFSENSE_DOCS")
.map((doc) => doc.id),
- document_ids: docs
- .filter((doc) => doc.document_type !== "SURFSENSE_DOCS")
- .map((doc) => doc.id),
+ document_ids: docs.filter((doc) => doc.document_type !== "SURFSENSE_DOCS").map((doc) => doc.id),
folder_ids: folders.map((f) => f.id),
};
});
diff --git a/surfsense_web/components/new-chat/document-mention-picker.tsx b/surfsense_web/components/new-chat/document-mention-picker.tsx
index 0881b11b6..0d68c8df8 100644
--- a/surfsense_web/components/new-chat/document-mention-picker.tsx
+++ b/surfsense_web/components/new-chat/document-mention-picker.tsx
@@ -301,8 +301,7 @@ export const DocumentMentionPicker = forwardRef<
// folder entries lift the existing kind-aware key so the same
// matchers used by the chip atom apply unchanged.
const selectedKeys = useMemo(
- () =>
- new Set(initialSelectedDocuments.map((d) => getMentionDocKey(d))),
+ () => new Set(initialSelectedDocuments.map((d) => getMentionDocKey(d))),
[initialSelectedDocuments]
);
@@ -583,9 +582,7 @@ export const DocumentMentionPicker = forwardRef<
{(surfsenseDocsList.length > 0 || userDocsList.length > 0) && (
)}
-
- Folders
-
+ Folders
{folderMentions.map((folder) => {
const folderKey = getMentionDocKey(folder);
const isAlreadySelected = selectedKeys.has(folderKey);
From d9ec4018356e56667ad517c458d7a3da5e0ac5bc Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Wed, 13 May 2026 03:34:28 +0530
Subject: [PATCH 9/9] chore: remove caret from @rocicorp/zero dependency
version
---
surfsense_web/package.json | 2 +-
surfsense_web/pnpm-lock.yaml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/surfsense_web/package.json b/surfsense_web/package.json
index efeb45fd8..d71298c72 100644
--- a/surfsense_web/package.json
+++ b/surfsense_web/package.json
@@ -79,7 +79,7 @@
"@remotion/media": "^4.0.438",
"@remotion/player": "^4.0.438",
"@remotion/web-renderer": "^4.0.438",
- "@rocicorp/zero": "^1.4.0",
+ "@rocicorp/zero": "1.4.0",
"@slate-serializers/html": "^2.2.3",
"@streamdown/code": "^1.0.2",
"@streamdown/math": "^1.0.2",
diff --git a/surfsense_web/pnpm-lock.yaml b/surfsense_web/pnpm-lock.yaml
index 62056a215..8602feb8d 100644
--- a/surfsense_web/pnpm-lock.yaml
+++ b/surfsense_web/pnpm-lock.yaml
@@ -165,7 +165,7 @@ importers:
specifier: ^4.0.438
version: 4.0.438(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@rocicorp/zero':
- specifier: ^1.4.0
+ specifier: 1.4.0
version: 1.4.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.0))
'@slate-serializers/html':
specifier: ^2.2.3