Skip to content

Commit ac93f6b

Browse files
authored
Merge branch 'dev' into feat/cli-server-mode
2 parents 5273e94 + 9989c34 commit ac93f6b

45 files changed

Lines changed: 1209 additions & 442 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@deepagent-code/app",
3-
"version": "1.4.2",
3+
"version": "1.4.3",
44
"description": "",
55
"type": "module",
66
"exports": {

packages/app/src/app.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Font } from "@deepagent-code/ui/font"
99
import { Splash } from "@deepagent-code/ui/logo"
1010
import { ThemeProvider } from "@deepagent-code/ui/theme/context"
1111
import { MetaProvider } from "@solidjs/meta"
12-
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
12+
import { type BaseRouterProps, Navigate, Route, Router, useLocation, useNavigate } from "@solidjs/router"
1313
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
1414
import { Effect } from "effect"
1515
import {
@@ -45,7 +45,7 @@ import { PromptProvider } from "@/context/prompt"
4545
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
4646
import { SettingsProvider } from "@/context/settings"
4747
import { TerminalProvider } from "@/context/terminal"
48-
import { TabsProvider } from "@/context/tabs"
48+
import { TabsProvider, useTabs } from "@/context/tabs"
4949
import { WslServersProvider } from "@/wsl/context"
5050
import DirectoryLayout from "@/pages/directory-layout"
5151
import Layout from "@/pages/layout"
@@ -143,6 +143,23 @@ function SessionProviders(props: ParentProps) {
143143
}
144144

145145
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
146+
const tabs = useTabs()
147+
const navigate = useNavigate()
148+
const location = useLocation()
149+
150+
// On startup: as soon as persisted tabs are loaded (local disk, no HTTP calls),
151+
// navigate to the last visited project's session list. This matches opencode's approach —
152+
// use local/cached data to drive the first frame, let the server data fill in asynchronously.
153+
// We navigate to /:dir/session (no specific session ID) so autoselecting can handle
154+
// the final session selection independently without conflicting navigation.
155+
createEffect(() => {
156+
if (!tabs.ready()) return
157+
if (location.pathname !== "/") return
158+
const first = tabs.store[0]
159+
if (!first) return
160+
navigate(`/${first.dirBase64}/session`, { replace: true })
161+
})
162+
146163
return (
147164
<AppShellProviders>
148165
{/*<Suspense fallback={<Loading />}>*/}

packages/app/src/components/deepagent/oversight-dashboard.tsx

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,16 @@ function MetricCard(props: { label: string; value: string; tone?: "ok" | "warn"
5555
)
5656
}
5757

58-
export const OversightDashboard: Component = () => {
58+
// Phase 2: OversightDashboard now accepts a selected subagent session ID so takeover/rollback
59+
// automatically target the chosen session, and trace nodes can reverse-select a subagent.
60+
export type OversightDashboardProps = {
61+
/** When provided, takeover and rollback default to this session ID. */
62+
selectedSessionID?: string
63+
/** Called when a trace node with a sessionID is clicked so the caller can select that subagent. */
64+
onSessionSelect?: (sessionID: string) => void
65+
}
66+
67+
export const OversightDashboard: Component<OversightDashboardProps> = (props) => {
5968
const sdk = useSDK()
6069
const language = useLanguage()
6170
const client = () => sdk.client as unknown as OversightClient
@@ -118,7 +127,9 @@ export const OversightDashboard: Component = () => {
118127
if (!reason) return
119128
setTakeoverBusy(true)
120129
setTakeoverNote(null)
121-
const result = await recordHumanTakeover(client(), { reason })
130+
// Phase 2: automatically pass the selected session ID when present.
131+
const sessionID = props.selectedSessionID
132+
const result = await recordHumanTakeover(client(), { reason, ...(sessionID ? { sessionID } : {}) })
122133
setTakeoverBusy(false)
123134
if (result.ok) {
124135
setTakeoverReason("")
@@ -133,21 +144,25 @@ export const OversightDashboard: Component = () => {
133144
}
134145

135146
// ── §D2 rollback (P4.4) ─────────────────────────────────────────────────────────
136-
const [rollbackSession, setRollbackSession] = createSignal("")
147+
// Phase 2: rollback input is pre-seeded with the selected session ID. Users may override it.
137148
const [rollbackReason, setRollbackReason] = createSignal("")
138149
const [rollbackBusy, setRollbackBusy] = createSignal(false)
139150
const [rollbackNote, setRollbackNote] = createSignal<string | null>(null)
140151

152+
// The effective session ID for rollback: the input field overrides; falls back to selectedSessionID.
153+
const [rollbackSessionOverride, setRollbackSessionOverride] = createSignal("")
154+
const effectiveRollbackSession = () => rollbackSessionOverride() || props.selectedSessionID || ""
155+
141156
const submitRollback = async () => {
142-
const sessionID = rollbackSession().trim()
157+
const sessionID = effectiveRollbackSession().trim()
143158
if (!sessionID) return
144159
const reason = rollbackReason().trim()
145160
setRollbackBusy(true)
146161
setRollbackNote(null)
147162
const result = await recordRollback(client(), { sessionID, ...(reason ? { reason } : {}) })
148163
setRollbackBusy(false)
149164
if (result.ok) {
150-
setRollbackSession("")
165+
setRollbackSessionOverride("")
151166
setRollbackReason("")
152167
setRollbackNote(
153168
result.record?.outcome === "noop"
@@ -328,6 +343,17 @@ export const OversightDashboard: Component = () => {
328343
</div>
329344
</Show>
330345
<div class="text-11-regular text-text-weaker">{fmtTime(node.createdAt)}</div>
346+
{/* Phase 2: if this node is tied to a specific subagent session, offer
347+
a reverse-select link so the user can jump to that subagent row. */}
348+
<Show when={node.sessionID && props.onSessionSelect}>
349+
<button
350+
type="button"
351+
class="mt-0.5 text-11-regular text-text-link hover:underline"
352+
onClick={() => props.onSessionSelect!(node.sessionID!)}
353+
>
354+
{language.t("oversight.trace.selectAgent")}
355+
</button>
356+
</Show>
331357
</div>
332358
</div>
333359
)}
@@ -372,11 +398,13 @@ export const OversightDashboard: Component = () => {
372398
<section>
373399
<h3 class="mb-1 text-13-medium text-text-strong">{language.t("oversight.rollback.title")}</h3>
374400
<p class="mb-2 text-11-regular text-text-weak">{language.t("oversight.rollback.description")}</p>
401+
{/* Phase 2: when a subagent is selected the placeholder is replaced by the session ID.
402+
Typing in this field overrides the pre-selected session. */}
375403
<input
376404
class="w-full rounded-md border border-border-weak-base bg-surface-base px-2 py-1.5 text-12-regular text-text-strong outline-none focus:ring-2 focus:ring-accent-base font-mono"
377-
placeholder={language.t("oversight.rollback.sessionPlaceholder")}
378-
value={rollbackSession()}
379-
onInput={(e) => setRollbackSession(e.currentTarget.value)}
405+
placeholder={props.selectedSessionID || language.t("oversight.rollback.sessionPlaceholder")}
406+
value={rollbackSessionOverride()}
407+
onInput={(e) => setRollbackSessionOverride(e.currentTarget.value)}
380408
/>
381409
<textarea
382410
class="mt-2 w-full rounded-md border border-border-weak-base bg-surface-base px-2 py-1.5 text-12-regular text-text-strong outline-none resize-none focus:ring-2 focus:ring-accent-base"
@@ -391,7 +419,7 @@ export const OversightDashboard: Component = () => {
391419
size="small"
392420
icon="reset"
393421
onClick={submitRollback}
394-
disabled={rollbackBusy() || !rollbackSession().trim()}
422+
disabled={rollbackBusy() || !effectiveRollbackSession().trim()}
395423
>
396424
<Show when={rollbackBusy()}>
397425
<Spinner />

packages/app/src/components/deepagent/oversight.api.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ export type OversightTraceNode = {
5454
source: string
5555
causationID?: string
5656
createdAt: number
57+
// Phase 2: trace nodes may carry the session that emitted them so the UI can reverse-select
58+
// the subagent row when the user clicks a trace entry.
59+
sessionID?: string
5760
}
5861
export type OversightTrace = { nodes: OversightTraceNode[] }
5962

@@ -148,7 +151,7 @@ export type HumanTakeoverRecord = {
148151
*/
149152
export const recordHumanTakeover = async (
150153
client: OversightClient,
151-
input: { reason: string; scope?: string },
154+
input: { reason: string; scope?: string; sessionID?: string },
152155
): Promise<{ ok: true; record?: HumanTakeoverRecord } | { ok: false; unsupported: boolean; error: string }> => {
153156
try {
154157
const response = await client.client.request<HumanTakeoverRecord>({

packages/app/src/components/session/session-header.tsx

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { DOCK_PANEL_IDS, useLayout } from "@/context/layout"
1818
import { usePlatform } from "@/context/platform"
1919
import { useServer } from "@/context/server"
2020
import { useSync } from "@/context/sync"
21-
import { useTerminal } from "@/context/terminal"
21+
import { useTerminalHosts } from "@/context/terminal"
2222
import { focusTerminalById } from "@/pages/session/helpers"
2323
import { useSessionLayout } from "@/pages/session/session-layout"
2424
import { PANEL_VIEW_META } from "@/pages/session/panel-view-registry"
@@ -144,7 +144,7 @@ export function SessionHeader() {
144144
const platform = usePlatform()
145145
const language = useLanguage()
146146
const sync = useSync()
147-
const terminal = useTerminal()
147+
const terminalHosts = useTerminalHosts()
148148
const { params, view } = useSessionLayout()
149149

150150
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
@@ -208,19 +208,18 @@ export function SessionHeader() {
208208
]
209209
})
210210

211-
const terminalOpen = createMemo(() => {
212-
const panel = view().panel
213-
return panel.location("terminal") === "bottom"
214-
? panel.bottom.opened() && panel.bottom.activeView() === "terminal"
215-
: view().rightPanel.mode() === "terminal"
216-
})
211+
// Phase 3: terminal toggle button in the header controls the **bottom** terminal only.
212+
const terminalOpen = createMemo(() =>
213+
view().panel.bottom.opened() && view().panel.bottom.activeView() === "terminal",
214+
)
217215

218216
const toggleTerminal = () => {
219217
view().panel.toggle("terminal")
220-
const panel = view().panel
221-
if (panel.location("terminal") === "side" && view().rightPanel.mode() !== "terminal") return
222-
const id = terminal.active()
223-
if (id) focusTerminalById(id)
218+
// Focus the bottom terminal's active pane after opening.
219+
if (terminalOpen()) {
220+
const id = terminalHosts.bottom.active()
221+
if (id) focusTerminalById(id)
222+
}
224223
}
225224

226225
const bottomPanelOpen = createMemo(() => view().panel.bottom.opened())

packages/app/src/context/layout.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ type SessionView = {
105105
// U3/U4/U7: added "worktree" (isolated worktree diff/merge), "subagents" (child-session list),
106106
// "browser" (isolated WebContentsView).
107107
// T3.2: the "menu" mode is gone — an always-on icon rail replaced the full-panel menu list.
108+
// Phase 2: "oversight" removed as standalone panel; kept in union only for backward-compat
109+
// migration — any stored "oversight" is silently mapped to "subagents" at read time.
108110
rightPanelMode?:
109111
| "review"
110112
| "files"
@@ -326,7 +328,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
326328
{ ...target, migrate },
327329
createStore({
328330
sidebar: {
329-
opened: false,
331+
opened: true,
330332
width: DEFAULT_SIDEBAR_WIDTH,
331333
workspaces: {} as Record<string, boolean>,
332334
workspacesDefault: false,
@@ -731,19 +733,24 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
731733
},
732734
},
733735
dock: {
734-
// Deprecated compatibility metadata. New UI must use view(session).panel;
735-
// only locations and the shared Bottom Panel dimension remain global.
736+
// Phase 3: terminal is now side-native in the right panel (independent host).
737+
// dock.location("terminal") is forced to "bottom" to avoid stale stored "side"
738+
// values making the bottom terminal unreachable. Move operations for terminal
739+
// are intentionally no-ops; only debug-console and problems remain movable.
736740
location(id: DockPanelID): DockLocation {
741+
if (id === "terminal") return "bottom"
737742
return store.dock?.location?.[id] ?? DOCK_DEFAULT_LOCATION[id]
738743
},
739744
setLocation(id: DockPanelID, location: DockLocation) {
745+
if (id === "terminal") return // terminal location is now fixed
740746
if (!store.dock) {
741747
setStore("dock", { location: { [id]: location } as Record<DockPanelID, DockLocation> })
742748
return
743749
}
744750
setStore("dock", "location", id, location)
745751
},
746752
move(id: DockPanelID) {
753+
if (id === "terminal") return // terminal is no longer movable
747754
const current = store.dock?.location?.[id] ?? DOCK_DEFAULT_LOCATION[id]
748755
const next: DockLocation = current === "bottom" ? "side" : "bottom"
749756
if (!store.dock) {
@@ -753,7 +760,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
753760
setStore("dock", "location", id, next)
754761
},
755762
bottomCount: createMemo(
756-
() => DOCK_PANEL_IDS.filter((id) => (store.dock?.location?.[id] ?? DOCK_DEFAULT_LOCATION[id]) === "bottom").length,
763+
() => DOCK_PANEL_IDS.filter((id) => (id === "terminal" ? true : (store.dock?.location?.[id] ?? DOCK_DEFAULT_LOCATION[id]) === "bottom")).length,
757764
),
758765
},
759766
review: {
@@ -905,7 +912,13 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
905912
const key = createSessionKeyReader(sessionKey, ensureKey)
906913
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
907914
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true)
908-
const rightPanelMode = createMemo(() => store.sessionView[key()]?.rightPanelMode)
915+
const rightPanelMode = createMemo(() => {
916+
const mode = store.sessionView[key()]?.rightPanelMode
917+
// Phase 2 backward-compat: "oversight" was merged into "subagents". Any session that
918+
// persisted "oversight" should silently open the unified subagents panel instead.
919+
if (mode === "oversight") return "subagents" as SessionRightPanelMode
920+
return mode
921+
})
909922
const bottomPanel = createMemo(() => {
910923
const current = store.sessionView[key()]
911924
// One-way compatibility migration from the pre-panel global terminal flag.

packages/app/src/context/terminal.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,34 @@ describe("runtime terminal controller", () => {
339339
}
340340
})
341341

342+
test("collapses a split to its surviving pane when closing that pane's final terminal", async () => {
343+
let sequence = 0
344+
const harness = terminalHarness(async () => {
345+
sequence += 1
346+
return { data: { id: `pty-${sequence}`, title: `Terminal ${sequence}` } }
347+
})
348+
try {
349+
await harness.session.new()
350+
harness.session.setPaneBounds(harness.session.focusedPaneId(), { width: 1_000, height: 400 })
351+
expect(await harness.session.split("horizontal")).toBeTrue()
352+
353+
const [first, second] = harness.session.all()
354+
expect(first).toBeDefined()
355+
expect(second).toBeDefined()
356+
await harness.session.close(second!.id)
357+
358+
const root = harness.session.root()
359+
expect(root.kind).toBe("leaf")
360+
if (root.kind !== "leaf") throw new Error("expected split to collapse to a leaf")
361+
expect(root.ptys).toEqual([first!.id])
362+
expect(root.activeId).toBe(first!.id)
363+
expect(harness.session.focusedPaneId()).toBe(root.id)
364+
expect(harness.session.closeRequest()).toBe(0)
365+
} finally {
366+
harness.dispose()
367+
}
368+
})
369+
342370
test("requests panel close only when the user closes the final tab", async () => {
343371
const harness = terminalHarness(async () => ({ data: { id: "pty-server-1", title: "Terminal 1" } }))
344372
try {

0 commit comments

Comments
 (0)