Skip to content

Commit de7aeaa

Browse files
authored
Merge branch 'dev' into fix/plugin-local-version-and-copilot-limits
2 parents ecb5aa9 + 68676f2 commit de7aeaa

21 files changed

Lines changed: 702 additions & 147 deletions

packages/opencode/src/acp/service.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -330,25 +330,34 @@ export function make(input: {
330330
}
331331
})
332332

333-
const closeSession = Effect.fn("ACP.closeSession")(function* (params: CloseSessionRequest) {
334-
const removed = yield* session.remove(params.sessionId)
335-
registeredMcp.delete(params.sessionId)
336-
sessionSnapshots.delete(params.sessionId)
337-
if (!removed) return {}
338-
333+
const abortBackingSession = Effect.fn("ACP.abortBackingSession")(function* (current: ACPSession.Info) {
339334
yield* request(
340-
() => input.sdk.session.abort({ directory: removed.cwd, sessionID: params.sessionId }, { throwOnError: true }),
335+
() => input.sdk.session.abort({ directory: current.cwd, sessionID: current.id }, { throwOnError: true }),
341336
"session",
342337
).pipe(
343338
Effect.catch((error) =>
344339
Effect.sync(() => {
345-
log.error("failed to abort session while closing ACP session", { error, sessionID: params.sessionId })
340+
log.error("failed to abort ACP backing session", { error, sessionID: current.id })
346341
}),
347342
),
348343
)
344+
})
345+
346+
const closeSession = Effect.fn("ACP.closeSession")(function* (params: CloseSessionRequest) {
347+
const removed = yield* session.remove(params.sessionId)
348+
registeredMcp.delete(params.sessionId)
349+
sessionSnapshots.delete(params.sessionId)
350+
if (!removed) return {}
351+
352+
yield* abortBackingSession(removed)
349353
return {}
350354
})
351355

356+
const cancel = Effect.fn("ACP.cancel")(function* (params: CancelNotification) {
357+
const current = yield* session.get(params.sessionId)
358+
yield* abortBackingSession(current)
359+
})
360+
352361
const forkSession = Effect.fn("ACP.forkSession")(function* (params: ForkSessionRequest) {
353362
const snapshot = yield* directorySnapshot(params.cwd)
354363
const forked = yield* request(
@@ -563,9 +572,7 @@ export function make(input: {
563572
yield* sendUsageUpdate(input.usage, input.sdk, input.connection, current.id, current.cwd)
564573
return promptResponse(undefined, params.messageId)
565574
}),
566-
cancel: Effect.fn("ACP.cancel")(function* (_input: CancelNotification) {
567-
return yield* new ACPError.UnsupportedOperationError({ method: "session/cancel" })
568-
}),
575+
cancel,
569576
}
570577
}
571578

packages/opencode/src/cli/cmd/prompt-display.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" })
22

3-
function promptOffsetWidth(value: string) {
3+
export function promptOffsetWidth(value: string) {
44
let width = 0
55
for (const part of graphemes.segment(value)) {
66
// Textarea offsets count newlines as one position; Bun.stringWidth counts them as zero.

packages/opencode/src/cli/cmd/run/footer.command.tsx

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import fuzzysort from "fuzzysort"
55
import { createEffect, createMemo, createSignal, type Accessor } from "solid-js"
66
import { RunFooterMenu, createFooterMenuState, type RunFooterMenuItem } from "./footer.menu"
77
import type { RunFooterTheme } from "./theme"
8-
import type { FooterSubagentTab, RunCommand, RunInput, RunProvider } from "./types"
8+
import type { FooterQueuedPrompt, FooterSubagentTab, RunCommand, RunInput, RunProvider } from "./types"
99

1010
type PanelEntry = RunFooterMenuItem & {
1111
category: string
@@ -14,6 +14,7 @@ type PanelEntry = RunFooterMenuItem & {
1414

1515
type CommandEntry =
1616
| (PanelEntry & { action: "model" })
17+
| (PanelEntry & { action: "queued" })
1718
| (PanelEntry & { action: "subagent" })
1819
| (PanelEntry & { action: "variant.cycle" })
1920
| (PanelEntry & { action: "variant.list" })
@@ -37,6 +38,10 @@ type SubagentEntry = PanelEntry & {
3738
current: boolean
3839
}
3940

41+
type QueuedEntry = PanelEntry & {
42+
prompt: FooterQueuedPrompt
43+
}
44+
4045
type MenuState = ReturnType<typeof createFooterMenuState>
4146

4247
const PANEL_PAD = 2
@@ -294,11 +299,13 @@ export function RunCommandMenuBody(props: {
294299
theme: Accessor<RunFooterTheme>
295300
commands: Accessor<RunCommand[] | undefined>
296301
subagents: Accessor<FooterSubagentTab[]>
302+
queued: Accessor<FooterQueuedPrompt[]>
297303
variants: Accessor<string[]>
298304
variantCycle: string
299305
onClose: () => void
300306
onModel: () => void
301307
onSubagent: () => void
308+
onQueued: () => void
302309
onVariant: () => void
303310
onVariantCycle: () => void
304311
onCommand: (name: string) => void
@@ -315,6 +322,20 @@ export function RunCommandMenuBody(props: {
315322
category: "Suggested",
316323
display: "Switch model",
317324
},
325+
...(props.queued().length > 0
326+
? [
327+
{
328+
action: "queued" as const,
329+
category: "Suggested",
330+
display: "Manage queued prompts",
331+
footer: `${props.queued().length} queued`,
332+
keywords: props
333+
.queued()
334+
.map((item) => item.prompt.text)
335+
.join(" "),
336+
},
337+
]
338+
: []),
318339
...(props.subagents().length > 0
319340
? [
320341
{
@@ -387,6 +408,11 @@ export function RunCommandMenuBody(props: {
387408
return
388409
}
389410

411+
if (item.action === "queued") {
412+
props.onQueued()
413+
return
414+
}
415+
390416
if (item.action === "variant.cycle") {
391417
props.onVariantCycle()
392418
return
@@ -559,6 +585,102 @@ export function RunSubagentSelectBody(props: {
559585
)
560586
}
561587

588+
export function RunQueuedPromptSelectBody(props: {
589+
theme: Accessor<RunFooterTheme>
590+
prompts: Accessor<FooterQueuedPrompt[]>
591+
onClose: () => void
592+
onEdit: (prompt: FooterQueuedPrompt) => void | Promise<void>
593+
onDelete: (prompt: FooterQueuedPrompt) => void | Promise<void>
594+
onRows?: (rows: number) => void
595+
}) {
596+
let field: InputRenderable | undefined
597+
const [query, setQuery] = createSignal("")
598+
const entries = createMemo<QueuedEntry[]>(() =>
599+
props.prompts().map((prompt) => ({
600+
category: "",
601+
display: prompt.prompt.text.replaceAll("\n", " "),
602+
footer: "queued · ctrl+e edit · ctrl+d remove",
603+
keywords: prompt.prompt.text,
604+
prompt,
605+
})),
606+
)
607+
const items = createMemo<QueuedEntry[]>(() => match(query(), entries()))
608+
const menu = createFooterMenuState({ count: () => items().length, limit: SUBAGENT_LIST_ROWS })
609+
const selected = () => items()[menu.selected()]
610+
611+
createEffect(() => {
612+
query()
613+
menu.reset()
614+
})
615+
616+
createEffect(() => {
617+
props.onRows?.(menu.rows() + PANEL_FRAME_ROWS)
618+
})
619+
620+
useKeyboard((event) => {
621+
if (event.defaultPrevented) {
622+
return
623+
}
624+
625+
const item = selected()
626+
const ctrl = event.ctrl && !event.meta && !event.shift && !event.super
627+
if (item && (event.name === "delete" || (ctrl && event.name === "d"))) {
628+
event.preventDefault()
629+
props.onDelete(item.prompt)
630+
return
631+
}
632+
633+
if (item && ctrl && event.name === "e") {
634+
event.preventDefault()
635+
props.onEdit(item.prompt)
636+
return
637+
}
638+
639+
handleKey({
640+
event,
641+
menu,
642+
field: () => field,
643+
setQuery,
644+
select: () => {
645+
const item = selected()
646+
if (item) props.onEdit(item.prompt)
647+
},
648+
close: props.onClose,
649+
})
650+
})
651+
652+
return (
653+
<PanelShell
654+
id="run-direct-footer-queued-panel"
655+
title="Queued prompts"
656+
query={query()}
657+
count={items().length}
658+
total={entries().length}
659+
placeholder="Search"
660+
theme={props.theme}
661+
inputRef={(input) => {
662+
field = input
663+
}}
664+
onQuery={setQuery}
665+
>
666+
<RunFooterMenu
667+
id="run-direct-footer-queued-list"
668+
theme={props.theme}
669+
items={items}
670+
selected={menu.selected}
671+
offset={menu.offset}
672+
rows={menu.rows}
673+
limit={SUBAGENT_LIST_ROWS}
674+
empty="No queued prompts"
675+
border={false}
676+
paddingLeft={PANEL_PAD}
677+
paddingRight={PANEL_PAD}
678+
grouped={false}
679+
/>
680+
</PanelShell>
681+
)
682+
}
683+
562684
export function RunVariantSelectBody(props: {
563685
theme: Accessor<RunFooterTheme>
564686
variants: Accessor<string[]>

packages/opencode/src/cli/cmd/run/footer.prompt.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ type PromptInput = {
6363
directory: string
6464
findFiles: (query: string) => Promise<string[]>
6565
agents: Accessor<RunAgent[]>
66-
subagents: Accessor<number>
6766
resources: Accessor<RunResource[]>
6867
commands: Accessor<RunCommand[] | undefined>
6968
tuiConfig: RunTuiConfig
@@ -79,7 +78,6 @@ type PromptInput = {
7978
onInputClear: () => void
8079
onExitRequest?: () => boolean
8180
onExit: () => void
82-
onSubagentMenu?: () => void
8381
onRows: (rows: number) => void
8482
onStatus: (text: string) => void
8583
}
@@ -98,6 +96,7 @@ export type PromptState = {
9896
onKeyDown: (event: KeyEvent) => void
9997
onContentChange: () => void
10098
replaceDraft: (text: string) => void
99+
replacePrompt: (prompt: RunPrompt) => void
101100
bind: (area?: TextareaRenderable) => void
102101
}
103102

@@ -791,19 +790,32 @@ export function createPromptState(input: PromptInput): PromptState {
791790
}
792791

793792
if (next.kind === "slash") {
794-
const text = `/${next.name} `
795793
const cursor = area.cursorOffset
794+
const head = slashHead(area.plainText)
795+
const local = !shell() && (next.name === "new" || next.name === "exit")
796+
const separator = !shell() && !local && head && /\s/.test(area.plainText[head.end] ?? "") ? "" : " "
797+
const text = `/${next.name}${separator}`
796798

797799
area.cursorOffset = 0
798800
const start = area.logicalCursor
799-
area.cursorOffset = cursor
801+
area.cursorOffset =
802+
shell() || !head
803+
? cursor
804+
: local
805+
? Bun.stringWidth(area.plainText)
806+
: Bun.stringWidth(area.plainText.slice(0, head.end))
800807
const end = area.logicalCursor
801808

802809
area.deleteRange(start.row, start.col, end.row, end.col)
803810
area.insertText(text)
804811
area.cursorOffset = Bun.stringWidth(text)
805812
hide()
806813
syncDraft()
814+
if (!shell()) {
815+
submitPrompt(clonePrompt(draft))
816+
return
817+
}
818+
807819
scheduleRows()
808820
area.focus()
809821
return
@@ -888,6 +900,7 @@ export function createPromptState(input: PromptInput): PromptState {
888900
if (current === "command") return false
889901
if (current === "model") return false
890902
if (current === "variant") return false
903+
if (current === "queued-menu") return false
891904
if (current === "subagent-menu") return false
892905
return true
893906
}
@@ -957,17 +970,6 @@ export function createPromptState(input: PromptInput): PromptState {
957970
mode: OPENCODE_BASE_MODE,
958971
enabled: input.prompt() && !visible(),
959972
bindings: [
960-
{
961-
key: "down",
962-
desc: "View subagents",
963-
group: "Prompt",
964-
cmd() {
965-
if (!area || area.isDestroyed) return false
966-
if (area.plainText.length !== 0) return false
967-
if (input.subagents() === 0) return false
968-
input.onSubagentMenu?.()
969-
},
970-
},
971973
{
972974
key: "!",
973975
desc: "Shell mode",
@@ -1199,6 +1201,7 @@ export function createPromptState(input: PromptInput): PromptState {
11991201
scheduleRows()
12001202
},
12011203
replaceDraft,
1204+
replacePrompt: restore,
12021205
bind,
12031206
}
12041207
}

0 commit comments

Comments
 (0)