Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/run/footer.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ export function RunFooterView(props: RunFooterViewProps) {
if (queuedPrompts().length > 0 && queuedShortcut()) {
items.push({ kind: "queued", key: queuedShortcut(), label: `${queue()} queued` })
}
if (activeTabs().length > 0 && subagentShortcut()) {
if (tabs().length > 0 && subagentShortcut()) {
items.push({ kind: "subagents", key: subagentShortcut(), label: "subagents" })
}

Expand Down
31 changes: 28 additions & 3 deletions packages/opencode/test/cli/run/footer.view.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,7 @@ test("direct footer separates a lone context hint from model and command hint",
}
})

test("direct footer hides the subagent hint when only completed subagents remain", async () => {
test("direct footer keeps the subagent hint when only completed subagents remain", async () => {
const app = await renderFooter({
providers: [provider()],
currentModel: { providerID: "opencode", modelID: "gpt-5" },
Expand All @@ -1072,8 +1072,33 @@ test("direct footer hides the subagent hint when only completed subagents remain
const frame = app.captureCharFrame()

expect(frame).toContain("GPT-5")
expect(frame).toContain("xhigh · ctrl+p cmd")
expect(frame).not.toContain("ctrl+x down subagents")
expect(frame).toContain("xhigh · ctrl+x down subagents · ctrl+p cmd")
} finally {
app.cleanup()
}
})

test("direct footer keeps the subagent hint when only cancelled subagents remain", async () => {
const app = await renderFooter({
providers: [provider()],
currentModel: { providerID: "opencode", modelID: "gpt-5" },
currentVariant: "xhigh",
subagents: {
tabs: [subagent({ sessionID: "s-1", label: "Explore", description: "Inspect auth flow", status: "cancelled" })],
details: {},
permissions: [],
questions: [],
},
backgroundSubagents: false,
width: 160,
})

try {
await app.renderOnce()
const frame = app.captureCharFrame()

expect(frame).toContain("GPT-5")
expect(frame).toContain("xhigh · ctrl+x down subagents · ctrl+p cmd")
} finally {
app.cleanup()
}
Expand Down
7 changes: 7 additions & 0 deletions packages/session-ui/src/components/message-part-task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function taskSessionIdFromMetadata(
stateMetadata: Record<string, unknown> | undefined,
partMetadata: Record<string, unknown> | undefined,
) {
const value = stateMetadata?.sessionId ?? stateMetadata?.sessionID ?? partMetadata?.sessionId ?? partMetadata?.sessionID
return typeof value === "string" && value ? value : undefined
}
15 changes: 15 additions & 0 deletions packages/session-ui/src/components/message-part.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import { describe, expect, test } from "bun:test"
import { taskSessionIdFromMetadata } from "./message-part-task"
import { readPartText } from "./message-part-text"

describe("taskSessionIdFromMetadata", () => {
test("uses state metadata when available", () => {
expect(taskSessionIdFromMetadata({ sessionId: "child-1" }, { sessionId: "child-2" })).toBe("child-1")
})

test("falls back to top-level part metadata for cancelled task cards", () => {
expect(taskSessionIdFromMetadata(undefined, { sessionId: "child-1" })).toBe("child-1")
})

test("accepts legacy sessionID casing", () => {
expect(taskSessionIdFromMetadata({}, { sessionID: "child-1" })).toBe("child-1")
})
})

describe("readPartText", () => {
test("returns empty string when accum is undefined and part text is undefined", () => {
expect(readPartText(undefined, { id: "part_1" })).toBe("")
Expand Down
6 changes: 4 additions & 2 deletions packages/session-ui/src/components/message-part.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import { animate } from "motion"
import { useLocation } from "@solidjs/router"
import { attached, inline, kind } from "./message-file"
import { readPartText } from "./message-part-text"
import { taskSessionIdFromMetadata } from "./message-part-task"
import { SessionProgressIndicatorV2 } from "../v2/components/session-progress-indicator-v2"

async function writeClipboard(text: string): Promise<boolean> {
Expand Down Expand Up @@ -1501,10 +1502,11 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
const input = () => part().state?.input ?? emptyInput
// @ts-expect-error
const partMetadata = () => part().state?.metadata ?? emptyMetadata
const topLevelPartMetadata = () =>
("metadata" in part() ? part().metadata : undefined) as Record<string, unknown> | undefined
const taskId = createMemo(() => {
if (part().tool !== "task") return
const value = partMetadata().sessionId
if (typeof value === "string" && value) return value
return taskSessionIdFromMetadata(partMetadata(), topLevelPartMetadata())
})
const taskHref = createMemo(() => {
if (part().tool !== "task") return
Expand Down
23 changes: 17 additions & 6 deletions packages/tui/src/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,17 @@ export function Session() {
})
onCleanup(() => setEpilogue())
const children = createMemo(() => {
const parentID = session()?.parentID ?? session()?.id
const parentID = session()?.id
if (!parentID) return []
return sync.data.session
.filter((x) => x.parentID === parentID || x.id === parentID)
.filter((x) => x.parentID === parentID)
.toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
})
const siblings = createMemo(() => {
const parentID = session()?.parentID
if (!parentID) return []
return sync.data.session
.filter((x) => x.parentID === parentID)
.toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
})
const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
Expand Down Expand Up @@ -432,15 +440,14 @@ export function Session() {
}

function moveFirstChild() {
if (children().length === 1) return
const next = children().find((x) => !!x.parentID)
const next = children()[0]
if (next) enterChild(next.id)
}

function moveChild(direction: number) {
if (children().length === 1) return
const sessions = siblings()
if (sessions.length === 0) return

const sessions = children().filter((x) => !!x.parentID)
let next = sessions.findIndex((x) => x.id === session()?.id) - direction

if (next >= sessions.length) next = 0
Expand Down Expand Up @@ -1893,6 +1900,10 @@ function InlineTool(props: {
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
if (failed()) {
if (props.onClick) {
props.onClick()
return
}
setErrorExpanded((value) => !value)
return
}
Expand Down
Loading