diff --git a/src/App.tsx b/src/App.tsx index 9d2e5408..e8707d15 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,7 @@ import { Toaster } from "solid-toast" import type { Session } from "./types/session" import type { Attachment } from "./types/attachment" import type { SDKPart, ClientPart } from "./types/message" -import type { Permission } from "@opencode-ai/sdk" +import type { Permission, Command as SDKCommand } from "@opencode-ai/sdk" import FolderSelectionView from "./components/folder-selection-view" import InstanceWelcomeView from "./components/instance-welcome-view" import CommandPalette from "./components/command-palette" @@ -65,10 +65,12 @@ import { getSessionInfo, isSessionMessagesLoading, fetchSessions, + executeCustomCommand, } from "./stores/sessions" import { isSessionBusy } from "./stores/session-status" import { setupTabKeyboardShortcuts } from "./lib/keyboard" import { isOpen as isCommandPaletteOpen, showCommandPalette, hideCommandPalette } from "./stores/command-palette" +import { getCommands as getInstanceCommands } from "./stores/commands" import { registerNavigationShortcuts } from "./lib/shortcuts/navigation" import { registerInputShortcuts } from "./lib/shortcuts/input" import { registerAgentShortcuts } from "./lib/shortcuts/agent" @@ -845,43 +847,6 @@ const App: Component = () => { }, }) - commandRegistry.register({ - id: "init", - label: "Initialize AGENTS.md", - description: "Create or update AGENTS.md file", - category: "Agent & Model", - keywords: ["/init", "agents", "initialize"], - action: async () => { - const instance = activeInstance() - const sessionId = activeSessionIdForInstance() - if (!instance || !instance.client || !sessionId || sessionId === "info") return - - const sessions = getSessions(instance.id) - const session = sessions.find((s) => s.id === sessionId) - if (!session) return - - try { - // Generate ID similar to server format: timestamp in hex + random chars - const timestamp = Date.now() - const timePart = (timestamp * 0x1000).toString(16).padStart(12, "0") - const randomPart = Math.random().toString(16).substring(2, 16) - const messageID = `msg_${timePart}${randomPart}` - - await instance.client.session.init({ - path: { id: sessionId }, - body: { - messageID, - providerID: session.model.providerId, - modelID: session.model.modelId, - }, - }) - console.log("Initializing AGENTS.md...") - } catch (error) { - console.error("Failed to initialize AGENTS.md:", error) - } - }, - }) - commandRegistry.register({ id: "clear-input", label: "Clear Input", @@ -936,8 +901,17 @@ const App: Component = () => { refreshCommandPalette() } - function handleExecuteCommand(commandId: string) { - commandRegistry.execute(commandId) + function handleExecuteCommand(command: Command) { + try { + const result = command.action?.() + if (result instanceof Promise) { + void result.catch((error) => { + console.error("Command execution failed:", error) + }) + } + } catch (error) { + console.error("Command execution failed:", error) + } } @@ -952,7 +926,12 @@ const App: Component = () => { handleCloseInstance, handleNewSession, handleCloseSession, - showCommandPalette, + () => { + const instance = activeInstance() + if (instance) { + showCommandPalette(instance.id) + } + }, ) registerNavigationShortcuts() @@ -1039,7 +1018,7 @@ const App: Component = () => { const active = document.activeElement as HTMLElement active?.blur() }, - hideCommandPalette, + () => hideCommandPalette(), ) const handleKeyDown = (e: KeyboardEvent) => { @@ -1147,104 +1126,122 @@ const App: Component = () => { /> - {(instance) => ( - <> - 0} fallback={}> -
- {/* Session Sidebar */} -
- setActiveSession(instance.id, id)} - onClose={(id) => handleCloseSession(instance.id, id)} - onNew={() => handleNewSession(instance.id)} - showHeader - showFooter={false} - headerContent={ -
- Sessions -
- {(() => { - const shortcuts = [ - keyboardRegistry.get("session-prev"), - keyboardRegistry.get("session-next"), - ].filter((shortcut): shortcut is KeyboardShortcut => Boolean(shortcut)) - return shortcuts.length ? ( - - ) : null - })()} -
-
- } + {(instance) => { + const customCommands = createMemo(() => + buildCustomCommandEntries(instance.id, getInstanceCommands(instance.id)), + ) + const instancePaletteCommands = createMemo(() => [ + ...paletteCommands(), + ...customCommands(), + ]) + const paletteOpen = createMemo(() => isCommandPaletteOpen(instance.id)) - onWidthChange={setSessionSidebarWidth} - /> - -
- - {(activeSession) => ( - <> - -
- - - -
- - )} -
-
- {/* Main Content Area */} -
- -
-

No session selected

-

Select a session to view messages

-
-
- } - > - {(sessionId) => ( - - )} - - } + return ( + <> + 0} fallback={}> +
+ {/* Session Sidebar */} +
- - + setActiveSession(instance.id, id)} + onClose={(id) => handleCloseSession(instance.id, id)} + onNew={() => handleNewSession(instance.id)} + showHeader + showFooter={false} + headerContent={ +
+ Sessions +
+ {(() => { + const shortcuts = [ + keyboardRegistry.get("session-prev"), + keyboardRegistry.get("session-next"), + ].filter((shortcut): shortcut is KeyboardShortcut => Boolean(shortcut)) + return shortcuts.length ? ( + + ) : null + })()} +
+
+ } + + onWidthChange={setSessionSidebarWidth} + /> + +
+ + {(activeSession) => ( + <> + +
+ + + +
+ + )} +
+
+ {/* Main Content Area */} +
+ +
+

No session selected

+

Select a session to view messages

+
+
+ } + > + {(sessionId) => ( + + )} + + } + > + + +
-
- - - )} + + + hideCommandPalette(instance.id)} + commands={instancePaletteCommands()} + onExecute={handleExecuteCommand} + /> + + ) + }} } @@ -1258,12 +1255,6 @@ const App: Component = () => { /> -
@@ -1310,4 +1301,52 @@ const App: Component = () => { ) } +function commandRequiresArguments(template?: string) { + if (!template) return false + return /\$(?:\d+|ARGUMENTS)/.test(template) +} + +function promptForCommandArguments(command: SDKCommand.Info) { + if (!commandRequiresArguments(command.template)) { + return "" + } + const input = window.prompt(`Arguments for /${command.name}`, "") + if (input === null) { + return null + } + return input +} + +function formatCommandLabel(name: string) { + if (!name) return "" + return name.charAt(0).toUpperCase() + name.slice(1) +} + +function buildCustomCommandEntries(instanceId: string, commands: SDKCommand.Info[]): Command[] { + return commands.map((cmd) => ({ + id: `custom:${instanceId}:${cmd.name}`, + label: formatCommandLabel(cmd.name), + description: cmd.description ?? "Custom command", + category: "Custom Commands", + keywords: [cmd.name, ...(cmd.description ? cmd.description.split(/\s+/).filter(Boolean) : [])], + action: async () => { + const sessionId = activeSessionId().get(instanceId) + if (!sessionId || sessionId === "info") { + alert("Select a session before running a custom command.") + return + } + const args = promptForCommandArguments(cmd) + if (args === null) { + return + } + try { + await executeCustomCommand(instanceId, sessionId, cmd.name, args) + } catch (error) { + console.error("Failed to run custom command:", error) + alert("Failed to run custom command. Check the console for details.") + } + }, + })) +} + export default App diff --git a/src/components/command-palette.tsx b/src/components/command-palette.tsx index 9ed8c2b1..3d394f91 100644 --- a/src/components/command-palette.tsx +++ b/src/components/command-palette.tsx @@ -7,7 +7,7 @@ interface CommandPaletteProps { open: boolean onClose: () => void commands: Command[] - onExecute: (commandId: string) => void + onExecute: (command: Command) => void } function buildShortcutString(shortcut: Command["shortcut"]): string { @@ -30,7 +30,7 @@ const CommandPalette: Component = (props) => { let inputRef: HTMLInputElement | undefined let listRef: HTMLDivElement | undefined - const categoryOrder = ["Instance", "Session", "Agent & Model", "Input & Focus", "System", "Other"] as const + const categoryOrder = ["Custom Commands", "Instance", "Session", "Agent & Model", "Input & Focus", "System", "Other"] as const type CommandGroup = { category: string; commands: Command[]; startIndex: number } type ProcessedCommands = { groups: CommandGroup[]; ordered: Command[] } @@ -167,13 +167,15 @@ const CommandPalette: Component = (props) => { e.stopPropagation() const index = selectedIndex() if (index < 0 || index >= ordered.length) return - props.onExecute(ordered[index].id) + const command = ordered[index] + if (!command) return + props.onExecute(command) props.onClose() } } - function handleCommandClick(commandId: string) { - props.onExecute(commandId) + function handleCommandClick(command: Command) { + props.onExecute(command) props.onClose() } @@ -241,7 +243,7 @@ const CommandPalette: Component = (props) => {