From 9e36788cf506883463a8f917e1df4d2685e0f3ad Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 8 Feb 2026 08:58:23 -0800 Subject: [PATCH 01/11] Slash command --- .../src/lib/trpc/routers/ai-chat/index.ts | 20 ++ .../ChatPane/ChatInterface/ChatInterface.tsx | 185 ++++++++++++++---- .../SlashCommandMenu/SlashCommandMenu.tsx | 62 ++++++ .../components/SlashCommandMenu/index.ts | 1 + .../hooks/useSlashCommands/index.ts | 1 + .../useSlashCommands/useSlashCommands.ts | 154 +++++++++++++++ apps/streams/src/claude-agent.ts | 60 ++++++ 7 files changed, 450 insertions(+), 33 deletions(-) create mode 100644 apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandMenu/SlashCommandMenu.tsx create mode 100644 apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandMenu/index.ts create mode 100644 apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/index.ts create mode 100644 apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts diff --git a/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts b/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts index 19bd320a2fe..f76c21e802e 100644 --- a/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts +++ b/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts @@ -11,6 +11,9 @@ import { sessionStore, } from "./utils/session-manager"; +const CLAUDE_AGENT_URL = + process.env.CLAUDE_AGENT_URL || "http://localhost:9090"; + export const createAiChatRouter = () => { return router({ getConfig: publicProcedure.query(() => ({ @@ -21,6 +24,23 @@ export const createAiChatRouter = () => { null, })), + getSlashCommands: publicProcedure.query(async () => { + try { + const res = await fetch(`${CLAUDE_AGENT_URL}/commands`); + if (!res.ok) return { commands: [] }; + const data = (await res.json()) as { + commands: Array<{ + name: string; + description: string; + argumentHint: string; + }>; + }; + return { commands: data.commands ?? [] }; + } catch { + return { commands: [] }; + } + }), + startSession: publicProcedure .input( z.object({ diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx index df99ea86ce3..07902473f4e 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx @@ -1,3 +1,4 @@ +import type { DurableChatCollections } from "@superset/durable-session/react"; import { useDurableChat } from "@superset/durable-session/react"; import { Conversation, @@ -10,9 +11,11 @@ import { PromptInput, PromptInputButton, PromptInputFooter, + PromptInputProvider, PromptInputSubmit, PromptInputTextarea, PromptInputTools, + usePromptInputController, } from "@superset/ui/ai-elements/prompt-input"; import { Shimmer } from "@superset/ui/ai-elements/shimmer"; import { Suggestion, Suggestions } from "@superset/ui/ai-elements/suggestion"; @@ -27,8 +30,10 @@ import { electronTrpc } from "renderer/lib/electron-trpc"; import { ChatMessageItem } from "./components/ChatMessageItem"; import { ContextIndicator } from "./components/ContextIndicator"; import { ModelPicker } from "./components/ModelPicker"; +import { SlashCommandMenu } from "./components/SlashCommandMenu"; import { MODELS, SUGGESTIONS } from "./constants"; import { useClaudeCodeHistory } from "./hooks/useClaudeCodeHistory"; +import { useSlashCommands } from "./hooks/useSlashCommands"; import type { ModelOption } from "./types"; import { extractTitleFromMessages } from "./utils/extract-title"; @@ -198,6 +203,13 @@ export function ChatInterface({ [sendMessage], ); + const handleSendText = useCallback( + (text: string) => { + handleSend({ text }); + }, + [handleSend], + ); + const handleSuggestion = useCallback( (suggestion: string) => { handleSend({ text: suggestion }); @@ -249,6 +261,11 @@ export function ChatInterface({ [stop], ); + // TODO: Implement proper /clear handler that resets the conversation + const handleClear = useCallback(() => { + console.log("[chat] /clear requested"); + }, []); + return (
{error && ( @@ -313,41 +330,143 @@ export function ChatInterface({ ))} )} - - - - - - - - - - - - - -
- - -
-
-
+ + +
); } + +interface ChatInputAreaProps { + handleSend: (message: { text: string }) => void; + handleSendText: (text: string) => void; + handleClear: () => void; + isLoading: boolean; + handleStop: (e: React.MouseEvent) => void; + thinkingEnabled: boolean; + handleThinkingToggle: (enabled: boolean) => void; + selectedModel: ModelOption; + handleModelSelect: (model: ModelOption) => void; + modelSelectorOpen: boolean; + setModelSelectorOpen: (open: boolean) => void; + collections: DurableChatCollections; +} + +function ChatInputArea({ + handleSend, + handleSendText, + handleClear, + isLoading, + handleStop, + thinkingEnabled, + handleThinkingToggle, + selectedModel, + handleModelSelect, + modelSelectorOpen, + setModelSelectorOpen, + collections, +}: ChatInputAreaProps) { + const { textInput } = usePromptInputController(); + + const slashCommands = useSlashCommands({ + inputValue: textInput.value, + onClear: handleClear, + onSendMessage: handleSendText, + }); + + const handleKeyDownCapture = useCallback( + (e: React.KeyboardEvent) => { + if (slashCommands.isOpen) { + const handled = slashCommands.handleKeyDown(e); + if (handled) { + // For Enter/Tab, apply the selected command + if (e.key === "Enter" || e.key === "Tab") { + const cmd = + slashCommands.filteredCommands[slashCommands.selectedIndex]; + if (cmd) { + const result = slashCommands.handleSelectCommand(cmd); + textInput.setInput(result.text); + } + } + // For Escape, clear the slash input + if (e.key === "Escape") { + textInput.setInput(""); + } + } + } + }, + [slashCommands, textInput], + ); + + const handleMenuSelect = useCallback( + (command: Parameters[0]) => { + const result = slashCommands.handleSelectCommand(command); + textInput.setInput(result.text); + }, + [slashCommands, textInput], + ); + + return ( +
+ {slashCommands.isOpen && ( + + )} + {/* onKeyDownCapture intercepts keys before textarea handles them */} +
+ + + + + + + + + + + + + +
+ + +
+
+
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandMenu/SlashCommandMenu.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandMenu/SlashCommandMenu.tsx new file mode 100644 index 00000000000..d0edbaad696 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandMenu/SlashCommandMenu.tsx @@ -0,0 +1,62 @@ +import { useEffect, useRef } from "react"; +import type { SlashCommand } from "../../hooks/useSlashCommands"; + +interface SlashCommandMenuProps { + commands: SlashCommand[]; + selectedIndex: number; + onSelect: (command: SlashCommand) => void; + onHover: (index: number) => void; +} + +export function SlashCommandMenu({ + commands, + selectedIndex, + onSelect, + onHover, +}: SlashCommandMenuProps) { + const selectedRef = useRef(null); + + // biome-ignore lint/correctness/useExhaustiveDependencies: must scroll when selectedIndex changes + useEffect(() => { + selectedRef.current?.scrollIntoView({ block: "nearest" }); + }, [selectedIndex]); + + if (commands.length === 0) return null; + + return ( +
+ {commands.map((cmd, index) => ( + + ))} +
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandMenu/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandMenu/index.ts new file mode 100644 index 00000000000..f8ef97b2bff --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandMenu/index.ts @@ -0,0 +1 @@ +export { SlashCommandMenu } from "./SlashCommandMenu"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/index.ts new file mode 100644 index 00000000000..caaa2ca51c7 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/index.ts @@ -0,0 +1 @@ +export { type SlashCommand, useSlashCommands } from "./useSlashCommands"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts new file mode 100644 index 00000000000..cc3ece60a2e --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts @@ -0,0 +1,154 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { electronTrpc } from "renderer/lib/electron-trpc"; + +export interface SlashCommand { + name: string; + description: string; + argumentHint: string; +} + +const DEFAULT_COMMANDS: SlashCommand[] = [ + { name: "help", description: "Show available commands", argumentHint: "" }, + { + name: "clear", + description: "Clear conversation history", + argumentHint: "", + }, + { + name: "compact", + description: "Compact conversation context", + argumentHint: "[instructions]", + }, + { name: "config", description: "Show configuration", argumentHint: "" }, + { + name: "cost", + description: "Show token usage and cost", + argumentHint: "", + }, + { + name: "memory", + description: "Edit CLAUDE.md memory files", + argumentHint: "", + }, + { + name: "review", + description: "Review a pull request", + argumentHint: "[pr-url]", + }, + { name: "status", description: "Show status information", argumentHint: "" }, +]; + +interface UseSlashCommandsOptions { + inputValue: string; + onClear: () => void; + onSendMessage: (text: string) => void; +} + +export function useSlashCommands({ + inputValue, + onClear, + onSendMessage, +}: UseSlashCommandsOptions) { + const { data } = electronTrpc.aiChat.getSlashCommands.useQuery(undefined, { + staleTime: 5 * 60 * 1000, + }); + + const commands = useMemo(() => { + const fetched = data?.commands; + if (fetched && fetched.length > 0) return fetched; + return DEFAULT_COMMANDS; + }, [data]); + + const [selectedIndex, setSelectedIndex] = useState(0); + + const isOpen = + inputValue.startsWith("/") && + !inputValue.includes("\n") && + inputValue !== "/"; + + const query = isOpen ? inputValue.slice(1).toLowerCase() : ""; + + const filteredCommands = useMemo(() => { + if (!isOpen) return []; + if (query === "") return commands; + return commands.filter((cmd) => cmd.name.startsWith(query)); + }, [commands, isOpen, query]); + + // Reset selected index when filter changes + const prevQuery = useRef(query); + useEffect(() => { + if (prevQuery.current !== query) { + setSelectedIndex(0); + prevQuery.current = query; + } + }, [query]); + + const handleSelectCommand = useCallback( + (command: SlashCommand): { text: string; shouldSend: boolean } => { + if (command.name === "clear") { + onClear(); + return { text: "", shouldSend: false }; + } + + if (command.argumentHint) { + return { text: `/${command.name} `, shouldSend: false }; + } + + onSendMessage(`/${command.name}`); + return { text: "", shouldSend: true }; + }, + [onClear, onSendMessage], + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent): boolean => { + if (!isOpen || filteredCommands.length === 0) return false; + + switch (e.key) { + case "ArrowUp": { + e.preventDefault(); + e.stopPropagation(); + setSelectedIndex((prev) => + prev <= 0 ? filteredCommands.length - 1 : prev - 1, + ); + return true; + } + case "ArrowDown": { + e.preventDefault(); + e.stopPropagation(); + setSelectedIndex((prev) => + prev >= filteredCommands.length - 1 ? 0 : prev + 1, + ); + return true; + } + case "Enter": + case "Tab": { + e.preventDefault(); + e.stopPropagation(); + const cmd = filteredCommands[selectedIndex]; + if (cmd) { + handleSelectCommand(cmd); + } + return true; + } + case "Escape": { + e.preventDefault(); + e.stopPropagation(); + return true; + } + default: + return false; + } + }, + [isOpen, filteredCommands, selectedIndex, handleSelectCommand], + ); + + return { + isOpen: isOpen && filteredCommands.length > 0, + filteredCommands, + selectedIndex, + setSelectedIndex, + handleKeyDown, + handleSelectCommand, + }; +} diff --git a/apps/streams/src/claude-agent.ts b/apps/streams/src/claude-agent.ts index 3ba30082df9..f675eda4a5f 100644 --- a/apps/streams/src/claude-agent.ts +++ b/apps/streams/src/claude-agent.ts @@ -152,6 +152,48 @@ function buildNotificationHooks({ }; } +// --------------------------------------------------------------------------- +// Slash-command cache +// --------------------------------------------------------------------------- +interface SlashCommand { + name: string; + description: string; + argumentHint: string; +} + +const DEFAULT_COMMANDS: SlashCommand[] = [ + { name: "help", description: "Show available commands", argumentHint: "" }, + { + name: "clear", + description: "Clear conversation history", + argumentHint: "", + }, + { + name: "compact", + description: "Compact conversation context", + argumentHint: "[instructions]", + }, + { name: "config", description: "Show configuration", argumentHint: "" }, + { + name: "cost", + description: "Show token usage and cost", + argumentHint: "", + }, + { + name: "memory", + description: "Edit CLAUDE.md memory files", + argumentHint: "", + }, + { + name: "review", + description: "Review a pull request", + argumentHint: "[pr-url]", + }, + { name: "status", description: "Show status information", argumentHint: "" }, +]; + +let cachedCommands: SlashCommand[] | null = null; + const app = new Hono(); app.post("/", async (c) => { @@ -219,6 +261,20 @@ app.post("/", async (c) => { }, }); + // Populate slash-command cache on first query (fire-and-forget) + if (!cachedCommands) { + result + .supportedCommands() + .then((cmds) => { + cachedCommands = cmds.map((cmd) => ({ + name: cmd.name ?? "", + description: cmd.description ?? "", + argumentHint: cmd.argumentHint ?? "", + })); + }) + .catch(() => {}); + } + const converter = createConverter(); const requestSignal = c.req.raw.signal; @@ -318,6 +374,10 @@ app.get("/sessions/:sessionId", (c) => { return c.json({ claudeSessionId }); }); +app.get("/commands", (c) => { + return c.json({ commands: cachedCommands ?? DEFAULT_COMMANDS }); +}); + app.get("/health", (c) => { return c.json({ status: "ok", From 495ad1fd102d917bbb9d51ea612822b429a7007f Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 8 Feb 2026 09:05:03 -0800 Subject: [PATCH 02/11] Fix issues --- .../ChatPane/ChatInterface/ChatInterface.tsx | 31 ++++++++++--------- .../useSlashCommands/useSlashCommands.ts | 2 +- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx index 07902473f4e..eea7576bb29 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx @@ -392,22 +392,25 @@ function ChatInputArea({ const handleKeyDownCapture = useCallback( (e: React.KeyboardEvent) => { if (slashCommands.isOpen) { - const handled = slashCommands.handleKeyDown(e); - if (handled) { - // For Enter/Tab, apply the selected command - if (e.key === "Enter" || e.key === "Tab") { - const cmd = - slashCommands.filteredCommands[slashCommands.selectedIndex]; - if (cmd) { - const result = slashCommands.handleSelectCommand(cmd); - textInput.setInput(result.text); - } - } - // For Escape, clear the slash input - if (e.key === "Escape") { - textInput.setInput(""); + if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + textInput.setInput(""); + return; + } + if (e.key === "Enter" || e.key === "Tab") { + e.preventDefault(); + e.stopPropagation(); + const cmd = + slashCommands.filteredCommands[slashCommands.selectedIndex]; + if (cmd) { + const result = slashCommands.handleSelectCommand(cmd); + textInput.setInput(result.text); } + return; } + // ArrowUp/ArrowDown for navigation + slashCommands.handleKeyDown(e); } }, [slashCommands, textInput], diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts index cc3ece60a2e..0e7256aff78 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts @@ -64,7 +64,7 @@ export function useSlashCommands({ const isOpen = inputValue.startsWith("/") && !inputValue.includes("\n") && - inputValue !== "/"; + !inputValue.includes(" "); const query = isOpen ? inputValue.slice(1).toLowerCase() : ""; From 267e6db267bb57e1a6b8e697aede5c807b4ebd93 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 8 Feb 2026 09:08:39 -0800 Subject: [PATCH 03/11] Refactor --- .../ChatPane/ChatInterface/ChatInterface.tsx | 224 +++++++++--------- .../hooks/useSlashCommands/index.ts | 6 +- .../useSlashCommands/useSlashCommands.ts | 106 +++------ .../src/react/use-durable-chat.ts | 24 +- 4 files changed, 167 insertions(+), 193 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx index eea7576bb29..f9aaa41cf76 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx @@ -1,4 +1,3 @@ -import type { DurableChatCollections } from "@superset/durable-session/react"; import { useDurableChat } from "@superset/durable-session/react"; import { Conversation, @@ -20,7 +19,13 @@ import { import { Shimmer } from "@superset/ui/ai-elements/shimmer"; import { Suggestion, Suggestions } from "@superset/ui/ai-elements/suggestion"; import { ThinkingToggle } from "@superset/ui/ai-elements/thinking-toggle"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { + type ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from "react"; import { HiMiniAtSymbol, HiMiniChatBubbleLeftRight, @@ -33,7 +38,11 @@ import { ModelPicker } from "./components/ModelPicker"; import { SlashCommandMenu } from "./components/SlashCommandMenu"; import { MODELS, SUGGESTIONS } from "./constants"; import { useClaudeCodeHistory } from "./hooks/useClaudeCodeHistory"; -import { useSlashCommands } from "./hooks/useSlashCommands"; +import { + resolveCommandAction, + type SlashCommand, + useSlashCommands, +} from "./hooks/useSlashCommands"; import type { ModelOption } from "./types"; import { extractTitleFromMessages } from "./utils/extract-title"; @@ -203,13 +212,6 @@ export function ChatInterface({ [sendMessage], ); - const handleSendText = useCallback( - (text: string) => { - handleSend({ text }); - }, - [handleSend], - ); - const handleSuggestion = useCallback( (suggestion: string) => { handleSend({ text: suggestion }); @@ -261,10 +263,48 @@ export function ChatInterface({ [stop], ); - // TODO: Implement proper /clear handler that resets the conversation - const handleClear = useCallback(() => { - console.log("[chat] /clear requested"); - }, []); + // TODO: Wire to actual session/conversation reset + const handleClear = useCallback(() => {}, []); + + const handleSlashCommandSend = useCallback( + (command: SlashCommand) => { + handleSend({ text: `/${command.name}` }); + }, + [handleSend], + ); + + const footerContent: ReactNode = ( + + + + + + + + + + + +
+ + +
+
+ ); return (
@@ -331,19 +371,11 @@ export function ChatInterface({ )} -
@@ -352,76 +384,72 @@ export function ChatInterface({ ); } -interface ChatInputAreaProps { - handleSend: (message: { text: string }) => void; - handleSendText: (text: string) => void; - handleClear: () => void; - isLoading: boolean; - handleStop: (e: React.MouseEvent) => void; - thinkingEnabled: boolean; - handleThinkingToggle: (enabled: boolean) => void; - selectedModel: ModelOption; - handleModelSelect: (model: ModelOption) => void; - modelSelectorOpen: boolean; - setModelSelectorOpen: (open: boolean) => void; - collections: DurableChatCollections; +// --------------------------------------------------------------------------- +// Inner component — must live inside PromptInputProvider to access textInput +// --------------------------------------------------------------------------- + +interface SlashCommandInputProps { + onSubmit: (message: { text: string }) => void; + onClear: () => void; + onCommandSend: (command: SlashCommand) => void; + footer: ReactNode; } -function ChatInputArea({ - handleSend, - handleSendText, - handleClear, - isLoading, - handleStop, - thinkingEnabled, - handleThinkingToggle, - selectedModel, - handleModelSelect, - modelSelectorOpen, - setModelSelectorOpen, - collections, -}: ChatInputAreaProps) { +function SlashCommandInput({ + onSubmit, + onClear, + onCommandSend, + footer, +}: SlashCommandInputProps) { const { textInput } = usePromptInputController(); - const slashCommands = useSlashCommands({ - inputValue: textInput.value, - onClear: handleClear, - onSendMessage: handleSendText, - }); + const slashCommands = useSlashCommands({ inputValue: textInput.value }); + + const executeCommand = useCallback( + (command: SlashCommand) => { + const action = resolveCommandAction(command); + if (action.isClear) { + onClear(); + } else if (action.shouldSend) { + onCommandSend(command); + } + textInput.setInput(action.text); + }, + [onClear, onCommandSend, textInput], + ); const handleKeyDownCapture = useCallback( (e: React.KeyboardEvent) => { - if (slashCommands.isOpen) { - if (e.key === "Escape") { + if (!slashCommands.isOpen) return; + + switch (e.key) { + case "Escape": e.preventDefault(); e.stopPropagation(); textInput.setInput(""); - return; - } - if (e.key === "Enter" || e.key === "Tab") { + break; + case "Enter": + case "Tab": { e.preventDefault(); e.stopPropagation(); const cmd = slashCommands.filteredCommands[slashCommands.selectedIndex]; - if (cmd) { - const result = slashCommands.handleSelectCommand(cmd); - textInput.setInput(result.text); - } - return; + if (cmd) executeCommand(cmd); + break; } - // ArrowUp/ArrowDown for navigation - slashCommands.handleKeyDown(e); + case "ArrowUp": + e.preventDefault(); + e.stopPropagation(); + slashCommands.navigateUp(); + break; + case "ArrowDown": + e.preventDefault(); + e.stopPropagation(); + slashCommands.navigateDown(); + break; } }, - [slashCommands, textInput], - ); - - const handleMenuSelect = useCallback( - (command: Parameters[0]) => { - const result = slashCommands.handleSelectCommand(command); - textInput.setInput(result.text); - }, - [slashCommands, textInput], + [slashCommands, textInput, executeCommand], ); return ( @@ -430,44 +458,14 @@ function ChatInputArea({ )} - {/* onKeyDownCapture intercepts keys before textarea handles them */}
- + - - - - - - - - - - - -
- - -
-
+ {footer}
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/index.ts index caaa2ca51c7..55788e4020a 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/index.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/index.ts @@ -1 +1,5 @@ -export { type SlashCommand, useSlashCommands } from "./useSlashCommands"; +export { + resolveCommandAction, + type SlashCommand, + useSlashCommands, +} from "./useSlashCommands"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts index 0e7256aff78..057f10fca96 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts @@ -38,25 +38,14 @@ const DEFAULT_COMMANDS: SlashCommand[] = [ { name: "status", description: "Show status information", argumentHint: "" }, ]; -interface UseSlashCommandsOptions { - inputValue: string; - onClear: () => void; - onSendMessage: (text: string) => void; -} - -export function useSlashCommands({ - inputValue, - onClear, - onSendMessage, -}: UseSlashCommandsOptions) { +export function useSlashCommands({ inputValue }: { inputValue: string }) { const { data } = electronTrpc.aiChat.getSlashCommands.useQuery(undefined, { staleTime: 5 * 60 * 1000, }); const commands = useMemo(() => { const fetched = data?.commands; - if (fetched && fetched.length > 0) return fetched; - return DEFAULT_COMMANDS; + return fetched && fetched.length > 0 ? fetched : DEFAULT_COMMANDS; }, [data]); const [selectedIndex, setSelectedIndex] = useState(0); @@ -74,7 +63,6 @@ export function useSlashCommands({ return commands.filter((cmd) => cmd.name.startsWith(query)); }, [commands, isOpen, query]); - // Reset selected index when filter changes const prevQuery = useRef(query); useEffect(() => { if (prevQuery.current !== query) { @@ -83,72 +71,42 @@ export function useSlashCommands({ } }, [query]); - const handleSelectCommand = useCallback( - (command: SlashCommand): { text: string; shouldSend: boolean } => { - if (command.name === "clear") { - onClear(); - return { text: "", shouldSend: false }; - } - - if (command.argumentHint) { - return { text: `/${command.name} `, shouldSend: false }; - } - - onSendMessage(`/${command.name}`); - return { text: "", shouldSend: true }; - }, - [onClear, onSendMessage], - ); + const navigateUp = useCallback(() => { + setSelectedIndex((prev) => + prev <= 0 ? filteredCommands.length - 1 : prev - 1, + ); + }, [filteredCommands.length]); - const handleKeyDown = useCallback( - (e: React.KeyboardEvent): boolean => { - if (!isOpen || filteredCommands.length === 0) return false; - - switch (e.key) { - case "ArrowUp": { - e.preventDefault(); - e.stopPropagation(); - setSelectedIndex((prev) => - prev <= 0 ? filteredCommands.length - 1 : prev - 1, - ); - return true; - } - case "ArrowDown": { - e.preventDefault(); - e.stopPropagation(); - setSelectedIndex((prev) => - prev >= filteredCommands.length - 1 ? 0 : prev + 1, - ); - return true; - } - case "Enter": - case "Tab": { - e.preventDefault(); - e.stopPropagation(); - const cmd = filteredCommands[selectedIndex]; - if (cmd) { - handleSelectCommand(cmd); - } - return true; - } - case "Escape": { - e.preventDefault(); - e.stopPropagation(); - return true; - } - default: - return false; - } - }, - [isOpen, filteredCommands, selectedIndex, handleSelectCommand], - ); + const navigateDown = useCallback(() => { + setSelectedIndex((prev) => + prev >= filteredCommands.length - 1 ? 0 : prev + 1, + ); + }, [filteredCommands.length]); return { isOpen: isOpen && filteredCommands.length > 0, filteredCommands, selectedIndex, setSelectedIndex, - handleKeyDown, - handleSelectCommand, + navigateUp, + navigateDown, }; } + +/** + * Determines the action to take when a slash command is selected. + * Returns the text to place in the input and whether to send immediately. + */ +export function resolveCommandAction(command: SlashCommand): { + text: string; + isClear: boolean; + shouldSend: boolean; +} { + if (command.name === "clear") { + return { text: "", isClear: true, shouldSend: false }; + } + if (command.argumentHint) { + return { text: `/${command.name} `, isClear: false, shouldSend: false }; + } + return { text: "", isClear: false, shouldSend: true }; +} diff --git a/packages/durable-session/src/react/use-durable-chat.ts b/packages/durable-session/src/react/use-durable-chat.ts index 0c1ba07bece..ee17ecb43c7 100644 --- a/packages/durable-session/src/react/use-durable-chat.ts +++ b/packages/durable-session/src/react/use-durable-chat.ts @@ -179,11 +179,17 @@ export function useDurableChat< } | null>(null); const key = `${clientOptions.sessionId}:${clientOptions.proxyUrl}`; - // Create or recreate client when key changes or client was disposed + // Create or recreate client when key changes or client was disposed. // The isDisposed check handles React Strict Mode: cleanup disposes the client, // so the next render must create a fresh one with a new AbortController. if (providedClient) { if (!clientRef.current || clientRef.current.client !== providedClient) { + const prev = clientRef.current?.client; + if (prev && !prev.isDisposed) { + // Defer disposal via microtask — dispose() triggers collection events + // which call setState, so it must not run during render or useEffect. + queueMicrotask(() => prev.dispose()); + } clientRef.current = { client: providedClient, key: "provided" }; } } else if ( @@ -191,8 +197,10 @@ export function useDurableChat< clientRef.current.key !== key || clientRef.current.client.isDisposed ) { - // Dispose old client if exists (may already be disposed, which is fine) - clientRef.current?.client.dispose(); + const prev = clientRef.current?.client; + if (prev && !prev.isDisposed) { + queueMicrotask(() => prev.dispose()); + } clientRef.current = { client: new DurableChatClient({ ...clientOptions, @@ -227,10 +235,16 @@ export function useDurableChat< }); } - // Cleanup: unsubscribe and dispose (disposal is idempotent) + // Cleanup: defer disposal via microtask — dispose() triggers collection events + // that call setState. When Radix UI uses flushSync for dropdown selection, + // this cleanup runs during a synchronous render which would cause + // "Cannot update a component while rendering" if dispose runs inline. return () => { if (!providedClient) { - client.dispose(); + const c = client; + queueMicrotask(() => { + if (!c.isDisposed) c.dispose(); + }); } }; }, [client, autoConnect, providedClient]); From 8c25e3408595693f7b750fd21c13061088924f34 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 8 Feb 2026 09:18:41 -0800 Subject: [PATCH 04/11] Update create PR skill --- .claude/commands/create-pr.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.claude/commands/create-pr.md b/.claude/commands/create-pr.md index e821a229183..bb0cb5911ca 100644 --- a/.claude/commands/create-pr.md +++ b/.claude/commands/create-pr.md @@ -13,11 +13,15 @@ Create a pull request for the current branch. ## Step 2: Analyze Changes +First, determine the base branch to diff against: +- Run `git fetch origin main --quiet` to ensure the remote ref is up to date +- Use `origin/main` (not local `main`) as the base for all diff/log commands — local `main` may be stale + Run in parallel: -- `git log main..HEAD --oneline` — commit history -- `git log main..HEAD --format="%B---"` — full commit messages for context -- `git diff main...HEAD --stat` — file change overview -- `git diff main...HEAD` — full diff +- `git log origin/main..HEAD --oneline` — commit history +- `git log origin/main..HEAD --format="%B---"` — full commit messages for context +- `git diff origin/main...HEAD --stat` — file change overview +- `git diff origin/main...HEAD` — full diff Read the diff carefully to understand what changed and why. From b48299341e0707a4f68b4b1933ca0353b0b8d34e Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 8 Feb 2026 09:38:25 -0800 Subject: [PATCH 05/11] Refactor --- .../ChatPane/ChatInterface/ChatInterface.tsx | 178 ++++-------------- .../SlashCommandInput/SlashCommandInput.tsx | 96 ++++++++++ .../components/SlashCommandInput/index.ts | 1 + .../useSlashCommands/useSlashCommands.ts | 4 - apps/streams/src/claude-agent.ts | 4 - 5 files changed, 134 insertions(+), 149 deletions(-) create mode 100644 apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandInput/SlashCommandInput.tsx create mode 100644 apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandInput/index.ts diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx index f9aaa41cf76..01bb0ac633e 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx @@ -7,25 +7,16 @@ import { } from "@superset/ui/ai-elements/conversation"; import { Message, MessageContent } from "@superset/ui/ai-elements/message"; import { - PromptInput, PromptInputButton, PromptInputFooter, PromptInputProvider, PromptInputSubmit, - PromptInputTextarea, PromptInputTools, - usePromptInputController, } from "@superset/ui/ai-elements/prompt-input"; import { Shimmer } from "@superset/ui/ai-elements/shimmer"; import { Suggestion, Suggestions } from "@superset/ui/ai-elements/suggestion"; import { ThinkingToggle } from "@superset/ui/ai-elements/thinking-toggle"; -import { - type ReactNode, - useCallback, - useEffect, - useRef, - useState, -} from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { HiMiniAtSymbol, HiMiniChatBubbleLeftRight, @@ -35,14 +26,10 @@ import { electronTrpc } from "renderer/lib/electron-trpc"; import { ChatMessageItem } from "./components/ChatMessageItem"; import { ContextIndicator } from "./components/ContextIndicator"; import { ModelPicker } from "./components/ModelPicker"; -import { SlashCommandMenu } from "./components/SlashCommandMenu"; +import { SlashCommandInput } from "./components/SlashCommandInput"; import { MODELS, SUGGESTIONS } from "./constants"; import { useClaudeCodeHistory } from "./hooks/useClaudeCodeHistory"; -import { - resolveCommandAction, - type SlashCommand, - useSlashCommands, -} from "./hooks/useSlashCommands"; +import type { SlashCommand } from "./hooks/useSlashCommands"; import type { ModelOption } from "./types"; import { extractTitleFromMessages } from "./utils/extract-title"; @@ -263,9 +250,6 @@ export function ChatInterface({ [stop], ); - // TODO: Wire to actual session/conversation reset - const handleClear = useCallback(() => {}, []); - const handleSlashCommandSend = useCallback( (command: SlashCommand) => { handleSend({ text: `/${command.name}` }); @@ -273,38 +257,8 @@ export function ChatInterface({ [handleSend], ); - const footerContent: ReactNode = ( - - - - - - - - - - - -
- - -
-
- ); + // TODO: Wire to actual session/conversation reset + const handleClear = useCallback(() => {}, []); return (
@@ -375,99 +329,41 @@ export function ChatInterface({ onSubmit={handleSend} onClear={handleClear} onCommandSend={handleSlashCommandSend} - footer={footerContent} - /> + > + + + + + + + + + + + +
+ + +
+
+
); } - -// --------------------------------------------------------------------------- -// Inner component — must live inside PromptInputProvider to access textInput -// --------------------------------------------------------------------------- - -interface SlashCommandInputProps { - onSubmit: (message: { text: string }) => void; - onClear: () => void; - onCommandSend: (command: SlashCommand) => void; - footer: ReactNode; -} - -function SlashCommandInput({ - onSubmit, - onClear, - onCommandSend, - footer, -}: SlashCommandInputProps) { - const { textInput } = usePromptInputController(); - - const slashCommands = useSlashCommands({ inputValue: textInput.value }); - - const executeCommand = useCallback( - (command: SlashCommand) => { - const action = resolveCommandAction(command); - if (action.isClear) { - onClear(); - } else if (action.shouldSend) { - onCommandSend(command); - } - textInput.setInput(action.text); - }, - [onClear, onCommandSend, textInput], - ); - - const handleKeyDownCapture = useCallback( - (e: React.KeyboardEvent) => { - if (!slashCommands.isOpen) return; - - switch (e.key) { - case "Escape": - e.preventDefault(); - e.stopPropagation(); - textInput.setInput(""); - break; - case "Enter": - case "Tab": { - e.preventDefault(); - e.stopPropagation(); - const cmd = - slashCommands.filteredCommands[slashCommands.selectedIndex]; - if (cmd) executeCommand(cmd); - break; - } - case "ArrowUp": - e.preventDefault(); - e.stopPropagation(); - slashCommands.navigateUp(); - break; - case "ArrowDown": - e.preventDefault(); - e.stopPropagation(); - slashCommands.navigateDown(); - break; - } - }, - [slashCommands, textInput, executeCommand], - ); - - return ( -
- {slashCommands.isOpen && ( - - )} -
- - - {footer} - -
-
- ); -} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandInput/SlashCommandInput.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandInput/SlashCommandInput.tsx new file mode 100644 index 00000000000..3a6eb9867b6 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandInput/SlashCommandInput.tsx @@ -0,0 +1,96 @@ +import { + PromptInput, + PromptInputTextarea, + usePromptInputController, +} from "@superset/ui/ai-elements/prompt-input"; +import { useCallback } from "react"; +import { + resolveCommandAction, + type SlashCommand, + useSlashCommands, +} from "../../hooks/useSlashCommands"; +import { SlashCommandMenu } from "../SlashCommandMenu"; + +interface SlashCommandInputProps { + onSubmit: (message: { text: string }) => void; + onClear: () => void; + onCommandSend: (command: SlashCommand) => void; + children: React.ReactNode; +} + +export function SlashCommandInput({ + onSubmit, + onClear, + onCommandSend, + children, +}: SlashCommandInputProps) { + const { textInput } = usePromptInputController(); + + const slashCommands = useSlashCommands({ inputValue: textInput.value }); + + const executeCommand = useCallback( + (command: SlashCommand) => { + const action = resolveCommandAction(command); + if (action.isClear) { + onClear(); + } else if (action.shouldSend) { + onCommandSend(command); + } + textInput.setInput(action.text); + }, + [onClear, onCommandSend, textInput], + ); + + const handleKeyDownCapture = useCallback( + (e: React.KeyboardEvent) => { + if (!slashCommands.isOpen) return; + + switch (e.key) { + case "Escape": + e.preventDefault(); + e.stopPropagation(); + textInput.setInput(""); + break; + case "Enter": + case "Tab": { + e.preventDefault(); + e.stopPropagation(); + const cmd = + slashCommands.filteredCommands[slashCommands.selectedIndex]; + if (cmd) executeCommand(cmd); + break; + } + case "ArrowUp": + e.preventDefault(); + e.stopPropagation(); + slashCommands.navigateUp(); + break; + case "ArrowDown": + e.preventDefault(); + e.stopPropagation(); + slashCommands.navigateDown(); + break; + } + }, + [slashCommands, textInput, executeCommand], + ); + + return ( +
+ {slashCommands.isOpen && ( + + )} +
+ + + {children} + +
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandInput/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandInput/index.ts new file mode 100644 index 00000000000..8b4a667d02b --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandInput/index.ts @@ -0,0 +1 @@ +export { SlashCommandInput } from "./SlashCommandInput"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts index 057f10fca96..dc1ddc876c6 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts @@ -93,10 +93,6 @@ export function useSlashCommands({ inputValue }: { inputValue: string }) { }; } -/** - * Determines the action to take when a slash command is selected. - * Returns the text to place in the input and whether to send immediately. - */ export function resolveCommandAction(command: SlashCommand): { text: string; isClear: boolean; diff --git a/apps/streams/src/claude-agent.ts b/apps/streams/src/claude-agent.ts index f675eda4a5f..eb2885156a3 100644 --- a/apps/streams/src/claude-agent.ts +++ b/apps/streams/src/claude-agent.ts @@ -152,9 +152,6 @@ function buildNotificationHooks({ }; } -// --------------------------------------------------------------------------- -// Slash-command cache -// --------------------------------------------------------------------------- interface SlashCommand { name: string; description: string; @@ -261,7 +258,6 @@ app.post("/", async (c) => { }, }); - // Populate slash-command cache on first query (fire-and-forget) if (!cachedCommands) { result .supportedCommands() From 445f26815afa6ad58b7ce382e64699cc25104c60 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 8 Feb 2026 10:11:43 -0800 Subject: [PATCH 06/11] feat(chat): scan .claude/commands/ for custom slash commands on load Adds filesystem scanning for custom commands instead of waiting for SDK cache. Also invalidates the command list when "/" is typed to ensure fresh results. --- .../src/lib/trpc/routers/ai-chat/index.ts | 78 +++++++++++++++---- .../ChatPane/ChatInterface/ChatInterface.tsx | 1 + .../SlashCommandInput/SlashCommandInput.tsx | 7 +- .../useSlashCommands/useSlashCommands.ts | 25 +++++- 4 files changed, 91 insertions(+), 20 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts b/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts index f76c21e802e..fd44e07ef86 100644 --- a/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts +++ b/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts @@ -1,3 +1,6 @@ +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; import { observable } from "@trpc/server/observable"; import { z } from "zod"; import { publicProcedure, router } from "../.."; @@ -14,6 +17,43 @@ import { const CLAUDE_AGENT_URL = process.env.CLAUDE_AGENT_URL || "http://localhost:9090"; +interface CommandEntry { + name: string; + description: string; + argumentHint: string; +} + +function scanCustomCommands(cwd: string): CommandEntry[] { + const dirs = [ + join(cwd, ".claude", "commands"), + join(homedir(), ".claude", "commands"), + ]; + const commands: CommandEntry[] = []; + const seen = new Set(); + + for (const dir of dirs) { + if (!existsSync(dir)) continue; + try { + for (const file of readdirSync(dir)) { + if (!file.endsWith(".md")) continue; + const name = file.replace(/\.md$/, ""); + if (seen.has(name)) continue; + seen.add(name); + const content = readFileSync(join(dir, file), "utf-8"); + const match = content.match(/^---\n([\s\S]*?)\n---/); + const descMatch = match?.[1]?.match(/^description:\s*(.+)$/m); + commands.push({ + name, + description: descMatch?.[1]?.trim() ?? "", + argumentHint: "", + }); + } + } catch {} + } + + return commands; +} + export const createAiChatRouter = () => { return router({ getConfig: publicProcedure.query(() => ({ @@ -24,22 +64,30 @@ export const createAiChatRouter = () => { null, })), - getSlashCommands: publicProcedure.query(async () => { - try { - const res = await fetch(`${CLAUDE_AGENT_URL}/commands`); - if (!res.ok) return { commands: [] }; - const data = (await res.json()) as { - commands: Array<{ - name: string; - description: string; - argumentHint: string; - }>; + getSlashCommands: publicProcedure + .input(z.object({ cwd: z.string() })) + .query(async ({ input }) => { + const customCommands = scanCustomCommands(input.cwd); + + let sdkCommands: CommandEntry[] = []; + try { + const res = await fetch(`${CLAUDE_AGENT_URL}/commands`); + if (res.ok) { + const data = (await res.json()) as { + commands: CommandEntry[]; + }; + sdkCommands = data.commands ?? []; + } + } catch {} + + const seen = new Set(sdkCommands.map((c) => c.name)); + return { + commands: [ + ...sdkCommands, + ...customCommands.filter((c) => !seen.has(c.name)), + ], }; - return { commands: data.commands ?? [] }; - } catch { - return { commands: [] }; - } - }), + }), startSession: publicProcedure .input( diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx index 01bb0ac633e..329a4648a91 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx @@ -329,6 +329,7 @@ export function ChatInterface({ onSubmit={handleSend} onClear={handleClear} onCommandSend={handleSlashCommandSend} + cwd={cwd} > diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandInput/SlashCommandInput.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandInput/SlashCommandInput.tsx index 3a6eb9867b6..65ecc841107 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandInput/SlashCommandInput.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandInput/SlashCommandInput.tsx @@ -15,6 +15,7 @@ interface SlashCommandInputProps { onSubmit: (message: { text: string }) => void; onClear: () => void; onCommandSend: (command: SlashCommand) => void; + cwd: string; children: React.ReactNode; } @@ -22,11 +23,15 @@ export function SlashCommandInput({ onSubmit, onClear, onCommandSend, + cwd, children, }: SlashCommandInputProps) { const { textInput } = usePromptInputController(); - const slashCommands = useSlashCommands({ inputValue: textInput.value }); + const slashCommands = useSlashCommands({ + inputValue: textInput.value, + cwd, + }); const executeCommand = useCallback( (command: SlashCommand) => { diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts index dc1ddc876c6..7335cac322b 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts @@ -38,10 +38,19 @@ const DEFAULT_COMMANDS: SlashCommand[] = [ { name: "status", description: "Show status information", argumentHint: "" }, ]; -export function useSlashCommands({ inputValue }: { inputValue: string }) { - const { data } = electronTrpc.aiChat.getSlashCommands.useQuery(undefined, { - staleTime: 5 * 60 * 1000, - }); +export function useSlashCommands({ + inputValue, + cwd, +}: { + inputValue: string; + cwd: string; +}) { + const utils = electronTrpc.useUtils(); + + const { data } = electronTrpc.aiChat.getSlashCommands.useQuery( + { cwd }, + { staleTime: 5 * 60 * 1000 }, + ); const commands = useMemo(() => { const fetched = data?.commands; @@ -71,6 +80,14 @@ export function useSlashCommands({ inputValue }: { inputValue: string }) { } }, [query]); + const prevIsOpen = useRef(false); + useEffect(() => { + if (isOpen && !prevIsOpen.current) { + void utils.aiChat.getSlashCommands.invalidate(); + } + prevIsOpen.current = isOpen; + }, [isOpen, utils]); + const navigateUp = useCallback(() => { setSelectedIndex((prev) => prev <= 0 ? filteredCommands.length - 1 : prev - 1, From 517ec88eba8006e79a4d0345be7ce3112e9a8179 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 8 Feb 2026 12:59:20 -0800 Subject: [PATCH 07/11] More fixes --- .../src/lib/trpc/routers/ai-chat/index.ts | 9 +-- .../useSlashCommands/useSlashCommands.ts | 31 +--------- apps/streams/src/claude-agent.ts | 32 +---------- apps/streams/src/sdk-to-ai-chunks.ts | 57 ++++++++++++++++++- 4 files changed, 63 insertions(+), 66 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts b/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts index fd44e07ef86..711a1301145 100644 --- a/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts +++ b/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts @@ -39,13 +39,14 @@ function scanCustomCommands(cwd: string): CommandEntry[] { const name = file.replace(/\.md$/, ""); if (seen.has(name)) continue; seen.add(name); - const content = readFileSync(join(dir, file), "utf-8"); - const match = content.match(/^---\n([\s\S]*?)\n---/); - const descMatch = match?.[1]?.match(/^description:\s*(.+)$/m); + const raw = readFileSync(join(dir, file), "utf-8"); + const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/); + const descMatch = fmMatch?.[1]?.match(/^description:\s*(.+)$/m); + const argMatch = fmMatch?.[1]?.match(/^argument-hint:\s*(.+)$/m); commands.push({ name, description: descMatch?.[1]?.trim() ?? "", - argumentHint: "", + argumentHint: argMatch?.[1]?.trim() ?? "", }); } } catch {} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts index 7335cac322b..a8b5ed29a1c 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts @@ -7,36 +7,7 @@ export interface SlashCommand { argumentHint: string; } -const DEFAULT_COMMANDS: SlashCommand[] = [ - { name: "help", description: "Show available commands", argumentHint: "" }, - { - name: "clear", - description: "Clear conversation history", - argumentHint: "", - }, - { - name: "compact", - description: "Compact conversation context", - argumentHint: "[instructions]", - }, - { name: "config", description: "Show configuration", argumentHint: "" }, - { - name: "cost", - description: "Show token usage and cost", - argumentHint: "", - }, - { - name: "memory", - description: "Edit CLAUDE.md memory files", - argumentHint: "", - }, - { - name: "review", - description: "Review a pull request", - argumentHint: "[pr-url]", - }, - { name: "status", description: "Show status information", argumentHint: "" }, -]; +const DEFAULT_COMMANDS: SlashCommand[] = []; export function useSlashCommands({ inputValue, diff --git a/apps/streams/src/claude-agent.ts b/apps/streams/src/claude-agent.ts index eb2885156a3..bca802b74f8 100644 --- a/apps/streams/src/claude-agent.ts +++ b/apps/streams/src/claude-agent.ts @@ -158,36 +158,7 @@ interface SlashCommand { argumentHint: string; } -const DEFAULT_COMMANDS: SlashCommand[] = [ - { name: "help", description: "Show available commands", argumentHint: "" }, - { - name: "clear", - description: "Clear conversation history", - argumentHint: "", - }, - { - name: "compact", - description: "Compact conversation context", - argumentHint: "[instructions]", - }, - { name: "config", description: "Show configuration", argumentHint: "" }, - { - name: "cost", - description: "Show token usage and cost", - argumentHint: "", - }, - { - name: "memory", - description: "Edit CLAUDE.md memory files", - argumentHint: "", - }, - { - name: "review", - description: "Review a pull request", - argumentHint: "[pr-url]", - }, - { name: "status", description: "Show status information", argumentHint: "" }, -]; +const DEFAULT_COMMANDS: SlashCommand[] = []; let cachedCommands: SlashCommand[] | null = null; @@ -250,6 +221,7 @@ app.post("/", async (c) => { maxTurns: MAX_AGENT_TURNS, includePartialMessages: true, permissionMode: "bypassPermissions" as const, + settingSources: ["project", "user"], ...(binaryPath && { pathToClaudeCodeExecutable: binaryPath }), env: queryEnv, abortController, diff --git a/apps/streams/src/sdk-to-ai-chunks.ts b/apps/streams/src/sdk-to-ai-chunks.ts index 4434481fee2..2031e3bb216 100644 --- a/apps/streams/src/sdk-to-ai-chunks.ts +++ b/apps/streams/src/sdk-to-ai-chunks.ts @@ -48,6 +48,8 @@ interface SDKResultMessage { total_cost_usd: number; usage: { input_tokens: number; output_tokens: number }; session_id: string; + result?: string; + errors?: string[]; } interface SDKSystemMessage { @@ -162,11 +164,13 @@ function convertMessage( case "user": return handleUserMessage(message as SDKUserMessage); + case "assistant": + return handleAssistantMessage(state, message); + case "result": return handleResultMessage(state, message as SDKResultMessage); default: - // Skip system, assistant, status, hook, and other message types return []; } } @@ -399,6 +403,38 @@ function handleUserMessage(message: SDKUserMessage): StreamChunk[] { return chunks; } +// ============================================================================ +// Assistant Message Handler (non-streamed responses, e.g. slash command output) +// ============================================================================ + +function handleAssistantMessage( + state: ConversionState, + message: SDKMessage, +): StreamChunk[] { + const msg = message as { + type: "assistant"; + message?: { content?: Array<{ type: string; text?: string }> }; + }; + const content = msg.message?.content; + if (!content || !Array.isArray(content)) return []; + + const now = Date.now(); + const chunks: StreamChunk[] = []; + + for (const block of content) { + if (block.type === "text" && block.text) { + chunks.push({ + type: "TEXT_MESSAGE_CONTENT", + messageId: state.messageId, + delta: block.text, + timestamp: now, + } satisfies StreamChunk); + } + } + + return chunks; +} + // ============================================================================ // Result Message Handler // ============================================================================ @@ -411,11 +447,19 @@ function handleResultMessage( const chunks: StreamChunk[] = []; if (message.subtype?.startsWith("error")) { + const errorText = + message.errors?.join("\n") ?? `Claude agent error: ${message.subtype}`; + chunks.push({ + type: "TEXT_MESSAGE_CONTENT", + messageId: state.messageId, + delta: errorText, + timestamp: now, + } satisfies StreamChunk); chunks.push({ type: "RUN_ERROR", runId: state.runId, error: { - message: `Claude agent error: ${message.subtype}`, + message: errorText, code: message.subtype, }, timestamp: now, @@ -423,6 +467,15 @@ function handleResultMessage( return chunks; } + if (message.result) { + chunks.push({ + type: "TEXT_MESSAGE_CONTENT", + messageId: state.messageId, + delta: message.result, + timestamp: now, + } satisfies StreamChunk); + } + const finishReason = message.stop_reason === "end_turn" || message.stop_reason === "stop_sequence" From 7826260adc70f5b5b951c21add0d09a7c25d3086 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 8 Feb 2026 16:08:10 -0800 Subject: [PATCH 08/11] Remove speculative change --- .../src/lib/trpc/routers/ai-chat/index.ts | 26 ++---------------- apps/streams/src/claude-agent.ts | 27 ------------------- apps/streams/src/sdk-to-ai-chunks.ts | 21 +-------------- 3 files changed, 3 insertions(+), 71 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts b/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts index f6f36a42b77..d39619d52e0 100644 --- a/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts +++ b/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts @@ -14,9 +14,6 @@ import { sessionStore, } from "./utils/session-manager"; -const CLAUDE_AGENT_URL = - process.env.CLAUDE_AGENT_URL || "http://localhost:9090"; - interface CommandEntry { name: string; description: string; @@ -67,27 +64,8 @@ export const createAiChatRouter = () => { getSlashCommands: publicProcedure .input(z.object({ cwd: z.string() })) - .query(async ({ input }) => { - const customCommands = scanCustomCommands(input.cwd); - - let sdkCommands: CommandEntry[] = []; - try { - const res = await fetch(`${CLAUDE_AGENT_URL}/commands`); - if (res.ok) { - const data = (await res.json()) as { - commands: CommandEntry[]; - }; - sdkCommands = data.commands ?? []; - } - } catch {} - - const seen = new Set(sdkCommands.map((c) => c.name)); - return { - commands: [ - ...sdkCommands, - ...customCommands.filter((c) => !seen.has(c.name)), - ], - }; + .query(({ input }) => { + return { commands: scanCustomCommands(input.cwd) }; }), startSession: publicProcedure diff --git a/apps/streams/src/claude-agent.ts b/apps/streams/src/claude-agent.ts index 356e33dc799..2e287ce45d3 100644 --- a/apps/streams/src/claude-agent.ts +++ b/apps/streams/src/claude-agent.ts @@ -52,16 +52,6 @@ const answerBodySchema = z.object({ originalInput: z.record(z.string(), z.unknown()).optional(), }); -interface SlashCommand { - name: string; - description: string; - argumentHint: string; -} - -const DEFAULT_COMMANDS: SlashCommand[] = []; - -let cachedCommands: SlashCommand[] | null = null; - const app = new Hono(); app.post("/", async (c) => { @@ -198,19 +188,6 @@ app.post("/", async (c) => { }, }); - if (!cachedCommands) { - result - .supportedCommands() - .then((cmds) => { - cachedCommands = cmds.map((cmd) => ({ - name: cmd.name ?? "", - description: cmd.description ?? "", - argumentHint: cmd.argumentHint ?? "", - })); - }) - .catch(() => {}); - } - const abortHandler = () => { abortController.abort(); result.interrupt().catch(() => {}); @@ -380,10 +357,6 @@ app.get("/sessions/:sessionId", (c) => { return c.json({ claudeSessionId }); }); -app.get("/commands", (c) => { - return c.json({ commands: cachedCommands ?? DEFAULT_COMMANDS }); -}); - app.get("/health", (c) => { return c.json({ status: "ok", diff --git a/apps/streams/src/sdk-to-ai-chunks.ts b/apps/streams/src/sdk-to-ai-chunks.ts index 2031e3bb216..b2d52c27e89 100644 --- a/apps/streams/src/sdk-to-ai-chunks.ts +++ b/apps/streams/src/sdk-to-ai-chunks.ts @@ -48,8 +48,6 @@ interface SDKResultMessage { total_cost_usd: number; usage: { input_tokens: number; output_tokens: number }; session_id: string; - result?: string; - errors?: string[]; } interface SDKSystemMessage { @@ -447,19 +445,11 @@ function handleResultMessage( const chunks: StreamChunk[] = []; if (message.subtype?.startsWith("error")) { - const errorText = - message.errors?.join("\n") ?? `Claude agent error: ${message.subtype}`; - chunks.push({ - type: "TEXT_MESSAGE_CONTENT", - messageId: state.messageId, - delta: errorText, - timestamp: now, - } satisfies StreamChunk); chunks.push({ type: "RUN_ERROR", runId: state.runId, error: { - message: errorText, + message: `Claude agent error: ${message.subtype}`, code: message.subtype, }, timestamp: now, @@ -467,15 +457,6 @@ function handleResultMessage( return chunks; } - if (message.result) { - chunks.push({ - type: "TEXT_MESSAGE_CONTENT", - messageId: state.messageId, - delta: message.result, - timestamp: now, - } satisfies StreamChunk); - } - const finishReason = message.stop_reason === "end_turn" || message.stop_reason === "stop_sequence" From 708fa41019967684444b65513f375c8699ead3c6 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 8 Feb 2026 16:24:24 -0800 Subject: [PATCH 09/11] fix(streams,desktop): make SDK errors visible in chat and remove dead /clear case Emit a TEXT_MESSAGE_CONTENT chunk before RUN_ERROR so error messages appear in the chat bubble (RUN_ERROR alone is invisible in the materialization pipeline). Also remove the dead /clear branch from resolveCommandAction and its associated onClear plumbing. --- .../TabView/ChatPane/ChatInterface/ChatInterface.tsx | 4 ---- .../components/SlashCommandInput/SlashCommandInput.tsx | 8 ++------ .../hooks/useSlashCommands/useSlashCommands.ts | 8 ++------ apps/streams/src/sdk-to-ai-chunks.ts | 6 ++++++ 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx index f102609772a..2f51b7547cf 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx @@ -278,9 +278,6 @@ export function ChatInterface({ [handleSend], ); - // TODO: Wire to actual session/conversation reset - const handleClear = useCallback(() => {}, []); - return (
{connectionStatus !== "connected" && @@ -329,7 +326,6 @@ export function ChatInterface({ diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandInput/SlashCommandInput.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandInput/SlashCommandInput.tsx index 4c063572ad5..54770f49722 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandInput/SlashCommandInput.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/SlashCommandInput/SlashCommandInput.tsx @@ -9,14 +9,12 @@ import { import { SlashCommandMenu } from "../SlashCommandMenu"; interface SlashCommandInputProps { - onClear: () => void; onCommandSend: (command: SlashCommand) => void; cwd: string; children: React.ReactNode; } export function SlashCommandInput({ - onClear, onCommandSend, cwd, children, @@ -31,14 +29,12 @@ export function SlashCommandInput({ const executeCommand = useCallback( (command: SlashCommand) => { const action = resolveCommandAction(command); - if (action.isClear) { - onClear(); - } else if (action.shouldSend) { + if (action.shouldSend) { onCommandSend(command); } textInput.setInput(action.text); }, - [onClear, onCommandSend, textInput], + [onCommandSend, textInput], ); const handleKeyDownCapture = useCallback( diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts index a8b5ed29a1c..f0c1a077401 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/hooks/useSlashCommands/useSlashCommands.ts @@ -83,14 +83,10 @@ export function useSlashCommands({ export function resolveCommandAction(command: SlashCommand): { text: string; - isClear: boolean; shouldSend: boolean; } { - if (command.name === "clear") { - return { text: "", isClear: true, shouldSend: false }; - } if (command.argumentHint) { - return { text: `/${command.name} `, isClear: false, shouldSend: false }; + return { text: `/${command.name} `, shouldSend: false }; } - return { text: "", isClear: false, shouldSend: true }; + return { text: "", shouldSend: true }; } diff --git a/apps/streams/src/sdk-to-ai-chunks.ts b/apps/streams/src/sdk-to-ai-chunks.ts index b2d52c27e89..1ec7d333bf3 100644 --- a/apps/streams/src/sdk-to-ai-chunks.ts +++ b/apps/streams/src/sdk-to-ai-chunks.ts @@ -445,6 +445,12 @@ function handleResultMessage( const chunks: StreamChunk[] = []; if (message.subtype?.startsWith("error")) { + chunks.push({ + type: "TEXT_MESSAGE_CONTENT", + messageId: state.messageId, + delta: `Error: ${message.subtype}`, + timestamp: now, + } satisfies StreamChunk); chunks.push({ type: "RUN_ERROR", runId: state.runId, From 19bb8844c1634653a6e79bbb27331495398082ee Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 8 Feb 2026 16:33:38 -0800 Subject: [PATCH 10/11] fix(desktop): log filesystem errors in scanCustomCommands instead of swallowing --- apps/desktop/src/lib/trpc/routers/ai-chat/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts b/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts index d39619d52e0..2e7bb7cf5ea 100644 --- a/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts +++ b/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts @@ -46,7 +46,9 @@ function scanCustomCommands(cwd: string): CommandEntry[] { argumentHint: argMatch?.[1]?.trim() ?? "", }); } - } catch {} + } catch (err) { + console.warn(`[ai-chat/scanCustomCommands] Failed to read commands from ${dir}:`, err); + } } return commands; From 051ec3f4dce6ef934cd3198c0c227ca838fd4931 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 8 Feb 2026 16:59:57 -0800 Subject: [PATCH 11/11] Lint --- apps/desktop/src/lib/trpc/routers/ai-chat/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts b/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts index 2e7bb7cf5ea..2d6acb51a9d 100644 --- a/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts +++ b/apps/desktop/src/lib/trpc/routers/ai-chat/index.ts @@ -47,7 +47,10 @@ function scanCustomCommands(cwd: string): CommandEntry[] { }); } } catch (err) { - console.warn(`[ai-chat/scanCustomCommands] Failed to read commands from ${dir}:`, err); + console.warn( + `[ai-chat/scanCustomCommands] Failed to read commands from ${dir}:`, + err, + ); } }