From 149d6a9b44d07066cf576846c07fd8f2504bdaba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=99=E5=AD=90?= <113168461+706412584@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:19:11 +0800 Subject: [PATCH] feat(desktop): wire LspStatusIndicator into WorkspacePanel (#51) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the bare 'LSP {state} . N diagnostics' span in the file preview header with the existing LspStatusIndicator component, which was already authored, tested, and shipped on main but had no parent rendering it. What you now get on the workspace file preview header: - Spinning loader for starting/idle, green check for ready, red alert with N error count when ready+errors, red retry button when unavailable - Click the pill to open a diagnostics dropdown listing path:line:col plus truncated message; Enter on a row opens that file in the preview - Retry button round-trips POST /api/sessions/:id/lsp/restart and re-reads state, since LspManager keeps unavailable workspaces sticky until an explicit restart Type bridge: - The server wire shape (WorkspaceLspState: idle/starting/ready/unavailable with an error string) does not carry LspUnavailableReason, but LspStatusIndicator was authored against the legacy four-state shape with reason+errorCount - New pure adapter desktop/src/lib/lspStateMap.ts maps the two shapes: idle-as-starting, ready threads errorCount derived from severity=error diagnostics, unavailable collapses to init-failed (Retry path) Tested: - bunx vitest src/lib/lspStateMap.test.ts (8 cases — all four state transitions plus diagnostic severity counting) - bunx vitest LspStatusIndicator + workspacePanelStore — 53/53 pass - bun run lint (tsc --noEmit) clean Not-tested: live-LSP retry round-trip (covered by lspManager server tests separately) Confidence: high Scope-risk: narrow Co-authored-by: 你的姓名 --- .../components/workspace/WorkspacePanel.tsx | 35 ++++--- desktop/src/lib/lspStateMap.test.ts | 99 +++++++++++++++++++ desktop/src/lib/lspStateMap.ts | 51 ++++++++++ 3 files changed, 174 insertions(+), 11 deletions(-) create mode 100644 desktop/src/lib/lspStateMap.test.ts create mode 100644 desktop/src/lib/lspStateMap.ts diff --git a/desktop/src/components/workspace/WorkspacePanel.tsx b/desktop/src/components/workspace/WorkspacePanel.tsx index 4bf57ff0..35ca9bbc 100644 --- a/desktop/src/components/workspace/WorkspacePanel.tsx +++ b/desktop/src/components/workspace/WorkspacePanel.tsx @@ -31,6 +31,9 @@ import { import { WorkspaceFileOpenWith } from './WorkspaceFileOpenWith' import { WorkspaceEditor, saveWorkspaceBuffer } from './WorkspaceEditor' import { UnsavedChangesModal } from './UnsavedChangesModal' +import { LspStatusIndicator } from './LspStatusIndicator' +import { errorCountFromDiagnostics, toLegacyLspState } from '../../lib/lspStateMap' +import { sessionsApi } from '../../api/sessions' type WorkspacePanelProps = { sessionId: string @@ -981,6 +984,7 @@ export function WorkspacePanel({ sessionId, embedded = false }: WorkspacePanelPr const closePreviewTabs = useWorkspacePanelStore((state) => state.closePreviewTabs) const initBuffer = useWorkspacePanelStore((state) => state.initBuffer) const syncLsp = useWorkspacePanelStore((state) => state.syncLsp) + const loadLspState = useWorkspacePanelStore((state) => state.loadLspState) const bufferStateByTabId = useWorkspacePanelStore((state) => state.bufferStateByTabId) const closePanel = useWorkspacePanelStore((state) => state.closePanel) const addWorkspaceReference = useWorkspaceChatContextStore((state) => state.addReference) @@ -1354,17 +1358,26 @@ export function WorkspacePanel({ sessionId, embedded = false }: WorkspacePanelPr ))}
{activePreviewTab.kind === 'file' && ( - - {t('workspace.lspState', { state: lspState?.state ?? 'idle' })} · {t('workspace.lspDiagnostics', { count: activeLspDiagnostics?.diagnosticsTotal ?? 0 })} - + { + // Server-side LspManager keeps `unavailable` workspaces + // sticky until an explicit restart, so this round-trips + // through /lsp/restart and then re-reads state. + void sessionsApi + .restartWorkspaceLsp(sessionId, { path: activePreviewTab.path }) + .catch(() => undefined) + .then(() => loadLspState(sessionId, activePreviewTab.path)) + }} + onDiagnosticOpen={(diagnostic) => { + void openPreview(sessionId, diagnostic.path, 'file') + }} + /> )}