dsh-ssh-tui 0.3.0 → 0.3.2

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.
@@ -23,6 +23,8 @@ export interface Config {
23
23
  provider?: string;
24
24
  /** CLI-supplied model override; otherwise the saved default is used. */
25
25
  model?: string;
26
+ /** Minimum milliseconds between paints; see DSH_TUI_PAINT_MS. */
27
+ paintIntervalMs?: number;
26
28
  }
27
29
  /**
28
30
  * Mount the SSH TUI. The `main` agent is created here after the loader
@@ -4,15 +4,30 @@
4
4
  *
5
5
  * The provider is deliberately inherited from the running parent session:
6
6
  * the subagent selection only overrides the provider when the user stored an
7
- * explicit route. The model defaults to the lightweight `deepseek-v4-flash`.
7
+ * explicit route. When the parent provider changes, the TUI picks a
8
+ * same-family default (DeepSeek flash, Grok 4.5, otherwise the first listed
9
+ * lightweight model) unless `/submodel` stored an explicit model.
8
10
  */
9
11
  import z from '@deepseek-ai/schemastery';
10
12
  import type { Context } from '@deepseek-ai/cordis';
11
13
  import { type ReasoningEffortId as ReasoningEffort } from '@deepseek-ai/dsh-llm';
12
14
  /** Settings namespace carrying the TUI's subagent model selection. */
13
15
  export declare const SUBAGENT_SETTINGS_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
14
- /** Lightweight default model used for subagent children. */
16
+ /** Lightweight default model used for DeepSeek-family subagent children. */
15
17
  export declare const DEFAULT_SUBAGENT_MODEL = "deepseek-v4-flash";
18
+ /** Preferred subagent default when the parent route is SuperGrok / xAI. */
19
+ export declare const DEFAULT_XAI_SUBAGENT_MODEL = "grok-4.5";
20
+ /**
21
+ * Choose the default subagent model for a parent provider, preferring a
22
+ * same-family listed model over a leftover DeepSeek flash id.
23
+ */
24
+ export declare function defaultSubagentModelForProvider(provider: string, listed?: readonly string[]): string;
25
+ /**
26
+ * True when the stored subagent model still belongs to the parent provider
27
+ * family. An explicit leftover DeepSeek flash id after switching to xAI is
28
+ * treated as stale so the TUI can pick a same-family default.
29
+ */
30
+ export declare function subagentModelMatchesProvider(provider: string, model: string, listed?: readonly string[]): boolean;
16
31
  /** Raw settings document shape. */
