From 24d65c92fe53e264663b03b6fcbed7703f841cb7 Mon Sep 17 00:00:00 2001 From: idevlab Date: Mon, 24 Aug 2026 01:42:25 +0800 Subject: [PATCH] feat(desktop): refresh empty session workspace --- apps/desktop/src/App.tsx | 191 ++++++++++-------- apps/desktop/src/i18n/strings.ts | 33 +++ apps/desktop/src/plugins/PluginUiSlot.tsx | 10 +- apps/desktop/src/session/Composer.tsx | 4 +- .../src/session/EmptySessionStarters.tsx | 79 ++++++++ .../src/session/SessionHeaderActions.tsx | 46 +++-- apps/desktop/src/sidebar/SessionRail.tsx | 41 ++-- apps/desktop/src/styles.css | 20 ++ .../emptySessionStartersRendered.test.tsx | 39 ++++ .../tests/sessionRailRendered.test.tsx | 8 +- .../tests/windowChromeContract.test.ts | 6 +- 11 files changed, 348 insertions(+), 129 deletions(-) create mode 100644 apps/desktop/src/session/EmptySessionStarters.tsx create mode 100644 apps/desktop/tests/emptySessionStartersRendered.test.tsx diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 5d71ba22..696fa497 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -245,6 +245,7 @@ import { } from "./session/SceneBanner"; import { SessionHeaderActions } from "./session/SessionHeaderActions"; import { SideChatPanel, type SideChatSeed } from "./session/SideChatPanel"; +import { EmptySessionStarters } from "./session/EmptySessionStarters"; import { ProjectActionDialog } from "./session/ProjectActionDialog"; import { projectActionBindings } from "./session/projectActions"; import { StageTrack } from "./session/StageTrack"; @@ -5654,7 +5655,7 @@ export default function App() { the traffic lights and the expand button takes the wordmark's place. */}
@@ -5666,44 +5667,44 @@ export default function App() { /> )} {/* Breadcrumb, reference-style: project / thread. */} - - {activeProjectName && ( - <> - - {activeProjectName} - - - / - - - )} - {activeBoardTask ? ( - - ) : null} - - {activeTitle} - - {activeBoardTask && activeSessionTitle ? ( - <> - / - - {activeSessionTitle} - - - ) : null} - {!activeBoardTask && activeSession ? ( - - {t("rail.newTemporarySession")} +
+ + {activeProjectName && ( + <> + + {activeProjectName} + + / + + )} + {activeBoardTask ? ( + + ) : null} + + {activeTitle} - ) : null} + {activeBoardTask && activeSessionTitle ? ( + <> + / + + {activeSessionTitle} + + + ) : null} + {!activeBoardTask && activeSession ? ( + + {t("rail.newTemporarySession")} + + ) : null} +
@@ -5818,61 +5819,81 @@ export default function App() { Compact, the wrapper is just the composer's slot; expanded, it is the document column inside the outer transcript/document row. Keep this wrapper vertical so banners and plugin contributions remain above the document instead of consuming its width. An empty - thread is the hero state: the heading and the card sit together in the centre. */} + thread gives the welcome choices the quiet middle of the page while the Composer stays + near the bottom as the stable primary action. */}
{/* "What should we build in ?" — the project name is the project switcher. */} {!docMode && turns.length === 0 && !sessionLoading && ( -

- {t("transcript.greetingIn")}{" "} - - - {activeProjectName ?? t("rail.noProject")} - - } - /> - - {projects.length > 0 && ( +
+
+

+ {activeProjectName ? ( <> - - {projects.map((project) => ( - selectProject(project.path)} - > - - - {project.name} - - {project.path === activeProject && } + {t("transcript.greetingIn")}{" "} + + + {activeProjectName} + + } + /> + + {projects.length > 0 && ( + <> + + {projects.map((project) => ( + selectProject(project.path)} + > + + + {project.name} + + {project.path === activeProject && } + + ))} + + + + )} + void addProjectFolder()}> + + {t("rail.addProject")} - ))} - - + + + {t("transcript.greetingEnd")} + ) : ( + t("transcript.greeting") )} - void addProjectFolder()}> - - {t("rail.addProject")} - - - - {t("transcript.greetingEnd")} -

+

