@deepseek-harness-tui/dsh-tui 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -124
- package/bin/dsh-tui.js +748 -22
- package/cordis.patch.yml +38 -10
- package/cordis.yml +2 -0
- package/lib/types/adapter/channel/session-projection.d.ts.map +1 -1
- package/lib/types/adapter/channel/session-projection.js +2 -0
- package/lib/types/adapter/channel/ui-policy.d.ts +4 -1
- package/lib/types/adapter/channel/ui-policy.d.ts.map +1 -1
- package/lib/types/adapter/channel/ui-policy.js +5 -1
- package/lib/types/adapter/kernel/host-facade.d.ts.map +1 -1
- package/lib/types/adapter/kernel/host-facade.js +1 -0
- package/lib/types/adapter/kernel/replay.d.ts.map +1 -1
- package/lib/types/adapter/kernel/replay.js +3 -0
- package/lib/types/adapter/kernel/runtime.d.ts.map +1 -1
- package/lib/types/adapter/kernel/runtime.js +2 -0
- package/lib/types/adapter/kernel/slices/workspace.d.ts.map +1 -1
- package/lib/types/adapter/kernel/slices/workspace.js +1 -0
- package/lib/types/adapter/ports/channel-ui.d.ts +22 -2
- package/lib/types/adapter/ports/channel-ui.d.ts.map +1 -1
- package/lib/types/adapter/ports/channel-view.d.ts +66 -0
- package/lib/types/adapter/ports/channel-view.d.ts.map +1 -1
- package/lib/types/adapter/ports/channel-workspace.d.ts +23 -0
- package/lib/types/adapter/ports/channel-workspace.d.ts.map +1 -1
- package/lib/types/adapter/ports/workspace.d.ts +2 -0
- package/lib/types/adapter/ports/workspace.d.ts.map +1 -1
- package/lib/types/adapter/upstream/workspace-driver.d.ts.map +1 -1
- package/lib/types/adapter/upstream/workspace-driver.js +10 -2
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +1 -0
- package/lib/types/components/ContextBarView.d.ts +15 -7
- package/lib/types/components/ContextBarView.d.ts.map +1 -1
- package/lib/types/components/ContextBarView.js +26 -22
- package/lib/types/components/ImagePreviewOverlay.d.ts.map +1 -1
- package/lib/types/components/ImagePreviewOverlay.js +11 -5
- package/lib/types/components/Markdown.d.ts +11 -1
- package/lib/types/components/Markdown.d.ts.map +1 -1
- package/lib/types/components/Markdown.js +20 -2
- package/lib/types/components/MermaidDiagram.d.ts +13 -0
- package/lib/types/components/MermaidDiagram.d.ts.map +1 -0
- package/lib/types/components/MermaidDiagram.js +61 -0
- package/lib/types/components/MessageList.d.ts +26 -1
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +72 -7
- package/lib/types/components/PageMargin.d.ts +1 -1
- package/lib/types/components/PromptInput.d.ts +37 -1
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +243 -28
- package/lib/types/components/ScrollbarGutter.d.ts +20 -5
- package/lib/types/components/ScrollbarGutter.d.ts.map +1 -1
- package/lib/types/components/ScrollbarGutter.js +88 -10
- package/lib/types/components/StreamingMarkdown.d.ts.map +1 -1
- package/lib/types/components/StreamingMarkdown.js +30 -28
- package/lib/types/components/Tooltip.d.ts.map +1 -1
- package/lib/types/components/Tooltip.js +45 -7
- package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
- package/lib/types/components/messages/UserPromptMessage.js +5 -7
- package/lib/types/components/messages/useImageInspection.d.ts.map +1 -1
- package/lib/types/components/messages/useImageInspection.js +19 -9
- package/lib/types/components/promptDraftCache.d.ts +102 -0
- package/lib/types/components/promptDraftCache.d.ts.map +1 -0
- package/lib/types/components/promptDraftCache.js +76 -0
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts +17 -1
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +40 -7
- package/lib/types/components/questions/QuestionMinimizedBar.d.ts +12 -0
- package/lib/types/components/questions/QuestionMinimizedBar.d.ts.map +1 -0
- package/lib/types/components/questions/QuestionMinimizedBar.js +43 -0
- package/lib/types/components/sessions/SessionListRow.d.ts +23 -1
- package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
- package/lib/types/components/sessions/SessionListRow.js +52 -5
- package/lib/types/components/workspaces/HomeWorkspaceRow.d.ts +49 -0
- package/lib/types/components/workspaces/HomeWorkspaceRow.d.ts.map +1 -0
- package/lib/types/components/workspaces/HomeWorkspaceRow.js +50 -0
- package/lib/types/dsh-adapter/bundled-presets.d.ts +10 -0
- package/lib/types/dsh-adapter/bundled-presets.d.ts.map +1 -0
- package/lib/types/dsh-adapter/bundled-presets.js +80 -0
- package/lib/types/dsh-adapter/channel/action-readiness.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/action-readiness.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/action-readiness.js +3 -0
- package/lib/types/dsh-adapter/channel/agent-view-projection.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/agent-view-projection.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/agent-view-projection.js +19 -2
- package/lib/types/dsh-adapter/channel/background-action.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/background-action.js +52 -29
- package/lib/types/dsh-adapter/channel/binding.d.ts +19 -2
- package/lib/types/dsh-adapter/channel/binding.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/binding.js +78 -18
- package/lib/types/dsh-adapter/channel/composer-images.d.ts +5 -0
- package/lib/types/dsh-adapter/channel/composer-images.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/composer-images.js +105 -6
- package/lib/types/dsh-adapter/channel/context-bookkeeping.d.ts +8 -0
- package/lib/types/dsh-adapter/channel/context-bookkeeping.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/context-bookkeeping.js +3 -2
- package/lib/types/dsh-adapter/channel/ide-selection.d.ts +58 -0
- package/lib/types/dsh-adapter/channel/ide-selection.d.ts.map +1 -0
- package/lib/types/dsh-adapter/channel/ide-selection.js +226 -0
- package/lib/types/dsh-adapter/channel/input-delivery.d.ts +6 -1
- package/lib/types/dsh-adapter/channel/input-delivery.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/input-delivery.js +9 -1
- package/lib/types/dsh-adapter/channel/local-actions.d.ts +3 -19
- package/lib/types/dsh-adapter/channel/local-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/local-actions.js +2 -1
- package/lib/types/dsh-adapter/channel/model-actions.d.ts +4 -1
- package/lib/types/dsh-adapter/channel/model-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/model-actions.js +82 -6
- package/lib/types/dsh-adapter/channel/model-switch.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/model-switch.js +49 -31
- package/lib/types/dsh-adapter/channel/projection.d.ts +4 -0
- package/lib/types/dsh-adapter/channel/projection.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/projection.js +29 -19
- package/lib/types/dsh-adapter/channel/reports.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/reports.js +4 -12
- package/lib/types/dsh-adapter/channel/session-fork.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-fork.js +26 -2
- package/lib/types/dsh-adapter/channel/session-live-adoption.d.ts +3 -0
- package/lib/types/dsh-adapter/channel/session-live-adoption.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-live-adoption.js +1 -0
- package/lib/types/dsh-adapter/channel/session-metadata.d.ts +10 -0
- package/lib/types/dsh-adapter/channel/session-metadata.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-metadata.js +2 -2
- package/lib/types/dsh-adapter/channel/session-resume.d.ts +11 -2
- package/lib/types/dsh-adapter/channel/session-resume.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-resume.js +291 -94
- package/lib/types/dsh-adapter/channel/session-rewind.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-rewind.js +34 -16
- package/lib/types/dsh-adapter/channel/session-tree-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-tree-actions.js +19 -3
- package/lib/types/dsh-adapter/channel/settings-host.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/settings-host.js +4 -3
- package/lib/types/dsh-adapter/channel/state.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/state.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/state.js +1 -1
- package/lib/types/dsh-adapter/channel/transcript.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/transcript.js +6 -12
- package/lib/types/dsh-adapter/channel/types.d.ts +6 -1
- package/lib/types/dsh-adapter/channel/types.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/workspace-actions.d.ts +4 -1
- package/lib/types/dsh-adapter/channel/workspace-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/workspace-actions.js +41 -1
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +67 -6
- package/lib/types/dsh-adapter/compat/messages.d.ts +15 -0
- package/lib/types/dsh-adapter/compat/messages.d.ts.map +1 -0
- package/lib/types/dsh-adapter/compat/messages.js +15 -0
- package/lib/types/dsh-adapter/compat/settings.d.ts +33 -0
- package/lib/types/dsh-adapter/compat/settings.d.ts.map +1 -0
- package/lib/types/dsh-adapter/compat/settings.js +43 -0
- package/lib/types/dsh-adapter/compat/shell.d.ts +25 -0
- package/lib/types/dsh-adapter/compat/shell.d.ts.map +1 -0
- package/lib/types/dsh-adapter/compat/shell.js +11 -0
- package/lib/types/dsh-adapter/contract.d.ts +4 -4
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +15 -6
- package/lib/types/dsh-adapter/ide-channel.d.ts +224 -0
- package/lib/types/dsh-adapter/ide-channel.d.ts.map +1 -0
- package/lib/types/dsh-adapter/ide-channel.js +556 -0
- package/lib/types/dsh-adapter/index.d.ts +18 -3
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +32 -5
- package/lib/types/dsh-adapter/plugin.d.ts +5 -2
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +173 -42
- package/lib/types/dsh-adapter/preset-resolution.d.ts +8 -0
- package/lib/types/dsh-adapter/preset-resolution.d.ts.map +1 -1
- package/lib/types/dsh-adapter/preset-resolution.js +10 -0
- package/lib/types/dsh-adapter/session-mount-heartbeat.d.ts +53 -0
- package/lib/types/dsh-adapter/session-mount-heartbeat.d.ts.map +1 -0
- package/lib/types/dsh-adapter/session-mount-heartbeat.js +100 -0
- package/lib/types/dsh-adapter/sessionTree.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sessionTree.js +3 -2
- package/lib/types/dsh-adapter/subagents.d.ts.map +1 -1
- package/lib/types/dsh-adapter/subagents.js +6 -5
- package/lib/types/dsh-adapter/workspaces.d.ts +46 -3
- package/lib/types/dsh-adapter/workspaces.d.ts.map +1 -1
- package/lib/types/dsh-adapter/workspaces.js +116 -0
- package/lib/types/effortPrefs.d.ts +15 -3
- package/lib/types/effortPrefs.d.ts.map +1 -1
- package/lib/types/effortPrefs.js +37 -3
- package/lib/types/homePrefs.d.ts +15 -0
- package/lib/types/homePrefs.d.ts.map +1 -0
- package/lib/types/homePrefs.js +53 -0
- package/lib/types/i18n.d.ts +257 -296
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +78 -84
- package/lib/types/ink/colorize.d.ts +10 -0
- package/lib/types/ink/colorize.d.ts.map +1 -1
- package/lib/types/ink/colorize.js +21 -0
- package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
- package/lib/types/ink/components/ScrollBox.js +12 -1
- package/lib/types/ink/dom.d.ts +1 -0
- package/lib/types/ink/dom.d.ts.map +1 -1
- package/lib/types/ink/hooks/use-copy-on-select.d.ts +5 -1
- package/lib/types/ink/hooks/use-copy-on-select.d.ts.map +1 -1
- package/lib/types/ink/hooks/use-copy-on-select.js +15 -1
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +36 -1
- package/lib/types/ink/output.d.ts +10 -1
- package/lib/types/ink/output.d.ts.map +1 -1
- package/lib/types/ink/output.js +109 -17
- package/lib/types/ink/render-border.d.ts +6 -1
- package/lib/types/ink/render-border.d.ts.map +1 -1
- package/lib/types/ink/render-border.js +25 -9
- package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
- package/lib/types/ink/render-node-to-output.js +48 -6
- package/lib/types/ink/renderer.d.ts +6 -0
- package/lib/types/ink/renderer.d.ts.map +1 -1
- package/lib/types/ink/renderer.js +2 -1
- package/lib/types/ink/selection.d.ts +60 -0
- package/lib/types/ink/selection.d.ts.map +1 -1
- package/lib/types/ink/selection.js +129 -0
- package/lib/types/ink/sixel-graphics.d.ts +19 -1
- package/lib/types/ink/sixel-graphics.d.ts.map +1 -1
- package/lib/types/ink/sixel-graphics.js +124 -12
- package/lib/types/ink/terminal-image.d.ts +18 -1
- package/lib/types/ink/terminal-image.d.ts.map +1 -1
- package/lib/types/ink/terminal.d.ts +17 -0
- package/lib/types/ink/terminal.d.ts.map +1 -1
- package/lib/types/ink/terminal.js +20 -1
- package/lib/types/screens/Chat.d.ts +17 -1
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +284 -72
- package/lib/types/screens/SessionSupervisor.d.ts +84 -0
- package/lib/types/screens/SessionSupervisor.d.ts.map +1 -0
- package/lib/types/screens/SessionSupervisor.js +350 -0
- package/lib/types/screens/SessionTree.js +2 -2
- package/lib/types/screens/Settings.d.ts.map +1 -1
- package/lib/types/screens/Settings.js +32 -9
- package/lib/types/screens/StatusLine.d.ts +13 -0
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +36 -17
- package/lib/types/screens/StatusMetrics.d.ts +79 -22
- package/lib/types/screens/StatusMetrics.d.ts.map +1 -1
- package/lib/types/screens/StatusMetrics.js +153 -88
- package/lib/types/screens/sessionSupervisor/model.d.ts +83 -0
- package/lib/types/screens/sessionSupervisor/model.d.ts.map +1 -0
- package/lib/types/screens/sessionSupervisor/model.js +69 -0
- package/lib/types/screens/sessionSupervisor/useSessionSupervisor.d.ts +120 -0
- package/lib/types/screens/sessionSupervisor/useSessionSupervisor.d.ts.map +1 -0
- package/lib/types/screens/sessionSupervisor/useSessionSupervisor.js +593 -0
- package/lib/types/sessionMounts.d.ts +272 -0
- package/lib/types/sessionMounts.d.ts.map +1 -0
- package/lib/types/sessionMounts.js +706 -0
- package/lib/types/sessions/format.d.ts.map +1 -1
- package/lib/types/sessions/format.js +4 -1
- package/lib/types/sessions/resumeFailure.d.ts +38 -0
- package/lib/types/sessions/resumeFailure.d.ts.map +1 -0
- package/lib/types/sessions/resumeFailure.js +58 -0
- package/lib/types/terminal-utils/mermaid.d.ts +44 -0
- package/lib/types/terminal-utils/mermaid.d.ts.map +1 -0
- package/lib/types/terminal-utils/mermaid.js +83 -0
- package/lib/types/tips.d.ts.map +1 -1
- package/lib/types/tips.js +175 -19
- package/lib/types/tuiDisplayPrefs.d.ts +7 -9
- package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
- package/lib/types/tuiDisplayPrefs.js +31 -28
- package/lib/types/update.d.ts +61 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +194 -0
- package/lib/types/utils/imageResize.d.ts +90 -0
- package/lib/types/utils/imageResize.d.ts.map +1 -0
- package/lib/types/utils/imageResize.js +272 -0
- package/lib/types/utils/keymap.d.ts +5 -1
- package/lib/types/utils/keymap.d.ts.map +1 -1
- package/lib/types/utils/keymap.js +23 -0
- package/lib/types/workspaces.d.ts +2 -1
- package/lib/types/workspaces.d.ts.map +1 -1
- package/lib/types/workspaces.js +1 -1
- package/node_modules/@deepseek-harness-tui/dsh-auth/package.json +10 -10
- package/package.json +120 -103
- package/presets/liangshen/.dsh-tui-managed.json +1 -1
- package/presets/liangshen/agent.cordis.yml +15 -0
- package/lib/types/screens/AgentView.d.ts +0 -23
- package/lib/types/screens/AgentView.d.ts.map +0 -1
- package/lib/types/screens/AgentView.js +0 -546
- package/lib/types/screens/SessionBrowser.d.ts +0 -42
- package/lib/types/screens/SessionBrowser.d.ts.map +0 -1
- package/lib/types/screens/SessionBrowser.js +0 -911
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IDE selection channel (PR-B groundwork): the dsh-tui side of a loopback
|
|
3
|
+
* WebSocket link with a companion IDE extension (dsh-tui-vscode). The
|
|
4
|
+
* extension broadcasts caret-selection changes; the TUI consumes them to
|
|
5
|
+
* attach `<attached-file …>` blocks at submit time (ADR-001).
|
|
6
|
+
*
|
|
7
|
+
* Discovery has two paths (DESIGN D4):
|
|
8
|
+
* 1. env direct — DSH_TUI_IDE_PORT + DSH_TUI_IDE_TOKEN, injected when the
|
|
9
|
+
* extension spawns this TUI itself;
|
|
10
|
+
* 2. lock scan — `~/.dsh-tui/ide/*.lock` files written by the extension
|
|
11
|
+
* ({port, token, workspaceFolders, pid}); locks whose workspaceFolders
|
|
12
|
+
* match the session cwd connect first.
|
|
13
|
+
*
|
|
14
|
+
* Both paths fail → silent degradation (AC-4): no error, no retry,
|
|
15
|
+
* connected=false, every other TUI feature unaffected. The client is Node's
|
|
16
|
+
* native WebSocket global (engines ^22.19 || >=24, D5) — zero dependencies
|
|
17
|
+
* and no auto-reconnect (degrade-on-drop is the design).
|
|
18
|
+
*
|
|
19
|
+
* Protocol constants here are a cross-repo contract (the extension's server
|
|
20
|
+
* side implements the mirror); changing them requires updating both ends
|
|
21
|
+
* plus ADR-001 (DESIGN §9.5). Protocol version 2:
|
|
22
|
+
* - Handshake: the client's first frame is
|
|
23
|
+
* `{"method":"ide/hello","params":{"token","protocolVersion"}}`; the
|
|
24
|
+
* server validates the token and answers
|
|
25
|
+
* `{"method":"ide/hello_ack","params":{"protocolVersion",
|
|
26
|
+
* "workspaceFolders"}}`. The client is connected only after a valid ack —
|
|
27
|
+
* a wrong token closes the socket and the client tries its next
|
|
28
|
+
* candidate.
|
|
29
|
+
* - Notifications: `{"method":"selection_changed","params":{path,
|
|
30
|
+
* startLine, endLine, isEmpty, text, documentVersion}}` — coordinates are
|
|
31
|
+
* 0-based, and `text` carries the editor buffer's own selection text
|
|
32
|
+
* (unsaved edits included); the TUI attaches that text verbatim and only
|
|
33
|
+
* falls back to reading the file from disk for protocol-1 pushes.
|
|
34
|
+
*/
|
|
35
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
36
|
+
import { join } from 'node:path';
|
|
37
|
+
import { DATA_DIR } from '../utils/paths.js';
|
|
38
|
+
/** Env var carrying the extension's loopback WS port (spawn-injected). */
|
|
39
|
+
export const IDE_PORT_ENV = 'DSH_TUI_IDE_PORT';
|
|
40
|
+
/** Env var carrying the extension's handshake token (spawn-injected). */
|
|
41
|
+
export const IDE_TOKEN_ENV = 'DSH_TUI_IDE_TOKEN';
|
|
42
|
+
/** Handshake frame the client must send as its very first message. */
|
|
43
|
+
const HELLO_METHOD = 'ide/hello';
|
|
44
|
+
/**
|
|
45
|
+
* Ack frame a protocol-2 server must answer with once the hello token is
|
|
46
|
+
* validated. Until it arrives the client is NOT connected: a server that
|
|
47
|
+
* rejects the token just closes, and the client moves on to the next
|
|
48
|
+
* candidate instead of mistaking an open socket for an authenticated one
|
|
49
|
+
* (maintainer review round 3).
|
|
50
|
+
*/
|
|
51
|
+
const HELLO_ACK_METHOD = 'ide/hello_ack';
|
|
52
|
+
/** Wire protocol this client speaks; hello_ack carries the server's value. */
|
|
53
|
+
export const IDE_PROTOCOL_VERSION = 2;
|
|
54
|
+
/** Selection notification method broadcast by the extension. */
|
|
55
|
+
const SELECTION_METHOD = 'selection_changed';
|
|
56
|
+
/** Total connection budget across all candidates, in milliseconds. */
|
|
57
|
+
const CONNECT_BUDGET_MS = 300;
|
|
58
|
+
/** Where the extension advertises its loopback server (`~/.dsh-tui/ide`). */
|
|
59
|
+
export function ideLockDir(dataDir = DATA_DIR) {
|
|
60
|
+
return join(dataDir, 'ide');
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Parse the direct-connect environment. Returns undefined when the pair is
|
|
64
|
+
* absent or malformed — port must be an integer within 1..65535 and the
|
|
65
|
+
* token non-empty — so a bad env degrades to lock discovery instead of
|
|
66
|
+
* throwing.
|
|
67
|
+
*/
|
|
68
|
+
export function envDirect(env) {
|
|
69
|
+
const portRaw = env[IDE_PORT_ENV];
|
|
70
|
+
const token = env[IDE_TOKEN_ENV];
|
|
71
|
+
if (typeof portRaw !== 'string' || portRaw === '')
|
|
72
|
+
return undefined;
|
|
73
|
+
if (typeof token !== 'string' || token === '')
|
|
74
|
+
return undefined;
|
|
75
|
+
const port = Number(portRaw);
|
|
76
|
+
if (!Number.isSafeInteger(port))
|
|
77
|
+
return undefined;
|
|
78
|
+
if (port < 1 || port > 65535)
|
|
79
|
+
return undefined;
|
|
80
|
+
return { port, token };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Validate an inbound `ide/hello_ack` frame. Returns the server's protocol
|
|
84
|
+
* version and workspace folders, or undefined for anything else — wrong
|
|
85
|
+
* method, malformed envelope, or a version this client does not speak (both
|
|
86
|
+
* ends ship together, so a mismatch is a foreign server and must not be
|
|
87
|
+
* treated as connected).
|
|
88
|
+
*/
|
|
89
|
+
export function parseHelloAck(message) {
|
|
90
|
+
if (message === null || typeof message !== 'object')
|
|
91
|
+
return undefined;
|
|
92
|
+
const record = message;
|
|
93
|
+
if (record.method !== HELLO_ACK_METHOD)
|
|
94
|
+
return undefined;
|
|
95
|
+
const params = record.params;
|
|
96
|
+
if (params === null || typeof params !== 'object' || Array.isArray(params))
|
|
97
|
+
return undefined;
|
|
98
|
+
const payload = params;
|
|
99
|
+
const version = payload.protocolVersion;
|
|
100
|
+
const folders = payload.workspaceFolders;
|
|
101
|
+
if (typeof version !== 'number' || !Number.isSafeInteger(version))
|
|
102
|
+
return undefined;
|
|
103
|
+
if (version !== IDE_PROTOCOL_VERSION)
|
|
104
|
+
return undefined;
|
|
105
|
+
if (!Array.isArray(folders) || !folders.every(folder => typeof folder === 'string')) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
return { protocolVersion: version, workspaceFolders: folders };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Normalize a path for comparison: backslashes folded to forward slashes,
|
|
112
|
+
* trailing slashes stripped, case folded when the platform's filesystem is
|
|
113
|
+
* case-insensitive (same semantics as channel.ts normalizeCwd — sessions.ts
|
|
114
|
+
* precedent). Exported so verifiers can pin the Windows behavior.
|
|
115
|
+
*/
|
|
116
|
+
export function normalizeIdePath(path, caseInsensitive) {
|
|
117
|
+
const normalized = path === '/' ? '/' : path.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
118
|
+
return caseInsensitive ? normalized.toLowerCase() : normalized;
|
|
119
|
+
}
|
|
120
|
+
function platformCaseInsensitive() {
|
|
121
|
+
return process.platform === 'win32' || process.platform === 'darwin';
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Read and parse one lock file. Returns undefined for unreadable or
|
|
125
|
+
* malformed content (broken JSON, wrong field types) — a single bad lock
|
|
126
|
+
* must never break the whole scan.
|
|
127
|
+
*/
|
|
128
|
+
export function readLockEntry(file) {
|
|
129
|
+
let raw;
|
|
130
|
+
try {
|
|
131
|
+
raw = readFileSync(file, 'utf8');
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
let value;
|
|
137
|
+
try {
|
|
138
|
+
value = JSON.parse(raw);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
144
|
+
return undefined;
|
|
145
|
+
const record = value;
|
|
146
|
+
const port = record.port;
|
|
147
|
+
const token = record.token;
|
|
148
|
+
const folders = record.workspaceFolders;
|
|
149
|
+
if (typeof port !== 'number' || !Number.isSafeInteger(port) || port < 1 || port > 65535) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
if (typeof token !== 'string' || token === '')
|
|
153
|
+
return undefined;
|
|
154
|
+
if (!Array.isArray(folders) || !folders.every(folder => typeof folder === 'string')) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
const pid = record.pid;
|
|
158
|
+
if (typeof pid !== 'number' || !Number.isSafeInteger(pid))
|
|
159
|
+
return undefined;
|
|
160
|
+
return { port, token, workspaceFolders: folders, pid };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* True when a process id is currently alive (used to skip stale locks whose
|
|
164
|
+
* owning extension already exited). `process.kill(pid, 0)` probes existence
|
|
165
|
+
* without signalling; a missing process throws ESRCH and reads as dead.
|
|
166
|
+
* Same user, so no permission false-negatives for the loopback's own locks.
|
|
167
|
+
*/
|
|
168
|
+
function isProcessAlive(pid) {
|
|
169
|
+
try {
|
|
170
|
+
process.kill(pid, 0);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Scan a lock directory and return the candidates whose workspaceFolders
|
|
179
|
+
* actually CONTAIN the session cwd, most-specific root first; ties keep
|
|
180
|
+
* directory order (stable sort). Malformed and stale locks are skipped, and
|
|
181
|
+
* an unmatched lock is NOT a candidate at all — dialing another workspace's
|
|
182
|
+
* IDE would attach that project's selections here (maintainer review round 3:
|
|
183
|
+
* "unmatched trails every match" used to make start() connect to an
|
|
184
|
+
* unrelated window when nothing matched). Never throws — an unreadable
|
|
185
|
+
* directory degrades to an empty list.
|
|
186
|
+
*
|
|
187
|
+
* `pid` is part of the lock contract and reserved for future disambiguation
|
|
188
|
+
* (DESIGN R5); matching currently relies on workspaceFolders only.
|
|
189
|
+
*/
|
|
190
|
+
export function pickLockCandidates(lockDir, cwd, pid) {
|
|
191
|
+
void pid;
|
|
192
|
+
let names;
|
|
193
|
+
try {
|
|
194
|
+
names = readdirSync(lockDir)
|
|
195
|
+
.filter(name => name.endsWith('.lock'))
|
|
196
|
+
.sort();
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
const caseInsensitive = platformCaseInsensitive();
|
|
202
|
+
const cwdNorm = normalizeIdePath(cwd, caseInsensitive);
|
|
203
|
+
const entries = [];
|
|
204
|
+
for (const name of names) {
|
|
205
|
+
const entry = readLockEntry(join(lockDir, name));
|
|
206
|
+
// Stale lock: the extension that wrote it has already exited — a dead
|
|
207
|
+
// process can no longer own the handshake token, so never dial it
|
|
208
|
+
// (maintainer review round 2: stale locks could otherwise win discovery).
|
|
209
|
+
if (entry === undefined || !isProcessAlive(entry.pid))
|
|
210
|
+
continue;
|
|
211
|
+
// Boundary-checked prefix: `/repo/a` must NOT match a cwd of `/repo/abc`
|
|
212
|
+
// (another workspace's window), only `/repo/a` itself or a path under it.
|
|
213
|
+
// Without the separator guard the wrong window's lock is dialed first and
|
|
214
|
+
// its selections attach here. The POSIX root `/` matches every absolute
|
|
215
|
+
// cwd WITHOUT a `//` boundary. `matchLength` records the longest matching
|
|
216
|
+
// workspace root so candidates rank most-specific-first below.
|
|
217
|
+
let matchLength = 0;
|
|
218
|
+
for (const folder of entry.workspaceFolders) {
|
|
219
|
+
const root = normalizeIdePath(folder, caseInsensitive);
|
|
220
|
+
if (root === '')
|
|
221
|
+
continue;
|
|
222
|
+
const hits = root === '/'
|
|
223
|
+
? cwdNorm.startsWith('/')
|
|
224
|
+
: cwdNorm === root || cwdNorm.startsWith(`${root}/`);
|
|
225
|
+
if (hits && root.length > matchLength)
|
|
226
|
+
matchLength = root.length;
|
|
227
|
+
}
|
|
228
|
+
// No workspace folder covers the session cwd → not a candidate. Keeping
|
|
229
|
+
// unmatched locks "for later" is how a foreign project's selections ended
|
|
230
|
+
// up attachable here (see the doc comment above).
|
|
231
|
+
if (matchLength === 0)
|
|
232
|
+
continue;
|
|
233
|
+
entries.push({ entry, matchLength });
|
|
234
|
+
}
|
|
235
|
+
// Most-specific-first: a `/repo` workspace lock must precede the root `/`
|
|
236
|
+
// fallback when both match the same cwd (coderabbit review).
|
|
237
|
+
return entries
|
|
238
|
+
.sort((left, right) => right.matchLength - left.matchLength)
|
|
239
|
+
.map(item => item.entry);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Validate one inbound notification as a `selection_changed` frame. Returns
|
|
243
|
+
* undefined for anything else — wrong method, malformed envelope, missing or
|
|
244
|
+
* out-of-range coordinates (endLine >= startLine >= 0, non-empty path).
|
|
245
|
+
*/
|
|
246
|
+
export function parseSelectionChanged(message) {
|
|
247
|
+
if (message === null || typeof message !== 'object')
|
|
248
|
+
return undefined;
|
|
249
|
+
const record = message;
|
|
250
|
+
if (record.method !== SELECTION_METHOD)
|
|
251
|
+
return undefined;
|
|
252
|
+
const params = record.params;
|
|
253
|
+
if (params === null || typeof params !== 'object' || Array.isArray(params))
|
|
254
|
+
return undefined;
|
|
255
|
+
const payload = params;
|
|
256
|
+
const path = payload.path;
|
|
257
|
+
const startLine = payload.startLine;
|
|
258
|
+
const endLine = payload.endLine;
|
|
259
|
+
const isEmpty = payload.isEmpty;
|
|
260
|
+
if (typeof path !== 'string' || path === '')
|
|
261
|
+
return undefined;
|
|
262
|
+
if (typeof startLine !== 'number' || !Number.isSafeInteger(startLine) || startLine < 0) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
if (typeof endLine !== 'number' || !Number.isSafeInteger(endLine) || endLine < startLine) {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
if (typeof isEmpty !== 'boolean')
|
|
269
|
+
return undefined;
|
|
270
|
+
// Protocol 2 optional fields. A v2 extension always sends text; the
|
|
271
|
+
// fallback when it is missing is the disk read (see SelectionSnapshot).
|
|
272
|
+
const text = payload.text;
|
|
273
|
+
const documentVersion = payload.documentVersion;
|
|
274
|
+
const snapshot = { path, startLine, endLine, isEmpty };
|
|
275
|
+
if (typeof text === 'string' && text !== '')
|
|
276
|
+
snapshot.text = text;
|
|
277
|
+
if (typeof documentVersion === 'number' && Number.isSafeInteger(documentVersion)) {
|
|
278
|
+
snapshot.documentVersion = documentVersion;
|
|
279
|
+
}
|
|
280
|
+
return snapshot;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Connection lifecycle (DESIGN §3): idle → connecting (env first, then lock
|
|
284
|
+
* candidates under one shared budget) → connected, or silently disconnected.
|
|
285
|
+
* Once connected, an error or drop degrades to disconnected — no retry.
|
|
286
|
+
* The latest non-empty selection stays available after consumption so
|
|
287
|
+
* consecutive submits can reuse it; an isEmpty notification clears it.
|
|
288
|
+
*/
|
|
289
|
+
export class IdeChannel {
|
|
290
|
+
socket = null;
|
|
291
|
+
pendingSocket = null;
|
|
292
|
+
state = 'idle';
|
|
293
|
+
current = undefined;
|
|
294
|
+
/** Bumped on every connect attempt and on stop(): in-flight dials check it
|
|
295
|
+
* in `finish` and the discovery loop checks it before every next dial, so
|
|
296
|
+
* a stopped channel can neither be revived by a late onopen/ack nor keep
|
|
297
|
+
* dialing further candidates (maintainer review rounds 2–3). */
|
|
298
|
+
generation = 0;
|
|
299
|
+
/** Env/lockDir of the last start(), reused by rebind() so a cwd change
|
|
300
|
+
* rediscovers against the same environment it originally connected in. */
|
|
301
|
+
startEnv = process.env;
|
|
302
|
+
startLockDir = ideLockDir();
|
|
303
|
+
/** Workspace folders the connected server reported in hello_ack. */
|
|
304
|
+
ackedWorkspaceFolders = undefined;
|
|
305
|
+
listeners = new Set();
|
|
306
|
+
/** True only while the loopback link is up. */
|
|
307
|
+
get connected() {
|
|
308
|
+
return this.state === 'connected';
|
|
309
|
+
}
|
|
310
|
+
/** Workspace folders the connected server acked with, or undefined while
|
|
311
|
+
* not connected (test/introspection seam for the cwd-coverage rule). */
|
|
312
|
+
get workspaceFolders() {
|
|
313
|
+
return this.ackedWorkspaceFolders;
|
|
314
|
+
}
|
|
315
|
+
/** Latest non-empty selection, or undefined (never consumed-clearing). */
|
|
316
|
+
get selection() {
|
|
317
|
+
return this.current;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Subscribe to every selection notification, including empty ones.
|
|
321
|
+
* @returns An unsubscribe handle.
|
|
322
|
+
*/
|
|
323
|
+
onSelection(listener) {
|
|
324
|
+
this.listeners.add(listener);
|
|
325
|
+
return () => {
|
|
326
|
+
this.listeners.delete(listener);
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Discover and connect. All arguments are injectable so callers (and
|
|
331
|
+
* verifiers) can pin the environment, lock directory and session cwd;
|
|
332
|
+
* defaults read the live process values. Resolves without throwing in
|
|
333
|
+
* every outcome — check `connected` afterwards.
|
|
334
|
+
*/
|
|
335
|
+
async start(env = process.env, lockDir = ideLockDir(), cwd = process.cwd()) {
|
|
336
|
+
if (this.state !== 'idle')
|
|
337
|
+
return;
|
|
338
|
+
this.generation++;
|
|
339
|
+
// Operation token for the WHOLE discovery loop (maintainer review round
|
|
340
|
+
// 3): stop()/rebind() bump the generation; dials in flight check it in
|
|
341
|
+
// `finish`, and the loop below checks it before every next dial — a stop
|
|
342
|
+
// mid-discovery cancels the entire start, not just the one dial it
|
|
343
|
+
// interrupted (later candidates used to still connect after a stop).
|
|
344
|
+
const operation = this.generation;
|
|
345
|
+
this.startEnv = env;
|
|
346
|
+
this.startLockDir = lockDir;
|
|
347
|
+
const targets = [];
|
|
348
|
+
const direct = envDirect(env);
|
|
349
|
+
if (direct !== undefined)
|
|
350
|
+
targets.push(direct);
|
|
351
|
+
for (const lock of pickLockCandidates(lockDir, cwd)) {
|
|
352
|
+
targets.push({ port: lock.port, token: lock.token });
|
|
353
|
+
}
|
|
354
|
+
if (targets.length === 0) {
|
|
355
|
+
this.state = 'disconnected';
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
this.state = 'connecting';
|
|
359
|
+
const deadline = Date.now() + CONNECT_BUDGET_MS;
|
|
360
|
+
for (const target of targets) {
|
|
361
|
+
const remaining = deadline - Date.now();
|
|
362
|
+
if (remaining <= 0)
|
|
363
|
+
break;
|
|
364
|
+
if (operation !== this.generation)
|
|
365
|
+
return;
|
|
366
|
+
if (await this.tryConnect(target, remaining, operation))
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (operation === this.generation)
|
|
370
|
+
this.state = 'disconnected';
|
|
371
|
+
}
|
|
372
|
+
/** Drop the link (if any) and mark the channel disconnected. Cancels any
|
|
373
|
+
* dial still pending: the generation bump invalidates its finish, and the
|
|
374
|
+
* pending socket is aborted so a late onopen/ack can never re-connect.
|
|
375
|
+
* Cached selection state goes with the link — a stopped channel must not
|
|
376
|
+
* keep serving the old window's snapshot to anyone who reads `selection` /
|
|
377
|
+
* `workspaceFolders` (degradeToDisconnected already does this; stop() used
|
|
378
|
+
* to leave both behind, contradicting rebind()'s own contract). */
|
|
379
|
+
stop() {
|
|
380
|
+
this.generation++;
|
|
381
|
+
this.teardown();
|
|
382
|
+
this.state = 'disconnected';
|
|
383
|
+
this.current = undefined;
|
|
384
|
+
this.ackedWorkspaceFolders = undefined;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Re-target the channel after the session cwd changed (/workspace, /resume
|
|
388
|
+
* adopting another session, ...). The old link belongs to the old
|
|
389
|
+
* workspace — a selection pushed there must never attach into the new one
|
|
390
|
+
* — so drop it (stop() clears the cached selection and the acked folders)
|
|
391
|
+
* and rediscover against the new cwd with the env/lockDir start() was last
|
|
392
|
+
* given (maintainer review
|
|
393
|
+
* round 3: clearing the cached selection used to keep the stale link, and
|
|
394
|
+
* the terminal-on-disconnect design meant a plain stop() could never
|
|
395
|
+
* reconnect; rebind() is the one sanctioned way back to idle).
|
|
396
|
+
*/
|
|
397
|
+
async rebind(cwd) {
|
|
398
|
+
this.stop();
|
|
399
|
+
this.state = 'idle';
|
|
400
|
+
await this.start(this.startEnv, this.startLockDir, cwd);
|
|
401
|
+
}
|
|
402
|
+
teardown() {
|
|
403
|
+
const socket = this.socket;
|
|
404
|
+
this.socket = null;
|
|
405
|
+
const pending = this.pendingSocket;
|
|
406
|
+
this.pendingSocket = null;
|
|
407
|
+
if (socket !== null) {
|
|
408
|
+
this.detach(socket);
|
|
409
|
+
this.abort(socket);
|
|
410
|
+
}
|
|
411
|
+
if (pending !== null && pending !== socket) {
|
|
412
|
+
this.detach(pending);
|
|
413
|
+
this.abort(pending);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
detach(socket) {
|
|
417
|
+
socket.onopen = null;
|
|
418
|
+
socket.onmessage = null;
|
|
419
|
+
socket.onerror = null;
|
|
420
|
+
socket.onclose = null;
|
|
421
|
+
}
|
|
422
|
+
/** Close without waiting; closing a CONNECTING socket cancels the dial. */
|
|
423
|
+
abort(socket) {
|
|
424
|
+
try {
|
|
425
|
+
socket.close();
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
// Already closed or half-open — degradation must never throw.
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Dial one candidate and complete the token handshake. Resolves true only
|
|
433
|
+
* when the server answered `ide/hello_ack` with a matching protocol
|
|
434
|
+
* version — an OPEN socket alone proves nothing: a wrong-token server
|
|
435
|
+
* accepts the WS upgrade and then closes, and treating open as connected
|
|
436
|
+
* used to strand the client on a rejected candidate (maintainer review
|
|
437
|
+
* round 3). Anything slower than `timeoutMs`, any error, or any first
|
|
438
|
+
* frame that is not a valid v2 ack resolves false and cleans up.
|
|
439
|
+
*/
|
|
440
|
+
tryConnect(target, timeoutMs, operation) {
|
|
441
|
+
return new Promise(resolve => {
|
|
442
|
+
let settled = false;
|
|
443
|
+
let helloSent = false;
|
|
444
|
+
const gen = operation;
|
|
445
|
+
let socket;
|
|
446
|
+
try {
|
|
447
|
+
socket = new WebSocket(`ws://127.0.0.1:${target.port}`);
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
resolve(false);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
// Track the in-flight dial so stop() can abort it directly (not just
|
|
454
|
+
// via the generation guard) — cancel the CONNECTING socket to unblock
|
|
455
|
+
// the promise instead of letting it spin until the budget ends.
|
|
456
|
+
this.pendingSocket = socket;
|
|
457
|
+
const finish = (connected) => {
|
|
458
|
+
if (settled)
|
|
459
|
+
return;
|
|
460
|
+
settled = true;
|
|
461
|
+
clearTimeout(timer);
|
|
462
|
+
this.detach(socket);
|
|
463
|
+
// A stop() between dial and ack bumped the generation: this dial
|
|
464
|
+
// belongs to a cancelled session — drop it, never become connected.
|
|
465
|
+
if (gen !== this.generation) {
|
|
466
|
+
this.abort(socket);
|
|
467
|
+
resolve(false);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (this.pendingSocket === socket)
|
|
471
|
+
this.pendingSocket = null;
|
|
472
|
+
if (!connected) {
|
|
473
|
+
this.abort(socket);
|
|
474
|
+
resolve(false);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
this.socket = socket;
|
|
478
|
+
this.state = 'connected';
|
|
479
|
+
this.attachMessageHandler(socket);
|
|
480
|
+
resolve(true);
|
|
481
|
+
};
|
|
482
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
483
|
+
socket.onopen = () => {
|
|
484
|
+
try {
|
|
485
|
+
socket.send(JSON.stringify({
|
|
486
|
+
method: HELLO_METHOD,
|
|
487
|
+
params: { token: target.token, protocolVersion: IDE_PROTOCOL_VERSION },
|
|
488
|
+
}));
|
|
489
|
+
helloSent = true;
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
finish(false);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
socket.onmessage = event => {
|
|
496
|
+
if (!helloSent || settled)
|
|
497
|
+
return;
|
|
498
|
+
let parsed;
|
|
499
|
+
try {
|
|
500
|
+
parsed = JSON.parse(String(event.data));
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
finish(false);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const ack = parseHelloAck(parsed);
|
|
507
|
+
// Both ends ship together: a first frame that is not a valid v2 ack
|
|
508
|
+
// (wrong version, wrong method, garbage) fails this candidate.
|
|
509
|
+
if (ack === undefined) {
|
|
510
|
+
finish(false);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
this.ackedWorkspaceFolders = ack.workspaceFolders;
|
|
514
|
+
finish(true);
|
|
515
|
+
};
|
|
516
|
+
socket.onerror = () => finish(false);
|
|
517
|
+
socket.onclose = () => finish(false);
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
attachMessageHandler(socket) {
|
|
521
|
+
socket.onmessage = event => {
|
|
522
|
+
let parsed;
|
|
523
|
+
try {
|
|
524
|
+
parsed = JSON.parse(String(event.data));
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const snapshot = parseSelectionChanged(parsed);
|
|
530
|
+
if (snapshot === undefined)
|
|
531
|
+
return;
|
|
532
|
+
this.current = snapshot.isEmpty ? undefined : snapshot;
|
|
533
|
+
for (const listener of [...this.listeners])
|
|
534
|
+
listener(snapshot);
|
|
535
|
+
};
|
|
536
|
+
socket.onclose = () => this.degradeToDisconnected(socket);
|
|
537
|
+
socket.onerror = () => this.degradeToDisconnected(socket);
|
|
538
|
+
}
|
|
539
|
+
degradeToDisconnected(socket) {
|
|
540
|
+
if (this.socket !== socket)
|
|
541
|
+
return;
|
|
542
|
+
this.socket = null;
|
|
543
|
+
this.detach(socket);
|
|
544
|
+
this.state = 'disconnected';
|
|
545
|
+
this.ackedWorkspaceFolders = undefined;
|
|
546
|
+
// A dead connection's selection is stale: clear the cached snapshot and
|
|
547
|
+
// tell subscribers — the badge and submit auto-attach must not keep
|
|
548
|
+
// using whatever was selected before the disconnect.
|
|
549
|
+
const stale = this.current;
|
|
550
|
+
this.current = undefined;
|
|
551
|
+
if (stale !== undefined) {
|
|
552
|
+
for (const listener of [...this.listeners])
|
|
553
|
+
listener({ ...stale, isEmpty: true });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
@@ -11,6 +11,7 @@ import Schema from '@deepseek-ai/schemastery';
|
|
|
11
11
|
import type { SessionModeSpec } from '../sessionModes.js';
|
|
12
12
|
import { type PageMarginSetting, type ScrollGutterMode, type StatusBarConfig, type ToolBackground } from '../tuiDisplayPrefs.js';
|
|
13
13
|
import { type ShortcutActionId } from '../utils/keymap.js';
|
|
14
|
+
import { type RuntimeConfig } from './compat/settings.js';
|
|
14
15
|
export declare const name = "dsh-tui";
|
|
15
16
|
export declare const inject: string[];
|
|
16
17
|
/**
|
|
@@ -43,6 +44,13 @@ export interface Config {
|
|
|
43
44
|
* the startup status line until the first request header reports the
|
|
44
45
|
* live value. */
|
|
45
46
|
effort?: string;
|
|
47
|
+
/** Settings default for future sessions; overrides the `effort` fallback. */
|
|
48
|
+
effortDefault?: string;
|
|
49
|
+
/** Show the header whale and its idle animation. */
|
|
50
|
+
whale?: boolean;
|
|
51
|
+
whaleIdle?: boolean;
|
|
52
|
+
/** Reduce decorative header content and colors. */
|
|
53
|
+
minimal?: boolean;
|
|
46
54
|
/** Show the live working line derived in-process from base session events. */
|
|
47
55
|
activity?: boolean;
|
|
48
56
|
/** Working-activity indicator preset (`moon8`/`moon`/`comet`/`dots`/…
|
|
@@ -110,6 +118,13 @@ export interface Config {
|
|
|
110
118
|
* a ~30fps reveal instead of jumping per provider burst — bursty or
|
|
111
119
|
* one-shot deliveries read as an even flow. On by default. */
|
|
112
120
|
smoothStreaming?: boolean;
|
|
121
|
+
/** Mermaid diagrams (settings `dsh-tui.mermaidDiagrams`): ```mermaid
|
|
122
|
+
* fences in replies render as Unicode box-drawing art — flowcharts,
|
|
123
|
+
* sequence/state/class/ER diagrams, pie, mindmap, timeline, gitGraph —
|
|
124
|
+
* laid out in-process, no browser or image protocol. A diagram wider
|
|
125
|
+
* than the viewport or of an unsupported type keeps the fenced source.
|
|
126
|
+
* On by default; off always shows the source. */
|
|
127
|
+
mermaidDiagrams?: boolean;
|
|
113
128
|
/** Status-footer field visibility and compact presentation preferences. */
|
|
114
129
|
statusBar?: Partial<StatusBarConfig>;
|
|
115
130
|
/** Built-in action-shortcut overrides (`paste: 'alt+v'`), keyed by action
|
|
@@ -124,13 +139,13 @@ export interface Config {
|
|
|
124
139
|
* default/plan/full cycle (see sessionModes.ts). */
|
|
125
140
|
modes?: SessionModeSpec[];
|
|
126
141
|
}
|
|
127
|
-
export declare const Config: Schema<Config
|
|
142
|
+
export declare const Config: Schema<Config, RuntimeConfig<Config>>;
|
|
128
143
|
/**
|
|
129
144
|
* Start the interactive TUI front door, delegating to the JSX implementation
|
|
130
145
|
* in `./plugin.tsx` (see its module doc for the full contract).
|
|
131
146
|
* @param ctx - the plugin context.
|
|
132
147
|
* @param config - the validated dsh-tui configuration.
|
|
133
|
-
* @returns a promise settling when the
|
|
148
|
+
* @returns a promise settling when the Loader entry has scheduled its runtime.
|
|
134
149
|
*/
|
|
135
|
-
export declare function apply(ctx: Context, config: Config): Promise<void>;
|
|
150
|
+
export declare function apply(ctx: Context, config: RuntimeConfig<Config>): Promise<void>;
|
|
136
151
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAA2C,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACzK,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/index.ts"],"names":[],"mappings":"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"}
|