mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-27 21:51:49 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a146b0d34 |
@@ -0,0 +1,125 @@
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectSessionTitle } from "../utils/waits"
|
||||
|
||||
const directory = "C:/OpenCode/ChildSessionPicker"
|
||||
const projectID = "proj_child_session_picker"
|
||||
const rootID = "ses_root"
|
||||
const newestRunningID = "ses_new_running"
|
||||
const olderRunningID = "ses_old_running"
|
||||
const newestIdleID = "ses_new_idle"
|
||||
|
||||
test.use({ viewport: { width: 1280, height: 800 }, reducedMotion: "reduce" })
|
||||
|
||||
test("opens from the hotkey and navigates running child agents first", async ({ page }) => {
|
||||
await mockOpenCodeServer(page, {
|
||||
directory,
|
||||
project: {
|
||||
id: projectID,
|
||||
worktree: directory,
|
||||
vcs: "git",
|
||||
name: "child-session-picker",
|
||||
time: { created: 1700000000000, updated: 1700000000000 },
|
||||
sandboxes: [],
|
||||
},
|
||||
provider: {
|
||||
all: [
|
||||
{
|
||||
id: "opencode",
|
||||
name: "OpenCode",
|
||||
models: {
|
||||
"claude-opus-4-6": {
|
||||
id: "claude-opus-4-6",
|
||||
name: "Claude Opus 4.6",
|
||||
limit: { context: 200_000 },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connected: ["opencode"],
|
||||
default: { providerID: "opencode", modelID: "claude-opus-4-6" },
|
||||
},
|
||||
sessions: [
|
||||
session(rootID, "Coordinate the release", 1),
|
||||
session(olderRunningID, "Review accessibility (@review subagent)", 2, "review"),
|
||||
session(newestRunningID, "Trace the session state (@explore subagent)", 4, "explore"),
|
||||
session(newestIdleID, "Summarize the API (@docs subagent)", 5, "docs"),
|
||||
],
|
||||
statuses: {
|
||||
[olderRunningID]: { type: "busy" },
|
||||
[newestRunningID]: { type: "busy" },
|
||||
[newestIdleID]: { type: "idle" },
|
||||
},
|
||||
pageMessages: () => ({ items: [] }),
|
||||
})
|
||||
await configurePage(page)
|
||||
|
||||
await page.goto(sessionHref(rootID))
|
||||
await expectSessionTitle(page, "Coordinate the release")
|
||||
const input = page.getByRole("textbox", { name: /Ask anything/ })
|
||||
await expect(input).toBeVisible()
|
||||
|
||||
await page.keyboard.press("Control+Shift+ArrowDown")
|
||||
await expect(page.getByText("Child agents", { exact: true })).toBeVisible()
|
||||
await expect(input).toBeHidden()
|
||||
|
||||
const options = page.getByRole("option")
|
||||
await expect(options).toHaveCount(3)
|
||||
await expect(options.nth(0)).toContainText("@explore")
|
||||
await expect(options.nth(1)).toContainText("@review")
|
||||
await expect(options.nth(2)).toContainText("@docs")
|
||||
await expect(options.nth(0)).toBeFocused()
|
||||
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(page.getByText("Child agents", { exact: true })).toBeHidden()
|
||||
await expect(input).toBeFocused()
|
||||
|
||||
await page.keyboard.press("Control+Shift+ArrowDown")
|
||||
await expect(options.nth(0)).toBeFocused()
|
||||
await page.keyboard.press("ArrowDown")
|
||||
await expect(options.nth(1)).toBeFocused()
|
||||
await page.keyboard.press("Enter")
|
||||
await expect(page).toHaveURL(sessionHref(olderRunningID))
|
||||
await expectSessionTitle(page, "Review accessibility")
|
||||
})
|
||||
|
||||
function session(id: string, title: string, updated: number, agent?: string) {
|
||||
return {
|
||||
id,
|
||||
slug: id,
|
||||
projectID,
|
||||
directory,
|
||||
parentID: id === rootID ? undefined : rootID,
|
||||
title,
|
||||
agent,
|
||||
version: "dev",
|
||||
time: { created: updated, updated },
|
||||
}
|
||||
}
|
||||
|
||||
async function configurePage(page: Page) {
|
||||
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||
await page.addInitScript(
|
||||
({ directory, dirBase64, server, sessionID }) => {
|
||||
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
|
||||
localStorage.setItem(
|
||||
"opencode.global.dat:server",
|
||||
JSON.stringify({
|
||||
projects: { local: [{ worktree: directory, expanded: true }] },
|
||||
lastProject: { local: directory },
|
||||
}),
|
||||
)
|
||||
localStorage.setItem(
|
||||
"opencode.global.dat:tabs",
|
||||
JSON.stringify([{ type: "session", server, dirBase64, sessionId: sessionID }]),
|
||||
)
|
||||
},
|
||||
{ directory, dirBase64: base64Encode(directory), server, sessionID: rootID },
|
||||
)
|
||||
}
|
||||
|
||||
function sessionHref(sessionID: string) {
|
||||
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||
return `/server/${base64Encode(server)}/session/${sessionID}`
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Page, Route } from "@playwright/test"
|
||||
|
||||
const emptyList = new Set(["/skill", "/command", "/lsp", "/formatter", "/vcs/status", "/vcs/diff"])
|
||||
const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"])
|
||||
const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp"])
|
||||
|
||||
export interface MockServerConfig {
|
||||
provider: unknown
|
||||
@@ -17,6 +17,7 @@ export interface MockServerConfig {
|
||||
todos?: (sessionID: string) => unknown[]
|
||||
permissions?: unknown[] | (() => unknown[])
|
||||
questions?: unknown[] | (() => unknown[])
|
||||
statuses?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
||||
@@ -36,6 +37,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
||||
"/agent": [{ name: "build", mode: "primary" }],
|
||||
"/vcs": { branch: "main", default_branch: "main" },
|
||||
"/session": config.sessions,
|
||||
"/session/status": config.statuses ?? {},
|
||||
}
|
||||
|
||||
await page.route("**/*", async (route) => {
|
||||
|
||||
@@ -46,6 +46,8 @@ export const dict = {
|
||||
"command.language.set": "Use language: {{language}}",
|
||||
|
||||
"command.session.new": "New session",
|
||||
"command.session.child": "View child agents",
|
||||
"command.session.child.description": "Switch to a child agent session",
|
||||
"command.file.open": "Open file",
|
||||
"command.tab.close": "Close tab",
|
||||
"command.context.addSelection": "Add selection to context",
|
||||
@@ -242,6 +244,12 @@ export const dict = {
|
||||
"prompt.mode.shell.exit": "esc to exit",
|
||||
"session.child.promptDisabled": "Subagent sessions cannot be prompted.",
|
||||
"session.child.backToParent": "Back to main session.",
|
||||
"session.child.picker.title": "Child agents",
|
||||
"session.child.picker.description": "Choose an agent to view its session.",
|
||||
"session.child.picker.hint": "Use ↑↓ to navigate · Enter to open",
|
||||
"session.child.picker.agentFallback": "agent",
|
||||
"session.child.picker.running": "Running",
|
||||
"session.child.picker.complete": "Complete",
|
||||
|
||||
"prompt.example.1": "Fix a TODO in the codebase",
|
||||
"prompt.example.2": "What is the tech stack of this project?",
|
||||
|
||||
@@ -787,6 +787,20 @@ export default function Page() {
|
||||
inputRef?.focus()
|
||||
}
|
||||
|
||||
const closeChildSessions = () => {
|
||||
composer.closeChildSessions()
|
||||
if (isChildSession()) return
|
||||
requestAnimationFrame(() => inputRef?.focus())
|
||||
}
|
||||
|
||||
const toggleChildSessions = () => {
|
||||
if (composer.childSessionPicker()) {
|
||||
closeChildSessions()
|
||||
return
|
||||
}
|
||||
composer.openChildSessions()
|
||||
}
|
||||
|
||||
useComposerCommands()
|
||||
useSettingsCommand()
|
||||
useSessionCommands({
|
||||
@@ -794,6 +808,9 @@ export default function Page() {
|
||||
setActiveMessage,
|
||||
focusInput,
|
||||
review: reviewTab,
|
||||
childSessions: () => composer.childSessions().length,
|
||||
childSessionsBlocked: composer.blocked,
|
||||
toggleChildSessions,
|
||||
})
|
||||
|
||||
const openReviewFile = createOpenReviewFile({
|
||||
@@ -1619,6 +1636,15 @@ export default function Page() {
|
||||
: legacySessionHref(sdk().directory, id),
|
||||
)
|
||||
},
|
||||
onChildSessionSelect: (id) => {
|
||||
composer.closeChildSessions()
|
||||
navigate(
|
||||
params.serverKey
|
||||
? sessionHref(requireServerKey(params.serverKey), id)
|
||||
: legacySessionHref(sdk().directory, id),
|
||||
)
|
||||
},
|
||||
onChildSessionsClose: closeChildSessions,
|
||||
setPromptRef: (el) => {
|
||||
inputRef = el
|
||||
},
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt"
|
||||
import type { Session } from "@opencode-ai/sdk/v2"
|
||||
import { For, onCleanup, onMount } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { childSessionTitle } from "./session-child-sessions"
|
||||
|
||||
export function SessionChildSessionPicker(props: {
|
||||
sessions: Session[]
|
||||
activeID?: string
|
||||
working: (sessionID: string) => boolean
|
||||
onSelect: (sessionID: string) => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
const language = useLanguage()
|
||||
const [store, setStore] = createStore({
|
||||
active: Math.max(
|
||||
0,
|
||||
props.sessions.findIndex((session) => session.id === props.activeID),
|
||||
),
|
||||
})
|
||||
const refs: HTMLButtonElement[] = []
|
||||
let frame: number | undefined
|
||||
|
||||
const focus = (index: number) => {
|
||||
const total = props.sessions.length
|
||||
if (total === 0) return
|
||||
const next = (index + total) % total
|
||||
setStore("active", next)
|
||||
refs[next]?.focus()
|
||||
refs[next]?.scrollIntoView({ block: "nearest" })
|
||||
}
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
props.onClose()
|
||||
return
|
||||
}
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault()
|
||||
focus(store.active + 1)
|
||||
return
|
||||
}
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault()
|
||||
focus(store.active - 1)
|
||||
return
|
||||
}
|
||||
if (event.key === "Home") {
|
||||
event.preventDefault()
|
||||
focus(0)
|
||||
return
|
||||
}
|
||||
if (event.key === "End") {
|
||||
event.preventDefault()
|
||||
focus(props.sessions.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
frame = requestAnimationFrame(() => focus(store.active))
|
||||
})
|
||||
onCleanup(() => {
|
||||
if (frame !== undefined) cancelAnimationFrame(frame)
|
||||
})
|
||||
|
||||
return (
|
||||
<DockPrompt
|
||||
kind="question"
|
||||
onKeyDown={onKeyDown}
|
||||
header={
|
||||
<div class="flex min-w-0 flex-1 items-center justify-between gap-3">
|
||||
<div data-slot="question-header-title">{language.t("session.child.picker.title")}</div>
|
||||
<div class="shrink-0 text-12-regular text-text-weak">{props.sessions.length}</div>
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" size="large" onClick={props.onClose} aria-keyshortcuts="Escape">
|
||||
{language.t("common.dismiss")}
|
||||
</Button>
|
||||
<div data-slot="question-footer-actions" class="text-12-regular text-text-weak">
|
||||
{language.t("session.child.picker.hint")}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div data-slot="question-text">{language.t("session.child.picker.description")}</div>
|
||||
<div
|
||||
data-slot="question-options"
|
||||
role="listbox"
|
||||
aria-label={language.t("session.child.picker.title")}
|
||||
class="max-h-64"
|
||||
>
|
||||
<For each={props.sessions}>
|
||||
{(session, index) => {
|
||||
const running = () => props.working(session.id)
|
||||
return (
|
||||
<button
|
||||
ref={(el) => (refs[index()] = el)}
|
||||
type="button"
|
||||
role="option"
|
||||
data-slot="question-option"
|
||||
data-picked={session.id === props.activeID}
|
||||
aria-selected={session.id === props.activeID}
|
||||
onFocus={() => setStore("active", index())}
|
||||
onClick={() => props.onSelect(session.id)}
|
||||
>
|
||||
<span
|
||||
classList={{
|
||||
"mt-1.5 size-2 shrink-0 rounded-full": true,
|
||||
"bg-icon-success-base": running(),
|
||||
"bg-icon-weak-base": !running(),
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span data-slot="question-option-main">
|
||||
<span data-slot="option-label" class="flex items-center justify-between gap-3">
|
||||
<span class="truncate">@{session.agent ?? language.t("session.child.picker.agentFallback")}</span>
|
||||
<span
|
||||
classList={{
|
||||
"shrink-0 text-12-regular": true,
|
||||
"text-text-base": running(),
|
||||
"text-text-weak": !running(),
|
||||
}}
|
||||
>
|
||||
{running()
|
||||
? language.t("session.child.picker.running")
|
||||
: language.t("session.child.picker.complete")}
|
||||
</span>
|
||||
</span>
|
||||
<span data-slot="option-description" class="line-clamp-2">
|
||||
{childSessionTitle(session)}
|
||||
</span>
|
||||
</span>
|
||||
<Icon name="chevron-right" size="small" class="mt-1 shrink-0 text-icon-weak" aria-hidden="true" />
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</DockPrompt>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { Session } from "@opencode-ai/sdk/v2"
|
||||
import { childSessionTitle, listChildSessions } from "./session-child-sessions"
|
||||
|
||||
const session = (input: { id: string; parentID?: string; updated?: number; title?: string; agent?: string }) =>
|
||||
({
|
||||
id: input.id,
|
||||
parentID: input.parentID,
|
||||
title: input.title ?? input.id,
|
||||
agent: input.agent,
|
||||
time: { created: 1, updated: input.updated ?? 1 },
|
||||
}) as Session
|
||||
|
||||
describe("child session picker", () => {
|
||||
test("lists siblings from a root or child session", () => {
|
||||
const sessions = [
|
||||
session({ id: "root" }),
|
||||
session({ id: "first", parentID: "root" }),
|
||||
session({ id: "second", parentID: "root" }),
|
||||
session({ id: "nested", parentID: "first" }),
|
||||
]
|
||||
|
||||
expect(listChildSessions({ sessions, currentID: "root", working: () => false }).map((item) => item.id)).toEqual([
|
||||
"second",
|
||||
"first",
|
||||
])
|
||||
expect(listChildSessions({ sessions, currentID: "first", working: () => false }).map((item) => item.id)).toEqual([
|
||||
"second",
|
||||
"first",
|
||||
])
|
||||
})
|
||||
|
||||
test("sorts running sessions first and newest sessions within each group", () => {
|
||||
const sessions = [
|
||||
session({ id: "root" }),
|
||||
session({ id: "old-idle", parentID: "root", updated: 1 }),
|
||||
session({ id: "new-idle", parentID: "root", updated: 4 }),
|
||||
session({ id: "old-running", parentID: "root", updated: 2 }),
|
||||
session({ id: "new-running", parentID: "root", updated: 3 }),
|
||||
]
|
||||
const running = new Set(["old-running", "new-running"])
|
||||
|
||||
expect(
|
||||
listChildSessions({ sessions, currentID: "root", working: (id) => running.has(id) }).map((item) => item.id),
|
||||
).toEqual(["new-running", "old-running", "new-idle", "old-idle"])
|
||||
})
|
||||
|
||||
test("removes the generated subagent suffix from descriptions", () => {
|
||||
expect(
|
||||
childSessionTitle(session({ id: "child", title: "Inspect the composer (@explore subagent)", agent: "explore" })),
|
||||
).toBe("Inspect the composer")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { Session } from "@opencode-ai/sdk/v2"
|
||||
|
||||
export function listChildSessions(input: {
|
||||
sessions: Session[]
|
||||
currentID?: string
|
||||
working: (sessionID: string) => boolean
|
||||
}) {
|
||||
const current = input.currentID ? input.sessions.find((session) => session.id === input.currentID) : undefined
|
||||
const parentID = current?.parentID ?? current?.id
|
||||
if (!parentID) return []
|
||||
|
||||
return input.sessions
|
||||
.filter((session) => session.parentID === parentID)
|
||||
.toSorted((a, b) => {
|
||||
const running = Number(input.working(b.id)) - Number(input.working(a.id))
|
||||
if (running !== 0) return running
|
||||
if (a.time.updated !== b.time.updated) return b.time.updated - a.time.updated
|
||||
return b.id.localeCompare(a.id)
|
||||
})
|
||||
}
|
||||
|
||||
export function childSessionTitle(session: Session) {
|
||||
return session.title.replace(/\s+\(@[^)]+ subagent\)$/, "") || session.title
|
||||
}
|
||||
@@ -36,6 +36,8 @@ export function createSessionComposerRegionController(input: {
|
||||
revert: Accessor<SessionComposerRevertDock | undefined>
|
||||
onResponseSubmit: () => void
|
||||
openParent: () => void
|
||||
onChildSessionSelect: (sessionID: string) => void
|
||||
onChildSessionsClose: () => void
|
||||
setPromptRef: (el: HTMLDivElement) => void
|
||||
setDockRef: (el: HTMLDivElement) => void
|
||||
}) {
|
||||
@@ -127,11 +129,14 @@ export function createSessionComposerRegionController(input: {
|
||||
revert: input.revert,
|
||||
onResponseSubmit: input.onResponseSubmit,
|
||||
openParent: input.openParent,
|
||||
onChildSessionSelect: input.onChildSessionSelect,
|
||||
onChildSessionsClose: input.onChildSessionsClose,
|
||||
setPromptRef: input.setPromptRef,
|
||||
setDockRef: input.setDockRef,
|
||||
sessionID: input.sessionID,
|
||||
parentID,
|
||||
child: () => !!parentID(),
|
||||
showComposer: () => !input.state.blocked() || !!parentID(),
|
||||
showComposer: () => (!input.state.blocked() || !!parentID()) && !input.state.childSessionPicker(),
|
||||
handoffPrompt: () => getSessionHandoff(input.sessionKey())?.prompt,
|
||||
promptReady: () => input.prompt.ready() || promptReady(),
|
||||
dock: () => (store.ready && input.state.dock()) || value() > 0.001,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SessionFollowupDock } from "@/pages/session/composer/session-followup-d
|
||||
import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
|
||||
import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
|
||||
import type { SessionComposerRegionController } from "./session-composer-region-controller"
|
||||
import { SessionChildSessionPicker } from "./session-child-session-picker"
|
||||
|
||||
export function SessionComposerRegion(props: {
|
||||
controller: SessionComposerRegionController
|
||||
@@ -59,6 +60,16 @@ export function SessionComposerRegion(props: {
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Show when={controller.state.childSessionPicker()}>
|
||||
<SessionChildSessionPicker
|
||||
sessions={controller.state.childSessions()}
|
||||
activeID={controller.parentID() ? controller.sessionID() : undefined}
|
||||
working={controller.state.childSessionWorking}
|
||||
onSelect={controller.onChildSessionSelect}
|
||||
onClose={controller.onChildSessionsClose}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={controller.showComposer()}>
|
||||
<Show when={controller.dock()}>
|
||||
<div
|
||||
|
||||
@@ -9,6 +9,7 @@ import { usePermission } from "@/context/permission"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
|
||||
import { listChildSessions } from "./session-child-sessions"
|
||||
|
||||
export const todoState = (input: {
|
||||
count: number
|
||||
@@ -60,10 +61,18 @@ export function createSessionComposerController(options?: { closeMs?: number | (
|
||||
)
|
||||
|
||||
const live = createMemo(() => sync().data.session_working(params.id ?? "") || blocked())
|
||||
const childSessions = createMemo(() =>
|
||||
listChildSessions({
|
||||
sessions: sync().data.session,
|
||||
currentID: params.id,
|
||||
working: sync().data.session_working.bind(sync().data),
|
||||
}),
|
||||
)
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
sessionID: params.id,
|
||||
responding: undefined as string | undefined,
|
||||
childSessionPicker: false,
|
||||
dock: todos().length > 0 && !done() && live(),
|
||||
closing: false,
|
||||
opening: false,
|
||||
@@ -75,6 +84,11 @@ export function createSessionComposerController(options?: { closeMs?: number | (
|
||||
return store.responding === perm.id
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!blocked()) return
|
||||
setStore("childSessionPicker", false)
|
||||
})
|
||||
|
||||
const decide = (response: "once" | "always" | "reject") => {
|
||||
const perm = permissionRequest()
|
||||
if (!perm) return
|
||||
@@ -133,7 +147,13 @@ export function createSessionComposerController(options?: { closeMs?: number | (
|
||||
if (!previous || previous[0] !== id) {
|
||||
if (timer) window.clearTimeout(timer)
|
||||
timer = undefined
|
||||
setStore({ sessionID: id, dock: todoDockAtBoundary(next), closing: false, opening: false })
|
||||
setStore({
|
||||
sessionID: id,
|
||||
childSessionPicker: false,
|
||||
dock: todoDockAtBoundary(next),
|
||||
closing: false,
|
||||
opening: false,
|
||||
})
|
||||
if (next === "clear") clear()
|
||||
return
|
||||
}
|
||||
@@ -191,6 +211,14 @@ export function createSessionComposerController(options?: { closeMs?: number | (
|
||||
permissionRequest,
|
||||
permissionResponding,
|
||||
decide,
|
||||
childSessions,
|
||||
childSessionWorking: (sessionID: string) => sync().data.session_working(sessionID),
|
||||
childSessionPicker: () => store.childSessionPicker && !blocked() && childSessions().length > 0,
|
||||
openChildSessions: () => {
|
||||
if (blocked() || childSessions().length === 0) return
|
||||
setStore("childSessionPicker", true)
|
||||
},
|
||||
closeChildSessions: () => setStore("childSessionPicker", false),
|
||||
todos,
|
||||
dock: () =>
|
||||
store.sessionID === params.id
|
||||
|
||||
@@ -27,6 +27,9 @@ export type SessionCommandContext = {
|
||||
setActiveMessage: (message: UserMessage | undefined) => void
|
||||
focusInput: () => void
|
||||
review?: () => boolean
|
||||
childSessions: () => number
|
||||
childSessionsBlocked: () => boolean
|
||||
toggleChildSessions: () => void
|
||||
}
|
||||
|
||||
const withCategory = (category: string) => {
|
||||
@@ -419,6 +422,14 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
navigate(`/${params.dir}/session`)
|
||||
},
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.child.first",
|
||||
title: language.t("command.session.child"),
|
||||
description: language.t("command.session.child.description"),
|
||||
keybind: "mod+shift+arrowdown",
|
||||
disabled: actions.childSessions() === 0 || actions.childSessionsBlocked(),
|
||||
onSelect: actions.toggleChildSessions,
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.undo",
|
||||
title: language.t("command.session.undo"),
|
||||
|
||||
Reference in New Issue
Block a user