dsh-ssh-tui 0.2.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,55 @@ 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;
175
+ /** Human-facing kind for a live LLM route. */
176
+ export declare function describeProviderRoute(provider: string): {
177
+ kind: string;
178
+ short: string;
179
+ };
180
+ /** Routes that authenticate without a harness API-key credential. */
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;
149
207
  /**
150
208
  * Render workspace markdown into width-bounded terminal rows. Assistant
151
209
  * replies get a bold-white base; code blocks, headings, quotes, lists, rules,
@@ -154,6 +212,12 @@ export interface TuiController {
154
212
  export declare function renderMarkdownLines(text: string, width: number, color: boolean): string[];
155
213
  /** Cut one line to fit a width, appending an ellipsis when truncated. */
156
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;
157
221
  /** One renderable view of the input line: text plus the cursor's visual offset. */
158
222
  interface InputView {
159
223
  text: string;
@@ -185,6 +249,37 @@ export declare function openCodeSourceFor(provider: string, llmPiAiSection: unkn
185
249
  export declare function formatOpenCodeGoUsage(payload: unknown, source: OpenCodeSource): string;
186
250
  /** Whether `text` could still grow into a recognized escape sequence. */
187
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;
188
283
  /** Parse a todo_write payload into displayable plan items. */
189
284
  export declare function parsePlanTodos(value: unknown): PlanTodoItem[];
190
285
  /** Compact todo-list summary: done/total plus the first in-progress task. */
@@ -216,6 +311,7 @@ export declare function renderToolDiff(diffs: ToolDiffHunk[], maxLines: number):
216
311
  export declare function friendlyJsonLines(value: unknown, depth?: number): string[];
217
312
  /** Minimal tool-row shape the expanded-body renderer reads. */
218
313
  interface ToolBodySource {
314
+ name?: string;
219
315
  diff?: ToolDiffHunk[];
220
316
  command?: string;
221
317
  status?: 'running' | 'ok' | 'error';
@@ -306,6 +402,12 @@ export declare class SshTui {
306
402
  private lastTitleUpdateAt;
307
403
  private lastPaintRows;
308
404
  private lastChromeKey;
405
+ private lastPaintWidth;
406
+ private lastPaintHeight;
407
+ private readonly paintIntervalMs;
408
+ private searchHits;
409
+ private searchIndex;
410
+ private searchQuery;
309
411
  constructor(ctx: Context, agent: Agent, config: TuiConfig);
310
412
  /** Enter raw mode, switch to the alternate screen, and start listening. */
311
413
  start(): void;
@@ -320,6 +422,8 @@ export declare class SshTui {
320
422
  dispose(): Promise<void>;
321
423
  /** Human-facing exit with goodbye and flush; called from key handling. */
322
424
  requestExit(code: number): Promise<void>;
425
+ /** Capture one painted frame. Used by README screenshot fixtures. */
426
+ captureFrame(columns?: number, rows?: number): string[];
323
427
  private write;
324
428
  private markDirty;
325
429
  /** Append one transcript row, bounding memory on long sessions. */
@@ -329,7 +433,13 @@ export declare class SshTui {
329
433
  private spinnerFrame;
330
434
  private findSubagentRow;
331
435
  private findLivePlanRow;
436
+ /** Older / finished plans stay in the scrolling transcript. */
437
+ private archiveStalePlans;
332
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;
333
443
  private paintCollapsibleHeader;
334
444
  /** Move the expand/collapse focus among reasoning and tool rows. */
335
445
  private moveCollapsibleFocus;
@@ -337,9 +447,16 @@ export declare class SshTui {
337
447
  private toggleCollapsible;
338
448
  /** Expand all collapsible blocks, or collapse them again when all are open. */
339
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;
340
456
  private paint;
341
457
  private buildSuggestions;
342
458
  private suggestionsVisible;
459
+ private currentProviderId;
343
460
  private currentSelectionLabel;
344
461
  /** Replace one step's usage sample so a repeated report never double counts. */
345
462
  private recordUsage;
@@ -412,10 +529,22 @@ export declare class SshTui {
412
529
  * dialog so an endpoint with dozens of models stays selectable.
413
530
  */
414
531
  private pickModelOption;
415
- /** /model: pick a model and reasoning effort for the current provider. */
532
+ /** Live adapter routes the TUI can switch to, plus the current selection. */
533
+ private listSelectableProviders;
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. */
416
537
  private runModelCommand;
538
+ /** Persist a provider/model/effort choice and keep the subagent on the same family. */
539
+ private applyModelSelection;
417
540
  /** Provider route the next subagent request should use. */
418
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;
419
548
  /** Persist one subagent selection and publish it to the live request waterfall. */
420
549
  private saveSubagentSelection;
421
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.2.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"