@@ -110,7 +105,7 @@ export const SettingsServersV2: Component = () => {
0 || wslServers().length > 0}
+ when={filtered().length > 0}
fallback={
{store.filter ? language.t("palette.empty") : language.t("dialog.server.empty")}
@@ -121,7 +116,6 @@ export const SettingsServersV2: Component = () => {
}
>
-
{(item) => {
const key = ServerConnection.key(item)
diff --git a/packages/app/src/components/wiki/dialog-wiki.tsx b/packages/app/src/components/wiki/dialog-wiki.tsx
index f2ec1e874..44aa6d7b2 100644
--- a/packages/app/src/components/wiki/dialog-wiki.tsx
+++ b/packages/app/src/components/wiki/dialog-wiki.tsx
@@ -1,5 +1,4 @@
import { Component, createEffect, createMemo, createResource, createSignal, For, Show } from "solid-js"
-import { Dialog } from "@deepagent-code/ui/v2/dialog-v2"
import { Button } from "@deepagent-code/ui/button"
import { Collapsible } from "@deepagent-code/ui/collapsible"
import { Icon } from "@deepagent-code/ui/icon"
@@ -54,7 +53,9 @@ export const resolveWikiExpandedGroup = (
searching: boolean,
) => (searching ? groups.find((group) => group.items.length > 0)?.group : expanded)
-export const DialogWiki: Component<{ client: WikiClient }> = (props) => {
+// WS1: the wiki body is hosted as a tab inside the merged Knowledge dialog
+// (components/knowledge/dialog-knowledge), so this is a panel, not a standalone dialog.
+export const WikiPanel: Component<{ client: WikiClient }> = (props) => {
const language = useLanguage()
const [query, setQuery] = createSignal("")
const [selectedId, setSelectedId] = createSignal<{ docId: string; scope: string } | undefined>(undefined)
@@ -142,52 +143,50 @@ export const DialogWiki: Component<{ client: WikiClient }> = (props) => {
}
return (
-
- )}
-
-
-
- )
-}
diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx
index dd1ec9f98..3d1315125 100644
--- a/packages/app/src/pages/session/message-timeline.tsx
+++ b/packages/app/src/pages/session/message-timeline.tsx
@@ -69,6 +69,7 @@ import { useDialog } from "@deepagent-code/ui/context/dialog"
import { Tooltip } from "@deepagent-code/ui/tooltip"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { useLanguage } from "@/context/language"
+import { useCommand } from "@/context/command"
import { useSessionKey } from "@/pages/session/session-layout"
import { useServerSDK } from "@/context/server-sdk"
import { usePlatform } from "@/context/platform"
@@ -529,6 +530,7 @@ export function MessageTimeline(props: {
const settings = useSettings()
const dialog = useDialog()
const language = useLanguage()
+ const command = useCommand()
const { params, sessionKey } = useSessionKey()
const platform = usePlatform()
@@ -1924,6 +1926,9 @@ export function MessageTimeline(props: {
void exportSession(id)}>
{language.t("session.export.action")}
+
command.trigger("session.export")}>
+ {language.t("session.export.markdown")}
+
void archiveSession(id)}>
{language.t("common.archive")}
diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx
index 8ee403448..2f04c536c 100644
--- a/packages/app/src/pages/session/session-side-panel.tsx
+++ b/packages/app/src/pages/session/session-side-panel.tsx
@@ -23,8 +23,6 @@ import { useTerminalHosts } from "@/context/terminal"
import { usePlatform } from "@/context/platform"
import FileTree from "@/components/file-tree"
-import { SidePanelMcp } from "@/pages/session/side-panel-mcp"
-import { SidePanelPlugins } from "@/pages/session/side-panel-plugins"
import { useCommand } from "@/context/command"
import { useDebug } from "@/context/debug"
import { useFile, type SelectedLineRange } from "@/context/file"
@@ -39,11 +37,8 @@ import { SidePanelSubagents } from "@/pages/session/side-panel-subagents"
import { isSubagentInterrupted } from "@/pages/session/subagent-state"
import { SidePanelBrowser } from "@/pages/session/side-panel-browser"
import { SidePanelWorktree } from "@/pages/session/side-panel-worktree"
-import { SidePanelDebug } from "@/pages/session/side-panel-debug"
-import { SidePanelProfile } from "@/pages/session/side-panel-profile"
+import { SidePanelDev } from "@/pages/session/side-panel-dev"
import { SidePanelIM } from "@/pages/session/side-panel-im"
-import { SidePanelContext } from "@/pages/session/side-panel-context"
-import { SidePanelStats } from "@/pages/session/side-panel-stats"
import { SidePanelDockHeader, SidePanelTerminal, SidePanelDebugConsole } from "@/pages/session/side-panel-terminal"
import { ProblemsPanel } from "@/pages/session/problems-panel"
import { PanelErrorBoundary } from "@/pages/session/panel-error-boundary"
@@ -57,16 +52,11 @@ type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
type PanelMode =
| "review"
| "files"
- | "context"
| "subagents"
| "im"
| "browser"
| "worktree"
- | "mcp"
- | "plugins"
- | "debug"
- | "profile"
- | "stats"
+ | "dev"
| "terminal"
| "debug-console"
| "problems"
@@ -107,20 +97,15 @@ const PANELS: readonly PanelDef[] = [
bucket: "wide",
keybind: "fileTree.toggle",
},
- { mode: "context", icon: "shield", titleKey: "session.context.title", group: "code", bucket: "narrow" },
// Agents — subagents is now the unified "子Agent监督" entry (Phase 2: oversight merged in).
{ mode: "subagents", icon: "agent-tree", titleKey: "session.subagents.title", group: "agents", bucket: "narrow" },
{ mode: "im", icon: "bubble-5", titleKey: "session.tab.im", group: "agents", bucket: "narrow" },
- // Env
+ // Env — browser is registered on the desktop build only (see `panels()` below).
{ mode: "browser", icon: "window-cursor", titleKey: "browser.title", group: "env", bucket: "narrow" },
{ mode: "worktree", icon: "branch", titleKey: "worktree.title", group: "env", bucket: "narrow" },
- // Dev
- { mode: "mcp", icon: "mcp", titleKey: "status.popover.tab.mcp", group: "dev", bucket: "narrow" },
- { mode: "plugins", icon: "plugin", titleKey: "status.popover.tab.plugins", group: "dev", bucket: "narrow" },
- { mode: "debug", icon: "debug", titleKey: "session.panel.debug", group: "dev", bucket: "narrow" },
- { mode: "profile", icon: "profile", titleKey: "session.panel.profile", group: "dev", bucket: "narrow" },
- // PARITY-001: cross-session cost/token overview aggregated from the synced session store.
- { mode: "stats", icon: "dollar", titleKey: "session.stats.title", group: "dev", bucket: "narrow" },
+ // Dev — WS1: debug + profiler + context-evidence merged into one tabbed panel. Plugins moved
+ // to a Settings tab; Usage & cost moved into the SessionContextUsage dialog.
+ { mode: "dev", icon: "debug", titleKey: "session.panel.dev", group: "dev", bucket: "narrow" },
// Dock — the movable panels; only surface here when docked to the side (see gating below).
{
mode: "terminal",
@@ -177,6 +162,10 @@ export function SessionSidePanel(props: {
const isDesktop = createMediaQuery("(min-width: 768px)")
+ // WS1: the browser rail entry is registered on the desktop build only — its content is an
+ // Electron WebContentsView bridge (platform.browser), so on web there is nothing to show.
+ const panels = createMemo(() => PANELS.filter((p) => p.mode !== "browser" || platform.platform === "desktop"))
+
// Subagents = child sessions (parentID === current). Surface a live count on the sidebar icon so
// the user sees a spawn happened without opening the panel.
// Phase 2 (§3.5): badge covers running + interrupted so the user notices any subagent that needs
@@ -197,13 +186,13 @@ export function SessionSidePanel(props: {
const activeMode = createMemo
(() => {
if (!isDesktop()) return undefined
const mode = view().rightPanel.mode()
- return mode && PANELS.some((p) => p.mode === mode) ? (mode as PanelMode) : undefined
+ return mode && panels().some((p) => p.mode === mode) ? (mode as PanelMode) : undefined
})
const isActive = (mode: PanelMode) => activeMode() === mode
const open = createMemo(() => activeMode() !== undefined)
// Per-panel remembered width (T3.3): the wide bucket for diff/files, the narrow bucket for the rest.
const activeBucket = createMemo(
- () => PANELS.find((p) => p.mode === activeMode())?.bucket ?? "wide",
+ () => panels().find((p) => p.mode === activeMode())?.bucket ?? "wide",
)
const contentWidth = createMemo(() => layout.rightPanel.width(activeBucket()))
const panelWidth = createMemo(() => (open() ? `${contentWidth()}px` : "0px"))
@@ -402,7 +391,7 @@ export function SessionSidePanel(props: {
return undefined
}
const railItems = createMemo(() =>
- PANELS
+ panels()
// Phase 3: terminal is side-native — always show in the rail.
// Other movable dock panels (debug-console, problems) only appear when docked to the side.
.filter((p) =>
@@ -676,29 +665,14 @@ export function SessionSidePanel(props: {
)}
/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/packages/app/src/pages/session/side-panel-debug.tsx b/packages/app/src/pages/session/side-panel-debug.tsx
index cc2b865e9..09e85d226 100644
--- a/packages/app/src/pages/session/side-panel-debug.tsx
+++ b/packages/app/src/pages/session/side-panel-debug.tsx
@@ -1,7 +1,7 @@
/**
* SidePanelDebug — V3.7 Phase 4.3/4.5 DAP 调试可视化面板
*
- * 挂载到右侧工作面板 "debug" 模式。状态来自共享 DebugContext(Phase 4.5):
+ * 挂载到右侧工作面板 "dev" 模式的 Debug 标签页(WS1 三合一)。状态来自共享 DebugContext(Phase 4.5):
* - 无会话时显示"启动调试"表单(P1-3)
* - 有会话时:SessionBar → ControlBar → StackView → VariableTree → WatchInput
*
diff --git a/packages/app/src/pages/session/side-panel-dev.tsx b/packages/app/src/pages/session/side-panel-dev.tsx
new file mode 100644
index 000000000..778e74325
--- /dev/null
+++ b/packages/app/src/pages/session/side-panel-dev.tsx
@@ -0,0 +1,41 @@
+import type { Component } from "solid-js"
+import { Tabs } from "@deepagent-code/ui/tabs"
+import { useLanguage } from "@/context/language"
+import { SidePanelDebug } from "@/pages/session/side-panel-debug"
+import { SidePanelProfile } from "@/pages/session/side-panel-profile"
+import { SidePanelContext } from "@/pages/session/side-panel-context"
+
+// WS1 (V2.0.1-001 §2): the Debug / Profiler / Context-evidence rail entries merged into ONE
+// "Dev" panel with three tabs. The tab bodies are the original panels unchanged — each keeps
+// its own header (title + refresh + close), and close still closes the whole right panel.
+export const SidePanelDev: Component<{
+ onClose: () => void
+ onNavigate?: (file: string, line: number) => void
+}> = (props) => {
+ const language = useLanguage()
+
+ return (
+
+
+
+ {language.t("session.panel.debug")}
+
+
+ {language.t("session.panel.profile")}
+
+
+ {language.t("session.context.title")}
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/app/src/pages/session/side-panel-mcp.tsx b/packages/app/src/pages/session/side-panel-mcp.tsx
deleted file mode 100644
index f1dd7fac3..000000000
--- a/packages/app/src/pages/session/side-panel-mcp.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Icon } from "@deepagent-code/ui/icon"
-import { IconButton } from "@deepagent-code/ui/icon-button"
-import { McpManagement } from "@/components/mcp-management"
-import { useLanguage } from "@/context/language"
-
-export function SidePanelMcp(props: { onClose: () => void }) {
- const language = useLanguage()
-
- return (
-
-
-
-
- {language.t("status.popover.tab.mcp")}
-
-
-
-
-
-
-
- )
-}
diff --git a/packages/app/src/pages/session/side-panel-oversight.tsx b/packages/app/src/pages/session/side-panel-oversight.tsx
deleted file mode 100644
index 5f5d5db7f..000000000
--- a/packages/app/src/pages/session/side-panel-oversight.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { type Component } from "solid-js"
-import { IconButton } from "@deepagent-code/ui/icon-button"
-import { useLanguage } from "@/context/language"
-import { OversightDashboard } from "@/components/deepagent/oversight-dashboard"
-
-// V4.0 §D2 — Oversight as a right-side-panel tab. Mirrors SidePanelIM: owns a header + close button
-// (calls onClose), fills the panel with the OversightDashboard body. Read-mostly observability +
-// approval queue + trace + human-takeover control, all scoped to the routed workspace/directory.
-export const SidePanelOversight: Component<{ onClose: () => void }> = (props) => {
- const language = useLanguage()
-
- return (
-
-
- Oversight
-
-
-
-
-
-
- )
-}
diff --git a/packages/app/src/pages/session/side-panel-plugins.tsx b/packages/app/src/pages/session/side-panel-plugins.tsx
deleted file mode 100644
index e077096d1..000000000
--- a/packages/app/src/pages/session/side-panel-plugins.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Component, createMemo, For, type JSXElement, Show } from "solid-js"
-import { useSync } from "@/context/sync"
-import { useLanguage } from "@/context/language"
-import { Icon } from "@deepagent-code/ui/icon"
-import { IconButton } from "@deepagent-code/ui/icon-button"
-
-const pluginEmptyMessage = (value: string, file: string): JSXElement => {
- const parts = value.split(file)
- if (parts.length === 1) return value
- return (
- <>
- {parts[0]}
- {file}
- {parts.slice(1).join(file)}
- >
- )
-}
-
-// Plugins side-panel view. Displays the list of configured plugin paths (from deepagent-code.json).
-// Plugins are configured via the config file only (no UI add/remove), so this panel is read-only.
-export const SidePanelPlugins: Component<{ onClose: () => void }> = (props) => {
- const sync = useSync()
- const language = useLanguage()
-
- const plugins = createMemo(() =>
- (sync.data.config.plugin ?? []).map((item) => (typeof item === "string" ? item : item[0])),
- )
- const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "deepagent-code.json"))
-
- return (
-
-
-
-
- {language.t("status.popover.tab.plugins")}
-
-
-
-
-
-
-
0}
- fallback={{pluginEmpty()}
}
- >
-
- {(plugin) => (
-
- )}
-
-
-
-
-
- )
-}
diff --git a/packages/app/src/pages/session/side-panel-profile.tsx b/packages/app/src/pages/session/side-panel-profile.tsx
index 080dbd878..6e7e2342c 100644
--- a/packages/app/src/pages/session/side-panel-profile.tsx
+++ b/packages/app/src/pages/session/side-panel-profile.tsx
@@ -1,7 +1,7 @@
/**
* SidePanelProfile — V3.7 Phase 4.4 PAP 性能剖析可视化面板
*
- * 挂载到右侧工作面板 "profile" 模式。
+ * 挂载到右侧工作面板 "dev" 模式的 Profiler 标签页(WS1 三合一)。
* 结构:RunBar → HotspotTable → MetricCards → DiffBadge
*
* 数据来源:
diff --git a/packages/app/src/pages/session/side-panel-stats.tsx b/packages/app/src/pages/session/side-panel-stats.tsx
index 357ebc95e..777ae1d09 100644
--- a/packages/app/src/pages/session/side-panel-stats.tsx
+++ b/packages/app/src/pages/session/side-panel-stats.tsx
@@ -1,23 +1,19 @@
// PARITY-001: GUI cross-session cost/token overview.
//
// The CLI `stats` command aggregates the whole database in-process; the GUI only had per-session
-// context diagnostics (side-panel-context.tsx). This panel aggregates the sessions already synced
+// context diagnostics (side-panel-context.tsx). This view aggregates the sessions already synced
// into the directory store (sync.data.session — the same list the session drawer renders) into a
// per-session cost/token/model overview. Zero new API endpoints: the SDK Session type carries
// `cost`, `tokens` and `model` rollups.
-import { Icon } from "@deepagent-code/ui/icon"
-import { IconButton } from "@deepagent-code/ui/icon-button"
+//
+// WS1: no longer a right-rail panel of its own — hosted as the "全部会话/All sessions" tab of the
+// SessionContextUsage dialog (components/session-context-usage.tsx).
import { For, Show, createMemo, type Component, type JSX } from "solid-js"
import { useLanguage } from "@/context/language"
import { useSync } from "@/context/sync"
-import {
- aggregateSessionStats,
- formatCost,
- formatTokenCount,
- sortModelUsage,
-} from "@/utils/session-stats"
+import { aggregateSessionStats, formatCost, formatTokenCount, sortModelUsage } from "@/utils/session-stats"
-export const SidePanelStats: Component<{ onClose: () => void }> = (props) => {
+export const SessionStatsContent: Component = () => {
const sync = useSync()
const language = useLanguage()
@@ -42,20 +38,6 @@ export const SidePanelStats: Component<{ onClose: () => void }> = (props) => {
return (
-
-
0}
diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx
index b5bd961a1..f74831967 100644
--- a/packages/app/src/pages/session/use-session-commands.tsx
+++ b/packages/app/src/pages/session/use-session-commands.tsx
@@ -19,7 +19,7 @@ import { type Session, UserMessage } from "@deepagent-code/sdk"
import { useSessionLayout } from "@/pages/session/session-layout"
import { errorMessage } from "@/pages/layout/helpers"
import { useSettings } from "@/context/settings"
-import { formatTranscript } from "@/utils/transcript"
+import { formatTranscript, transcriptFilename } from "@deepagent-code/sdk/transcript"
import { createPromptStash } from "@/pages/session/prompt-stash"
import { DialogPromptStash } from "@/components/dialog-prompt-stash"
@@ -374,8 +374,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
})
}
- // W3-4 — human-readable Markdown export (parity with the TUI /export command; the formatter is
- // the ported TUI transcript renderer). Triggers a browser download of the .md file.
+ // W3-4 — human-readable Markdown export (parity with the TUI /export command; both use the
+ // canonical formatter from @deepagent-code/sdk/transcript). Triggers a browser download of
+ // the .md file; the browser itself dedupes the name on collision.
const exportTranscript = async () => {
const sessionID = params.id
const sessionInfo = info()
@@ -394,7 +395,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const url = URL.createObjectURL(blob)
const anchor = document.createElement("a")
anchor.href = url
- anchor.download = `session-${sessionID.slice(0, 8)}.md`
+ anchor.download = transcriptFilename({ id: sessionInfo.id, title: sessionInfo.title, time: sessionInfo.time })
anchor.click()
URL.revokeObjectURL(url)
}
diff --git a/packages/app/src/utils/path-key.test.ts b/packages/app/src/utils/path-key.test.ts
new file mode 100644
index 000000000..1737d1b21
--- /dev/null
+++ b/packages/app/src/utils/path-key.test.ts
@@ -0,0 +1,20 @@
+import { describe, expect, test } from "bun:test"
+import { pathKey } from "./path-key"
+
+describe("pathKey", () => {
+ test("normalizes posix paths", () => {
+ expect(String(pathKey("/a/b/"))).toBe("/a/b")
+ expect(String(pathKey("/"))).toBe("/")
+ })
+
+ test("normalizes windows paths to forward slashes", () => {
+ expect(String(pathKey("C:\\Users\\me"))).toBe("C:/Users/me")
+ expect(String(pathKey("C:"))).toBe("C:/")
+ })
+
+ test("degrades a missing directory to a neutral key instead of crashing", () => {
+ // Regression: session payloads without a directory reached the sidebar sort and the
+ // undefined access took the whole layout down via the error boundary.
+ expect(String(pathKey(undefined as unknown as string))).toBe("")
+ })
+})
diff --git a/packages/app/src/utils/path-key.ts b/packages/app/src/utils/path-key.ts
index 68d53e91d..1f810cfdb 100644
--- a/packages/app/src/utils/path-key.ts
+++ b/packages/app/src/utils/path-key.ts
@@ -16,6 +16,9 @@ const trimTrailingSlashes = (value: string) => {
const isWindowsPath = (value: string) => value[1] === ":" || value.startsWith("\\\\")
export const pathKey = (path: string) => {
+ // Malformed session payloads can arrive without a directory; a crash here takes down the
+ // whole layout via the error boundary, so degrade to a neutral key instead.
+ if (!path) return "" as PathKey
const value = isWindowsPath(path) ? path.replaceAll("\\", "/") : path
const trimmed = trimTrailingSlashes(value)
if (!trimmed && value.startsWith("/")) return "/" as PathKey
diff --git a/packages/app/src/utils/transcript.ts b/packages/app/src/utils/transcript.ts
deleted file mode 100644
index 3cd877cf2..000000000
--- a/packages/app/src/utils/transcript.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-import type { AssistantMessage, Part, Provider, UserMessage } from "@deepagent-code/sdk"
-
-/**
- * W3-4 — human-readable session transcript → Markdown. Ported from the TUI's
- * util/transcript.ts (kept behavior-identical so both surfaces export the same shape);
- * self-contained here to avoid a TUI-package dependency in the web app.
- */
-
-export type TranscriptOptions = {
- thinking: boolean
- toolDetails: boolean
- assistantMetadata: boolean
- providers?: Provider[]
-}
-
-export type SessionInfo = {
- id: string
- title: string
- time: {
- created: number
- updated: number
- }
-}
-
-export type MessageWithParts = {
- info: UserMessage | AssistantMessage
- parts: Part[]
-}
-
-const titlecase = (value: string) => (value.length === 0 ? value : value[0].toUpperCase() + value.slice(1))
-
-const providerIndex = (list: Provider[] | undefined) => new Map((list ?? []).map((item) => [item.id, item] as const))
-
-const modelName = (
- list: Provider[] | ReadonlyMap | undefined,
- providerID: string,
- modelID: string,
-) => {
- const provider =
- list instanceof Map ? list.get(providerID) : Array.isArray(list) ? list.find((item) => item.id === providerID) : undefined
- return provider?.models[modelID]?.name ?? modelID
-}
-
-export function formatTranscript(session: SessionInfo, messages: MessageWithParts[], options: TranscriptOptions): string {
- const providers = providerIndex(options.providers)
- let transcript = `# ${session.title}\n\n`
- transcript += `**Session ID:** ${session.id}\n`
- transcript += `**Created:** ${new Date(session.time.created).toLocaleString()}\n`
- transcript += `**Updated:** ${new Date(session.time.updated).toLocaleString()}\n\n`
- transcript += `---\n\n`
-
- for (const msg of messages) {
- transcript += formatMessage(msg.info, msg.parts, options, providers)
- transcript += `---\n\n`
- }
-
- return transcript
-}
-
-function formatMessage(
- msg: UserMessage | AssistantMessage,
- parts: Part[],
- options: TranscriptOptions,
- providers?: Provider[] | ReadonlyMap,
-): string {
- let result = ""
-
- if (msg.role === "user") {
- result += `## User\n\n`
- } else {
- result += formatAssistantHeader(msg, options.assistantMetadata, providers ?? options.providers)
- }
-
- for (const part of parts) {
- result += formatPart(part, options)
- }
-
- return result
-}
-
-function formatAssistantHeader(
- msg: AssistantMessage,
- includeMetadata: boolean,
- providers?: Provider[] | ReadonlyMap,
-): string {
- if (!includeMetadata) return `## Assistant\n\n`
- const duration =
- msg.time.completed && msg.time.created ? ((msg.time.completed - msg.time.created) / 1000).toFixed(1) + "s" : ""
- const model = modelName(providers, msg.providerID, msg.modelID)
- return `## Assistant (${titlecase(msg.agent)} · ${model}${duration ? ` · ${duration}` : ""})\n\n`
-}
-
-function formatPart(part: Part, options: TranscriptOptions): string {
- if (part.type === "text" && !part.synthetic) {
- return `${part.text}\n\n`
- }
-
- if (part.type === "reasoning") {
- if (options.thinking) {
- return `_Thinking:_\n\n${part.text}\n\n`
- }
- return ""
- }
-
- if (part.type === "tool") {
- let result = `**Tool: ${part.tool}**\n`
- if (options.toolDetails && part.state.input) {
- result += `\n**Input:**\n\`\`\`json\n${JSON.stringify(part.state.input, null, 2)}\n\`\`\`\n`
- }
- if (options.toolDetails && part.state.status === "completed" && part.state.output) {
- result += `\n**Output:**\n\`\`\`\n${part.state.output}\n\`\`\`\n`
- }
- if (options.toolDetails && part.state.status === "error" && part.state.error) {
- result += `\n**Error:**\n\`\`\`\n${part.state.error}\n\`\`\`\n`
- }
- result += `\n`
- return result
- }
-
- return ""
-}
diff --git a/packages/app/src/wsl/context.tsx b/packages/app/src/wsl/context.tsx
deleted file mode 100644
index bfa2bdc94..000000000
--- a/packages/app/src/wsl/context.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { createSimpleContext } from "@deepagent-code/ui/context"
-import { queryOptions, useQuery, useQueryClient } from "@tanstack/solid-query"
-import { createEffect, onCleanup } from "solid-js"
-import type { WslServersState } from "./types"
-import { usePlatform } from "../context/platform"
-
-const wslServersQueryKey = ["platform", "wslServers"] as const
-
-export const { use: useWslServers, provider: WslServersProvider } = createSimpleContext({
- name: "WslServers",
- init: () => {
- const platform = usePlatform()
- const queryClient = useQueryClient()
- const query = useQuery(() => {
- const api = platform.wslServers
- return queryOptions({
- queryKey: wslServersQueryKey,
- queryFn: () => api!.getState(),
- enabled: !!api,
- staleTime: Number.POSITIVE_INFINITY,
- gcTime: Number.POSITIVE_INFINITY,
- })
- })
-
- createEffect(() => {
- const api = platform.wslServers
- if (!api) return
- const off = api.subscribe((event) => {
- queryClient.setQueryData(wslServersQueryKey, event.state)
- })
- onCleanup(off)
- })
-
- return query as typeof query & { readonly data: WslServersState | undefined }
- },
-})
diff --git a/packages/app/src/wsl/dialog-add-server.tsx b/packages/app/src/wsl/dialog-add-server.tsx
deleted file mode 100644
index 296e69f4a..000000000
--- a/packages/app/src/wsl/dialog-add-server.tsx
+++ /dev/null
@@ -1,623 +0,0 @@
-import { Button } from "@deepagent-code/ui/button"
-import { useDialog } from "@deepagent-code/ui/context/dialog"
-import { Spinner } from "@deepagent-code/ui/spinner"
-import { showToast } from "@deepagent-code/ui/toast"
-import { createEffect, createMemo, For, Match, onCleanup, Show, Switch } from "solid-js"
-import { createStore } from "solid-js/store"
-import { useLanguage } from "@/context/language"
-import { usePlatform } from "@/context/platform"
-import { useWslServers } from "./context"
-import { enterWslDeepagentCodeStep } from "./settings-model"
-
-type WslServerStep = "wsl" | "distro" | "deepagent-code"
-
-const STEPS: WslServerStep[] = ["wsl", "distro", "deepagent-code"]
-
-function isHiddenDistro(name: string) {
- return /^docker-desktop(?:-data)?$/i.test(name)
-}
-
-interface DialogWslServerProps {
- onAdded?: (distro: string) => void | Promise
-}
-
-export function DialogAddWslServer(props: DialogWslServerProps = {}) {
- const language = useLanguage()
- const platform = usePlatform()
- const dialog = useDialog()
- const wslServers = useWslServers()
- const api = platform.wslServers!
- const [store, setStore] = createStore({
- step: undefined as WslServerStep | undefined,
- selectedDistro: null as string | null,
- installTarget: undefined as string | undefined,
- adding: false,
- })
- const current = () => wslServers.data
- let disposed = false
- onCleanup(() => {
- disposed = true
- })
- const busy = createMemo(() => !!current()?.job || store.adding)
- const visibleInstalledDistros = createMemo(() =>
- (current()?.installed ?? []).filter((item) => !isHiddenDistro(item.name)),
- )
- const visibleOnlineDistros = createMemo(() => (current()?.online ?? []).filter((item) => !isHiddenDistro(item.name)))
- const defaultInstalledDistro = createMemo(() => visibleInstalledDistros().find((item) => item.isDefault) ?? null)
- const existingServerDistros = createMemo(() => new Set((current()?.servers ?? []).map((item) => item.config.distro)))
- const addableInstalledDistros = createMemo(() => {
- return visibleInstalledDistros().filter((item) => !existingServerDistros().has(item.name))
- })
- const selectedDistro = createMemo(() => {
- if (store.selectedDistro && addableInstalledDistros().some((item) => item.name === store.selectedDistro)) {
- return store.selectedDistro
- }
- const distro = defaultInstalledDistro()
- if (distro && !existingServerDistros().has(distro.name)) return distro.name
- return null
- })
- const selectedProbe = createMemo(() => {
- const distro = selectedDistro()
- if (!distro) return null
- return current()?.distroProbes[distro] ?? null
- })
- const selectedInstalled = createMemo(() => {
- const distro = selectedDistro()
- if (!distro) return null
- return (current()?.installed ?? []).find((item) => item.name === distro) ?? null
- })
- const deepagentCodeCheck = createMemo(() => {
- const distro = selectedDistro()
- if (!distro) return null
- return current()?.deepagentCodeChecks[distro] ?? null
- })
- const wslReady = createMemo(() => !!current()?.runtime?.available && !current()?.pendingRestart)
- const distroReady = createMemo(() => {
- const probe = selectedProbe()
- if (!probe || !selectedDistro()) return false
- if (selectedInstalled()?.version === 1) return false
- return probe.canExecute && probe.hasBash && probe.hasCurl
- })
- const deepagentCodeReady = createMemo(() => {
- const check = deepagentCodeCheck()
- return !!check?.resolvedPath && !check.error
- })
- const distroWarningProbe = createMemo(() => {
- const probe = selectedProbe()
- if (!probe) return null
- if (distroReady()) return null
- return probe
- })
- const distroUnavailableMessage = createMemo(() => {
- const probe = distroWarningProbe()
- const distro = selectedDistro()
- if (!probe || probe.canExecute || !distro) return null
- if (!selectedInstalled()) return language.t("wsl.onboarding.distroNotInstalled", { distro })
- return language.t("wsl.onboarding.openDistroOnce", { distro })
- })
- const distroMissingTools = createMemo(() => {
- const probe = distroWarningProbe()
- if (!probe?.canExecute) return null
- if (probe.hasBash && probe.hasCurl) return null
- return probe
- })
- const installableDistros = createMemo(() => {
- const online = visibleOnlineDistros()
- const installed = new Set(visibleInstalledDistros().map((item) => item.name))
- const hasVersionedUbuntu = online.some((item) => /^Ubuntu-\d/.test(item.name))
- return online
- .filter((item) => !installed.has(item.name))
- .filter((item) => !(item.name === "Ubuntu" && hasVersionedUbuntu))
- })
- const installTarget = createMemo(
- () => installableDistros().find((item) => item.name === store.installTarget) ?? installableDistros()[0] ?? null,
- )
- const installingDistro = createMemo(() => current()?.job?.kind === "install-distro")
- const installingDeepagentCode = createMemo(() => {
- const job = current()?.job
- return job?.kind === "install-deepagent-code" && job.distro === selectedDistro()
- })
- const allReady = createMemo(() => wslReady() && distroReady() && deepagentCodeReady())
- const addDisabled = createMemo(() => {
- const job = current()?.job
- if (!job) return store.adding
- return store.adding || job.kind !== "probe-deepagent-code"
- })
- const recommendedStep = createMemo(() => {
- if (!wslReady()) return "wsl"
- if (!distroReady()) return "distro"
- return "deepagent-code"
- })
- // activeStep falls back to recommendedStep when the user hasn't picked one.
- // Once the user clicks a step tab we respect their choice rather than snapping
- // them back when a probe result updates recommendedStep.
- const activeStep = createMemo(() => store.step ?? recommendedStep())
-
- const autoProbe = createMemo(() => {
- const state = current()
- if (!state || busy()) return null
- if (state.pendingRestart) return null
- if (!state.runtime) return { key: "runtime", run: () => api.probeRuntime() }
- if (!wslReady()) return null
- if (!state.installed.length && !state.online.length) {
- return { key: "distros", run: () => api.refreshDistros() }
- }
- const distro = selectedDistro()
- if (distro && !state.distroProbes[distro]) {
- return { key: `probe-distro:${distro}`, run: () => api.probeDistro(distro) }
- }
- if (!distro || !distroReady()) return null
- if (!state.deepagentCodeChecks[distro]) {
- return { key: `probe-deepagent-code:${distro}`, run: () => api.probeDeepagentCode(distro) }
- }
- return null
- })
-
- let lastAutoProbe: string | null = null
- createEffect(() => {
- const probe = autoProbe()
- if (!probe || probe.key === lastAutoProbe) return
- const key = probe.key
- lastAutoProbe = key
- void (async () => {
- try {
- await probe.run()
- } catch (err) {
- if (disposed) return
- // Allow the same probe to run again when reactive inputs next change
- // (e.g. user reselects a distro). Without this the user would be stuck
- // on a transient wsl.exe failure until they pick a different distro.
- if (lastAutoProbe === key) lastAutoProbe = null
- requestError(language, err)
- }
- })()
- })
-
- const wslMessage = createMemo(() => {
- const state = current()
- if (!state || state.job?.kind === "runtime") return language.t("wsl.onboarding.checkingRuntime")
- if (state.pendingRestart) return language.t("wsl.onboarding.restartRequired")
- if (state.runtime?.available) return state.runtime.version ?? language.t("wsl.onboarding.ready")
- return state.runtime?.error ?? language.t("wsl.onboarding.required")
- })
-
- const distroMessage = createMemo(() => {
- const state = current()
- if (!state) return language.t("wsl.onboarding.checkingDistros")
- const distro = selectedDistro()
- if (state.job?.kind === "install-distro")
- return language.t("wsl.onboarding.installingDistro", { distro: state.job.distro })
- if (state.job?.kind === "probe-distro")
- return language.t("wsl.onboarding.checkingDistro", { distro: state.job.distro })
- if (state.job?.kind === "distros") return language.t("wsl.onboarding.listingDistros")
- if (distroUnavailableMessage()) return distroUnavailableMessage()!
- if (selectedProbe() && distroReady())
- return language.t("wsl.onboarding.distroReady", { distro: selectedProbe()!.name })
- if (distro) return language.t("wsl.onboarding.finishingDistro", { distro })
- return language.t("wsl.onboarding.pickDistro")
- })
-
- const deepagentCodeMessage = createMemo(() => {
- const state = current()
- if (!state) return language.t("wsl.onboarding.checkingDeepagentCode")
- const distro = selectedDistro()
- if (state.job?.kind === "install-deepagent-code") {
- return distro
- ? language.t("wsl.onboarding.updatingDeepagentCodeIn", { distro })
- : language.t("wsl.onboarding.updatingDeepagentCode")
- }
- if (state.job?.kind === "probe-deepagent-code") {
- return distro
- ? language.t("wsl.onboarding.checkingDeepagentCodeIn", { distro })
- : language.t("wsl.onboarding.checkingDeepagentCode")
- }
- if (deepagentCodeCheck()?.error) return deepagentCodeCheck()!.error
- if (deepagentCodeCheck()?.matchesDesktop === false) {
- return distro
- ? language.t("wsl.onboarding.updateDeepagentCodeIn", { distro })
- : language.t("wsl.onboarding.updateDeepagentCode")
- }
- if (deepagentCodeReady()) {
- return distro
- ? language.t("wsl.onboarding.deepagentCodeReadyIn", { distro })
- : language.t("wsl.onboarding.deepagentCodeReady")
- }
- return distro
- ? language.t("wsl.onboarding.installDeepagentCodeIn", { distro })
- : language.t("wsl.onboarding.chooseDistroFirst")
- })
-
- const run = async (action: () => Promise) => {
- try {
- await action()
- } catch (err) {
- requestError(language, err)
- }
- }
-
- const runSelectedDistro = (action: (distro: string) => Promise) => {
- const distro = selectedDistro()
- if (!distro) return
- void run(() => action(distro))
- }
-
- const selectDistro = (name: string) => {
- setStore("selectedDistro", name)
- setStore("step", undefined)
- }
-
- const openDeepagentCodeStep = () => {
- const distro = selectedDistro()
- if (!distro) return
- void run(() => enterWslDeepagentCodeStep(distro, api.probeDeepagentCode, (step) => setStore("step", step)))
- }
-
- const finish = async () => {
- const distro = selectedDistro()
- if (!distro) return
- setStore("adding", true)
- try {
- await api.addServer(distro)
- if (props.onAdded) {
- await props.onAdded(distro)
- } else {
- dialog.close()
- }
- } catch (err) {
- requestError(language, err)
- } finally {
- setStore("adding", false)
- }
- }
-
- const steps = createMemo(() => {
- const active = activeStep()
- const activeIndex = STEPS.indexOf(active)
- const recommendedIndex = STEPS.indexOf(recommendedStep())
- return STEPS.map((step) => {
- const index = STEPS.indexOf(step)
- return {
- step,
- title:
- step === "wsl"
- ? language.t("wsl.server.label")
- : step === "distro"
- ? language.t("wsl.onboarding.step.distro")
- : language.t("wsl.onboarding.step.deepagent-code"),
- state:
- active === step
- ? "current"
- : step === "wsl"
- ? wslReady()
- ? "done"
- : "warning"
- : step === "distro"
- ? distroReady()
- ? "done"
- : index > activeIndex
- ? "locked"
- : "warning"
- : deepagentCodeCheck()?.matchesDesktop === false
- ? "warning"
- : deepagentCodeReady()
- ? "done"
- : index > activeIndex
- ? "locked"
- : "warning",
- locked: index > recommendedIndex,
- }
- })
- })
- const loadError = createMemo(() => {
- const error = wslServers.error
- if (!error) return language.t("wsl.onboarding.loadFailed")
- return error instanceof Error ? error.message : String(error)
- })
-
- return (
-
- {language.t("wsl.onboarding.loading")}
}
- >
- {loadError()} }
- >
-
-
- {(item) => (
-
- )}
-
-
-
-
-
-
-
-
{language.t("wsl.server.label")}
-
-
-
-
-
{wslMessage()}
-
-
-
- {language.t("wsl.onboarding.windowsRestartRequired")}
-
-
-
-
-
-
-
-
-
-
-
-
-
{language.t("wsl.onboarding.step.distro")}
-
-
-
-
-
{distroMessage()}
-
-
-
0}
- fallback={
-
- {visibleInstalledDistros().length
- ? language.t("wsl.onboarding.allDistrosAdded")
- : current()?.runtime?.available
- ? language.t("wsl.onboarding.noDistros")
- : language.t("wsl.onboarding.checkingDistros")}
-
- }
- >
-
- {(item) => (
-
- )}
-
-
-
-
-
0}>
-
-
-
{language.t("wsl.onboarding.install")}
-
-
-
-
-
-
-
-
-
- {(item) => {
- const selected = () => installTarget()?.name === item.name
- return (
-
- )
- }}
-
-
-
-
-
-
-
-
-
- {language.t("wsl.onboarding.wsl2Required")}
-
-
-
- {(message) => {message()}
}
-
-
-
- {language.t("wsl.onboarding.toolsRequired")}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{language.t("wsl.onboarding.step.deepagent-code")}
-
-
-
-
-
-
-
-
-
-
{deepagentCodeMessage()}
-
- {(check) => (
-
-
- {language.t("wsl.onboarding.path", {
- path: check().resolvedPath ?? language.t("wsl.onboarding.notFound"),
- })}
-
-
- {language.t("wsl.onboarding.version", {
- version: check().version ?? language.t("wsl.onboarding.unknown"),
- })}
-
- {(expected) => (
- {` · ${language.t("wsl.onboarding.desktopVersion", { version: expected() })}`}
- )}
-
-
-
- {language.t("wsl.onboarding.versionMismatch")}
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
-
-function requestError(language: ReturnType