diff options
| author | Adam <[email protected]> | 2025-12-18 06:55:05 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-18 11:16:32 -0600 |
| commit | b7875256f362d85e9644b376562e7525ee7191ae (patch) | |
| tree | 726f82347e715e4db5530fa07d7a032ebba5870a /packages/desktop/src | |
| parent | 7bc47fb9043fc7c8b4b013355f85fad63df8680b (diff) | |
| download | opencode-b7875256f362d85e9644b376562e7525ee7191ae.tar.gz opencode-b7875256f362d85e9644b376562e7525ee7191ae.zip | |
feat(desktop): shell mode
Diffstat (limited to 'packages/desktop/src')
| -rw-r--r-- | packages/desktop/src/components/prompt-input.tsx | 101 |
1 files changed, 74 insertions, 27 deletions
diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index 1a6e233e3..02fa700bf 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -99,6 +99,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { placeholder: number dragging: boolean imageAttachments: ImageAttachmentPart[] + mode: "normal" | "shell" }>({ popover: null, historyIndex: -1, @@ -106,6 +107,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { placeholder: Math.floor(Math.random() * PLACEHOLDERS.length), dragging: false, imageAttachments: [], + mode: "normal", }) const MAX_HISTORY = 100 @@ -579,6 +581,24 @@ export const PromptInput: Component<PromptInputProps> = (props) => { } const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "!" && store.mode === "normal") { + const cursorPosition = getCursorPosition(editorRef) + if (cursorPosition === 0) { + setStore("mode", "shell") + setStore("popover", null) + event.preventDefault() + return + } + } + if (store.mode === "shell") { + const cursorPosition = getCursorPosition(editorRef) + if ((event.key === "Backspace" && cursorPosition === 0) || event.key === "Escape") { + setStore("mode", "normal") + event.preventDefault() + return + } + } + if (store.popover && (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter")) { if (store.popover === "file") { onKeyDown(event) @@ -688,10 +708,28 @@ export const PromptInput: Component<PromptInputProps> = (props) => { filename: attachment.filename, })) + const isShellMode = store.mode === "shell" tabs().setActive(undefined) editorRef.innerHTML = "" prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0) setStore("imageAttachments", []) + setStore("mode", "normal") + + const model = { + modelID: local.model.current()!.id, + providerID: local.model.current()!.provider.id, + } + const agent = local.agent.current()!.name + + if (isShellMode) { + sdk.client.session.shell({ + sessionID: existing.id, + agent, + model, + command: text, + }) + return + } if (text.startsWith("/")) { const [cmdName, ...args] = text.split(" ") @@ -702,19 +740,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => { sessionID: existing.id, command: commandName, arguments: args.join(" "), - agent: local.agent.current()!.name, - model: `${local.model.current()!.provider.id}/${local.model.current()!.id}`, + agent, + model: `${model.providerID}/${model.modelID}`, }) return } } - const model = { - modelID: local.model.current()!.id, - providerID: local.model.current()!.provider.id, - } - const agent = local.agent.current()!.name - sync.session.addOptimisticMessage({ sessionID: existing.id, text, @@ -883,30 +915,45 @@ export const PromptInput: Component<PromptInputProps> = (props) => { /> <Show when={!prompt.dirty() && store.imageAttachments.length === 0}> <div class="absolute top-0 left-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none"> - Ask anything... "{PLACEHOLDERS[store.placeholder]}" + {store.mode === "shell" + ? "Enter shell command..." + : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`} </div> </Show> </div> <div class="relative p-3 flex items-center justify-between"> <div class="flex items-center justify-start gap-1"> - <Select - options={local.agent.list().map((agent) => agent.name)} - current={local.agent.current().name} - onSelect={local.agent.set} - class="capitalize" - variant="ghost" - /> - <Button - as="div" - variant="ghost" - onClick={() => - dialog.show(() => (providers.paid().length > 0 ? <DialogSelectModel /> : <DialogSelectModelUnpaid />)) - } - > - {local.model.current()?.name ?? "Select model"} - <span class="ml-0.5 text-text-weak text-12-regular">{local.model.current()?.provider.name}</span> - <Icon name="chevron-down" size="small" /> - </Button> + <Switch> + <Match when={store.mode === "shell"}> + <div class="flex items-center gap-2 px-2 h-6"> + <Icon name="console" size="small" class="text-icon-primary" /> + <span class="text-12-regular text-text-primary">Shell</span> + <span class="text-12-regular text-text-weak">esc to exit</span> + </div> + </Match> + <Match when={store.mode === "normal"}> + <Select + options={local.agent.list().map((agent) => agent.name)} + current={local.agent.current().name} + onSelect={local.agent.set} + class="capitalize" + variant="ghost" + /> + <Button + as="div" + variant="ghost" + onClick={() => + dialog.show(() => + providers.paid().length > 0 ? <DialogSelectModel /> : <DialogSelectModelUnpaid />, + ) + } + > + {local.model.current()?.name ?? "Select model"} + <span class="ml-0.5 text-text-weak text-12-regular">{local.model.current()?.provider.name}</span> + <Icon name="chevron-down" size="small" /> + </Button> + </Match> + </Switch> </div> <div class="flex items-center gap-1 absolute right-2 bottom-2"> <input |
