Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
53181ab
fix(provider): 修复 xhigh 推理强度三处遗漏
deepagent-ai Jul 21, 2026
6fce2e7
fix(session): 修复子agent StructuredOutput 循环推理死锁与异常退出
deepagent-ai Jul 21, 2026
6e68e58
fix(session): 修正子agent StructuredOutput 修复的3个审核 bug (B1/B2/B3)
deepagent-ai Jul 21, 2026
7a99b6b
fix(desktop): listener ready 后立即显示主界面,health check 后台化
deepagent-ai Jul 21, 2026
c09c65b
feat(task): Phase 1 子Agent成果回收 — interrupted状态+recovery pointer+durab…
deepagent-ai Jul 21, 2026
3c3c37f
fix(app): 启动时自动恢复上次会话 tab,消除空白右侧栏
deepagent-ai Jul 21, 2026
c7c6a99
feat(app): Phase 2 统一子Agent监督窗口 — subagents+oversight合并
deepagent-ai Jul 21, 2026
a39a010
feat(app): Phase 3 底栏和右侧栏使用独立终端 — bottom/side双host
deepagent-ai Jul 21, 2026
70f6d2d
fix(app): 启动导航改为同步就绪后打开最近项目,修复文件列表竞态
deepagent-ai Jul 21, 2026
5e3b5e8
perf(app): 移除 autoselecting 渲染门控,右侧栏立即可见
deepagent-ai Jul 21, 2026
6c9b2e3
perf(app): 启动提速 — 侧栏默认展开+直达最近会话+Session预加载
deepagent-ai Jul 21, 2026
36562d3
fix(desktop): 恢复健康检查门控,修复 localserver 断联问题
deepagent-ai Jul 21, 2026
dd67ca1
fix(desktop): 用 Effect.ignore 替换不存在的 Effect.orElse
deepagent-ai Jul 21, 2026
fa3093d
revert(app): 还原启动导航改动,阻止 Session 视图过早挂载
deepagent-ai Jul 21, 2026
c7d2c08
fix(app): 去掉 autoselecting 渲染门控,修复右侧栏15秒等待
deepagent-ai Jul 21, 2026
73e4f8d
perf(app): 对标 opencode — 用 tabs.ready() 驱动首帧,不等服务端
deepagent-ai Jul 21, 2026
8ed69a1
fix(app): 左侧栏默认展开,底栏/右侧栏默认关闭
deepagent-ai Jul 21, 2026
9603247
perf(core): 移除启动时串行 wal_checkpoint,减少 sidecar 启动时间 1-3s
deepagent-ai Jul 21, 2026
d8df278
perf(desktop): sidecar 进程启动时立即预加载 server bundle
deepagent-ai Jul 21, 2026
b649f30
fix(session): route async prompts through steering
deepagent-ai Jul 22, 2026
8b36b81
feat(session): add editable steer drafts
deepagent-ai Jul 22, 2026
e722446
fix(reliability): preserve durable task and event state
deepagent-ai Jul 22, 2026
b101559
fix(terminal): close the correct host after final tab
deepagent-ai Jul 22, 2026
70696f1
fix(runtime): clean up durable consumer groups
deepagent-ai Jul 22, 2026
59720ff
fix(task): preserve interrupted state and paginate history
deepagent-ai Jul 22, 2026
7edb52f
fix(tasks): preserve interruption and page cursors
deepagent-ai Jul 22, 2026
8383547
fix(app): restore active tab after server switch
deepagent-ai Jul 22, 2026
c749380
fix(app): wire tab restore and interrupted helper imports
deepagent-ai Jul 22, 2026
6f177aa
fix(app): 终端不再因项目/对话切换而关闭
deepagent-ai Jul 22, 2026
f171a9a
fix(steer): preserve all prompt parts and add canonical ID with corre…
deepagent-ai Jul 22, 2026
aebad4a
feat(cli): Server Edition server-mode — gateway login/workspace comma…
thomas-yanga Jul 22, 2026
0a9ca2a
fix(app): 恢复的终端切回项目后立即显示,不再等待 WebSocket 握手
deepagent-ai Jul 22, 2026
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/app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@deepagent-code/app",
"version": "1.4.2",
"version": "1.4.3",
"description": "",
"type": "module",
"exports": {
Expand Down
23 changes: 21 additions & 2 deletions packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Font } from "@deepagent-code/ui/font"
import { Splash } from "@deepagent-code/ui/logo"
import { ThemeProvider } from "@deepagent-code/ui/theme/context"
import { MetaProvider } from "@solidjs/meta"
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
import { type BaseRouterProps, Navigate, Route, Router, useLocation, useNavigate } from "@solidjs/router"
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
import { Effect } from "effect"
import {
Expand Down Expand Up @@ -45,7 +45,7 @@ import { PromptProvider } from "@/context/prompt"
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
import { SettingsProvider } from "@/context/settings"
import { TerminalProvider } from "@/context/terminal"
import { TabsProvider } from "@/context/tabs"
import { startupTab, TabsProvider, useTabs } from "@/context/tabs"
import { WslServersProvider } from "@/wsl/context"
import DirectoryLayout from "@/pages/directory-layout"
import Layout from "@/pages/layout"
Expand Down Expand Up @@ -143,6 +143,25 @@ function SessionProviders(props: ParentProps) {
}

function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
const tabs = useTabs()
const server = useServer()
const navigate = useNavigate()
const location = useLocation()

// Restore the explicitly persisted active tab. A cross-server directory is never
// navigated until the target server has become active.
createEffect(() => {
if (!tabs.ready()) return
if (location.pathname !== "/") return
const tab = startupTab(tabs.store, tabs.active.key, server.list)
if (!tab) return
if (server.key !== tab.server) {
server.setActive(tab.server)
return
}
navigate(`/${tab.dirBase64}/session`, { replace: true })
})

return (
<AppShellProviders>
{/*<Suspense fallback={<Loading />}>*/}
Expand Down
46 changes: 37 additions & 9 deletions packages/app/src/components/deepagent/oversight-dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,16 @@ function MetricCard(props: { label: string; value: string; tone?: "ok" | "warn"
)
}

export const OversightDashboard: Component = () => {
// Phase 2: OversightDashboard now accepts a selected subagent session ID so takeover/rollback
// automatically target the chosen session, and trace nodes can reverse-select a subagent.
export type OversightDashboardProps = {
/** When provided, takeover and rollback default to this session ID. */
selectedSessionID?: string
/** Called when a trace node with a sessionID is clicked so the caller can select that subagent. */
onSessionSelect?: (sessionID: string) => void
}

export const OversightDashboard: Component<OversightDashboardProps> = (props) => {
const sdk = useSDK()
const language = useLanguage()
const client = () => sdk.client as unknown as OversightClient
Expand Down Expand Up @@ -118,7 +127,9 @@ export const OversightDashboard: Component = () => {
if (!reason) return
setTakeoverBusy(true)
setTakeoverNote(null)
const result = await recordHumanTakeover(client(), { reason })
// Phase 2: automatically pass the selected session ID when present.
const sessionID = props.selectedSessionID
const result = await recordHumanTakeover(client(), { reason, ...(sessionID ? { sessionID } : {}) })
setTakeoverBusy(false)
if (result.ok) {
setTakeoverReason("")
Expand All @@ -133,21 +144,25 @@ export const OversightDashboard: Component = () => {
}

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

// The effective session ID for rollback: the input field overrides; falls back to selectedSessionID.
const [rollbackSessionOverride, setRollbackSessionOverride] = createSignal("")
const effectiveRollbackSession = () => rollbackSessionOverride() || props.selectedSessionID || ""

const submitRollback = async () => {
const sessionID = rollbackSession().trim()
const sessionID = effectiveRollbackSession().trim()
if (!sessionID) return
const reason = rollbackReason().trim()
setRollbackBusy(true)
setRollbackNote(null)
const result = await recordRollback(client(), { sessionID, ...(reason ? { reason } : {}) })
setRollbackBusy(false)
if (result.ok) {
setRollbackSession("")
setRollbackSessionOverride("")
setRollbackReason("")
setRollbackNote(
result.record?.outcome === "noop"
Expand Down Expand Up @@ -328,6 +343,17 @@ export const OversightDashboard: Component = () => {
</div>
</Show>
<div class="text-11-regular text-text-weaker">{fmtTime(node.createdAt)}</div>
{/* Phase 2: if this node is tied to a specific subagent session, offer
a reverse-select link so the user can jump to that subagent row. */}
<Show when={node.sessionID && props.onSessionSelect}>
<button
type="button"
class="mt-0.5 text-11-regular text-text-link hover:underline"
onClick={() => props.onSessionSelect!(node.sessionID!)}
>
{language.t("oversight.trace.selectAgent")}
</button>
</Show>
</div>
</div>
)}
Expand Down Expand Up @@ -372,11 +398,13 @@ export const OversightDashboard: Component = () => {
<section>
<h3 class="mb-1 text-13-medium text-text-strong">{language.t("oversight.rollback.title")}</h3>
<p class="mb-2 text-11-regular text-text-weak">{language.t("oversight.rollback.description")}</p>
{/* Phase 2: when a subagent is selected the placeholder is replaced by the session ID.
Typing in this field overrides the pre-selected session. */}
<input
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"
placeholder={language.t("oversight.rollback.sessionPlaceholder")}
value={rollbackSession()}
onInput={(e) => setRollbackSession(e.currentTarget.value)}
placeholder={props.selectedSessionID || language.t("oversight.rollback.sessionPlaceholder")}
value={rollbackSessionOverride()}
onInput={(e) => setRollbackSessionOverride(e.currentTarget.value)}
/>
<textarea
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"
Expand All @@ -391,7 +419,7 @@ export const OversightDashboard: Component = () => {
size="small"
icon="reset"
onClick={submitRollback}
disabled={rollbackBusy() || !rollbackSession().trim()}
disabled={rollbackBusy() || !effectiveRollbackSession().trim()}
>
<Show when={rollbackBusy()}>
<Spinner />
Expand Down
5 changes: 4 additions & 1 deletion packages/app/src/components/deepagent/oversight.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export type OversightTraceNode = {
source: string
causationID?: string
createdAt: number
// Phase 2: trace nodes may carry the session that emitted them so the UI can reverse-select
// the subagent row when the user clicks a trace entry.
sessionID?: string
}
export type OversightTrace = { nodes: OversightTraceNode[] }

Expand Down Expand Up @@ -148,7 +151,7 @@ export type HumanTakeoverRecord = {
*/
export const recordHumanTakeover = async (
client: OversightClient,
input: { reason: string; scope?: string },
input: { reason: string; scope?: string; sessionID?: string },
): Promise<{ ok: true; record?: HumanTakeoverRecord } | { ok: false; unsupported: boolean; error: string }> => {
try {
const response = await client.client.request<HumanTakeoverRecord>({
Expand Down
23 changes: 11 additions & 12 deletions packages/app/src/components/session/session-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { DOCK_PANEL_IDS, useLayout } from "@/context/layout"
import { usePlatform } from "@/context/platform"
import { useServer } from "@/context/server"
import { useSync } from "@/context/sync"
import { useTerminal } from "@/context/terminal"
import { useTerminalHosts } from "@/context/terminal"
import { focusTerminalById } from "@/pages/session/helpers"
import { useSessionLayout } from "@/pages/session/session-layout"
import { PANEL_VIEW_META } from "@/pages/session/panel-view-registry"
Expand Down Expand Up @@ -144,7 +144,7 @@ export function SessionHeader() {
const platform = usePlatform()
const language = useLanguage()
const sync = useSync()
const terminal = useTerminal()
const terminalHosts = useTerminalHosts()
const { params, view } = useSessionLayout()

const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
Expand Down Expand Up @@ -208,19 +208,18 @@ export function SessionHeader() {
]
})

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

const toggleTerminal = () => {
view().panel.toggle("terminal")
const panel = view().panel
if (panel.location("terminal") === "side" && view().rightPanel.mode() !== "terminal") return
const id = terminal.active()
if (id) focusTerminalById(id)
// Focus the bottom terminal's active pane after opening.
if (terminalOpen()) {
const id = terminalHosts.bottom.active()
if (id) focusTerminalById(id)
}
}

const bottomPanelOpen = createMemo(() => view().panel.bottom.opened())
Expand Down
29 changes: 29 additions & 0 deletions packages/app/src/components/terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export interface TerminalProps extends ComponentProps<"div"> {
runtimeId?: string
onSubmit?: () => void
onStatusChange?: (status: TerminalStatus, error?: TerminalFailure) => void
/** When true (restored PTY), show the terminal immediately after xterm initialises
* instead of waiting for the WebSocket handshake. The WebSocket still connects in
* the background; input typed before it's ready is buffered. */
optimisticReady?: boolean
}

let shared: Promise<{ mod: typeof import("ghostty-web"); ghostty: Ghostty }> | undefined
Expand Down Expand Up @@ -159,6 +163,7 @@ export const Terminal = (props: TerminalProps) => {
"runtimeId",
"onSubmit",
"onStatusChange",
"optimisticReady",
])
const id = local.pty.ptyId
let ws: WebSocket | undefined
Expand Down Expand Up @@ -435,6 +440,8 @@ export const Terminal = (props: TerminalProps) => {
})
cleanups.push(() => disposeIfDisposable(onResize))
const onData = t.onData((data) => {
// When optimisticReady is active the buffering handler below takes over.
if (local.optimisticReady) return
if (ws?.readyState === WebSocket.OPEN) ws.send(data)
})
cleanups.push(() => disposeIfDisposable(onData))
Expand All @@ -456,6 +463,28 @@ export const Terminal = (props: TerminalProps) => {
scheduleSize(t.cols, t.rows)
startResize()

// For restored PTYs: show the terminal surface immediately after xterm is
// initialised instead of waiting for the full WebSocket handshake (which can
// take 2-4 s). Input typed before the socket opens is buffered and flushed
// once the connection is established.
let inputBuffer = local.optimisticReady ? "" : undefined
if (local.optimisticReady) {
markReady()
// Intercept onData to buffer keystrokes until the WebSocket is open.
const onDataOpt = t.onData((data) => {
if (ws?.readyState === WebSocket.OPEN) {
if (inputBuffer) {
ws.send(inputBuffer)
inputBuffer = undefined
}
ws.send(data)
} else {
inputBuffer = (inputBuffer ?? "") + data
}
})
cleanups.push(() => disposeIfDisposable(onDataOpt))
}

const once = { value: false }
const decoder = new TextDecoder()

Expand Down
28 changes: 23 additions & 5 deletions packages/app/src/context/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ type SessionView = {
// U3/U4/U7: added "worktree" (isolated worktree diff/merge), "subagents" (child-session list),
// "browser" (isolated WebContentsView).
// T3.2: the "menu" mode is gone — an always-on icon rail replaced the full-panel menu list.
// Phase 2: "oversight" removed as standalone panel; kept in union only for backward-compat
// migration — any stored "oversight" is silently mapped to "subagents" at read time.
rightPanelMode?:
| "review"
| "files"
Expand Down Expand Up @@ -326,7 +328,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
{ ...target, migrate },
createStore({
sidebar: {
opened: false,
opened: true,
width: DEFAULT_SIDEBAR_WIDTH,
workspaces: {} as Record<string, boolean>,
workspacesDefault: false,
Expand Down Expand Up @@ -731,19 +733,24 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
},
dock: {
// Deprecated compatibility metadata. New UI must use view(session).panel;
// only locations and the shared Bottom Panel dimension remain global.
// Phase 3: terminal is now side-native in the right panel (independent host).
// dock.location("terminal") is forced to "bottom" to avoid stale stored "side"
// values making the bottom terminal unreachable. Move operations for terminal
// are intentionally no-ops; only debug-console and problems remain movable.
location(id: DockPanelID): DockLocation {
if (id === "terminal") return "bottom"
return store.dock?.location?.[id] ?? DOCK_DEFAULT_LOCATION[id]
},
setLocation(id: DockPanelID, location: DockLocation) {
if (id === "terminal") return // terminal location is now fixed
if (!store.dock) {
setStore("dock", { location: { [id]: location } as Record<DockPanelID, DockLocation> })
return
}
setStore("dock", "location", id, location)
},
move(id: DockPanelID) {
if (id === "terminal") return // terminal is no longer movable
const current = store.dock?.location?.[id] ?? DOCK_DEFAULT_LOCATION[id]
const next: DockLocation = current === "bottom" ? "side" : "bottom"
if (!store.dock) {
Expand All @@ -753,7 +760,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("dock", "location", id, next)
},
bottomCount: createMemo(
() => DOCK_PANEL_IDS.filter((id) => (store.dock?.location?.[id] ?? DOCK_DEFAULT_LOCATION[id]) === "bottom").length,
() => DOCK_PANEL_IDS.filter((id) => (id === "terminal" ? true : (store.dock?.location?.[id] ?? DOCK_DEFAULT_LOCATION[id]) === "bottom")).length,
),
},
review: {
Expand Down Expand Up @@ -905,7 +912,13 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const key = createSessionKeyReader(sessionKey, ensureKey)
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true)
const rightPanelMode = createMemo(() => store.sessionView[key()]?.rightPanelMode)
const rightPanelMode = createMemo(() => {
const mode = store.sessionView[key()]?.rightPanelMode
// Phase 2 backward-compat: "oversight" was merged into "subagents". Any session that
// persisted "oversight" should silently open the unified subagents panel instead.
if (mode === "oversight") return "subagents" as SessionRightPanelMode
return mode
})
const bottomPanel = createMemo(() => {
const current = store.sessionView[key()]
// One-way compatibility migration from the pre-panel global terminal flag.
Expand All @@ -925,6 +938,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const session = key()
const current = store.sessionView[session]
const bottom = next.bottomPanel
// Keep the project-level terminal flag in sync so that switching to a
// session that has never explicitly opened the panel doesn't close it.
// `bottomPanel` memo falls back to `store.terminal?.opened` for new
// sessions — this makes that fallback reflect the current user intent.
setTerminalOpened(bottom?.opened === true && bottom?.activeView === "terminal")
if (!current) {
setStore("sessionView", session, { scroll: {}, bottomPanel: bottom, rightPanelMode: next.rightPanelMode })
return
Expand Down
38 changes: 38 additions & 0 deletions packages/app/src/context/tabs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, expect, test } from "bun:test"
import { ServerConnection } from "./server"
import { startupTab, tabKey, type Tab } from "./tabs"

const primary = ServerConnection.Key.make("http://primary")
const secondary = ServerConnection.Key.make("http://secondary")

const servers = [
{ type: "http" as const, http: { url: "http://primary" } },
{ type: "http" as const, http: { url: "http://secondary" } },
]

const first: Tab = {
type: "session",
server: primary,
dirBase64: "L3ByaW1hcnk=",
sessionId: "first",
}
const lastActive: Tab = {
type: "session",
server: secondary,
dirBase64: "L3NlY29uZGFyeQ==",
sessionId: "last-active",
}

describe("startup tab recovery", () => {
test("restores the persisted active tab rather than the first tab", () => {
expect(startupTab([first, lastActive], tabKey(lastActive), servers)).toBe(lastActive)
})

test("falls back to the first stored tab when the active key is stale", () => {
expect(startupTab([first, lastActive], "stale", servers)).toBe(first)
})

test("does not restore a target whose server is unavailable", () => {
expect(startupTab([first, lastActive], tabKey(lastActive), [servers[0]])).toBeUndefined()
})
})
Loading
Loading