dsh-ssh-tui 0.1.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.
@@ -0,0 +1,365 @@
1
+ /**
2
+ * A small, dependency-light interactive terminal channel for DeepSeek
3
+ * Harness. It renders the durable session transcript, streams assistant
4
+ * output, shows tool-call cards, answers approval requests and
5
+ * `ask_user_question` prompts from the keyboard, and drives one configured
6
+ * agent with followup/steer.
7
+ *
8
+ * The renderer uses plain ANSI and a throttled full repaint, which keeps it
9
+ * predictable over slow SSH links and avoids terminal-library dependency
10
+ * drift inside the plugin.
11
+ */
12
+ import type { Agent, ModelSelection, ModelSelectionRef } from '@deepseek-ai/dsh-agent';
13
+ import type { Context } from '@deepseek-ai/cordis';
14
+ /** Presentation configuration for the terminal channel. */
15
+ export interface TuiConfig {
16
+ /** Exact shared agent/session identity driven by this terminal. */
17
+ sessionId: string;
18
+ /** Render model reasoning blocks. */
19
+ showReasoning?: boolean;
20
+ /** Maximum tool-result body lines retained on each card. */
21
+ maxToolOutputLines?: number;
22
+ /** Apply ANSI colors. */
23
+ color?: boolean;
24
+ /** Banner subtitle line shown while no session title exists. */
25
+ welcome?: string;
26
+ /** Whether this launch resumes an existing persisted session. */
27
+ resume?: boolean;
28
+ /** Provider route selected at launch (defaults to deepseek-official). */
29
+ provider?: string;
30
+ /** Model selected at launch (defaults to the saved/fallback model). */
31
+ model?: string;
32
+ /** Live model-selection ref installed on the agent; mutated by /model. */
33
+ selectionRef?: ModelSelectionRef;
34
+ /** Active agent-preset id (standard/code/minimal/cordis/...). */
35
+ presetId?: string;
36
+ /** Display name of the active preset. */
37
+ presetName?: string;
38
+ /** Switch the running TUI to another session (used by /resume). */
39
+ onSwitchSession?: (sessionId: string) => Promise<void> | void;
40
+ /** Notify the launcher of an explicit in-process selection change. */
41
+ onSelectionChanged?: (selection: ModelSelection) => void;
42
+ /** Open the history-session picker immediately after mounting (--resume). */
43
+ resumePicker?: boolean;
44
+ }
45
+ type Row = {
46
+ kind: 'user';
47
+ text: string;
48
+ } | {
49
+ kind: 'assistant';
50
+ text: string;
51
+ } | {
52
+ kind: 'reasoning';
53
+ text: string;
54
+ expanded: boolean;
55
+ } | {
56
+ kind: 'brand';
57
+ text: string;
58
+ } | {
59
+ kind: 'brand-logo';
60
+ } | {
61
+ kind: 'tool';
62
+ callId: string;
63
+ name: string;
64
+ args: string;
65
+ status?: 'running' | 'ok' | 'error';
66
+ output: string;
67
+ title: string;
68
+ summary: string;
69
+ command?: string;
70
+ cwd?: string;
71
+ diff?: ToolDiffHunk[];
72
+ exitCode?: number;
73
+ signal?: string;
74
+ expanded: boolean;
75
+ } | {
76
+ kind: 'system';
77
+ text: string;
78
+ } | {
79
+ kind: 'error';
80
+ text: string;
81
+ };
82
+ type DisplayKind = Row['kind'] | 'tool-result' | 'diff-add' | 'diff-del' | 'diff-path';
83
+ /** One file's change, matching the web diff-card contract (`card: 'diff'`). */
84
+ interface ToolDiffHunk {
85
+ path: string;
86
+ oldText: string | null;
87
+ newText: string;
88
+ }
89
+ /** Lifecycle handle for a mounted interactive terminal channel. */
90
+ export interface TuiController {
91
+ dispose(): Promise<void>;
92
+ }
93
+ /**
94
+ * Render workspace markdown into width-bounded terminal rows. Assistant
95
+ * replies get a bold-white base; code blocks, headings, quotes, lists, rules,
96
+ * links and inline spans keep their own ANSI treatment.
97
+ */
98
+ export declare function renderMarkdownLines(text: string, width: number, color: boolean): string[];
99
+ /** One renderable view of the input line: text plus the cursor's visual offset. */
100
+ interface InputView {
101
+ text: string;
102
+ cursorOffset: number;
103
+ folded: boolean;
104
+ }
105
+ /**
106
+ * Fold a long single-line input into one terminal row around the cursor.
107
+ * Only the *display* is clipped; the caller keeps the original `input` intact
108
+ * for editing and submission.
109
+ */
110
+ export declare function foldInputView(input: string, cursor: number, maxWidth: number): InputView;
111
+ /** A recognized OpenCode provider route, used by /usage and /quota. */
112
+ export type OpenCodeFlavor = 'zen' | 'go';
113
+ export interface OpenCodeSource {
114
+ provider: string;
115
+ flavor: OpenCodeFlavor;
116
+ label: string;
117
+ apiKeyEnv: string;
118
+ baseURL?: string;
119
+ }
120
+ /**
121
+ * Classify the currently selected provider as an OpenCode route. Built-in
122
+ * `opencode`/`opencode-go` ids are recognized directly, and custom llm-pi-ai
123
+ * routes are recognized by their `opencode.ai` base URL.
124
+ */
125
+ export declare function openCodeSourceFor(provider: string, llmPiAiSection: unknown): OpenCodeSource | null;
126
+ /** Render the OpenCode Go quota payload as a transcript block. */
127
+ export declare function formatOpenCodeGoUsage(payload: unknown, source: OpenCodeSource): string;
128
+ /** One-line friendly tool-call presentation (command / path / arg summary). */
129
+ export declare function presentToolCall(name: string, args: string): {
130
+ title: string;
131
+ summary: string;
132
+ command?: string;
133
+ cwd?: string;
134
+ diff?: ToolDiffHunk[];
135
+ };
136
+ /** Validate a tool/result meta payload's structured diff, mirroring the web card. */
137
+ export declare function diffMetaDiffs(meta: unknown): ToolDiffHunk[] | null;
138
+ /** One rendered diff body line with its display role. */
139
+ interface DiffDisplayLine {
140
+ kind: DisplayKind;
141
+ text: string;
142
+ }
143
+ /** Flatten hunks into git-style `-`/`+` lines plus the web-compatible footer. */
144
+ export declare function renderToolDiff(diffs: ToolDiffHunk[], maxLines: number): DiffDisplayLine[];
145
+ /** Convert any parsed JSON value into readable indented display lines. */
146
+ export declare function friendlyJsonLines(value: unknown, depth?: number): string[];
147
+ /** Minimal tool-row shape the expanded-body renderer reads. */
148
+ interface ToolBodySource {
149
+ diff?: ToolDiffHunk[];
150
+ command?: string;
151
+ status?: 'running' | 'ok' | 'error';
152
+ output: string;
153
+ args: string;
154
+ }
155
+ /**
156
+ * The expanded body of one tool card: diffs and shell output keep their
157
+ * dedicated views; every other tool's JSON arguments and JSON result are
158
+ * converted into readable indented content instead of raw JSON text.
159
+ */
160
+ export declare function toolBodyLines(row: ToolBodySource, maxLines: number): DiffDisplayLine[];
161
+ /** Recover the shell tools' exit marker, mirroring @deepseek-ai/dsh-shell/render. */
162
+ export declare function parseExitStatus(text: string): {
163
+ body: string;
164
+ exitCode?: number;
165
+ signal?: string;
166
+ };
167
+ /** Compact token count, matching the web stats line (517 / 12.2K / 1.2M). */
168
+ export declare function formatTokens(n: number): string;
169
+ /** Compact duration, matching the web stats line (45.2s / 2m42s). */
170
+ export declare function formatDuration(ms: number): string;
171
+ export declare function formatTokensPerSecond(tokensPerSecond: number): string;
172
+ /** Owns one interactive terminal channel and its agent event wiring. */
173
+ export declare class SshTui {
174
+ private readonly ctx;
175
+ private readonly agent;
176
+ private readonly rows;
177
+ private streaming;
178
+ private input;
179
+ private cursor;
180
+ private inputFolded;
181
+ private history;
182
+ private historyIndex;
183
+ private status;
184
+ private dialog;
185
+ private dirty;
186
+ private disposed;
187
+ private exiting;
188
+ private renderTimer;
189
+ private readonly decoder;
190
+ private readonly color;
191
+ private readonly maxToolOutputLines;
192
+ private readonly showReasoning;
193
+ private readonly goodbye;
194
+ private readonly resume;
195
+ private readonly providerName;
196
+ private readonly selectionRef;
197
+ private readonly onSwitchSession;
198
+ private readonly onSelectionChanged;
199
+ private readonly resumePicker;
200
+ private readonly disposers;
201
+ private userQuestionDisposer;
202
+ private presetId;
203
+ private presetName;
204
+ private readonly useAlternateScreen;
205
+ private agentGone;
206
+ private onboarding;
207
+ private commandSuggestions;
208
+ private suggestionIndex;
209
+ private focusedRow;
210
+ private pendingMessages;
211
+ private lastActivity;
212
+ private stalledWarningShown;
213
+ private lastPaintAt;
214
+ private activeSubagents;
215
+ private subagentSessions;
216
+ private openToolCalls;
217
+ private readonly stats;
218
+ private openStepStats;
219
+ private readonly pendingToolTimes;
220
+ private readonly usageByStep;
221
+ private lastStatsTurn;
222
+ private scrollOffset;
223
+ private readonly clickableRows;
224
+ private streamingReasoning;
225
+ private escapeBuffer;
226
+ private escapeTimer;
227
+ private thinkingStartedAt;
228
+ private completionSignaled;
229
+ private completedAt;
230
+ private lastTitleUpdateAt;
231
+ private lastPaintRows;
232
+ private lastChromeKey;
233
+ constructor(ctx: Context, agent: Agent, config: TuiConfig);
234
+ /** Enter raw mode, switch to the alternate screen, and start listening. */
235
+ start(): void;
236
+ /** Replay the durable session log so a resumed session renders its history. */
237
+ replayHistory(): void;
238
+ /** Show the first-launch provider/API-key onboarding when nothing is configured. */
239
+ private maybeRunOnboarding;
240
+ /** Run the provider/API-key onboarding wizard. Resolves true when saved. */
241
+ private runOnboarding;
242
+ private cancelOnboarding;
243
+ /** Restore the terminal, flush the session, and request process exit. */
244
+ dispose(): Promise<void>;
245
+ /** Human-facing exit with goodbye and flush; called from key handling. */
246
+ requestExit(code: number): Promise<void>;
247
+ private write;
248
+ private markDirty;
249
+ /** Append one transcript row, bounding memory on long sessions. */
250
+ private pushRow;
251
+ /** The transcript rows that support per-row expand/collapse. */
252
+ private collapsibleRows;
253
+ /** Move the expand/collapse focus among reasoning and tool rows. */
254
+ private moveCollapsibleFocus;
255
+ /** Toggle the focused block; without focus, toggle the most recent one. */
256
+ private toggleCollapsible;
257
+ /** Expand all collapsible blocks, or collapse them again when all are open. */
258
+ private toggleAllCollapsible;
259
+ private paint;
260
+ private buildSuggestions;
261
+ private suggestionsVisible;
262
+ private currentSelectionLabel;
263
+ /** Replace one step's usage sample so a repeated report never double counts. */
264
+ private recordUsage;
265
+ /** The web-aligned session stats strip: counts, timings, cache, tokens. */
266
+ private statsText;
267
+ /** Refresh the terminal window title (throttled while running). */
268
+ private updateTerminalTitle;
269
+ /** Terminal bell on completion (opt out with DSH_TUI_NO_BELL=1). */
270
+ private playCompletionSignal;
271
+ private render;
272
+ private styleLine;
273
+ private readonly handleSessionEvent;
274
+ private readonly handleStatus;
275
+ private readonly handleError;
276
+ private readonly handleInboxClaimed;
277
+ private readonly handleInboxDiscarded;
278
+ private readonly handleDisposed;
279
+ /** Render a live subagent's own session events so its progress is visible. */
280
+ private readonly handleSubagentSessionEvent;
281
+ private readonly handleSubagentStart;
282
+ private readonly handleSubagentEnd;
283
+ private readonly handleApproval;
284
+ private readonly handleUserQuestions;
285
+ private openConfirm;
286
+ private closeConfirm;
287
+ private openQuestion;
288
+ /** Open one question dialog and await its answer (cancellation rejects). */
289
+ private askQuestion;
290
+ /** The stored llm-pi-ai profile for one provider route, when settings provide one. */
291
+ private piAiProviderProfile;
292
+ /** Default listing endpoint for a built-in OpenCode route with no stored base URL. */
293
+ private openCodeListingBaseURL;
294
+ /**
295
+ * Fetch the live model list for an OpenCode or third-party provider from its
296
+ * OpenAI-compatible listing endpoint. The provider route is deliberately not
297
+ * passed to discovery: pi-ai would answer a catalog route from its installed
298
+ * registry, while the TUI wants the endpoint's current list.
299
+ */
300
+ private discoverEndpointModels;
301
+ /** Add one endpoint-listed model to the stored provider profile when needed. */
302
+ private ensureProviderModelConfigured;
303
+ /** How many endpoint-listed models fit on one picker page alongside navigation. */
304
+ private readonly MODEL_PAGE_SIZE;
305
+ private readonly MODEL_PAGE_PREV;
306
+ private readonly MODEL_PAGE_NEXT;
307
+ /**
308
+ * One pick across a possibly long model list, paging through the digit
309
+ * dialog so an endpoint with dozens of models stays selectable.
310
+ */
311
+ private pickModelOption;
312
+ /** /model: pick a model and reasoning effort for the current provider. */
313
+ private runModelCommand;
314
+ /** /mode: pick an agent preset (standard / PTC / minimal / ...). */
315
+ private runModeCommand;
316
+ /** /resume: switch to a past session, or open a picker when no id is given. */
317
+ private runResumeCommand;
318
+ /** Current provider route selected for the running agent. */
319
+ private currentProvider;
320
+ /** Resolve one credential reference without exposing its value. */
321
+ private resolveCredential;
322
+ /** Query the OpenCode Go quota endpoint. */
323
+ private fetchOpenCodeGoUsage;
324
+ /** Explain Zen metered billing instead of pretending it has a quota. */
325
+ private zenUsageText;
326
+ /** /usage and /quota: show Zen billing info or live Go quota usage. */
327
+ private runUsageCommand;
328
+ private readonly handleData;
329
+ private handlePlainText;
330
+ private handleChar;
331
+ private handleDialogChar;
332
+ private handleOnboardingChar;
333
+ private advanceOnboarding;
334
+ /** Fetch the endpoint's model list into the onboarding wizard's models step. */
335
+ private fetchOnboardingModels;
336
+ private saveOnboarding;
337
+ /** Store one credential, falling back to a launch-environment override on shadow/absence. */
338
+ private saveCredential;
339
+ /** Write launch-environment overrides so they beat system-injected variables. */
340
+ private writeLaunchEnv;
341
+ /** Persist one variable into the Windows user environment (best-effort). */
342
+ private setWindowsEnv;
343
+ /** Idempotently source $DSH_HOME/env.sh from the user's POSIX shell rc files. */
344
+ private ensurePosixEnvHook;
345
+ private handleEscape;
346
+ /** Toggle the collapsible row under a click on the transcript area. */
347
+ private handleMouseClick;
348
+ private handleCtrlC;
349
+ private submit;
350
+ private runCommand;
351
+ private backspace;
352
+ private deleteAtCursor;
353
+ private moveCursor;
354
+ private historyBack;
355
+ private historyForward;
356
+ }
357
+ /**
358
+ * Mount the terminal channel once the configured agent exists.
359
+ *
360
+ * @param ctx - context supplying the agent registry, sessions, and event stream.
361
+ * @param config - target agent and presentation config.
362
+ * @returns lifecycle controller used by the Cordis effect disposer.
363
+ */
364
+ export declare function mountTui(ctx: Context, config: TuiConfig): TuiController;
365
+ export {};
package/package.json ADDED
@@ -0,0 +1,119 @@
1
+ {
2
+ "name": "dsh-ssh-tui",
3
+ "version": "0.1.0",
4
+ "description": "SSH-friendly interactive terminal TUI plugin for DeepSeek Harness",
5
+ "keywords": [
6
+ "deepseek-harness",
7
+ "dsh",
8
+ "plugin",
9
+ "tui",
10
+ "terminal",
11
+ "ssh"
12
+ ],
13
+ "author": "cyjyyd",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/cyjyyd/dsh-ssh-tui.git"
17
+ },
18
+ "homepage": "https://github.com/cyjyyd/dsh-ssh-tui#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/cyjyyd/dsh-ssh-tui/issues"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "type": "module",
26
+ "main": "lib/index.js",
27
+ "types": "lib/types/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./lib/types/index.d.ts",
31
+ "default": "./lib/index.js"
32
+ },
33
+ "./startup": {
34
+ "types": "./lib/types/startup.d.ts",
35
+ "default": "./lib/startup.js"
36
+ },
37
+ "./cordis.patch.yml": "./cordis.patch.yml",
38
+ "./package.json": "./package.json"
39
+ },
40
+ "files": [
41
+ "lib",
42
+ "cordis.patch.yml",
43
+ "README.md",
44
+ "README.zh-CN.md",
45
+ "LICENSE"
46
+ ],
47
+ "license": "MIT",
48
+ "dsh": {
49
+ "bundle": {
50
+ "patch": "./cordis.patch.yml"
51
+ }
52
+ },
53
+ "scripts": {
54
+ "build": "tsc -p tsconfig.json",
55
+ "typecheck": "tsc -p tsconfig.json --noEmit",
56
+ "clean": "rm -rf lib",
57
+ "prepare": "npm run build",
58
+ "prepack": "npm run build",
59
+ "prepublishOnly": "npm run typecheck",
60
+ "install:dsh": "bash scripts/install.sh",
61
+ "install:npm": "bash scripts/install-npm.sh",
62
+ "uninstall:dsh": "bash scripts/uninstall.sh",
63
+ "verify:dsh": "bash scripts/verify.sh"
64
+ },
65
+ "engines": {
66
+ "node": ">=22.19"
67
+ },
68
+ "dependencies": {
69
+ "commander": "^14.0.0"
70
+ },
71
+ "peerDependencies": {
72
+ "@deepseek-ai/cordis": "^4.0.1",
73
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
74
+ "@deepseek-ai/dsh-agent-default-model": "^0.1.0-rc.6",
75
+ "@deepseek-ai/dsh-agent-loop": "^0.1.0-rc.6",
76
+ "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6",
77
+ "@deepseek-ai/dsh-cmdline": "^0.1.0-rc.6",
78
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.6",
79
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
80
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
81
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
82
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
83
+ "@deepseek-ai/dsh-subagent": "^0.1.0-rc.6",
84
+ "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.6",
85
+ "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.6"
86
+ },
87
+ "peerDependenciesMeta": {
88
+ "@deepseek-ai/dsh-user-approval": {
89
+ "optional": true
90
+ },
91
+ "@deepseek-ai/dsh-user-questions": {
92
+ "optional": true
93
+ },
94
+ "@deepseek-ai/dsh-credentials": {
95
+ "optional": true
96
+ },
97
+ "@deepseek-ai/dsh-settings": {
98
+ "optional": true
99
+ }
100
+ },
101
+ "devDependencies": {
102
+ "@deepseek-ai/cordis": "^4.0.1",
103
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
104
+ "@deepseek-ai/dsh-agent-default-model": "^0.1.0-rc.6",
105
+ "@deepseek-ai/dsh-agent-loop": "^0.1.0-rc.6",
106
+ "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6",
107
+ "@deepseek-ai/dsh-cmdline": "^0.1.0-rc.6",
108
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.6",
109
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
110
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
111
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
112
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
113
+ "@deepseek-ai/dsh-subagent": "^0.1.0-rc.6",
114
+ "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.6",
115
+ "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.6",
116
+ "@types/node": "^24.0.0",
117
+ "typescript": "^5.9.0"
118
+ }
119
+ }