+ { + insertTextRef.current?.(prompt); + setTimeout(() => focusEditorRef.current?.(), 0); + }} + /> +
+
)} {/* An archived chat reads, but doesn't run: the composer yields its slot to this notice until the session is restored. The composer stays mounted (hidden) — unmounting @@ -5941,12 +5962,14 @@ export default function App() { {greetingEnd}". "transcript.greetingIn": "What should we build in", "transcript.greetingEnd": "?", + "transcript.starters": "Ways to get started", + "transcript.starter.explore.title": "Explore the project", + "transcript.starter.explore.description": "Understand its structure and execution flow", + "transcript.starter.explore.prompt": + "Explore this project and explain its architecture, key modules, and main execution flow.", + "transcript.starter.build.title": "Build a feature", + "transcript.starter.build.description": "Turn an idea into a working implementation", + "transcript.starter.build.prompt": + "Build a new feature in this project. Start by understanding the relevant code, then implement and verify it.", + "transcript.starter.review.title": "Review the code", + "transcript.starter.review.description": "Find risks and concrete improvements", + "transcript.starter.review.prompt": + "Review the current code and suggest concrete improvements, prioritizing correctness, maintainability, and user impact.", + "transcript.starter.fix.title": "Fix an issue", + "transcript.starter.fix.description": "Trace the cause, repair it, and verify the result", + "transcript.starter.fix.prompt": + "Find and fix an issue in this project. Reproduce it, identify the root cause, implement the smallest safe fix, and verify it.", "transcript.label": "Conversation transcript", "transcript.jumpLatest": "Jump to latest", "transcript.hint": @@ -2340,6 +2357,22 @@ export const zhCN: Record = { "transcript.greeting": "今天做点什么?", "transcript.greetingIn": "在", "transcript.greetingEnd": "里做点什么?", + "transcript.starters": "开始方式", + "transcript.starter.explore.title": "理解项目", + "transcript.starter.explore.description": "梳理结构、模块与运行流程", + "transcript.starter.explore.prompt": "探索这个项目,说明整体架构、关键模块和主要运行流程。", + "transcript.starter.build.title": "构建功能", + "transcript.starter.build.description": "把想法变成可运行的实现", + "transcript.starter.build.prompt": + "在这个项目中构建一个新功能。先理解相关代码,再完成实现并验证结果。", + "transcript.starter.review.title": "审查代码", + "transcript.starter.review.description": "找出风险并提出具体改进", + "transcript.starter.review.prompt": + "审查当前代码并提出具体改进,优先关注正确性、可维护性和用户影响。", + "transcript.starter.fix.title": "修复问题", + "transcript.starter.fix.description": "定位原因、完成修复并验证", + "transcript.starter.fix.prompt": + "查找并修复这个项目中的一个问题。先复现并定位根因,再实施最小安全修复并完成验证。", "transcript.label": "对话记录", "transcript.jumpLatest": "回到最新", "transcript.hint": "在下面写提示词——输入 / 插入技能,@ 引用文件。", diff --git a/apps/desktop/src/plugins/PluginUiSlot.tsx b/apps/desktop/src/plugins/PluginUiSlot.tsx index 439846b6..8c9f34db 100644 --- a/apps/desktop/src/plugins/PluginUiSlot.tsx +++ b/apps/desktop/src/plugins/PluginUiSlot.tsx @@ -69,14 +69,14 @@ export function PluginUiSlot({ type="button" variant="ghost" size="compact" - className="max-w-44 shrink-0" + className="session-header-labeled-button max-w-44 shrink-0" title={contribution.description || `${contribution.pluginName}: ${contribution.label}`} aria-label={`${contribution.pluginName}: ${contribution.label}`} disabled={busy !== null} onClick={() => void invoke(contribution)} > {busy === key ? : } - {contribution.label} + {contribution.label} ); })} @@ -159,19 +159,19 @@ export function PluginUiSlot({ return (
+ ); +} diff --git a/apps/desktop/src/session/SessionHeaderActions.tsx b/apps/desktop/src/session/SessionHeaderActions.tsx index 27d9da7c..f88fdfec 100644 --- a/apps/desktop/src/session/SessionHeaderActions.tsx +++ b/apps/desktop/src/session/SessionHeaderActions.tsx @@ -37,7 +37,7 @@ function SplitMenuTrigger({ label, disabled = false }: { label: string; disabled render={( {actions.slice(0, 2).map((action) => ( ))} @@ -152,7 +166,7 @@ export function SessionHeaderActions({ render={( @@ -214,14 +230,16 @@ export function SessionHeaderActions({
} {/* ---- 1 · title ---------------------------------------------------------------------- */} - {/* Keep the controls centred in the same 48px title row as the main header, with enough - clearance for the macOS traffic lights. */} + {/* One quiet title row gives the rail a stable identity without adding another search card. */}
-
+ + {t("app.name")} + + + + + } + /> + + {t("rail.searchLabel")} + {searchHint && {searchHint}} + + {t("rail.collapse")}
-
- -
{/* ---- 2 · features ------------------------------------------------------------------- */}