17
32
  export interface SubagentSettings {
18
33
  /** Explicit provider override; omitted means "same provider as the parent". */
@@ -5,9 +5,9 @@
5
5
  * `ask_user_question` prompts from the keyboard, and drives one configured
6
6
  * agent with followup/steer.
7
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.
8
+ * The renderer uses plain ANSI and coalesces each frame into one stdout
9
+ * write of dirty rows only — jump-host / proxied SSH should see one packet
10
+ * per paint, not one per line. Cadence is DSH_TUI_PAINT_MS (default 160).
11
11
  */
12
12
  import type { Agent, ModelSelection, ModelSelectionRef } from '@deepseek-ai/dsh-agent';
13
13
  import type { Context } from '@deepseek-ai/cordis';
@@ -50,6 +50,12 @@ export interface TuiConfig {
50
50
  onSelectionChanged?: (selection: ModelSelection) => void;
51
51
  /** Open the history-session picker immediately after mounting (--resume). */
52
52
  resumePicker?: boolean;
53
+ /**
54
+ * Minimum milliseconds between paints while a turn is streaming.
55
+ * Jump-host / proxied SSH can raise this so token ticks do not flood the
56
+ * link. Defaults from `DSH_TUI_PAINT_MS` (160).
57
+ */
58
+ paintIntervalMs?: number;
53
59
  }
54
60
  type SubagentLogKind = 'user' | 'assistant' | 'tool' | 'result' | 'turn' | 'approval' | 'team' | 'system';
55
61
  /** One child-session event folded into a parent-side subagent card. */
@@ -111,7 +117,15 @@ type Row = {
111
117
  active: boolean;
112
118
  pending: boolean;
113
119
  todos: PlanTodoItem[];
120
+ planMarkdown?: string;
114
121
  expanded: boolean;
122
+ /** When true the plan stays in the scrolling transcript, not the dock. */
123
+ archived?: boolean;
124
+ /**
125
+ * Display-only: the last turn ended while todos were still open.
126
+ * Does not rewrite the session log.
127
+ */
128
+ turnLeftOpen?: boolean;
115
129
  } | {
116
130
  kind: 'question';
117
131
  questionId: string;
@@ -135,7 +149,7 @@ type Row = {
135
149
  kind: 'error';
136
150
  text: string;
137
151
  };
138
- type DisplayKind = Row['kind'] | 'tool-result' | 'diff-add' | 'diff-del' | 'diff-path';
152
+ type DisplayKind = Row['kind'] | 'tool-result' | 'diff-add' | 'diff-del' | 'diff-path' | 'todo-done' | 'todo-active' | 'todo-pending' | 'plan-dock';
139
153
  /** One file's change, matching the web diff-card contract (`card: 'diff'`). */
140
154
  interface ToolDiffHunk {
141
155
  path: string;
@@ -146,6 +160,23 @@ interface ToolDiffHunk {
146
160
  export interface TuiController {
147
161
  dispose(): Promise<void>;
148
162
  }
163
+ /**
164
+ * Paint cadence for jump-host / proxied SSH. Token ticks coalesce into one
165
+ * frame; the default stays snappy, slower links raise `DSH_TUI_PAINT_MS`.
166
+ */
167
+ export declare function resolvePaintIntervalMs(configured?: number, env?: NodeJS.ProcessEnv): number;
168
+ /** One incremental paint as a single stdout write (one SSH packet when corked). */
169
+ export declare function composePaintOutput(options: {
170
+ width: number;
171
+ height: number;
172
+ paintRows: readonly string[];
173
+ previousRows: readonly string[];
174
+ sizeChanged: boolean;
175
+ chromeChanged: boolean;
176
+ chromeStart: number;
177
+ cursorRow: number;
178
+ cursorColumn: number;
179
+ }): string;
149
180
  /** Human-facing kind for a live LLM route. */
150
181
  export declare function describeProviderRoute(provider: string): {
151
182
  kind: string;
@@ -153,6 +184,31 @@ export declare function describeProviderRoute(provider: string): {
153
184
  };
154
185
  /** Routes that authenticate without a harness API-key credential. */
155
186
  export declare function providerUsesLocalOAuth(provider: string): boolean;
187
+ /**
188
+ * Terminal cell width for one string.
189
+ *
190
+ * Match glibc wcwidth / typical UTF-8 SSH terminals: CJK ideographs and
191
+ * fullwidth forms occupy two cells; East-Asian Ambiguous box-drawing and
192
+ * ornaments (`─`, `●`, `·`, `▸`, `❯`, Braille spinners) occupy one. Counting
193
+ * those ambiguous glyphs as two made `repeatToWidth('─', cols)` paint a
194
+ * half-width rule and parked the input cursor half a cell past the text.
195
+ *
196
+ * Overflow into the input box is handled by clipping/padding painted rows to
197
+ * the measured column count, not by inflating glyph width.
198
+ */
199
+ export declare function displayWidth(text: string): number;
200
+ /** Pad or clip one already-sanitized line so it occupies exactly `width` cells. */
201
+ export declare function padToWidth(text: string, width: number): string;
202
+ /**
203
+ * Pad an already-styled ANSI line to `width` cells without resetting SGR.
204
+ * Diff add/del rows keep their background across the whole terminal row
205
+ * instead of only the glyphs.
206
+ */
207
+ export declare function padAnsiToWidth(text: string, width: number): string;
208
+ /** Visible width of an ANSI-styled line, ignoring CSI / OSC sequences. */
209
+ export declare function visibleWidth(text: string): number;
210
+ /** Repeat a glyph until it occupies exactly `width` cells. */
211
+ export declare function repeatToWidth(glyph: string, width: number): string;
156
212
  /**
157
213
  * Render workspace markdown into width-bounded terminal rows. Assistant
158
214
  * replies get a bold-white base; code blocks, headings, quotes, lists, rules,
@@ -161,6 +217,12 @@ export declare function providerUsesLocalOAuth(provider: string): boolean;
161
217
  export declare function renderMarkdownLines(text: string, width: number, color: boolean): string[];
162
218
  /** Cut one line to fit a width, appending an ellipsis when truncated. */
163
219
  export declare function truncateToWidth(text: string, width: number): string;
220
+ /**
221
+ * Clip an already-styled ANSI line to `width` terminal cells without dropping
222
+ * the reset/SGR sequences. Used by the incremental painter so a leftover wide
223
+ * glyph cannot wrap into the next row.
224
+ */
225
+ export declare function clipAnsiToWidth(text: string, width: number): string;
164
226
  /** One renderable view of the input line: text plus the cursor's visual offset. */
165
227
  interface InputView {
166
228
  text: string;
@@ -192,6 +254,51 @@ export declare function openCodeSourceFor(provider: string, llmPiAiSection: unkn
192
254
  export declare function formatOpenCodeGoUsage(payload: unknown, source: OpenCodeSource): string;
193
255
  /** Whether `text` could still grow into a recognized escape sequence. */
194
256
  export declare function isEscapePrefix(text: string): boolean;
257
+ /** True while a plan still belongs in the dock (latest incomplete work). */
258
+ export declare function planIsLive(plan: {
259
+ active: boolean;
260
+ pending: boolean;
261
+ todos: readonly PlanTodoItem[];
262
+ planMarkdown?: string;
263
+ archived?: boolean;
264
+ }): boolean;
265
+ /** Open todos left behind when a turn ends without a completing todo_write. */
266
+ export declare function planTurnLeftOpen(plan: {
267
+ todos: readonly PlanTodoItem[];
268
+ }): boolean;
269
+ /** Mark leftover in-progress/pending todos as display-stale after turn/end. */
270
+ export declare function applyTurnEndToPlan<T extends {
271
+ todos: PlanTodoItem[];
272
+ turnLeftOpen?: boolean;
273
+ }>(plan: T): T;
274
+ /** Follow-up that asks the model to close leftover todos. One per open list. */
275
+ export declare function planCloseNudgeText(plan: {
276
+ todos: readonly PlanTodoItem[];
277
+ }): string;
278
+ export type CardCategory = 'thinking' | 'plan' | 'subagent' | 'reply' | 'tool' | 'question' | 'goal';
279
+ /** Category for jump / search. Assistant replies are not collapsible cards. */
280
+ export declare function cardCategoryOf(row: {
281
+ kind: string;
282
+ }): CardCategory | undefined;
283
+ /** Split `/find thinking padAnsi` into an optional category and a query. */
284
+ export declare function parseFindQuery(raw: string): {
285
+ category?: CardCategory;
286
+ query: string;
287
+ };
288
+ /** Transcript rows matching a `/find` query, newest last. */
289
+ export declare function matchTranscriptRows(rows: readonly Row[], raw: string): Row[];
290
+ /** One-line note under an expanded plan strip. */
291
+ export declare function planDockNote(plan: {
292
+ active: boolean;
293
+ pending: boolean;
294
+ todos: readonly PlanTodoItem[];
295
+ planMarkdown?: string;
296
+ turnLeftOpen?: boolean;
297
+ }): string;
298
+ /** Compact per-status counts matching the web plan strip. */
299
+ export declare function todoProgressLabel(todos: readonly PlanTodoItem[]): string;
300
+ /** First markdown heading of an exit_plan_mode plan body. */
301
+ export declare function planTitleFromMarkdown(markdown: string): string | undefined;
195
302
  /** Parse a todo_write payload into displayable plan items. */
196
303
  export declare function parsePlanTodos(value: unknown): PlanTodoItem[];
197
304
  /** Compact todo-list summary: done/total plus the first in-progress task. */
@@ -223,6 +330,7 @@ export declare function renderToolDiff(diffs: ToolDiffHunk[], maxLines: number):
223
330
  export declare function friendlyJsonLines(value: unknown, depth?: number): string[];
224
331
  /** Minimal tool-row shape the expanded-body renderer reads. */
225
332
  interface ToolBodySource {
333
+ name?: string;
226
334
  diff?: ToolDiffHunk[];
227
335
  command?: string;
228
336
  status?: 'running' | 'ok' | 'error';
@@ -313,6 +421,14 @@ export declare class SshTui {
313
421
  private lastTitleUpdateAt;
314
422
  private lastPaintRows;
315
423
  private lastChromeKey;
424
+ private lastPaintWidth;
425
+ private lastPaintHeight;
426
+ private readonly paintIntervalMs;
427
+ private searchHits;
428
+ private searchIndex;
429
+ private searchQuery;
430
+ private planNudgePending;
431
+ private pendingReveal;
316
432
  constructor(ctx: Context, agent: Agent, config: TuiConfig);
317
433
  /** Enter raw mode, switch to the alternate screen, and start listening. */
318
434
  start(): void;
@@ -327,6 +443,8 @@ export declare class SshTui {
327
443
  dispose(): Promise<void>;
328
444
  /** Human-facing exit with goodbye and flush; called from key handling. */
329
445
  requestExit(code: number): Promise<void>;
446
+ /** Capture one painted frame. Used by README screenshot fixtures. */
447
+ captureFrame(columns?: number, rows?: number): string[];
330
448
  private write;
331
449
  private markDirty;
332
450
  /** Append one transcript row, bounding memory on long sessions. */
@@ -336,7 +454,15 @@ export declare class SshTui {
336
454
  private spinnerFrame;
337
455
  private findSubagentRow;
338
456
  private findLivePlanRow;
457
+ /** Older / finished plans stay in the scrolling transcript. */
458
+ private archiveStalePlans;
339
459
  private upsertPlanRow;
460
+ /** Whether the live plan strip should occupy the workspace footer. */
461
+ private shouldDockPlan;
462
+ /** One follow-up per leftover list; replay and cancelled turns stay quiet. */
463
+ private queuePlanCloseNudge;
464
+ /** Compact web-style plan strip pinned above the input, not in the transcript. */
465
+ private paintPlanDock;
340
466
  private paintCollapsibleHeader;
341
467
  /** Move the expand/collapse focus among reasoning and tool rows. */
342
468
  private moveCollapsibleFocus;
@@ -344,6 +470,14 @@ export declare class SshTui {
344
470
  private toggleCollapsible;
345
471
  /** Expand all collapsible blocks, or collapse them again when all are open. */
346
472
  private toggleAllCollapsible;
473
+ private highlightSearchLine;
474
+ private revealRow;
475
+ private focusCard;
476
+ /** Jump to the newest card in a category (thinking / plan / subagent / reply). */
477
+ private jumpToCategory;
478
+ private applySearchHits;
479
+ private runFindCommand;
480
+ private stepSearch;
347
481
  private paint;
348
482
  private buildSuggestions;
349
483
  private suggestionsVisible;
@@ -422,10 +556,20 @@ export declare class SshTui {
422
556
  private pickModelOption;
423
557
  /** Live adapter routes the TUI can switch to, plus the current selection. */
424
558
  private listSelectableProviders;
425
- /** /model: pick a provider, then a model and reasoning effort on that route. */
559
+ /** Built-in SuperGrok catalog used when the live adapter list is still warming up. */
560
+ private static readonly XAI_FALLBACK_MODELS;
561
+ /** /model: stay on the current provider by default; switching providers is opt-in. */
426
562
  private runModelCommand;
563
+ /** Persist a provider/model/effort choice and keep the subagent on the same family. */
564
+ private applyModelSelection;
427
565
  /** Provider route the next subagent request should use. */
428
566
  private effectiveSubagentProvider;
567
+ /**
568
+ * When the parent provider changes (OAuth or API key), keep the subagent
569
+ * on a same-family model. An explicit leftover DeepSeek flash id after
570
+ * switching to xAI is treated as stale.
571
+ */
572
+ private syncSubagentToProvider;
429
573
  /** Persist one subagent selection and publish it to the live request waterfall. */
430
574
  private saveSubagentSelection;
431
575
  /** Resolve the picker model list for one provider (endpoint first, then catalog). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ssh-tui",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "SSH-friendly interactive terminal TUI plugin for DeepSeek Harness",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -42,6 +42,9 @@
42
42
  "cordis.patch.yml",
43
43
  "README.md",
44
44
  "README.en.md",
45
+ "docs/screenshots/compare.png",
46
+ "docs/screenshots/headless.png",
47
+ "docs/screenshots/workspace.png",
45
48
  "LICENSE"
46
49
  ],
47
50
  "license": "MIT",
@@ -61,7 +64,8 @@
61
64
  "install:dsh": "bash scripts/install.sh",
62
65
  "install:npm": "bash scripts/install-npm.sh",
63
66
  "uninstall:dsh": "bash scripts/uninstall.sh",
64
- "verify:dsh": "bash scripts/verify.sh"
67
+ "verify:dsh": "bash scripts/verify.sh",
68
+ "screenshots": "npm run build && node scripts/capture-readme-frames.mjs"
65
69
  },
66
70
  "engines": {
67
71
  "node": ">=22.19"