summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-18 06:55:05 -0600
committerAdam <[email protected]>2025-12-18 11:16:32 -0600
commitb7875256f362d85e9644b376562e7525ee7191ae (patch)
tree726f82347e715e4db5530fa07d7a032ebba5870a /packages/desktop/src
parent7bc47fb9043fc7c8b4b013355f85fad63df8680b (diff)
downloadopencode-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.tsx101
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