dsh-ssh-tui 0.3.0 → 0.3.1

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 120).
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` (120).
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,10 @@ 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;
115
124
  } | {
116
125
  kind: 'question';
117
126
  questionId: string;
@@ -135,7 +144,7 @@ type Row = {
135
144
  kind: 'error';
136
145
  text: string;
137
146
  };
138
- type DisplayKind = Row['kind'] | 'tool-result' | 'diff-add' | 'diff-del' | 'diff-path';
147
+ type DisplayKind = Row['kind'] | 'tool-result' | 'diff-add' | 'diff-del' | 'diff-path' | 'todo-done' | 'todo-active' | 'todo-pending' | 'plan-dock';
139
148
  /** One file's change, matching the web diff-card contract (`card: 'diff'`). */
140
149
  interface ToolDiffHunk {
141
150
  path: string;
@@ -146,6 +155,23 @@ interface ToolDiffHunk {
146
155
  export interface TuiController {
147
156
  dispose(): Promise<void>;
148
157
  }
158
+ /**
159
+ * Paint cadence for jump-host / proxied SSH. Token ticks coalesce into one
160
+ * frame; the default stays snappy, slower links raise `DSH_TUI_PAINT_MS`.
161
+ */
162
+ export declare function resolvePaintIntervalMs(configured?: number, env?: NodeJS.ProcessEnv): number;
163
+ /** One incremental paint as a single stdout write (one SSH packet when corked). */
164
+ export declare function composePaintOutput(options: {
165
+ width: number;
166
+ height: number;
167
+ paintRows: readonly string[];
168
+ previousRows: readonly string[];
169
+ sizeChanged: boolean;
170
+ chromeChanged: boolean;
171
+ chromeStart: number;
172
+ cursorRow: number;
173
+ cursorColumn: number;
174
+ }): string;
149
175
  /** Human-facing kind for a live LLM route. */
150
176
  export declare function describeProviderRoute(provider: string): {
151
177
  kind: string;
@@ -153,6 +179,31 @@ export declare function describeProviderRoute(provider: string): {
153
179
  };
154
180
  /** Routes that authenticate without a harness API-key credential. */
155
181
  export declare function providerUsesLocalOAuth(provider: string): boolean;
182
+ /**
183
+ * Terminal cell width for one string.
184
+ *
185
+ * Match glibc wcwidth / typical UTF-8 SSH terminals: CJK ideographs and
186
+ * fullwidth forms occupy two cells; East-Asian Ambiguous box-drawing and
187
+ * ornaments (`─`, `●`, `·`, `▸`, `❯`, Braille spinners) occupy one. Counting
188
+ * those ambiguous glyphs as two made `repeatToWidth('─', cols)` paint a
189
+ * half-width rule and parked the input cursor half a cell past the text.
190
+ *
191
+ * Overflow into the input box is handled by clipping/padding painted rows to
192
+ * the measured column count, not by inflating glyph width.
193
+ */
194
+ export declare function displayWidth(text: string): number;
195
+ /** Pad or clip one already-sanitized line so it occupies exactly `width` cells. */
196
+ export declare function padToWidth(text: string, width: number): string;
197
+ /**
198
+ * Pad an already-styled ANSI line to `width` cells without resetting SGR.
199
+ * Diff add/del rows keep their background across the whole terminal row
200
+ * instead of only the glyphs.
201
+ */
202
+ export declare function padAnsiToWidth(text: string, width: number): string;
203
+ /** Visible width of an ANSI-styled line, ignoring CSI / OSC sequences. */
204
+ export declare function visibleWidth(text: string): number;
205
+ /** Repeat a glyph until it occupies exactly `width` cells. */
206
+ export declare function repeatToWidth(glyph: string, width: number): string;
156
207
  /**
157
208
  * Render workspace markdown into width-bounded terminal rows. Assistant
158
209
  * replies get a bold-white base; code blocks, headings, quotes, lists, rules,
@@ -161,6 +212,12 @@ export declare function providerUsesLocalOAuth(provider: string): boolean;
161
212
  export declare function renderMarkdownLines(text: string, width: number, color: boolean): string[];
162
213
  /** Cut one line to fit a width, appending an ellipsis when truncated. */
163
214
  export declare function truncateToWidth(text: string, width: number): string;
215
+ /**
216
+ * Clip an already-styled ANSI line to `width` terminal cells without dropping
217
+ * the reset/SGR sequences. Used by the incremental painter so a leftover wide
218
+ * glyph cannot wrap into the next row.
219
+ */
220
+ export declare function clipAnsiToWidth(text: string, width: number): string;
164
221
  /** One renderable view of the input line: text plus the cursor's visual offset. */
165
222
  interface InputView {
166
223
  text: string;
@@ -192,6 +249,37 @@ export declare function openCodeSourceFor(provider: string, llmPiAiSection: unkn
192
249
  export declare function formatOpenCodeGoUsage(payload: unknown, source: OpenCodeSource): string;
193
250
  /** Whether `text` could still grow into a recognized escape sequence. */
194
251
  export declare function isEscapePrefix(text: string): boolean;
252
+ /** True while a plan still belongs in the dock (latest incomplete work). */
253
+ export declare function planIsLive(plan: {
254
+ active: boolean;
255
+ pending: boolean;
256
+ todos: readonly PlanTodoItem[];
257
+ planMarkdown?: string;
258
+ archived?: boolean;
259
+ }): boolean;
260
+ export type CardCategory = 'thinking' | 'plan' | 'subagent' | 'reply' | 'tool' | 'question' | 'goal';
261
+ /** Category for jump / search. Assistant replies are not collapsible cards. */
262
+ export declare function cardCategoryOf(row: {
263
+ kind: string;
264
+ }): CardCategory | undefined;
265
+ /** Split `/find thinking padAnsi` into an optional category and a query. */
266
+ export declare function parseFindQuery(raw: string): {
267
+ category?: CardCategory;
268
+ query: string;
269
+ };
270
+ /** Transcript rows matching a `/find` query, newest last. */
271
+ export declare function matchTranscriptRows(rows: readonly Row[], raw: string): Row[];
272
+ /** One-line note under an expanded plan strip. */
273
+ export declare function planDockNote(plan: {
274
+ active: boolean;
275
+ pending: boolean;
276
+ todos: readonly PlanTodoItem[];
277
+ planMarkdown?: string;
278
+ }): string;
279
+ /** Compact per-status counts matching the web plan strip. */
280
+ export declare function todoProgressLabel(todos: readonly PlanTodoItem[]): string;
281
+ /** First markdown heading of an exit_plan_mode plan body. */
282
+ export declare function planTitleFromMarkdown(markdown: string): string | undefined;
195
283
  /** Parse a todo_write payload into displayable plan items. */
196
284
  export declare function parsePlanTodos(value: unknown): PlanTodoItem[];
197
285
  /** Compact todo-list summary: done/total plus the first in-progress task. */
@@ -223,6 +311,7 @@ export declare function renderToolDiff(diffs: ToolDiffHunk[], maxLines: number):
223
311
  export declare function friendlyJsonLines(value: unknown, depth?: number): string[];
224
312
  /** Minimal tool-row shape the expanded-body renderer reads. */
225
313
  interface ToolBodySource {
314
+ name?: string;
226
315
  diff?: ToolDiffHunk[];
227
316
  command?: string;
228
317
  status?: 'running' | 'ok' | 'error';
@@ -313,6 +402,12 @@ export declare class SshTui {
313
402
  private lastTitleUpdateAt;
314
403
  private lastPaintRows;
315
404
  private lastChromeKey;
405
+ private lastPaintWidth;
406
+ private lastPaintHeight;
407
+ private readonly paintIntervalMs;
408
+ private searchHits;
409
+ private searchIndex;
410
+ private searchQuery;
316
411
  constructor(ctx: Context, agent: Agent, config: TuiConfig);
317
412
  /** Enter raw mode, switch to the alternate screen, and start listening. */
318
413
  start(): void;
@@ -327,6 +422,8 @@ export declare class SshTui {
327
422
  dispose(): Promise<void>;
328
423
  /** Human-facing exit with goodbye and flush; called from key handling. */
329
424
  requestExit(code: number): Promise<void>;
425
+ /** Capture one painted frame. Used by README screenshot fixtures. */
426
+ captureFrame(columns?: number, rows?: number): string[];
330
427
  private write;
331
428
  private markDirty;
332
429
  /** Append one transcript row, bounding memory on long sessions. */
@@ -336,7 +433,13 @@ export declare class SshTui {
336
433
  private spinnerFrame;
337
434
  private findSubagentRow;
338
435
  private findLivePlanRow;
436
+ /** Older / finished plans stay in the scrolling transcript. */
437
+ private archiveStalePlans;
339
438
  private upsertPlanRow;
439
+ /** Whether the live plan strip should occupy the workspace footer. */
440
+ private shouldDockPlan;
441
+ /** Compact web-style plan strip pinned above the input, not in the transcript. */
442
+ private paintPlanDock;
340
443
  private paintCollapsibleHeader;
341
444
  /** Move the expand/collapse focus among reasoning and tool rows. */
342
445
  private moveCollapsibleFocus;
@@ -344,6 +447,12 @@ export declare class SshTui {
344
447
  private toggleCollapsible;
345
448
  /** Expand all collapsible blocks, or collapse them again when all are open. */
346
449
  private toggleAllCollapsible;
450
+ private focusCard;
451
+ /** Jump to the newest card in a category (thinking / plan / subagent / reply). */
452
+ private jumpToCategory;
453
+ private applySearchHits;
454
+ private runFindCommand;
455
+ private stepSearch;
347
456
  private paint;
348
457
  private buildSuggestions;
349
458
  private suggestionsVisible;
@@ -422,10 +531,20 @@ export declare class SshTui {
422
531
  private pickModelOption;
423
532
  /** Live adapter routes the TUI can switch to, plus the current selection. */
424
533
  private listSelectableProviders;
425
- /** /model: pick a provider, then a model and reasoning effort on that route. */
534
+ /** Built-in SuperGrok catalog used when the live adapter list is still warming up. */
535
+ private static readonly XAI_FALLBACK_MODELS;
536
+ /** /model: stay on the current provider by default; switching providers is opt-in. */
426
537
  private runModelCommand;
538
+ /** Persist a provider/model/effort choice and keep the subagent on the same family. */
539
+ private applyModelSelection;
427
540
  /** Provider route the next subagent request should use. */
428
541
  private effectiveSubagentProvider;
542
+ /**
543
+ * When the parent provider changes (OAuth or API key), keep the subagent
544
+ * on a same-family model. An explicit leftover DeepSeek flash id after
545
+ * switching to xAI is treated as stale.
546
+ */
547
+ private syncSubagentToProvider;
429
548
  /** Persist one subagent selection and publish it to the live request waterfall. */
430
549
  private saveSubagentSelection;
431
550
  /** 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.1",
4
4
  "description": "SSH-friendly interactive terminal TUI plugin for DeepSeek Harness",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -42,6 +42,7 @@
42
42
  "cordis.patch.yml",
43
43
  "README.md",
44
44
  "README.en.md",
45
+ "docs/screenshots",
45
46
  "LICENSE"
46
47
  ],
47
48
  "license": "MIT",
@@ -61,7 +62,8 @@
61
62
  "install:dsh": "bash scripts/install.sh",
62
63
  "install:npm": "bash scripts/install-npm.sh",
63
64
  "uninstall:dsh": "bash scripts/uninstall.sh",
64
- "verify:dsh": "bash scripts/verify.sh"
65
+ "verify:dsh": "bash scripts/verify.sh",
66
+ "screenshots": "npm run build && node scripts/capture-readme-frames.mjs"
65
67
  },
66
68
  "engines": {
67
69
  "node": ">=22.19"