diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 8a91b6e85..165845564 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -10,18 +10,15 @@ import { createAutoScroll } from "@opencode-ai/ui/hooks" import { Mark } from "@opencode-ai/ui/logo" import { useSync } from "@/context/sync" -import { useTerminal } from "@/context/terminal" import { useLayout } from "@/context/layout" import { checksum, base64Encode } from "@opencode-ai/util/encode" import { useDialog } from "@opencode-ai/ui/context/dialog" -import { useCommand } from "@/context/command" import { useLanguage } from "@/context/language" import { useNavigate, useParams } from "@solidjs/router" import { UserMessage } from "@opencode-ai/sdk/v2" import { useSDK } from "@/context/sdk" import { usePrompt } from "@/context/prompt" import { useComments } from "@/context/comments" -import { usePermission } from "@/context/permission" import { SessionHeader, NewSessionView } from "@/components/session" import { same } from "@/utils/same" import { createOpenReviewFile } from "@/pages/session/helpers" @@ -40,16 +37,13 @@ export default function Page() { const local = useLocal() const file = useFile() const sync = useSync() - const terminal = useTerminal() const dialog = useDialog() - const command = useCommand() const language = useLanguage() const params = useParams() const navigate = useNavigate() const sdk = useSDK() const prompt = usePrompt() const comments = useComments() - const permission = usePermission() const [ui, setUi] = createStore({ pendingMessage: undefined as string | undefined, @@ -285,7 +279,6 @@ export default function Page() { return sync.data.session_diff[id] !== undefined }) - const idle = { type: "idle" as const } let inputRef!: HTMLDivElement let promptDock: HTMLDivElement | undefined let dockHeight = 0 @@ -327,8 +320,6 @@ export default function Page() { ), ) - const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle) - createEffect( on( sessionKey, @@ -361,11 +352,6 @@ export default function Page() { return lines.slice(0, 2).join("\n") } - const addSelectionToContext = (path: string, selection: FileSelection) => { - const preview = selectionPreview(path, selection) - prompt.context.add({ type: "file", path, selection, preview }) - } - const addCommentToContext = (input: { file: string selection: SelectedLineRange @@ -457,29 +443,8 @@ export default function Page() { const focusInput = () => inputRef?.focus() useSessionCommands({ - command, - dialog, - file, - language, - local, - permission, - prompt, - sdk, - sync, - terminal, - layout, - params, - navigate, - tabs, - view, - info, - status, - userMessages, - visibleUserMessages, - showAllFiles, navigateMessageByOffset, setActiveMessage, - addSelectionToContext, focusInput, }) diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index 4d68afd66..461351878 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -22,29 +22,8 @@ import { UserMessage } from "@opencode-ai/sdk/v2" import { canAddSelectionContext } from "@/pages/session/session-command-helpers" export type SessionCommandContext = { - command: ReturnType - dialog: ReturnType - file: ReturnType - language: ReturnType - local: ReturnType - permission: ReturnType - prompt: ReturnType - sdk: ReturnType - sync: ReturnType - terminal: ReturnType - layout: ReturnType - params: ReturnType - navigate: ReturnType - tabs: () => ReturnType["tabs"]> - view: () => ReturnType["view"]> - info: () => { revert?: { messageID?: string }; share?: { url?: string } } | undefined - status: () => { type: string } - userMessages: () => UserMessage[] - visibleUserMessages: () => UserMessage[] - showAllFiles: () => void navigateMessageByOffset: (offset: number) => void setActiveMessage: (message: UserMessage | undefined) => void - addSelectionToContext: (path: string, selection: FileSelection) => void focusInput: () => void } @@ -55,45 +34,98 @@ const withCategory = (category: string) => { }) } -export const useSessionCommands = (input: SessionCommandContext) => { - const sessionCommand = withCategory(input.language.t("command.category.session")) - const fileCommand = withCategory(input.language.t("command.category.file")) - const contextCommand = withCategory(input.language.t("command.category.context")) - const viewCommand = withCategory(input.language.t("command.category.view")) - const terminalCommand = withCategory(input.language.t("command.category.terminal")) - const modelCommand = withCategory(input.language.t("command.category.model")) - const mcpCommand = withCategory(input.language.t("command.category.mcp")) - const agentCommand = withCategory(input.language.t("command.category.agent")) - const permissionsCommand = withCategory(input.language.t("command.category.permissions")) +export const useSessionCommands = (actions: SessionCommandContext) => { + const command = useCommand() + const dialog = useDialog() + const file = useFile() + const language = useLanguage() + const local = useLocal() + const permission = usePermission() + const prompt = usePrompt() + const sdk = useSDK() + const sync = useSync() + const terminal = useTerminal() + const layout = useLayout() + const params = useParams() + const navigate = useNavigate() + + const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) + const tabs = createMemo(() => layout.tabs(sessionKey)) + const view = createMemo(() => layout.view(sessionKey)) + const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) + + const idle = { type: "idle" as const } + const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle) + const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) + const userMessages = createMemo(() => messages().filter((m) => m.role === "user") as UserMessage[]) + const visibleUserMessages = createMemo(() => { + const revert = info()?.revert?.messageID + if (!revert) return userMessages() + return userMessages().filter((m) => m.id < revert) + }) + + const showAllFiles = () => { + if (layout.fileTree.tab() !== "changes") return + layout.fileTree.setTab("all") + } + + const selectionPreview = (path: string, selection: FileSelection) => { + const content = file.get(path)?.content?.content + if (!content) return undefined + const start = Math.max(1, Math.min(selection.startLine, selection.endLine)) + const end = Math.max(selection.startLine, selection.endLine) + const lines = content.split("\n").slice(start - 1, end) + if (lines.length === 0) return undefined + return lines.slice(0, 2).join("\n") + } + + const addSelectionToContext = (path: string, selection: FileSelection) => { + const preview = selectionPreview(path, selection) + prompt.context.add({ type: "file", path, selection, preview }) + } + + const navigateMessageByOffset = actions.navigateMessageByOffset + const setActiveMessage = actions.setActiveMessage + const focusInput = actions.focusInput + + const sessionCommand = withCategory(language.t("command.category.session")) + const fileCommand = withCategory(language.t("command.category.file")) + const contextCommand = withCategory(language.t("command.category.context")) + const viewCommand = withCategory(language.t("command.category.view")) + const terminalCommand = withCategory(language.t("command.category.terminal")) + const modelCommand = withCategory(language.t("command.category.model")) + const mcpCommand = withCategory(language.t("command.category.mcp")) + const agentCommand = withCategory(language.t("command.category.agent")) + const permissionsCommand = withCategory(language.t("command.category.permissions")) const sessionCommands = createMemo(() => [ sessionCommand({ id: "session.new", - title: input.language.t("command.session.new"), + title: language.t("command.session.new"), keybind: "mod+shift+s", slash: "new", - onSelect: () => input.navigate(`/${input.params.dir}/session`), + onSelect: () => navigate(`/${params.dir}/session`), }), ]) const fileCommands = createMemo(() => [ fileCommand({ id: "file.open", - title: input.language.t("command.file.open"), - description: input.language.t("palette.search.placeholder"), + title: language.t("command.file.open"), + description: language.t("palette.search.placeholder"), keybind: "mod+p", slash: "open", - onSelect: () => input.dialog.show(() => ), + onSelect: () => dialog.show(() => ), }), fileCommand({ id: "tab.close", - title: input.language.t("command.tab.close"), + title: language.t("command.tab.close"), keybind: "mod+w", - disabled: !input.tabs().active(), + disabled: !tabs().active(), onSelect: () => { - const active = input.tabs().active() + const active = tabs().active() if (!active) return - input.tabs().close(active) + tabs().close(active) }, }), ]) @@ -101,30 +133,30 @@ export const useSessionCommands = (input: SessionCommandContext) => { const contextCommands = createMemo(() => [ contextCommand({ id: "context.addSelection", - title: input.language.t("command.context.addSelection"), - description: input.language.t("command.context.addSelection.description"), + title: language.t("command.context.addSelection"), + description: language.t("command.context.addSelection.description"), keybind: "mod+shift+l", disabled: !canAddSelectionContext({ - active: input.tabs().active(), - pathFromTab: input.file.pathFromTab, - selectedLines: input.file.selectedLines, + active: tabs().active(), + pathFromTab: file.pathFromTab, + selectedLines: file.selectedLines, }), onSelect: () => { - const active = input.tabs().active() + const active = tabs().active() if (!active) return - const path = input.file.pathFromTab(active) + const path = file.pathFromTab(active) if (!path) return - const range = input.file.selectedLines(path) as SelectedLineRange | null | undefined + const range = file.selectedLines(path) as SelectedLineRange | null | undefined if (!range) { showToast({ - title: input.language.t("toast.context.noLineSelection.title"), - description: input.language.t("toast.context.noLineSelection.description"), + title: language.t("toast.context.noLineSelection.title"), + description: language.t("toast.context.noLineSelection.description"), }) return } - input.addSelectionToContext(path, selectionFromLines(range)) + addSelectionToContext(path, selectionFromLines(range)) }, }), ]) @@ -132,37 +164,37 @@ export const useSessionCommands = (input: SessionCommandContext) => { const viewCommands = createMemo(() => [ viewCommand({ id: "terminal.toggle", - title: input.language.t("command.terminal.toggle"), + title: language.t("command.terminal.toggle"), keybind: "ctrl+`", slash: "terminal", - onSelect: () => input.view().terminal.toggle(), + onSelect: () => view().terminal.toggle(), }), viewCommand({ id: "review.toggle", - title: input.language.t("command.review.toggle"), + title: language.t("command.review.toggle"), keybind: "mod+shift+r", - onSelect: () => input.view().reviewPanel.toggle(), + onSelect: () => view().reviewPanel.toggle(), }), viewCommand({ id: "fileTree.toggle", - title: input.language.t("command.fileTree.toggle"), + title: language.t("command.fileTree.toggle"), keybind: "mod+\\", - onSelect: () => input.layout.fileTree.toggle(), + onSelect: () => layout.fileTree.toggle(), }), viewCommand({ id: "input.focus", - title: input.language.t("command.input.focus"), + title: language.t("command.input.focus"), keybind: "ctrl+l", - onSelect: () => input.focusInput(), + onSelect: () => focusInput(), }), terminalCommand({ id: "terminal.new", - title: input.language.t("command.terminal.new"), - description: input.language.t("command.terminal.new.description"), + title: language.t("command.terminal.new"), + description: language.t("command.terminal.new.description"), keybind: "ctrl+alt+t", onSelect: () => { - if (input.terminal.all().length > 0) input.terminal.new() - input.view().terminal.open() + if (terminal.all().length > 0) terminal.new() + view().terminal.open() }, }), ]) @@ -170,61 +202,61 @@ export const useSessionCommands = (input: SessionCommandContext) => { const messageCommands = createMemo(() => [ sessionCommand({ id: "message.previous", - title: input.language.t("command.message.previous"), - description: input.language.t("command.message.previous.description"), + title: language.t("command.message.previous"), + description: language.t("command.message.previous.description"), keybind: "mod+arrowup", - disabled: !input.params.id, - onSelect: () => input.navigateMessageByOffset(-1), + disabled: !params.id, + onSelect: () => navigateMessageByOffset(-1), }), sessionCommand({ id: "message.next", - title: input.language.t("command.message.next"), - description: input.language.t("command.message.next.description"), + title: language.t("command.message.next"), + description: language.t("command.message.next.description"), keybind: "mod+arrowdown", - disabled: !input.params.id, - onSelect: () => input.navigateMessageByOffset(1), + disabled: !params.id, + onSelect: () => navigateMessageByOffset(1), }), ]) const agentCommands = createMemo(() => [ modelCommand({ id: "model.choose", - title: input.language.t("command.model.choose"), - description: input.language.t("command.model.choose.description"), + title: language.t("command.model.choose"), + description: language.t("command.model.choose.description"), keybind: "mod+'", slash: "model", - onSelect: () => input.dialog.show(() => ), + onSelect: () => dialog.show(() => ), }), mcpCommand({ id: "mcp.toggle", - title: input.language.t("command.mcp.toggle"), - description: input.language.t("command.mcp.toggle.description"), + title: language.t("command.mcp.toggle"), + description: language.t("command.mcp.toggle.description"), keybind: "mod+;", slash: "mcp", - onSelect: () => input.dialog.show(() => ), + onSelect: () => dialog.show(() => ), }), agentCommand({ id: "agent.cycle", - title: input.language.t("command.agent.cycle"), - description: input.language.t("command.agent.cycle.description"), + title: language.t("command.agent.cycle"), + description: language.t("command.agent.cycle.description"), keybind: "mod+.", slash: "agent", - onSelect: () => input.local.agent.move(1), + onSelect: () => local.agent.move(1), }), agentCommand({ id: "agent.cycle.reverse", - title: input.language.t("command.agent.cycle.reverse"), - description: input.language.t("command.agent.cycle.reverse.description"), + title: language.t("command.agent.cycle.reverse"), + description: language.t("command.agent.cycle.reverse.description"), keybind: "shift+mod+.", - onSelect: () => input.local.agent.move(-1), + onSelect: () => local.agent.move(-1), }), modelCommand({ id: "model.variant.cycle", - title: input.language.t("command.model.variant.cycle"), - description: input.language.t("command.model.variant.cycle.description"), + title: language.t("command.model.variant.cycle"), + description: language.t("command.model.variant.cycle.description"), keybind: "shift+mod+d", onSelect: () => { - input.local.model.variant.cycle() + local.model.variant.cycle() }, }), ]) @@ -233,22 +265,22 @@ export const useSessionCommands = (input: SessionCommandContext) => { permissionsCommand({ id: "permissions.autoaccept", title: - input.params.id && input.permission.isAutoAccepting(input.params.id, input.sdk.directory) - ? input.language.t("command.permissions.autoaccept.disable") - : input.language.t("command.permissions.autoaccept.enable"), + params.id && permission.isAutoAccepting(params.id, sdk.directory) + ? language.t("command.permissions.autoaccept.disable") + : language.t("command.permissions.autoaccept.enable"), keybind: "mod+shift+a", - disabled: !input.params.id || !input.permission.permissionsEnabled(), + disabled: !params.id || !permission.permissionsEnabled(), onSelect: () => { - const sessionID = input.params.id + const sessionID = params.id if (!sessionID) return - input.permission.toggleAutoAccept(sessionID, input.sdk.directory) + permission.toggleAutoAccept(sessionID, sdk.directory) showToast({ - title: input.permission.isAutoAccepting(sessionID, input.sdk.directory) - ? input.language.t("toast.permissions.autoaccept.on.title") - : input.language.t("toast.permissions.autoaccept.off.title"), - description: input.permission.isAutoAccepting(sessionID, input.sdk.directory) - ? input.language.t("toast.permissions.autoaccept.on.description") - : input.language.t("toast.permissions.autoaccept.off.description"), + title: permission.isAutoAccepting(sessionID, sdk.directory) + ? language.t("toast.permissions.autoaccept.on.title") + : language.t("toast.permissions.autoaccept.off.title"), + description: permission.isAutoAccepting(sessionID, sdk.directory) + ? language.t("toast.permissions.autoaccept.on.description") + : language.t("toast.permissions.autoaccept.off.description"), }) }, }), @@ -257,71 +289,71 @@ export const useSessionCommands = (input: SessionCommandContext) => { const sessionActionCommands = createMemo(() => [ sessionCommand({ id: "session.undo", - title: input.language.t("command.session.undo"), - description: input.language.t("command.session.undo.description"), + title: language.t("command.session.undo"), + description: language.t("command.session.undo.description"), slash: "undo", - disabled: !input.params.id || input.visibleUserMessages().length === 0, + disabled: !params.id || visibleUserMessages().length === 0, onSelect: async () => { - const sessionID = input.params.id + const sessionID = params.id if (!sessionID) return - if (input.status()?.type !== "idle") { - await input.sdk.client.session.abort({ sessionID }).catch(() => {}) + if (status()?.type !== "idle") { + await sdk.client.session.abort({ sessionID }).catch(() => {}) } - const revert = input.info()?.revert?.messageID - const message = findLast(input.userMessages(), (x) => !revert || x.id < revert) + const revert = info()?.revert?.messageID + const message = findLast(userMessages(), (x) => !revert || x.id < revert) if (!message) return - await input.sdk.client.session.revert({ sessionID, messageID: message.id }) - const parts = input.sync.data.part[message.id] + await sdk.client.session.revert({ sessionID, messageID: message.id }) + const parts = sync.data.part[message.id] if (parts) { - const restored = extractPromptFromParts(parts, { directory: input.sdk.directory }) - input.prompt.set(restored) + const restored = extractPromptFromParts(parts, { directory: sdk.directory }) + prompt.set(restored) } - const priorMessage = findLast(input.userMessages(), (x) => x.id < message.id) - input.setActiveMessage(priorMessage) + const priorMessage = findLast(userMessages(), (x) => x.id < message.id) + setActiveMessage(priorMessage) }, }), sessionCommand({ id: "session.redo", - title: input.language.t("command.session.redo"), - description: input.language.t("command.session.redo.description"), + title: language.t("command.session.redo"), + description: language.t("command.session.redo.description"), slash: "redo", - disabled: !input.params.id || !input.info()?.revert?.messageID, + disabled: !params.id || !info()?.revert?.messageID, onSelect: async () => { - const sessionID = input.params.id + const sessionID = params.id if (!sessionID) return - const revertMessageID = input.info()?.revert?.messageID + const revertMessageID = info()?.revert?.messageID if (!revertMessageID) return - const nextMessage = input.userMessages().find((x) => x.id > revertMessageID) + const nextMessage = userMessages().find((x) => x.id > revertMessageID) if (!nextMessage) { - await input.sdk.client.session.unrevert({ sessionID }) - input.prompt.reset() - const lastMsg = findLast(input.userMessages(), (x) => x.id >= revertMessageID) - input.setActiveMessage(lastMsg) + await sdk.client.session.unrevert({ sessionID }) + prompt.reset() + const lastMsg = findLast(userMessages(), (x) => x.id >= revertMessageID) + setActiveMessage(lastMsg) return } - await input.sdk.client.session.revert({ sessionID, messageID: nextMessage.id }) - const priorMsg = findLast(input.userMessages(), (x) => x.id < nextMessage.id) - input.setActiveMessage(priorMsg) + await sdk.client.session.revert({ sessionID, messageID: nextMessage.id }) + const priorMsg = findLast(userMessages(), (x) => x.id < nextMessage.id) + setActiveMessage(priorMsg) }, }), sessionCommand({ id: "session.compact", - title: input.language.t("command.session.compact"), - description: input.language.t("command.session.compact.description"), + title: language.t("command.session.compact"), + description: language.t("command.session.compact.description"), slash: "compact", - disabled: !input.params.id || input.visibleUserMessages().length === 0, + disabled: !params.id || visibleUserMessages().length === 0, onSelect: async () => { - const sessionID = input.params.id + const sessionID = params.id if (!sessionID) return - const model = input.local.model.current() + const model = local.model.current() if (!model) { showToast({ - title: input.language.t("toast.model.none.title"), - description: input.language.t("toast.model.none.description"), + title: language.t("toast.model.none.title"), + description: language.t("toast.model.none.description"), }) return } - await input.sdk.client.session.summarize({ + await sdk.client.session.summarize({ sessionID, modelID: model.id, providerID: model.provider.id, @@ -330,29 +362,27 @@ export const useSessionCommands = (input: SessionCommandContext) => { }), sessionCommand({ id: "session.fork", - title: input.language.t("command.session.fork"), - description: input.language.t("command.session.fork.description"), + title: language.t("command.session.fork"), + description: language.t("command.session.fork.description"), slash: "fork", - disabled: !input.params.id || input.visibleUserMessages().length === 0, - onSelect: () => input.dialog.show(() => ), + disabled: !params.id || visibleUserMessages().length === 0, + onSelect: () => dialog.show(() => ), }), ]) const shareCommands = createMemo(() => { - if (input.sync.data.config.share === "disabled") return [] + if (sync.data.config.share === "disabled") return [] return [ sessionCommand({ id: "session.share", - title: input.info()?.share?.url - ? input.language.t("session.share.copy.copyLink") - : input.language.t("command.session.share"), - description: input.info()?.share?.url - ? input.language.t("toast.session.share.success.description") - : input.language.t("command.session.share.description"), + title: info()?.share?.url ? language.t("session.share.copy.copyLink") : language.t("command.session.share"), + description: info()?.share?.url + ? language.t("toast.session.share.success.description") + : language.t("command.session.share.description"), slash: "share", - disabled: !input.params.id, + disabled: !params.id, onSelect: async () => { - if (!input.params.id) return + if (!params.id) return const write = (value: string) => { const body = typeof document === "undefined" ? undefined : document.body @@ -382,7 +412,7 @@ export const useSessionCommands = (input: SessionCommandContext) => { const ok = await write(url) if (!ok) { showToast({ - title: input.language.t("toast.session.share.copyFailed.title"), + title: language.t("toast.session.share.copyFailed.title"), variant: "error", }) return @@ -390,27 +420,27 @@ export const useSessionCommands = (input: SessionCommandContext) => { showToast({ title: existing - ? input.language.t("session.share.copy.copied") - : input.language.t("toast.session.share.success.title"), - description: input.language.t("toast.session.share.success.description"), + ? language.t("session.share.copy.copied") + : language.t("toast.session.share.success.title"), + description: language.t("toast.session.share.success.description"), variant: "success", }) } - const existing = input.info()?.share?.url + const existing = info()?.share?.url if (existing) { await copy(existing, true) return } - const url = await input.sdk.client.session - .share({ sessionID: input.params.id }) + const url = await sdk.client.session + .share({ sessionID: params.id }) .then((res) => res.data?.share?.url) .catch(() => undefined) if (!url) { showToast({ - title: input.language.t("toast.session.share.failed.title"), - description: input.language.t("toast.session.share.failed.description"), + title: language.t("toast.session.share.failed.title"), + description: language.t("toast.session.share.failed.description"), variant: "error", }) return @@ -421,25 +451,25 @@ export const useSessionCommands = (input: SessionCommandContext) => { }), sessionCommand({ id: "session.unshare", - title: input.language.t("command.session.unshare"), - description: input.language.t("command.session.unshare.description"), + title: language.t("command.session.unshare"), + description: language.t("command.session.unshare.description"), slash: "unshare", - disabled: !input.params.id || !input.info()?.share?.url, + disabled: !params.id || !info()?.share?.url, onSelect: async () => { - if (!input.params.id) return - await input.sdk.client.session - .unshare({ sessionID: input.params.id }) + if (!params.id) return + await sdk.client.session + .unshare({ sessionID: params.id }) .then(() => showToast({ - title: input.language.t("toast.session.unshare.success.title"), - description: input.language.t("toast.session.unshare.success.description"), + title: language.t("toast.session.unshare.success.title"), + description: language.t("toast.session.unshare.success.description"), variant: "success", }), ) .catch(() => showToast({ - title: input.language.t("toast.session.unshare.failed.title"), - description: input.language.t("toast.session.unshare.failed.description"), + title: language.t("toast.session.unshare.failed.title"), + description: language.t("toast.session.unshare.failed.description"), variant: "error", }), ) @@ -448,7 +478,7 @@ export const useSessionCommands = (input: SessionCommandContext) => { ] }) - input.command.register("session", () => + command.register("session", () => [ sessionCommands(), fileCommands(),