scope custom commands
This commit is contained in:
325
src/App.tsx
325
src/App.tsx
@@ -4,7 +4,7 @@ import { Toaster } from "solid-toast"
|
|||||||
import type { Session } from "./types/session"
|
import type { Session } from "./types/session"
|
||||||
import type { Attachment } from "./types/attachment"
|
import type { Attachment } from "./types/attachment"
|
||||||
import type { SDKPart, ClientPart } from "./types/message"
|
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 FolderSelectionView from "./components/folder-selection-view"
|
||||||
import InstanceWelcomeView from "./components/instance-welcome-view"
|
import InstanceWelcomeView from "./components/instance-welcome-view"
|
||||||
import CommandPalette from "./components/command-palette"
|
import CommandPalette from "./components/command-palette"
|
||||||
@@ -65,10 +65,12 @@ import {
|
|||||||
getSessionInfo,
|
getSessionInfo,
|
||||||
isSessionMessagesLoading,
|
isSessionMessagesLoading,
|
||||||
fetchSessions,
|
fetchSessions,
|
||||||
|
executeCustomCommand,
|
||||||
} from "./stores/sessions"
|
} from "./stores/sessions"
|
||||||
import { isSessionBusy } from "./stores/session-status"
|
import { isSessionBusy } from "./stores/session-status"
|
||||||
import { setupTabKeyboardShortcuts } from "./lib/keyboard"
|
import { setupTabKeyboardShortcuts } from "./lib/keyboard"
|
||||||
import { isOpen as isCommandPaletteOpen, showCommandPalette, hideCommandPalette } from "./stores/command-palette"
|
import { isOpen as isCommandPaletteOpen, showCommandPalette, hideCommandPalette } from "./stores/command-palette"
|
||||||
|
import { getCommands as getInstanceCommands } from "./stores/commands"
|
||||||
import { registerNavigationShortcuts } from "./lib/shortcuts/navigation"
|
import { registerNavigationShortcuts } from "./lib/shortcuts/navigation"
|
||||||
import { registerInputShortcuts } from "./lib/shortcuts/input"
|
import { registerInputShortcuts } from "./lib/shortcuts/input"
|
||||||
import { registerAgentShortcuts } from "./lib/shortcuts/agent"
|
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({
|
commandRegistry.register({
|
||||||
id: "clear-input",
|
id: "clear-input",
|
||||||
label: "Clear Input",
|
label: "Clear Input",
|
||||||
@@ -936,8 +901,17 @@ const App: Component = () => {
|
|||||||
refreshCommandPalette()
|
refreshCommandPalette()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleExecuteCommand(commandId: string) {
|
function handleExecuteCommand(command: Command) {
|
||||||
commandRegistry.execute(commandId)
|
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,
|
handleCloseInstance,
|
||||||
handleNewSession,
|
handleNewSession,
|
||||||
handleCloseSession,
|
handleCloseSession,
|
||||||
showCommandPalette,
|
() => {
|
||||||
|
const instance = activeInstance()
|
||||||
|
if (instance) {
|
||||||
|
showCommandPalette(instance.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
registerNavigationShortcuts()
|
registerNavigationShortcuts()
|
||||||
@@ -1039,7 +1018,7 @@ const App: Component = () => {
|
|||||||
const active = document.activeElement as HTMLElement
|
const active = document.activeElement as HTMLElement
|
||||||
active?.blur()
|
active?.blur()
|
||||||
},
|
},
|
||||||
hideCommandPalette,
|
() => hideCommandPalette(),
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
@@ -1147,104 +1126,122 @@ const App: Component = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Show when={activeInstance()} keyed>
|
<Show when={activeInstance()} keyed>
|
||||||
{(instance) => (
|
{(instance) => {
|
||||||
<>
|
const customCommands = createMemo(() =>
|
||||||
<Show when={activeSessions().size > 0} fallback={<InstanceWelcomeView instance={instance} />}>
|
buildCustomCommandEntries(instance.id, getInstanceCommands(instance.id)),
|
||||||
<div class="flex flex-1 min-h-0">
|
)
|
||||||
{/* Session Sidebar */}
|
const instancePaletteCommands = createMemo(() => [
|
||||||
<div
|
...paletteCommands(),
|
||||||
class="session-sidebar flex flex-col bg-surface-secondary"
|
...customCommands(),
|
||||||
style={{ width: `${sessionSidebarWidth()}px` }}
|
])
|
||||||
>
|
const paletteOpen = createMemo(() => isCommandPaletteOpen(instance.id))
|
||||||
<SessionList
|
|
||||||
instanceId={instance.id}
|
|
||||||
sessions={activeSessions()}
|
|
||||||
activeSessionId={activeSessionIdForInstance()}
|
|
||||||
onSelect={(id) => setActiveSession(instance.id, id)}
|
|
||||||
onClose={(id) => handleCloseSession(instance.id, id)}
|
|
||||||
onNew={() => handleNewSession(instance.id)}
|
|
||||||
showHeader
|
|
||||||
showFooter={false}
|
|
||||||
headerContent={
|
|
||||||
<div class="session-sidebar-header">
|
|
||||||
<span class="session-sidebar-title text-sm font-semibold uppercase text-primary">Sessions</span>
|
|
||||||
<div class="session-sidebar-shortcuts">
|
|
||||||
{(() => {
|
|
||||||
const shortcuts = [
|
|
||||||
keyboardRegistry.get("session-prev"),
|
|
||||||
keyboardRegistry.get("session-next"),
|
|
||||||
].filter((shortcut): shortcut is KeyboardShortcut => Boolean(shortcut))
|
|
||||||
return shortcuts.length ? (
|
|
||||||
<KeyboardHint shortcuts={shortcuts} separator=" " showDescription={false} />
|
|
||||||
) : null
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
onWidthChange={setSessionSidebarWidth}
|
return (
|
||||||
/>
|
<>
|
||||||
|
<Show when={activeSessions().size > 0} fallback={<InstanceWelcomeView instance={instance} />}>
|
||||||
<div class="session-sidebar-separator border-t border-base" />
|
<div class="flex flex-1 min-h-0">
|
||||||
<Show when={activeSessionForInstance()}>
|
{/* Session Sidebar */}
|
||||||
{(activeSession) => (
|
<div
|
||||||
<>
|
class="session-sidebar flex flex-col bg-surface-secondary"
|
||||||
<ContextUsagePanel instanceId={instance.id} sessionId={activeSession().id} />
|
style={{ width: `${sessionSidebarWidth()}px` }}
|
||||||
<div class="session-sidebar-controls px-3 py-3 border-r border-base flex flex-col gap-3">
|
|
||||||
<AgentSelector
|
|
||||||
instanceId={instance.id}
|
|
||||||
sessionId={activeSession().id}
|
|
||||||
currentAgent={activeSession().agent}
|
|
||||||
onAgentChange={handleSidebarAgentChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ModelSelector
|
|
||||||
instanceId={instance.id}
|
|
||||||
sessionId={activeSession().id}
|
|
||||||
currentModel={activeSession().model}
|
|
||||||
onModelChange={handleSidebarModelChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<div class="content-area flex-1 min-h-0 overflow-hidden flex flex-col">
|
|
||||||
<Show
|
|
||||||
when={activeSessionIdForInstance() === "info"}
|
|
||||||
fallback={
|
|
||||||
<Show
|
|
||||||
when={activeSessionIdForInstance()}
|
|
||||||
keyed
|
|
||||||
fallback={
|
|
||||||
<div class="flex items-center justify-center h-full">
|
|
||||||
<div class="text-center text-gray-500 dark:text-gray-400">
|
|
||||||
<p class="mb-2">No session selected</p>
|
|
||||||
<p class="text-sm">Select a session to view messages</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(sessionId) => (
|
|
||||||
<SessionView
|
|
||||||
sessionId={sessionId}
|
|
||||||
activeSessions={activeSessions()}
|
|
||||||
instanceId={instance.id}
|
|
||||||
instanceFolder={instance.folder}
|
|
||||||
escapeInDebounce={escapeInDebounce()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<InfoView instanceId={instance.id} />
|
<SessionList
|
||||||
</Show>
|
instanceId={instance.id}
|
||||||
|
sessions={activeSessions()}
|
||||||
|
activeSessionId={activeSessionIdForInstance()}
|
||||||
|
onSelect={(id) => setActiveSession(instance.id, id)}
|
||||||
|
onClose={(id) => handleCloseSession(instance.id, id)}
|
||||||
|
onNew={() => handleNewSession(instance.id)}
|
||||||
|
showHeader
|
||||||
|
showFooter={false}
|
||||||
|
headerContent={
|
||||||
|
<div class="session-sidebar-header">
|
||||||
|
<span class="session-sidebar-title text-sm font-semibold uppercase text-primary">Sessions</span>
|
||||||
|
<div class="session-sidebar-shortcuts">
|
||||||
|
{(() => {
|
||||||
|
const shortcuts = [
|
||||||
|
keyboardRegistry.get("session-prev"),
|
||||||
|
keyboardRegistry.get("session-next"),
|
||||||
|
].filter((shortcut): shortcut is KeyboardShortcut => Boolean(shortcut))
|
||||||
|
return shortcuts.length ? (
|
||||||
|
<KeyboardHint shortcuts={shortcuts} separator=" " showDescription={false} />
|
||||||
|
) : null
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
onWidthChange={setSessionSidebarWidth}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="session-sidebar-separator border-t border-base" />
|
||||||
|
<Show when={activeSessionForInstance()}>
|
||||||
|
{(activeSession) => (
|
||||||
|
<>
|
||||||
|
<ContextUsagePanel instanceId={instance.id} sessionId={activeSession().id} />
|
||||||
|
<div class="session-sidebar-controls px-3 py-3 border-r border-base flex flex-col gap-3">
|
||||||
|
<AgentSelector
|
||||||
|
instanceId={instance.id}
|
||||||
|
sessionId={activeSession().id}
|
||||||
|
currentAgent={activeSession().agent}
|
||||||
|
onAgentChange={handleSidebarAgentChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModelSelector
|
||||||
|
instanceId={instance.id}
|
||||||
|
sessionId={activeSession().id}
|
||||||
|
currentModel={activeSession().model}
|
||||||
|
onModelChange={handleSidebarModelChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
{/* Main Content Area */}
|
||||||
|
<div class="content-area flex-1 min-h-0 overflow-hidden flex flex-col">
|
||||||
|
<Show
|
||||||
|
when={activeSessionIdForInstance() === "info"}
|
||||||
|
fallback={
|
||||||
|
<Show
|
||||||
|
when={activeSessionIdForInstance()}
|
||||||
|
keyed
|
||||||
|
fallback={
|
||||||
|
<div class="flex items-center justify-center h-full">
|
||||||
|
<div class="text-center text-gray-500 dark:text-gray-400">
|
||||||
|
<p class="mb-2">No session selected</p>
|
||||||
|
<p class="text-sm">Select a session to view messages</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(sessionId) => (
|
||||||
|
<SessionView
|
||||||
|
sessionId={sessionId}
|
||||||
|
activeSessions={activeSessions()}
|
||||||
|
instanceId={instance.id}
|
||||||
|
instanceFolder={instance.folder}
|
||||||
|
escapeInDebounce={escapeInDebounce()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<InfoView instanceId={instance.id} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
</Show>
|
|
||||||
</>
|
<CommandPalette
|
||||||
)}
|
open={paletteOpen()}
|
||||||
|
onClose={() => hideCommandPalette(instance.id)}
|
||||||
|
commands={instancePaletteCommands()}
|
||||||
|
onExecute={handleExecuteCommand}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -1258,12 +1255,6 @@ const App: Component = () => {
|
|||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<CommandPalette
|
|
||||||
open={isCommandPaletteOpen()}
|
|
||||||
onClose={hideCommandPalette}
|
|
||||||
commands={paletteCommands()}
|
|
||||||
onExecute={handleExecuteCommand}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Show when={showFolderSelection()}>
|
<Show when={showFolderSelection()}>
|
||||||
<div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
|
<div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
|
||||||
@@ -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
|
export default App
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface CommandPaletteProps {
|
|||||||
open: boolean
|
open: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
commands: Command[]
|
commands: Command[]
|
||||||
onExecute: (commandId: string) => void
|
onExecute: (command: Command) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildShortcutString(shortcut: Command["shortcut"]): string {
|
function buildShortcutString(shortcut: Command["shortcut"]): string {
|
||||||
@@ -30,7 +30,7 @@ const CommandPalette: Component<CommandPaletteProps> = (props) => {
|
|||||||
let inputRef: HTMLInputElement | undefined
|
let inputRef: HTMLInputElement | undefined
|
||||||
let listRef: HTMLDivElement | 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 CommandGroup = { category: string; commands: Command[]; startIndex: number }
|
||||||
type ProcessedCommands = { groups: CommandGroup[]; ordered: Command[] }
|
type ProcessedCommands = { groups: CommandGroup[]; ordered: Command[] }
|
||||||
@@ -167,13 +167,15 @@ const CommandPalette: Component<CommandPaletteProps> = (props) => {
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
const index = selectedIndex()
|
const index = selectedIndex()
|
||||||
if (index < 0 || index >= ordered.length) return
|
if (index < 0 || index >= ordered.length) return
|
||||||
props.onExecute(ordered[index].id)
|
const command = ordered[index]
|
||||||
|
if (!command) return
|
||||||
|
props.onExecute(command)
|
||||||
props.onClose()
|
props.onClose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCommandClick(commandId: string) {
|
function handleCommandClick(command: Command) {
|
||||||
props.onExecute(commandId)
|
props.onExecute(command)
|
||||||
props.onClose()
|
props.onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +243,7 @@ const CommandPalette: Component<CommandPaletteProps> = (props) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-command-index={commandIndex}
|
data-command-index={commandIndex}
|
||||||
onClick={() => handleCommandClick(command.id)}
|
onClick={() => handleCommandClick(command)}
|
||||||
class={`modal-item ${selectedCommandId() === command.id ? "modal-item-highlight" : ""}`}
|
class={`modal-item ${selectedCommandId() === command.id ? "modal-item-highlight" : ""}`}
|
||||||
onPointerMove={(event) => {
|
onPointerMove={(event) => {
|
||||||
if (event.movementX === 0 && event.movementY === 0) return
|
if (event.movementX === 0 && event.movementY === 0) return
|
||||||
|
|||||||
@@ -1,17 +1,36 @@
|
|||||||
import { createSignal } from "solid-js"
|
import { createSignal } from "solid-js"
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = createSignal(false)
|
const [openStates, setOpenStates] = createSignal<Map<string, boolean>>(new Map())
|
||||||
|
|
||||||
export function showCommandPalette() {
|
function updateState(instanceId: string, open: boolean) {
|
||||||
setIsOpen(true)
|
setOpenStates((prev) => {
|
||||||
|
const next = new Map(prev)
|
||||||
|
next.set(instanceId, open)
|
||||||
|
return next
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hideCommandPalette() {
|
export function showCommandPalette(instanceId: string) {
|
||||||
setIsOpen(false)
|
if (!instanceId) return
|
||||||
|
updateState(instanceId, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleCommandPalette() {
|
export function hideCommandPalette(instanceId?: string) {
|
||||||
setIsOpen(!isOpen())
|
if (!instanceId) {
|
||||||
|
setOpenStates(new Map())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateState(instanceId, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { isOpen }
|
export function toggleCommandPalette(instanceId: string) {
|
||||||
|
if (!instanceId) return
|
||||||
|
const current = openStates().get(instanceId) ?? false
|
||||||
|
updateState(instanceId, !current)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOpen(instanceId: string): boolean {
|
||||||
|
return openStates().get(instanceId) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
export { openStates }
|
||||||
|
|||||||
30
src/stores/commands.ts
Normal file
30
src/stores/commands.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { createSignal } from "solid-js"
|
||||||
|
import type { Command as SDKCommand } from "@opencode-ai/sdk"
|
||||||
|
import type { OpencodeClient } from "@opencode-ai/sdk/client"
|
||||||
|
|
||||||
|
const [commandMap, setCommandMap] = createSignal<Map<string, SDKCommand.Info[]>>(new Map())
|
||||||
|
|
||||||
|
export async function fetchCommands(instanceId: string, client: OpencodeClient): Promise<void> {
|
||||||
|
const response = await client.command.list()
|
||||||
|
const commands = response.data ?? []
|
||||||
|
setCommandMap((prev) => {
|
||||||
|
const next = new Map(prev)
|
||||||
|
next.set(instanceId, commands)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCommands(instanceId: string): SDKCommand.Info[] {
|
||||||
|
return commandMap().get(instanceId) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCommands(instanceId: string): void {
|
||||||
|
setCommandMap((prev) => {
|
||||||
|
if (!prev.has(instanceId)) return prev
|
||||||
|
const next = new Map(prev)
|
||||||
|
next.delete(instanceId)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export { commandMap as commands }
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
removeSessionIndexes,
|
removeSessionIndexes,
|
||||||
clearInstanceDraftPrompts,
|
clearInstanceDraftPrompts,
|
||||||
} from "./sessions"
|
} from "./sessions"
|
||||||
|
import { fetchCommands, clearCommands } from "./commands"
|
||||||
import { preferences, updateLastUsedBinary } from "./preferences"
|
import { preferences, updateLastUsedBinary } from "./preferences"
|
||||||
import { setHasInstances } from "./ui"
|
import { setHasInstances } from "./ui"
|
||||||
|
|
||||||
@@ -139,6 +140,7 @@ function removeInstance(id: string) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
removeLogContainer(id)
|
removeLogContainer(id)
|
||||||
|
clearCommands(id)
|
||||||
|
|
||||||
if (activeInstanceId() === id) {
|
if (activeInstanceId() === id) {
|
||||||
setActiveInstanceId(nextActiveId)
|
setActiveInstanceId(nextActiveId)
|
||||||
@@ -194,6 +196,7 @@ async function createInstance(folder: string, binaryPath?: string): Promise<stri
|
|||||||
await fetchSessions(id)
|
await fetchSessions(id)
|
||||||
await fetchAgents(id)
|
await fetchAgents(id)
|
||||||
await fetchProviders(id)
|
await fetchProviders(id)
|
||||||
|
await fetchCommands(id, client)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch initial data:", error)
|
console.error("Failed to fetch initial data:", error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2029,6 +2029,48 @@ async function sendMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function executeCustomCommand(
|
||||||
|
instanceId: string,
|
||||||
|
sessionId: string,
|
||||||
|
commandName: string,
|
||||||
|
args: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const instance = instances().get(instanceId)
|
||||||
|
if (!instance || !instance.client) {
|
||||||
|
throw new Error("Instance not ready")
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = sessions().get(instanceId)?.get(sessionId)
|
||||||
|
if (!session) {
|
||||||
|
throw new Error("Session not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
const body: {
|
||||||
|
command: string
|
||||||
|
arguments: string
|
||||||
|
messageID: string
|
||||||
|
agent?: string
|
||||||
|
model?: string
|
||||||
|
} = {
|
||||||
|
command: commandName,
|
||||||
|
arguments: args,
|
||||||
|
messageID: createId("msg"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.agent) {
|
||||||
|
body.agent = session.agent
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.model.providerId && session.model.modelId) {
|
||||||
|
body.model = `${session.model.providerId}/${session.model.modelId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
await instance.client.session.command({
|
||||||
|
path: { id: sessionId },
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function abortSession(instanceId: string, sessionId: string): Promise<void> {
|
async function abortSession(instanceId: string, sessionId: string): Promise<void> {
|
||||||
const instance = instances().get(instanceId)
|
const instance = instances().get(instanceId)
|
||||||
if (!instance || !instance.client) {
|
if (!instance || !instance.client) {
|
||||||
@@ -2266,4 +2308,5 @@ export {
|
|||||||
setSessionDraftPrompt,
|
setSessionDraftPrompt,
|
||||||
clearSessionDraftPrompt,
|
clearSessionDraftPrompt,
|
||||||
clearInstanceDraftPrompts,
|
clearInstanceDraftPrompts,
|
||||||
|
executeCustomCommand,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user