diff --git a/package.json b/package.json
index 2c69f46d2993..ed89c6351755 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "name": "opencode",
- "description": "AI-powered development tool",
+ "name": "overaicoding",
+ "description": "AI-powered development tool (custom fork)",
"private": true,
"type": "module",
"packageManager": "bun@1.3.8",
diff --git a/packages/opencode/bin/overaicoding b/packages/opencode/bin/overaicoding
new file mode 100755
index 000000000000..a05ff967a84c
--- /dev/null
+++ b/packages/opencode/bin/overaicoding
@@ -0,0 +1,84 @@
+#!/usr/bin/env node
+
+const childProcess = require("child_process")
+const fs = require("fs")
+const path = require("path")
+const os = require("os")
+
+function run(target) {
+ const result = childProcess.spawnSync(target, process.argv.slice(2), {
+ stdio: "inherit",
+ })
+ if (result.error) {
+ console.error(result.error.message)
+ process.exit(1)
+ }
+ const code = typeof result.status === "number" ? result.status : 0
+ process.exit(code)
+}
+
+const envPath = process.env.OVERAICODING_BIN_PATH || process.env.OPENCODE_BIN_PATH
+if (envPath) {
+ run(envPath)
+}
+
+const scriptPath = fs.realpathSync(__filename)
+const scriptDir = path.dirname(scriptPath)
+
+const platformMap = {
+ darwin: "darwin",
+ linux: "linux",
+ win32: "windows",
+}
+const archMap = {
+ x64: "x64",
+ arm64: "arm64",
+ arm: "arm",
+}
+
+let platform = platformMap[os.platform()]
+if (!platform) {
+ platform = os.platform()
+}
+let arch = archMap[os.arch()]
+if (!arch) {
+ arch = os.arch()
+}
+const base = "overaicoding-" + platform + "-" + arch
+const binary = platform === "windows" ? "overaicoding.exe" : "overaicoding"
+
+function findBinary(startDir) {
+ let current = startDir
+ for (;;) {
+ const modules = path.join(current, "node_modules")
+ if (fs.existsSync(modules)) {
+ const entries = fs.readdirSync(modules)
+ for (const entry of entries) {
+ if (!entry.startsWith(base)) {
+ continue
+ }
+ const candidate = path.join(modules, entry, "bin", binary)
+ if (fs.existsSync(candidate)) {
+ return candidate
+ }
+ }
+ }
+ const parent = path.dirname(current)
+ if (parent === current) {
+ return
+ }
+ current = parent
+ }
+}
+
+const resolved = findBinary(scriptDir)
+if (!resolved) {
+ console.error(
+ 'It seems that your package manager failed to install the right version of the overaicoding CLI for your platform. You can try manually installing the "' +
+ base +
+ '" package',
+ )
+ process.exit(1)
+}
+
+run(resolved)
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index a2024a7f7c17..c7f232fb37b5 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.1.53",
- "name": "opencode",
+ "name": "overaicoding",
"type": "module",
"license": "MIT",
"private": true,
@@ -18,7 +18,7 @@
"deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'"
},
"bin": {
- "opencode": "./bin/opencode"
+ "overaicoding": "./bin/overaicoding"
},
"randomField": "this-is-a-random-value-12345",
"exports": {
diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts
index f0b3fa828a78..4e8a4da9d107 100755
--- a/packages/opencode/script/build.ts
+++ b/packages/opencode/script/build.ts
@@ -149,7 +149,7 @@ for (const item of targets) {
autoloadTsconfig: true,
autoloadPackageJson: true,
target: name.replace(pkg.name, "bun") as any,
- outfile: `dist/${name}/bin/opencode`,
+ outfile: `dist/${name}/bin/overaicoding`,
execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"],
windows: {},
},
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-child-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-child-session-list.tsx
new file mode 100644
index 000000000000..0e1c772126a5
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-child-session-list.tsx
@@ -0,0 +1,75 @@
+import { useRoute } from "@tui/context/route"
+import { useSync } from "@tui/context/sync"
+import { useDialog } from "@tui/ui/dialog"
+import { DialogSelect } from "@tui/ui/dialog-select"
+import { useTheme } from "@tui/context/theme"
+import { createMemo, onMount } from "solid-js"
+import { buildChildSessionPickerOptions } from "../lib/child-session-picker"
+import { sessionRunState } from "../lib/session-tree"
+import "opentui-spinner/solid"
+
+export function DialogChildSessionList(props: { sessionID: string }) {
+ const dialog = useDialog()
+ const sync = useSync()
+ const route = useRoute()
+ const { theme } = useTheme()
+
+ onMount(() => {
+ dialog.setSize("large")
+ })
+
+ const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
+
+ const options = createMemo(() => {
+ const sessions = sync.data.session.map((s) => ({
+ id: s.id,
+ title: s.title,
+ parentID: s.parentID,
+ time: { created: s.time.created, updated: s.time.updated },
+ }))
+
+ const baseOptions = buildChildSessionPickerOptions({
+ currentSessionID: props.sessionID,
+ sessions,
+ permissionsBySession: sync.data.permission,
+ }).options
+
+ // Enhance options with status indicators
+ return baseOptions.map((opt) => {
+ const pending = sync.data.permission[opt.value]?.length ?? 0
+ const status = sync.data.session_status?.[opt.value] as { type?: string } | undefined
+ const state = sessionRunState(status)
+
+ const session = sync.data.session.find((s) => s.id === opt.value)
+ const isSubagent = session?.parentID !== undefined
+
+ const gutter = (() => {
+ if (pending > 0) return !
+ if (!isSubagent) return
+ if (state === "working") return
+ if (state === "waiting") return ◎
+ return ●
+ })()
+
+ return {
+ ...opt,
+ gutter,
+ }
+ })
+ })
+
+ return (
+ {
+ route.navigate({
+ type: "session",
+ sessionID: option.value,
+ })
+ dialog.clear()
+ }}
+ />
+ )
+}
diff --git a/packages/opencode/src/cli/cmd/tui/lib/child-session-picker.ts b/packages/opencode/src/cli/cmd/tui/lib/child-session-picker.ts
new file mode 100644
index 000000000000..3f4cbfc59cdb
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/lib/child-session-picker.ts
@@ -0,0 +1,100 @@
+import type { DialogSelectOption } from "@tui/ui/dialog-select"
+import { Locale } from "@/util/locale"
+import { buildSessionTree } from "./session-tree"
+
+export type ChildSessionPickerSession = {
+ id: string
+ title: string
+ parentID?: string
+ time: {
+ created: number
+ updated: number
+ }
+}
+
+type PermissionBySession = Record | undefined>
+
+function isDefaultSessionTitle(title: string): boolean {
+ const prefix =
+ title.startsWith("New session - ") || title.startsWith("Child session - ") || title.startsWith("Subagent - ")
+ if (!prefix) return false
+ return /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(title)
+}
+
+function shortID(id: string): string {
+ if (!id) return ""
+ if (id.length <= 8) return id
+ return id.slice(-8)
+}
+
+function permissionCount(permissions: PermissionBySession, sessionID: string): number {
+ const list = permissions[sessionID]
+ if (!list) return 0
+ return Array.isArray(list) ? list.length : 0
+}
+
+export function buildChildSessionPickerOptions(input: {
+ currentSessionID: string
+ sessions: ChildSessionPickerSession[]
+ permissionsBySession: PermissionBySession
+}): {
+ rootID: string
+ options: DialogSelectOption[]
+} {
+ const tree = buildSessionTree({
+ currentSessionID: input.currentSessionID,
+ sessions: input.sessions,
+ sort: "created",
+ })
+
+ const options = tree.list.map((item) => {
+ const sessionID = item.id
+ const session = tree.sessionByID.get(sessionID)
+ const sid = shortID(sessionID)
+
+ const indent = item.depth > 0 ? `${" ".repeat(item.depth - 1)}↳ ` : ""
+
+ const title = (() => {
+ if (sessionID === tree.rootID) {
+ const name = session?.title ?? "Root session"
+ return `Root · ${name} · ${sid}`
+ }
+
+ const name = (() => {
+ if (!session) return "Subagent session"
+ if (!isDefaultSessionTitle(session.title)) return session.title
+ return "Subagent session"
+ })()
+
+ return `${indent}${name} · ${sid}`
+ })()
+
+ const description = (() => {
+ const pending = permissionCount(input.permissionsBySession, sessionID)
+ if (pending > 0) return "Needs input"
+
+ if (!session) return
+ if (sessionID === tree.rootID) return
+ if (!isDefaultSessionTitle(session.title)) return
+ return session.title
+ })()
+
+ const footer = (() => {
+ const pending = permissionCount(input.permissionsBySession, sessionID)
+ if (pending > 0) return `${pending} pending`
+ if (session) return Locale.todayTimeOrDateTime(session.time.updated)
+ })()
+
+ return {
+ title,
+ value: sessionID,
+ description,
+ footer,
+ } satisfies DialogSelectOption
+ })
+
+ return {
+ rootID: tree.rootID,
+ options,
+ }
+}
diff --git a/packages/opencode/src/cli/cmd/tui/lib/session-tree.ts b/packages/opencode/src/cli/cmd/tui/lib/session-tree.ts
new file mode 100644
index 000000000000..6b936027287e
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/lib/session-tree.ts
@@ -0,0 +1,110 @@
+export type SessionTreeSession = {
+ id: string
+ parentID?: string
+ time?: {
+ created?: number
+ updated?: number
+ }
+}
+
+export type SessionTreeItem = {
+ id: string
+ depth: number
+}
+
+function sessionTime(session: T | undefined, key: "created" | "updated"): number {
+ const time = session?.time
+ if (!time) return 0
+ if (key === "created") return time.created ?? 0
+ return time.updated ?? 0
+}
+
+export function resolveRootID(input: {
+ currentSessionID: string
+ sessionByID: Map
+}): string {
+ const visited = new Set()
+ let current = input.currentSessionID
+
+ while (!visited.has(current)) {
+ visited.add(current)
+
+ const session = input.sessionByID.get(current)
+ const parent = session?.parentID
+ if (!parent) return current
+ if (!input.sessionByID.has(parent)) return parent
+ current = parent
+ }
+
+ return input.currentSessionID
+}
+
+export function buildSessionTree(input: {
+ currentSessionID: string
+ sessions: T[]
+ sort?: "created" | "updated"
+}): {
+ rootID: string
+ list: SessionTreeItem[]
+ sessionByID: Map
+} {
+ const sessionByID = new Map(input.sessions.map((s) => [s.id, s]))
+ const rootID = resolveRootID({
+ currentSessionID: input.currentSessionID,
+ sessionByID,
+ })
+
+ const childrenByParent = new Map()
+ for (const session of input.sessions) {
+ const parent = session.parentID
+ if (!parent) continue
+ const list = childrenByParent.get(parent)
+ if (list) {
+ list.push(session.id)
+ continue
+ }
+ childrenByParent.set(parent, [session.id])
+ }
+
+ const sortKey = input.sort ?? "created"
+ for (const list of childrenByParent.values()) {
+ list.sort((a, b) => {
+ const at = sessionTime(sessionByID.get(a), sortKey)
+ const bt = sessionTime(sessionByID.get(b), sortKey)
+ if (at !== bt) return at - bt
+ return a.localeCompare(b)
+ })
+ }
+
+ const seen = new Set([rootID])
+ const stack: SessionTreeItem[] = [{ id: rootID, depth: 0 }]
+ const out: SessionTreeItem[] = []
+
+ while (stack.length > 0) {
+ const item = stack.pop()
+ if (!item) continue
+
+ out.push(item)
+
+ const children = childrenByParent.get(item.id) ?? []
+ for (const child of children.toReversed()) {
+ if (seen.has(child)) continue
+ seen.add(child)
+ stack.push({ id: child, depth: item.depth + 1 })
+ }
+ }
+
+ return {
+ rootID,
+ list: out,
+ sessionByID,
+ }
+}
+
+export type SessionRunState = "working" | "waiting" | "done"
+
+export function sessionRunState(status: { type?: string } | undefined): SessionRunState {
+ if (status?.type === "busy" || status?.type === "retry") return "working"
+ if (status?.type === "waiting") return "waiting"
+ return "done"
+}
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-subagent.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-subagent.tsx
index c5ef70ef06f6..de60f517ab9a 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-subagent.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-subagent.tsx
@@ -1,18 +1,87 @@
+import type { DialogContext } from "@tui/ui/dialog"
import { DialogSelect } from "@tui/ui/dialog-select"
import { useRoute } from "@tui/context/route"
+import { useSync } from "@tui/context/sync"
+import { createMemo } from "solid-js"
+import { buildSessionTree } from "../../lib/session-tree"
export function DialogSubagent(props: { sessionID: string }) {
const route = useRoute()
+ const sync = useSync()
+
+ const currentID = createMemo(() => {
+ if (route.data.type !== "session") return undefined
+ return route.data.sessionID
+ })
+
+ const session = createMemo(() => sync.session.get(props.sessionID))
+ const status = createMemo(() => {
+ const s = sync.data.session_status?.[props.sessionID] as { type?: string } | undefined
+ if (s?.type === "busy" || s?.type === "retry") return "working"
+ if (s?.type === "waiting") return "waiting"
+ return "idle"
+ })
+
+ const name = createMemo(() => {
+ const s = session()
+ if (!s?.title) return "Subagent Session"
+ if (s.title.startsWith("Subagent - ")) return s.title.slice(11)
+ return s.title
+ })
+
+ const shortId = createMemo(() => props.sessionID.slice(-4))
+
+ const title = createMemo(() => {
+ const statusIcon = status() === "working" ? "◐" : status() === "waiting" ? "◎" : "✓"
+ return `${statusIcon} ${name()}#${shortId()}`
+ })
+
+ const parentID = createMemo(() => session()?.parentID)
+
+ const rootID = createMemo(() => {
+ const parent = parentID()
+ if (!parent) return undefined
+
+ return buildSessionTree({
+ currentSessionID: props.sessionID,
+ sessions: sync.data.session,
+ sort: "created",
+ }).rootID
+ })
+
+ const showRoot = createMemo(() => {
+ const root = rootID()
+ if (!root) return false
+
+ const parent = parentID()
+ if (!parent) return false
+ if (root === parent) return false
+
+ const current = currentID()
+ if (!current) return true
+
+ return root !== current
+ })
+
+ const showCaller = createMemo(() => {
+ const parent = parentID()
+ if (!parent) return false
+
+ const current = currentID()
+ if (!current) return true
+
+ return parent !== current
+ })
return (
{
+ description: "View this session",
+ onSelect: (dialog: DialogContext) => {
route.navigate({
type: "session",
sessionID: props.sessionID,
@@ -20,6 +89,38 @@ export function DialogSubagent(props: { sessionID: string }) {
dialog.clear()
},
},
+ ...(showRoot()
+ ? [
+ {
+ title: "Open root session",
+ value: "subagent.root",
+ description: "Go to the primary session for this thread",
+ onSelect: (dialog: DialogContext) => {
+ const root = rootID()
+ if (root) {
+ route.navigate({ type: "session", sessionID: root })
+ }
+ dialog.clear()
+ },
+ },
+ ]
+ : []),
+ ...(showCaller()
+ ? [
+ {
+ title: "Open parent session",
+ value: "subagent.parent",
+ description: "Go to the session that spawned this subagent",
+ onSelect: (dialog: DialogContext) => {
+ const parent = parentID()
+ if (parent) {
+ route.navigate({ type: "session", sessionID: parent })
+ }
+ dialog.clear()
+ },
+ },
+ ]
+ : []),
]}
/>
)
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx
index 0c5ea9a85723..462a2930e6cc 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx
@@ -7,7 +7,8 @@ import { SplitBorder } from "@tui/component/border"
import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2"
import { useCommandDialog } from "@tui/component/dialog-command"
import { useKeybind } from "../../context/keybind"
-import { useTerminalDimensions } from "@opentui/solid"
+import { Installation } from "@/installation"
+import { buildSessionTree, sessionRunState } from "../../lib/session-tree"
const Title = (props: { session: Accessor }) => {
const { theme } = useTheme()
@@ -35,6 +36,25 @@ export function Header() {
const session = createMemo(() => sync.session.get(route.sessionID)!)
const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
+ const tree = createMemo(() =>
+ buildSessionTree({
+ currentSessionID: route.sessionID,
+ sessions: sync.data.session,
+ sort: "created",
+ }),
+ )
+
+ const childCount = createMemo(() => Math.max(0, tree().list.length - 1))
+
+ const workingChildCount = createMemo(() => {
+ const t = tree()
+ return t.list.filter((item) => {
+ if (item.id === t.rootID) return false
+ const status = sync.data.session_status?.[item.id] as { type?: string } | undefined
+ return sessionRunState(status) !== "done"
+ }).length
+ })
+
const cost = createMemo(() => {
const total = pipe(
messages(),
@@ -62,9 +82,7 @@ export function Header() {
const { theme } = useTheme()
const keybind = useKeybind()
const command = useCommandDialog()
- const [hover, setHover] = createSignal<"parent" | "prev" | "next" | null>(null)
- const dimensions = useTerminalDimensions()
- const narrow = createMemo(() => dimensions().width < 80)
+ const [hover, setHover] = createSignal<"parent" | "prev" | "next" | "list" | null>(null)
return (
@@ -81,51 +99,78 @@ export function Header() {
>
-
-
+
+
+ Subagent session
+
+ setHover("parent")}
+ onMouseOut={() => setHover(null)}
+ onMouseUp={() => command.trigger("session.parent")}
+ backgroundColor={hover() === "parent" ? theme.backgroundElement : theme.backgroundPanel}
+ >
- Subagent session
+ Parent {keybind.print("session_parent")}
-
-
- setHover("parent")}
- onMouseOut={() => setHover(null)}
- onMouseUp={() => command.trigger("session.parent")}
- backgroundColor={hover() === "parent" ? theme.backgroundElement : theme.backgroundPanel}
- >
-
- Parent {keybind.print("session_parent")}
-
-
- setHover("prev")}
- onMouseOut={() => setHover(null)}
- onMouseUp={() => command.trigger("session.child.previous")}
- backgroundColor={hover() === "prev" ? theme.backgroundElement : theme.backgroundPanel}
- >
-
- Prev {keybind.print("session_child_cycle_reverse")}
-
-
- setHover("next")}
- onMouseOut={() => setHover(null)}
- onMouseUp={() => command.trigger("session.child.next")}
- backgroundColor={hover() === "next" ? theme.backgroundElement : theme.backgroundPanel}
- >
-
- Next {keybind.print("session_child_cycle")}
-
-
+ setHover("prev")}
+ onMouseOut={() => setHover(null)}
+ onMouseUp={() => command.trigger("session.child.previous")}
+ backgroundColor={hover() === "prev" ? theme.backgroundElement : theme.backgroundPanel}
+ >
+
+ Prev {keybind.print("session_child_cycle_reverse")}
+
+
+ setHover("next")}
+ onMouseOut={() => setHover(null)}
+ onMouseUp={() => command.trigger("session.child.next")}
+ backgroundColor={hover() === "next" ? theme.backgroundElement : theme.backgroundPanel}
+ >
+
+ Next {keybind.print("session_child_cycle")}
+
+
+ setHover("list")}
+ onMouseOut={() => setHover(null)}
+ onMouseUp={() => command.trigger("session.child.list")}
+ backgroundColor={hover() === "list" ? theme.backgroundElement : theme.backgroundPanel}
+ >
+
+ List {keybind.print("session_child_list")}
+
+
+
+
+
+ v{Installation.VERSION}
-
-
-
+
+
+
+ 0}>
+
+ 0 ? theme.warning : theme.success }}>
+ {workingChildCount() > 0 ? "◐" : "●"}
+ {" "}
+ {childCount()} subagent{childCount() > 1 ? "s" : ""}
+ 0}>
+ ({workingChildCount()} working)
+ {" "}
+ {keybind.print("session_child_list")}
+
+
+
+
+
+ v{Installation.VERSION}
+
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
index 70ff5eaf9fb6..01dcf11a02fe 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -77,6 +77,7 @@ import { PermissionPrompt } from "./permission"
import { QuestionPrompt } from "./question"
import { DialogExportOptions } from "../../ui/dialog-export-options"
import { formatTranscript } from "../../util/transcript"
+import { DialogChildSessionList } from "../../component/dialog-child-session-list"
import { UI } from "@/cli/ui.ts"
addDefaultParsers(parsers.parsers)
@@ -154,7 +155,6 @@ export function Session() {
const wide = createMemo(() => dimensions().width > 120)
const sidebarVisible = createMemo(() => {
- if (session()?.parentID) return false
if (sidebarOpen()) return true
if (sidebar() === "auto" && wide()) return true
return false
@@ -885,6 +885,22 @@ export function Session() {
dialog.clear()
},
},
+ {
+ title: "Switch subagent session",
+ value: "session.child.list",
+ keybind: "session_child_list",
+ category: "Session",
+ hidden: true,
+ onSelect: (dialog) => {
+ const childList = children()
+ if (childList.length <= 1) {
+ toast.show({ variant: "warning", message: "No subagent sessions found", duration: 2000 })
+ dialog.clear()
+ return
+ }
+ dialog.replace(() => )
+ },
+ },
])
const revertInfo = createMemo(() => session()?.revert)
@@ -958,7 +974,7 @@ export function Session() {
-
+
sync.session.get(props.sessionID)!)
- const diff = createMemo(() => sync.data.session_diff[props.sessionID] ?? [])
- const todo = createMemo(() => sync.data.todo[props.sessionID] ?? [])
- const messages = createMemo(() => sync.data.message[props.sessionID] ?? [])
+ const route = useRoute()
const [expanded, setExpanded] = createStore({
mcp: true,
diff: true,
todo: true,
lsp: true,
+ subagents: true,
})
+ const tree = createMemo(() =>
+ buildSessionTree({
+ currentSessionID: props.sessionID,
+ sessions: sync.data.session,
+ sort: "created",
+ }),
+ )
+
+ // 锚定到根会话:sidebar 始终显示根会话信息,hover 切换 subagent 时 sidebar 不会重绘
+ const rootSessionID = createMemo(() => tree().rootID)
+ const rootSession = createMemo(() => sync.session.get(rootSessionID())!)
+ const isInSubagent = createMemo(() => props.sessionID !== rootSessionID())
+
+ // 所有展示信息都基于根会话,保持 sidebar 稳定
+ const diff = createMemo(() => sync.data.session_diff[rootSessionID()] ?? [])
+ const todo = createMemo(() => sync.data.todo[rootSessionID()] ?? [])
+ const messages = createMemo(() => sync.data.message[rootSessionID()] ?? [])
+
+ const subagentSessions = createMemo(() => {
+ const t = tree()
+ const out: Array<{ session: (typeof sync.data.session)[number]; depth: number }> = []
+
+ for (const item of t.list) {
+ if (item.id === t.rootID) continue
+ const s = t.sessionByID.get(item.id)
+ if (!s) continue
+ out.push({ session: s, depth: item.depth })
+ }
+
+ return out
+ })
+
+ const getSessionStatus = (sessionID: string) => {
+ const status = sync.data.session_status?.[sessionID] as { type?: string } | undefined
+ return sessionRunState(status)
+ }
+
+ // Parent 悬停状态
+ const [parentHovered, setParentHovered] = createSignal(false)
+
// Sort MCP servers alphabetically for consistent display order
const mcpEntries = createMemo(() => Object.entries(sync.data.mcp).sort(([a], [b]) => a.localeCompare(b)))
@@ -69,11 +109,10 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
const gettingStartedDismissed = createMemo(() => kv.get("dismissed_getting_started", false))
return (
-
+
- {session().title}
+ {rootSession().title}
-
- {session().share!.url}
+
+ {rootSession().share!.url}
@@ -202,6 +241,100 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
+ {/* Subagents Section */}
+ 0}>
+
+ setExpanded("subagents", !expanded.subagents)}
+ >
+ {expanded.subagents ? "▼" : "▶"}
+
+ Subagents
+
+ ({subagentSessions().length})
+
+
+
+
+ {/* Parent 链接:始终显示,避免布局移位导致闪屏 */}
+ {
+ setParentHovered(true)
+ if (isInSubagent()) {
+ route.navigate({ type: "session", sessionID: rootSessionID() })
+ }
+ }}
+ onMouseOut={() => setParentHovered(false)}
+ onMouseDown={() => {
+ if (isInSubagent()) {
+ route.navigate({ type: "session", sessionID: rootSessionID() })
+ }
+ }}
+ >
+ ↑
+
+ {!isInSubagent() ? Parent : Parent}
+
+ {" "}
+ {(() => {
+ const title = rootSession()?.title ?? "Primary"
+ return title.length > 25 ? title.slice(0, 25) + "…" : title
+ })()}
+
+
+
+
+ {(item) => {
+ const sub = item.session
+ const status = createMemo(() => getSessionStatus(sub.id))
+ const isCurrent = createMemo(() => sub.id === props.sessionID)
+ const [hovered, setHovered] = createSignal(false)
+ const statusColor = createMemo(() => {
+ if (status() === "working") return theme.warning
+ if (status() === "waiting") return theme.accent
+ return theme.success
+ })
+ const statusIcon = createMemo(() => {
+ if (status() === "working") return "◐"
+ if (status() === "waiting") return "◎"
+ return "•"
+ })
+ const label = createMemo(() => {
+ const indent = item.depth > 0 ? `${" ".repeat(item.depth - 1)}↳ ` : ""
+ const title = sub.title ?? sub.id.slice(0, 16)
+ return indent + title
+ })
+ return (
+ {
+ setHovered(true)
+ if (!isCurrent()) route.navigate({ type: "session", sessionID: sub.id })
+ }}
+ onMouseOut={() => setHovered(false)}
+ onMouseDown={() => !isCurrent() && route.navigate({ type: "session", sessionID: sub.id })}
+ >
+
+ {statusIcon()}
+
+
+ {isCurrent() || hovered() ? {label()} : label()}
+ {status() === "waiting" && (waiting)}
+
+
+ )
+ }}
+
+
+
+
0 && todo().some((t) => t.status !== "completed")}>
right").describe("Next child session"),
session_child_cycle_reverse: z.string().optional().default("left").describe("Previous child session"),
session_parent: z.string().optional().default("up").describe("Go to parent session"),
+ session_child_list: z.string().optional().default("down").describe("List child sessions"),
terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"),
terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"),
tips_toggle: z.string().optional().default("h").describe("Toggle tips on home screen"),
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 9543e5b5796d..13031c31770b 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -1318,6 +1318,10 @@ export type KeybindsConfig = {
* Go to parent session
*/
session_parent?: string
+ /**
+ * List child sessions
+ */
+ session_child_list?: string
/**
* Suspend terminal
*/
diff --git a/packages/web/package.json b/packages/web/package.json
index b33c6524e4cf..caa07c4e6543 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -35,7 +35,7 @@
"vscode-languageserver-types": "3.17.5"
},
"devDependencies": {
- "opencode": "workspace:*",
+ "overaicoding": "workspace:*",
"@types/node": "catalog:",
"@astrojs/check": "0.9.6",
"typescript": "catalog:"