dsh-ssh-tui 0.5.3 → 0.5.5

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.
Files changed (57) hide show
  1. package/README.en.md +36 -10
  2. package/README.md +21 -12
  3. package/lib/approval-reviewer.js +56 -15
  4. package/lib/approval-reviewer.js.map +1 -1
  5. package/lib/auto-approval.js +251 -35
  6. package/lib/auto-approval.js.map +1 -1
  7. package/lib/copy-text.js +62 -0
  8. package/lib/copy-text.js.map +1 -0
  9. package/lib/dsh-compat.js +142 -1
  10. package/lib/dsh-compat.js.map +1 -1
  11. package/lib/footer.js +337 -0
  12. package/lib/footer.js.map +1 -0
  13. package/lib/i18n/en.js +62 -3
  14. package/lib/i18n/en.js.map +1 -1
  15. package/lib/i18n/zh.js +62 -3
  16. package/lib/i18n/zh.js.map +1 -1
  17. package/lib/index.js +7 -5
  18. package/lib/index.js.map +1 -1
  19. package/lib/json-args.js +30 -0
  20. package/lib/json-args.js.map +1 -0
  21. package/lib/paint.js +288 -0
  22. package/lib/paint.js.map +1 -0
  23. package/lib/picker.js +453 -66
  24. package/lib/picker.js.map +1 -1
  25. package/lib/plan.js +369 -0
  26. package/lib/plan.js.map +1 -0
  27. package/lib/quota.js +408 -0
  28. package/lib/quota.js.map +1 -0
  29. package/lib/session-index.js +80 -0
  30. package/lib/session-index.js.map +1 -0
  31. package/lib/session-list.js +210 -88
  32. package/lib/session-list.js.map +1 -1
  33. package/lib/term-text.js +953 -0
  34. package/lib/term-text.js.map +1 -0
  35. package/lib/tool-present.js +744 -0
  36. package/lib/tool-present.js.map +1 -0
  37. package/lib/transcript-types.js +6 -0
  38. package/lib/transcript-types.js.map +1 -0
  39. package/lib/tui.js +643 -3299
  40. package/lib/tui.js.map +1 -1
  41. package/lib/types/approval-reviewer.d.ts +8 -2
  42. package/lib/types/auto-approval.d.ts +28 -0
  43. package/lib/types/copy-text.d.ts +9 -0
  44. package/lib/types/dsh-compat.d.ts +78 -6
  45. package/lib/types/footer.d.ts +155 -0
  46. package/lib/types/json-args.d.ts +7 -0
  47. package/lib/types/paint.d.ts +80 -0
  48. package/lib/types/picker.d.ts +101 -7
  49. package/lib/types/plan.d.ts +80 -0
  50. package/lib/types/quota.d.ts +94 -0
  51. package/lib/types/session-index.d.ts +32 -0
  52. package/lib/types/session-list.d.ts +35 -2
  53. package/lib/types/term-text.d.ts +149 -0
  54. package/lib/types/tool-present.d.ts +165 -0
  55. package/lib/types/transcript-types.d.ts +152 -0
  56. package/lib/types/tui.d.ts +53 -723
  57. package/package.json +20 -18
@@ -9,6 +9,11 @@
9
9
  * write of dirty rows only — jump-host / proxied SSH should see one packet
10
10
  * per paint, not one per line. Cadence is DSH_TUI_PAINT_MS, else local 80 ms
11
11
  * or an SSH tier from a CSI 6n round-trip (default 160 ms).
12
+ *
13
+ * Pure helpers live next to this file (`term-text`, `paint`, `footer`,
14
+ * `quota`, `plan`, `tool-present`) and are re-exported here so existing
15
+ * `lib/tui.js` imports keep working. The launch picker imports `paint` /
16
+ * `term-text` directly and does not load this module.
12
17
  */
13
18
  import type { Agent, ModelSelection, ModelSelectionRef } from '@deepseek-ai/dsh-agent';
14
19
  import type { Context } from '@deepseek-ai/cordis';
@@ -18,7 +23,16 @@ import type { SubagentRunEndInfo, SubagentRunInfo } from '@deepseek-ai/dsh-subag
18
23
  import { type SubagentSelectionRef } from './subagent-model.js';
19
24
  import { type AskUserQuestionAnswer, type AskUserQuestionRequest } from '@deepseek-ai/dsh-user-questions';
20
25
  import type { ApprovalOutcome, ApprovalRequest } from '@deepseek-ai/dsh-user-approval';
21
- export type DisconnectPolicyName = 'pause' | 'continue';
26
+ import type { CollapsibleBlock, DisconnectPolicyName } from './transcript-types.js';
27
+ import { paintedLinkHits } from './term-text.js';
28
+ export type { CollapsibleBlock, DisplayKind, DisconnectPolicyName, PlanTodoItem, Row, SubagentLogEntry, ToolDiffHunk, } from './transcript-types.js';
29
+ export { clipAnsiToWidth, cursorVisualPosition, displayWidth, foldInputView, hrefAtColumn, osc52Clipboard, osc8Enabled, paintedLinkHits, fmtElapsedCompact, padAnsiToWidth, padToWidth, renderMarkdownLines, repeatToWidth, shimmerText, truncateToWidth, visibleWidth, waitCardCopy, waitSummaryFromReasoning, wrapWaitDetails, } from './term-text.js';
30
+ export { captureHangupSignals, composePaintOutput, detectSshSession, formatLinkQualityChip, ignoreFurtherHangupSignals, isEscapePrefix, isHangupErrno, linkQualityOf, linkSignalPips, paintIntervalForRtt, paintLinkLabel, parseCursorPositionReply, pickerWindowStart, probeTerminalRttMs, releaseHangupSignals, resolvePaintIntervalMs, waitUntilIdleOrTimeout, writeBootSplash, type LinkQuality, type PaintLinkKind, } from './paint.js';
31
+ export { CONTEXT_IDLE_COMPACT_RATIO, CONTEXT_PRESSURE_DANGER_RATIO, CONTEXT_PRESSURE_WARN_RATIO, CONTEXT_RING_EMPTY, CONTEXT_RING_SEGMENTS, contextPressureAlertText, contextPressureRingColor, contextPressureUsedTokens, contextPressureView, describeProviderRoute, dropFooterQuotaPlanName, fitFooterStatsLine, fitFooterStatusLine, footerActivity, footerIdentityParts, footerStatsGroups, formatContextPressureChip, formatContextPressureRing, formatContextPressureStatusLine, formatDuration, formatFooterQuota, formatQuotaBar, formatStatusReport, formatTokens, formatTokensPerSecond, parseContextPressure, promptPressureTokens, providerShortCode, providerUsesLocalOAuth, shouldIdleAutoCompact, type ContextPressureSample, type ContextPressureView, type FooterActivityKind, type FooterStatsInput, type FooterStatusInput, type StatusReportInput, } from './footer.js';
32
+ export { crossedQuotaThresholds, formatAccountBalance, formatFooterBalance, formatOpenCodeGoUsage, formatQuotaSnapshot, formatQuotaStatusLine, joinUrl, openCodeSourceFor, parseDeepSeekBalance, parseOpenAiCompatibleBalance, parseOpenCodeGoQuota, parseSuperGrokBilling, quotaAlertText, quotaRefreshEverySteps, quotaRefreshEveryTurns, remainingPercentFromUsed, tightestQuotaWindow, type AccountBalanceLine, type AccountBalanceSnapshot, type OpenCodeFlavor, type OpenCodeSource, type QuotaPeriod, type QuotaSnapshot, type QuotaWindow, } from './quota.js';
33
+ export { commandAcceptsAttachments, forEachSessionEvent, isAssistantStreamEvent, listPersistenceHeaders, inspectPersistenceSession, sessionEventType, sessionEvents, settingsNamespace, streamChunkOf, } from './dsh-compat.js';
34
+ export { applyTurnEndToPlan, askSummary, cardCategoryOf, compactionHeaderText, formatCompactCommandError, isPromptInjectionMessage, matchTranscriptRows, parseFindQuery, parsePlanTodos, planCloseNudgeText, planDockNote, planIsLive, planTitleFromMarkdown, planTurnLeftOpen, promptInjectionSources, promptInjectionTitle, subagentHeaderText, todoProgressLabel, todoSummary, type CardCategory, } from './plan.js';
35
+ export { buildToolHeader, canMergeToolCall, compactEditPath, compactToolBursts, compactToolGroups, countDiffAddDel, countDiffLines, countOutputLines, diffMetaDiffs, diffStatToken, friendlyJsonLines, parseExitStatus, presentToolCall, READ_TOOL_NAMES, renderToolDiff, toolBodyFitsWorkspace, toolBodyLines, toolStateColor, toolStateLabel, wrappedToolBodyLineCount, } from './tool-present.js';
22
36
  /** Presentation configuration for the terminal channel. */
23
37
  export interface TuiConfig {
24
38
  /** Exact shared agent/session identity driven by this terminal. */
@@ -70,537 +84,13 @@ export interface TuiConfig {
70
84
  /** Hangup policy while busy: pause cancels the turn; continue lets it finish detached. Idle hangup always exits. */
71
85
  disconnectPolicy?: DisconnectPolicyName;
72
86
  }
73
- type SubagentLogKind = 'user' | 'assistant' | 'tool' | 'result' | 'turn' | 'approval' | 'team' | 'system';
74
- /** One child-session event folded into a parent-side subagent card. */
75
- export interface SubagentLogEntry {
76
- kind: SubagentLogKind;
77
- text: string;
78
- }
79
- /** One todo-list item as the plan card renders it. */
80
- export interface PlanTodoItem {
81
- content: string;
82
- status: 'pending' | 'in_progress' | 'completed';
83
- }
84
- type Row = {
85
- kind: 'user';
86
- text: string;
87
- } | {
88
- kind: 'assistant';
89
- text: string;
90
- } | {
91
- kind: 'reasoning';
92
- text: string;
93
- expanded: boolean;
94
- } | {
95
- kind: 'brand';
96
- text: string;
97
- } | {
98
- kind: 'brand-logo';
99
- } | {
100
- kind: 'tool';
101
- callId: string;
102
- name: string;
103
- args: string;
104
- status?: 'running' | 'ok' | 'error';
105
- output: string;
106
- title: string;
107
- summary: string;
108
- command?: string;
109
- cwd?: string;
110
- diff?: ToolDiffHunk[];
111
- exitCode?: number;
112
- signal?: string;
113
- expanded: boolean;
114
- /** Consecutive same-path reads/edits folded into this card. */
115
- repeats?: number;
116
- /** Call ids folded into this card; results still match after merge. */
117
- mergedCallIds?: string[];
118
- /** Sum of output characters across folded reads. */
119
- totalChars?: number;
120
- /** Sum of output lines across folded reads. */
121
- totalLines?: number;
122
- /** Flip-card animation until this timestamp (ms since epoch). */
123
- flipUntil?: number;
124
- } | {
125
- kind: 'subagent';
126
- sessionId: string;
127
- runId: string;
128
- provider: string;
129
- local: boolean;
130
- label: string;
131
- status: 'running' | 'ok' | 'error' | 'aborted';
132
- startedAt: number;
133
- endedAt?: number;
134
- stopReason?: string;
135
- lastActivity: string;
136
- logs: SubagentLogEntry[];
137
- expanded: boolean;
138
- } | {
139
- kind: 'plan';
140
- active: boolean;
141
- pending: boolean;
142
- todos: PlanTodoItem[];
143
- planMarkdown?: string;
144
- expanded: boolean;
145
- /** When true the plan stays in the scrolling transcript, not the dock. */
146
- archived?: boolean;
147
- /**
148
- * Display-only: the last turn ended while todos were still open.
149
- * Does not rewrite the session log.
150
- */
151
- turnLeftOpen?: boolean;
152
- } | {
153
- kind: 'question';
154
- questionId: string;
155
- title: string;
156
- header?: string;
157
- detail?: string;
158
- intent: 'ask' | 'plan-review';
159
- status: 'waiting' | 'answered' | 'cancelled';
160
- summary: string;
161
- expanded: boolean;
162
- } | {
163
- kind: 'goal';
164
- objective: string;
165
- phase: 'active' | 'paused' | 'blocked' | 'complete' | 'cleared';
166
- blockedReason?: string;
167
- expanded: boolean;
168
- } | {
169
- kind: 'compaction';
170
- compactionId: string;
171
- status: 'running' | 'ok' | 'error';
172
- startedAt: number;
173
- endedAt?: number;
174
- pruneCount: number;
175
- prunedTokens: number;
176
- summary?: string;
177
- error?: string;
178
- expanded: boolean;
179
- } | {
180
- kind: 'prompt';
181
- sources: string[];
182
- text: string;
183
- plugin?: string;
184
- expanded: boolean;
185
- } | {
186
- kind: 'system';
187
- text: string;
188
- } | {
189
- kind: 'error';
190
- text: string;
191
- };
192
- /** A reasoning/tool/subagent/plan row or the live streaming-reasoning block. */
193
- type CollapsibleBlock = Extract<Row, {
194
- kind: 'reasoning';
195
- } | {
196
- kind: 'tool';
197
- } | {
198
- kind: 'subagent';
199
- } | {
200
- kind: 'plan';
201
- } | {
202
- kind: 'question';
203
- } | {
204
- kind: 'goal';
205
- } | {
206
- kind: 'compaction';
207
- } | {
208
- kind: 'prompt';
209
- }> | {
210
- kind: 'streaming-reasoning';
211
- expanded: boolean;
212
- };
213
- type DisplayKind = Row['kind'] | 'tool-result' | 'diff-add' | 'diff-del' | 'diff-path' | 'todo-done' | 'todo-active' | 'todo-pending' | 'plan-dock';
214
- /** One file's change, matching the web diff-card contract (`card: 'diff'`). */
215
- interface ToolDiffHunk {
216
- path: string;
217
- oldText: string | null;
218
- newText: string;
219
- }
220
87
  /** Lifecycle handle for a mounted interactive terminal channel. */
221
88
  export interface TuiController {
222
89
  dispose(): Promise<void>;
223
90
  handleHangup(): Promise<void>;
224
91
  disconnectPolicy(): DisconnectPolicyName;
225
92
  }
226
- export type PaintLinkKind = 'local' | 'ssh';
227
- /** Compact token count, matching the web stats line (517 / 12.2K / 1.2M). */
228
- export declare function formatTokens(n: number): string;
229
- /**
230
- * Prompt occupancy of the next request, from DSH `contextPressure`.
231
- * Provider-agnostic: uses the routed model's advertised window, not a
232
- * hardcoded xAI size. Compaction-basic still owns in-turn pressure at 80%.
233
- */
234
- export declare const CONTEXT_PRESSURE_WARN_RATIO = 0.8;
235
- export declare const CONTEXT_PRESSURE_DANGER_RATIO = 0.95;
236
- /** Idle auto-compact starts here so recovery finishes before the 80% in-turn trigger. */
237
- export declare const CONTEXT_IDLE_COMPACT_RATIO = 0.72;
238
- export interface ContextPressureSample {
239
- usedTokens: number;
240
- contextWindow: number;
241
- }
242
- export interface ContextPressureView {
243
- usedTokens: number;
244
- contextWindow: number;
245
- percent: number;
246
- level: 'ok' | 'warn' | 'danger';
247
- }
248
- /** Prompt-side occupancy of one usage sample: uncached input plus cache traffic. */
249
- export declare function promptPressureTokens(usage: {
250
- inputTokens: number;
251
- cacheReadTokens?: number;
252
- cacheWriteTokens?: number;
253
- }): number;
254
- /** Prefer the next-request projection; fall back to last-request pressure. */
255
- export declare function contextPressureUsedTokens(pressure: {
256
- projectedTokens?: number;
257
- pressureTokens?: number;
258
- } | undefined): number | undefined;
259
- export declare function parseContextPressure(value: unknown): ContextPressureSample | undefined;
260
- export declare function contextPressureView(sample: ContextPressureSample): ContextPressureView;
261
- /**
262
- * 8-segment Braille ring. Empty `⣀`; full `⣿`. Width is always 1 cell.
263
- * Index is `ceil(percent / 12.5)` clamped to 0..8.
264
- */
265
- export declare const CONTEXT_RING_EMPTY = "\u28C0";
266
- export declare const CONTEXT_RING_SEGMENTS: readonly ["⣀", "⠉", "⠋", "⠛", "⠞", "⠟", "⠿", "⡿", "⣿"];
267
- export declare function formatContextPressureRing(percent: number): string;
268
- export declare function contextPressureRingColor(level: ContextPressureView['level']): string;
269
- export declare function formatContextPressureChip(view: ContextPressureView, color?: boolean): string;
270
- export declare function formatContextPressureStatusLine(view: ContextPressureView | undefined): string;
271
- export declare function contextPressureAlertText(view: ContextPressureView): string;
272
- export declare function shouldIdleAutoCompact(view: ContextPressureView | undefined): boolean;
273
- /** Compact duration, matching the web stats line (45.2s / 2m42s). */
274
- export declare function formatDuration(ms: number): string;
275
- export declare function formatTokensPerSecond(tokensPerSecond: number): string;
276
- /**
277
- * Explicit env/config always wins. Otherwise local TTYs stay snappy and SSH
278
- * sessions pick a tier from a measured round-trip (CSI 6n), falling back to
279
- * 160 ms when the probe is missing.
280
- */
281
- export declare function resolvePaintIntervalMs(configured?: number, env?: NodeJS.ProcessEnv, options?: {
282
- ssh?: boolean;
283
- rttMs?: number;
284
- }): number;
285
- /** True when this process is attached to an SSH session (jump host / proxy). */
286
- export declare function detectSshSession(env?: NodeJS.ProcessEnv): boolean;
287
- /** Node errno on a write/close that means the TTY is gone (SSH drop, HUP). */
288
- export declare function isHangupErrno(error: unknown): boolean;
289
- /**
290
- * Replace launcher SIGTERM/SIGINT/SIGHUP handlers with `handler`. SSH drop
291
- * otherwise lets `dsh` dispose the whole tree before this plugin can detach.
292
- */
293
- export declare function captureHangupSignals(handler: () => void): void;
294
- export declare function releaseHangupSignals(handler: () => void): void;
295
- /** After detach, extra HUP/TERM from sshd must not kill the leftover Host. */
296
- export declare function ignoreFurtherHangupSignals(): void;
297
- /**
298
- * Wait until `isIdle` is true or `timeoutMs` elapses. Used after cancel so a
299
- * hangup can flush a settled session log instead of tearing a live write.
300
- */
301
- export declare function waitUntilIdleOrTimeout(isIdle: () => boolean, timeoutMs: number, now?: () => number, wait?: (ms: number) => Promise<void>): Promise<'idle' | 'timeout'>;
302
- /** Map a CSI-6n round-trip to a paint cadence. Unknown RTT uses the SSH default. */
303
- export declare function paintIntervalForRtt(rttMs: number | undefined): number;
304
- export declare function paintLinkLabel(kind: PaintLinkKind, intervalMs: number, probed: boolean): string;
305
- export type LinkQuality = 'local' | 'good' | 'ok' | 'slow' | 'poor' | 'unknown';
306
- /** Signal-bar quality from a measured SSH round-trip, or local TTY. */
307
- export declare function linkQualityOf(kind: PaintLinkKind, rttMs: number | undefined): LinkQuality;
308
- /** How many filled signal pips: 4 local/fast, 3 ok, 2 slow, 1 poor, 0 unknown. */
309
- export declare function linkSignalPips(quality: LinkQuality): number;
310
- /** Compact footer chip: `SSH ●●●○ 90ms` — 1 pip red, 2 yellow, 3+ green. */
311
- export declare function formatLinkQualityChip(kind: PaintLinkKind, intervalMs: number, rttMs: number | undefined, probed: boolean, color?: boolean): string;
312
- export declare function providerShortCode(provider: string): string;
313
- export interface FooterStatsInput {
314
- turns: number;
315
- steps: number;
316
- llmMs: number;
317
- toolMs: number;
318
- ttftMs: number;
319
- ttftSteps: number;
320
- decodeMs: number;
321
- decodeTokens: number;
322
- inputTokens: number;
323
- outputTokens: number;
324
- cacheReadTokens: number;
325
- cacheWriteTokens: number;
326
- }
327
- /** Stats groups in drop order (last is dropped first when the row is too wide). */
328
- export declare function footerStatsGroups(stats: FooterStatsInput): string[];
329
- export declare function fitFooterStatsLine(chip: string, groups: readonly string[], width: number): string;
330
- export type FooterActivityKind = 'plan-review' | 'waiting' | 'compacting' | 'retry' | 'subagents' | 'tools' | 'plan-open' | 'plan-pending' | 'goal' | 'waiting-llm' | 'idle';
331
- export interface FooterStatusInput {
332
- running: boolean;
333
- planReview: boolean;
334
- waitingQuestion: boolean;
335
- compacting: boolean;
336
- retry?: {
337
- retry: number;
338
- maxRetries: number;
339
- };
340
- subagents: number;
341
- tools: number;
342
- planLeftOpen: boolean;
343
- planPending: boolean;
344
- planActive: boolean;
345
- goalPhase?: 'active' | 'paused' | 'blocked';
346
- idleMs: number;
347
- model: string;
348
- effort?: string;
349
- preset?: string;
350
- provider: string;
351
- parentModel: string;
352
- subModel: string;
353
- subDiffers: boolean;
354
- quotaCode?: string;
355
- quotaPercent?: number;
356
- contextChip?: string;
357
- balanceText?: string;
358
- search?: {
359
- index: number;
360
- total: number;
361
- };
362
- foldedInput: boolean;
363
- multiLineInput: boolean;
364
- queued: number;
365
- cwdLabel?: string;
366
- compactView?: boolean;
367
- }
368
- export declare function footerActivity(input: FooterStatusInput): {
369
- kind: FooterActivityKind;
370
- text: string;
371
- };
372
- /** Short remaining-quota bar: 8 pips, filled from the left. */
373
- export declare function formatQuotaBar(remainingPercent: number, width?: number): string;
374
- export declare function footerIdentityParts(input: FooterStatusInput): string[];
375
- /** `SuperGrok ███████░ 82%`, or just the bar + percent when `code` is omitted. */
376
- export declare function formatFooterQuota(percent: number, code?: string): string;
377
- /**
378
- * Drop the Go / SuperGrok plan name from a quota identity part, keeping the
379
- * remaining-percent bar. Returns true when a part was rewritten.
380
- */
381
- export declare function dropFooterQuotaPlanName(parts: string[]): boolean;
382
- export declare function fitFooterStatusLine(activity: string, identity: readonly string[], width: number): string;
383
- /** One incremental paint as a single stdout write (one SSH packet when corked). */
384
- export declare function composePaintOutput(options: {
385
- width: number;
386
- height: number;
387
- paintRows: readonly string[];
388
- previousRows: readonly string[];
389
- sizeChanged: boolean;
390
- chromeChanged: boolean;
391
- chromeStart: number;
392
- previousChromeStart?: number;
393
- cursorRow: number;
394
- cursorColumn: number;
395
- }): string;
396
- export interface StatusReportInput {
397
- sessionId: string;
398
- pluginVersion: string;
399
- provider: string;
400
- model: string;
401
- effort?: string;
402
- agentStatus: string;
403
- preset: string;
404
- activeSubagents: number;
405
- plan: 'off' | 'pending' | 'on';
406
- paint: string;
407
- disconnect?: DisconnectPolicyName;
408
- waitingQuestions: number;
409
- quota?: QuotaSnapshot;
410
- context?: ContextPressureView;
411
- parentModel?: string;
412
- subProvider?: string;
413
- subModel: string;
414
- cwd?: string;
415
- }
416
- /** Lines printed by `/status` — SSH first-boot diagnostics, no extra command. */
417
- export declare function formatStatusReport(input: StatusReportInput): string[];
418
- /** Human-facing kind for a live LLM route. */
419
- export declare function describeProviderRoute(provider: string): {
420
- kind: string;
421
- short: string;
422
- };
423
- /** Routes that authenticate without a harness API-key credential. */
424
- export declare function providerUsesLocalOAuth(provider: string): boolean;
425
- /**
426
- * Terminal cell width for one string.
427
- *
428
- * Match glibc wcwidth / typical UTF-8 SSH terminals: CJK ideographs and
429
- * fullwidth forms occupy two cells; East-Asian Ambiguous box-drawing and
430
- * ornaments (`─`, `●`, `·`, `▸`, `❯`, Braille spinners) occupy one. Counting
431
- * those ambiguous glyphs as two made `repeatToWidth('─', cols)` paint a
432
- * half-width rule and parked the input cursor half a cell past the text.
433
- *
434
- * Overflow into the input box is handled by clipping/padding painted rows to
435
- * the measured column count, not by inflating glyph width.
436
- */
437
- /**
438
- * Codex-style compact elapsed: `0s`, `1m 05s`, `1h 01m 01s`.
439
- * Used by the workspace wait card while the model has not streamed yet.
440
- */
441
- export declare function fmtElapsedCompact(elapsedSecs: number): string;
442
- /**
443
- * Sweep highlight across `text` (Codex `shimmer.rs`). Truecolor blends a
444
- * highlight band; otherwise DIM / default / BOLD. Process-start based so
445
- * every paint of the same frame stays in phase.
446
- */
447
- export declare function shimmerText(text: string, nowMs: number, color: boolean): string;
448
- /**
449
- * Codex `extract_first_bold`: the first **closed** `**bold**` in the thinking
450
- * stream, else the first markdown heading. An unclosed `**` means the title
451
- * has not arrived yet, so return undefined and keep the default header —
452
- * never fall back to hard-truncated reasoning, reply, or prompt text.
453
- */
454
- export declare function waitSummaryFromReasoning(text: string): string | undefined;
455
- /** Wait-card header + optional detail. Header tracks model work when known. */
456
- export declare function waitCardCopy(input: {
457
- toolTitle?: string;
458
- toolSummary?: string;
459
- reasoning?: string;
460
- }): {
461
- header: string;
462
- detail?: string;
463
- };
464
- /**
465
- * Codex `wrapped_details_lines`: word-wrap the wait-card detail under the
466
- * ` └ ` prefix, continue wrapped rows at the prefix width, cap at 3 rows and
467
- * end the last one with an ellipsis when the text does not fit.
468
- */
469
- export declare function wrapWaitDetails(detail: string, width: number, maxLines?: number): string[];
470
- export declare function displayWidth(text: string): number;
471
- /** Pad or clip one already-sanitized line so it occupies exactly `width` cells. */
472
- export declare function padToWidth(text: string, width: number): string;
473
- /**
474
- * Pad an already-styled ANSI line to `width` cells without resetting SGR.
475
- * Diff add/del rows keep their background across the whole terminal row
476
- * instead of only the glyphs.
477
- */
478
- export declare function padAnsiToWidth(text: string, width: number): string;
479
- /** Visible width of an ANSI-styled line, ignoring CSI / OSC sequences. */
480
- export declare function visibleWidth(text: string): number;
481
- /** Repeat a glyph until it occupies exactly `width` cells. */
482
- export declare function repeatToWidth(glyph: string, width: number): string;
483
- /** One colored span inside a tool-card header line. Offsets are UTF-16 char indices. */
484
- interface TextSegment {
485
- start: number;
486
- end: number;
487
- sgr: string;
488
- }
489
- /**
490
- * Render workspace markdown into width-bounded terminal rows. Assistant
491
- * replies get a bold-white base; code blocks, headings, quotes, lists, rules,
492
- * links and inline spans keep their own ANSI treatment.
493
- */
494
- export declare function renderMarkdownLines(text: string, width: number, color: boolean): string[];
495
- /** Cut one line to fit a width, appending an ellipsis when truncated. */
496
- export declare function truncateToWidth(text: string, width: number): string;
497
- /**
498
- * Clip an already-styled ANSI line to `width` terminal cells without dropping
499
- * the reset/SGR sequences. Used by the incremental painter so a leftover wide
500
- * glyph cannot wrap into the next row.
501
- */
502
- export declare function clipAnsiToWidth(text: string, width: number): string;
503
- /** One renderable view of the input line: text plus the cursor's visual offset. */
504
- interface InputView {
505
- text: string;
506
- cursorOffset: number;
507
- folded: boolean;
508
- }
509
- /**
510
- * Fold a long input into one terminal row around the cursor.
511
- *
512
- * Newlines from a paste are display-only: they do not occupy cells, so a
513
- * naive `displayWidth(input)` under-counts a multi-line paste and parks the
514
- * caret in the middle of later text. Fold the *current line* (between the
515
- * surrounding newlines) and keep `\n` out of the visible slice.
516
- */
517
- export declare function foldInputView(input: string, cursor: number, maxWidth: number): InputView;
518
- /** A recognized OpenCode provider route, used by /usage and /quota. */
519
- export type OpenCodeFlavor = 'zen' | 'go';
520
- export interface OpenCodeSource {
521
- provider: string;
522
- flavor: OpenCodeFlavor;
523
- label: string;
524
- apiKeyEnv: string;
525
- baseURL?: string;
526
- }
527
- /**
528
- * Classify the currently selected provider as an OpenCode route. Built-in
529
- * `opencode`/`opencode-go` ids are recognized directly, and custom llm-pi-ai
530
- * routes are recognized by their `opencode.ai` base URL.
531
- */
532
- export declare function openCodeSourceFor(provider: string, llmPiAiSection: unknown): OpenCodeSource | null;
533
- export type QuotaPeriod = 'hourly' | 'weekly' | 'monthly' | 'unknown';
534
- export interface QuotaWindow {
535
- label: string;
536
- period: QuotaPeriod;
537
- /** Remaining percent of the window (100 = unused). */
538
- remainingPercent: number;
539
- resetsAt?: string;
540
- }
541
- export interface QuotaSnapshot {
542
- provider: string;
543
- plan: string;
544
- windows: QuotaWindow[];
545
- }
546
- export declare function remainingPercentFromUsed(usedPercent: number): number;
547
- /** Cross a remaining-percent threshold from above (50 / 25 / 10 / 5).
548
- * Only the tightest (lowest) crossed threshold is returned, so one drop
549
- * never paints 50/25/10 as three identical warnings. */
550
- export declare function crossedQuotaThresholds(previousRemaining: number | undefined, remaining: number): number[];
551
- export declare function quotaAlertText(snapshot: QuotaSnapshot, window: QuotaWindow): string;
552
- /**
553
- * How often to re-fetch quota or prepaid balance, counted in model steps.
554
- * Default is every 10 steps. Near a remaining-percent threshold, hourly
555
- * windows refresh every 4 steps.
556
- */
557
- export declare function quotaRefreshEverySteps(window: QuotaWindow | undefined): number;
558
- /** @deprecated Same cadence as {@link quotaRefreshEverySteps}; the name predates step accounting. */
559
- export declare const quotaRefreshEveryTurns: typeof quotaRefreshEverySteps;
560
- export declare function parseSuperGrokBilling(payload: unknown): QuotaSnapshot;
561
- export declare function parseOpenCodeGoQuota(payload: unknown, provider: string): QuotaSnapshot;
562
- export declare function formatQuotaSnapshot(snapshot: QuotaSnapshot): string;
563
- /** Compact `/status` quota line: tightest window first, then the rest. */
564
- export declare function formatQuotaStatusLine(snapshot: QuotaSnapshot | undefined): string;
565
- /** Tightest remaining window — used for threshold alerts. */
566
- export declare function tightestQuotaWindow(snapshot: QuotaSnapshot): QuotaWindow | undefined;
567
- /** Render the OpenCode Go quota payload as a transcript block. */
568
- export declare function formatOpenCodeGoUsage(payload: unknown, source: OpenCodeSource): string;
569
- export interface AccountBalanceLine {
570
- label: string;
571
- amount: string;
572
- currency?: string;
573
- }
574
- export interface AccountBalanceSnapshot {
575
- provider: string;
576
- plan: string;
577
- available?: boolean;
578
- lines: AccountBalanceLine[];
579
- sourcePath?: string;
580
- }
581
- export declare function joinUrl(base: string, path: string): string;
582
- export declare function parseDeepSeekBalance(payload: unknown, provider?: string): AccountBalanceSnapshot;
583
- /** Best-effort parse of OpenAI-compatible credit/balance JSON. */
584
- export declare function parseOpenAiCompatibleBalance(payload: unknown, provider: string, path: string): AccountBalanceSnapshot | undefined;
585
- /** Compact footer chip: `余额 86.42 CNY`. Prefers remaining/available lines. */
586
- export declare function formatFooterBalance(snapshot: AccountBalanceSnapshot): string | undefined;
587
- export declare function formatAccountBalance(snapshot: AccountBalanceSnapshot): string;
588
- /** Whether `text` could still grow into a recognized escape sequence. */
589
- export declare function isEscapePrefix(text: string): boolean;
590
- /** Parse a Device Status Report cursor reply (`CSI row;col R`). */
591
- export declare function parseCursorPositionReply(text: string): {
592
- row: number;
593
- column: number;
594
- } | undefined;
595
- /**
596
- * Round-trip to the attached terminal via CSI 6n. Returns undefined when the
597
- * reply never arrives (dumb pipe, blocked DSR). Does not interpret the
598
- * coordinates — only the elapsed milliseconds matter.
599
- */
600
- export declare function probeTerminalRttMs(stdin?: NodeJS.ReadStream, stdout?: NodeJS.WriteStream, timeoutMs?: number): Promise<number | undefined>;
601
93
  export type WorkspaceView = 'detailed' | 'compact';
602
- /** Sliding window of `windowSize` items that keeps `cursor` visible. */
603
- export declare function pickerWindowStart(cursor: number, total: number, windowSize?: number): number;
604
94
  export declare function parseDisconnectPolicy(raw: string): DisconnectPolicyName | undefined;
605
95
  /** Parse `/effort high` / `/subeffort default`. Empty or unknown → undefined. */
606
96
  export declare function parseEffortArg(raw: string): {
@@ -610,201 +100,6 @@ export declare function parseEffortArg(raw: string): {
610
100
  id: string;
611
101
  } | undefined;
612
102
  export declare function parseWorkspaceView(raw: string): WorkspaceView | undefined;
613
- export declare function countDiffLines(hunks: readonly ToolDiffHunk[] | undefined): number;
614
- /** Added / removed line counts for a diff (`oldText: null` means a new file). */
615
- export declare function countDiffAddDel(hunks: readonly ToolDiffHunk[] | undefined): {
616
- add: number;
617
- del: number;
618
- };
619
- /**
620
- * Git diffstat token, deletions first like `-13 +24`. Zero parts drop out
621
- * (a new file shows only `+24`); empty when the diff has no counted lines.
622
- */
623
- export declare function diffStatToken(add: number, del: number): string;
624
- export declare function toolTargetPath(name: string, args: string, fallback?: string): string;
625
- /** Path shown on a compact single-file edit summary. */
626
- export declare function compactEditPath(item: {
627
- name: string;
628
- args: string;
629
- summary?: string;
630
- diff?: readonly {
631
- path?: string;
632
- }[];
633
- }): string;
634
- export declare function countOutputLines(text: string): number;
635
- /**
636
- * Consecutive same-path reads (or edits) collapse onto one card.
637
- * A → B → C → A becomes four cards; A ×5 stays one card with repeats=5.
638
- */
639
- export declare function canMergeToolCall(previous: Extract<Row, {
640
- kind: 'tool';
641
- }> | undefined, next: {
642
- name: string;
643
- args: string;
644
- }): previous is Extract<Row, {
645
- kind: 'tool';
646
- }>;
647
- export declare function compactToolGroups(tools: readonly Extract<Row, {
648
- kind: 'tool';
649
- }>[]): {
650
- edits: Extract<Row, {
651
- kind: 'tool';
652
- }>[];
653
- calls: Extract<Row, {
654
- kind: 'tool';
655
- }>[];
656
- failedCalls: number;
657
- };
658
- /**
659
- * Split compact-view tools into the bursts that belong with each assistant
660
- * reply: tools after reply N sit with that reply, until the next reply.
661
- */
662
- export declare function compactToolBursts(rows: readonly Row[]): Array<{
663
- after: Extract<Row, {
664
- kind: 'assistant';
665
- }> | undefined;
666
- groups: ReturnType<typeof compactToolGroups>;
667
- }>;
668
- /** True while a plan still belongs in the dock (latest incomplete work). */
669
- export declare function planIsLive(plan: {
670
- active: boolean;
671
- pending: boolean;
672
- todos: readonly PlanTodoItem[];
673
- planMarkdown?: string;
674
- archived?: boolean;
675
- }): boolean;
676
- /** Open todos left behind when a turn ends without a completing todo_write. */
677
- export declare function planTurnLeftOpen(plan: {
678
- todos: readonly PlanTodoItem[];
679
- }): boolean;
680
- /** Mark leftover in-progress/pending todos as display-stale after turn/end. */
681
- export declare function applyTurnEndToPlan<T extends {
682
- todos: PlanTodoItem[];
683
- turnLeftOpen?: boolean;
684
- }>(plan: T): T;
685
- /** Follow-up that asks the model to close leftover todos. One per open list. */
686
- export declare function planCloseNudgeText(plan: {
687
- todos: readonly PlanTodoItem[];
688
- }): string;
689
- export type CardCategory = 'thinking' | 'plan' | 'subagent' | 'reply' | 'tool' | 'question' | 'goal' | 'prompt';
690
- /** Category for jump / search. Assistant replies are not collapsible cards. */
691
- export declare function cardCategoryOf(row: {
692
- kind: string;
693
- }): CardCategory | undefined;
694
- /** Split `/find thinking padAnsi` into an optional category and a query. */
695
- export declare function parseFindQuery(raw: string): {
696
- category?: CardCategory;
697
- query: string;
698
- };
699
- /** Classify one injected prompt blob into display sources. */
700
- export declare function promptInjectionSources(text: string, plugin?: string): string[];
701
- export declare function promptInjectionTitle(sources: readonly string[]): string;
702
- export declare function isPromptInjectionMessage(sourceKind: string, text: string, plugin?: string): boolean;
703
- /** Official `/compact` idle-only failures, mapped to a local sentence. */
704
- export declare function formatCompactCommandError(text: string): string;
705
- export declare function compactionHeaderText(row: {
706
- status: 'running' | 'ok' | 'error';
707
- pruneCount: number;
708
- prunedTokens: number;
709
- error?: string;
710
- }): string;
711
- /** Transcript rows matching a `/find` query, newest last. */
712
- export declare function matchTranscriptRows(rows: readonly Row[], raw: string): Row[];
713
- /** One-line note under an expanded plan strip. */
714
- export declare function planDockNote(plan: {
715
- active: boolean;
716
- pending: boolean;
717
- todos: readonly PlanTodoItem[];
718
- planMarkdown?: string;
719
- turnLeftOpen?: boolean;
720
- }): string;
721
- /** Compact per-status counts matching the web plan strip. */
722
- export declare function todoProgressLabel(todos: readonly PlanTodoItem[]): string;
723
- /** First markdown heading of an exit_plan_mode plan body. */
724
- export declare function planTitleFromMarkdown(markdown: string): string | undefined;
725
- /** Parse a todo_write payload into displayable plan items. */
726
- export declare function parsePlanTodos(value: unknown): PlanTodoItem[];
727
- /** Compact todo-list summary: done/total plus the first in-progress task. */
728
- export declare function todoSummary(value: unknown): string;
729
- /** Compact ask_user_question summary from tool arguments. */
730
- export declare function askSummary(value: unknown): string;
731
- /** One-line subagent card header used while collapsed. */
732
- export declare function subagentHeaderText(row: Extract<Row, {
733
- kind: 'subagent';
734
- }>, now?: number): string;
735
- /** One-line friendly tool-call presentation (command / path / arg summary). */
736
- export declare function presentToolCall(name: string, args: string): {
737
- title: string;
738
- summary: string;
739
- command?: string;
740
- cwd?: string;
741
- diff?: ToolDiffHunk[];
742
- };
743
- /** Validate a tool/result meta payload's structured diff, mirroring the web card. */
744
- export declare function diffMetaDiffs(meta: unknown): ToolDiffHunk[] | null;
745
- /** One rendered diff body line with its display role. */
746
- interface DiffDisplayLine {
747
- kind: DisplayKind;
748
- text: string;
749
- }
750
- /** Running / ok / error → ANSI color for the status dot and status word only. */
751
- export declare function toolStateColor(status: 'running' | 'ok' | 'error' | undefined): '33' | '32' | '31';
752
- export declare function toolStateLabel(status: 'running' | 'ok' | 'error' | undefined): string;
753
- /** Header + SGR spans: default title, dim operand, colored ●. `[ok]` is omitted — the green dot is enough. */
754
- export declare function buildToolHeader(input: {
755
- focused: boolean;
756
- expanded: boolean;
757
- title: string;
758
- summary: string;
759
- status?: 'running' | 'ok' | 'error';
760
- command?: string;
761
- signal?: string;
762
- exitCode?: number;
763
- spinner?: string;
764
- flipping?: boolean;
765
- diffStat?: {
766
- add: number;
767
- del: number;
768
- };
769
- }): {
770
- plain: string;
771
- segments: TextSegment[];
772
- };
773
- /** How many terminal rows a tool body occupies after wrapping. */
774
- export declare function wrappedToolBodyLineCount(lines: readonly {
775
- text: string;
776
- }[], width: number): number;
777
- /**
778
- * True when the full tool body plus a one-line header fits in the workspace
779
- * (the rows between the title bar and the input chrome). Oversized bodies
780
- * open a dedicated inspect overlay instead of dumping into the transcript.
781
- */
782
- export declare function toolBodyFitsWorkspace(bodyLines: number, workspaceRows: number): boolean;
783
- /** Flatten hunks into git-style `-`/`+` lines plus the web-compatible footer. */
784
- export declare function renderToolDiff(diffs: ToolDiffHunk[], maxLines: number): DiffDisplayLine[];
785
- /** Convert any parsed JSON value into readable indented display lines. */
786
- export declare function friendlyJsonLines(value: unknown, depth?: number): string[];
787
- /** Minimal tool-row shape the expanded-body renderer reads. */
788
- interface ToolBodySource {
789
- name?: string;
790
- diff?: ToolDiffHunk[];
791
- command?: string;
792
- status?: 'running' | 'ok' | 'error';
793
- output: string;
794
- args: string;
795
- }
796
- /**
797
- * The expanded body of one tool card: diffs and shell output keep their
798
- * dedicated views; every other tool's JSON arguments and JSON result are
799
- * converted into readable indented content instead of raw JSON text.
800
- */
801
- export declare function toolBodyLines(row: ToolBodySource, maxLines: number): DiffDisplayLine[];
802
- /** Recover the shell tools' exit marker, mirroring @deepseek-ai/dsh-shell/render. */
803
- export declare function parseExitStatus(text: string): {
804
- body: string;
805
- exitCode?: number;
806
- signal?: string;
807
- };
808
103
  /** Owns one interactive terminal channel and its agent event wiring. */
809
104
  export declare class SshTui {
810
105
  private readonly ctx;
@@ -891,6 +186,12 @@ export declare class SshTui {
891
186
  private lastStatsTurn;
892
187
  private scrollOffset;
893
188
  private readonly clickableRows;
189
+ private readonly paintedLinkHitsByRow;
190
+ private copyYank;
191
+ /** Last OSC-52 payload (tests; empty when nothing has been copied). */
192
+ get lastCopiedText(): string;
193
+ /** Screen-row → OSC 8 hits from the last paint (tests). */
194
+ get linkHitsByRow(): Map<number, ReturnType<typeof paintedLinkHits>>;
894
195
  private streamingReasoning;
895
196
  private escapeBuffer;
896
197
  private escapeTimer;
@@ -898,9 +199,13 @@ export declare class SshTui {
898
199
  private waitStartedAt;
899
200
  private completionSignaled;
900
201
  private replaying;
202
+ /** Display-line budget for the first paint after resume; 0 = full transcript. */
203
+ private paintTailBudget;
901
204
  private completedAt;
902
205
  private lastTitleUpdateAt;
903
206
  private lastPaintRows;
207
+ private lastPaintCursorColumn;
208
+ private lastPaintCursorRow;
904
209
  private lastChromeKey;
905
210
  private lastPaintWidth;
906
211
  private lastPaintHeight;
@@ -991,6 +296,10 @@ export declare class SshTui {
991
296
  private exitProcess;
992
297
  /** Capture one painted frame. Used by README screenshot fixtures. */
993
298
  captureFrame(columns?: number, rows?: number): string[];
299
+ /** Last CSI cursor column written by {@link paint} (1-based). Tests only. */
300
+ lastPaintedCursorColumn(): number;
301
+ /** Last CSI cursor row written by {@link paint} (1-based). Tests only. */
302
+ lastPaintedCursorRow(): number;
994
303
  private screenColumns;
995
304
  private screenRows;
996
305
  private write;
@@ -1069,6 +378,18 @@ export declare class SshTui {
1069
378
  private playCompletionSignal;
1070
379
  private render;
1071
380
  private styleLine;
381
+ /**
382
+ * Fold one live or durable stream chunk into the in-progress assistant
383
+ * row. 0.1.2 hosts append `assistant/chunk`; 0.1.5 emits the same chunk
384
+ * on `agent/assistant-stream` and never writes it to the log.
385
+ */
386
+ private applyStreamChunk;
387
+ /**
388
+ * 0.1.5 live tokens arrive as process-local `agent/assistant-stream`
389
+ * frames (start / chunk / end). Chunk frames carry the same
390
+ * `StreamChunk` the 0.1.2 log used to store as `assistant/chunk`.
391
+ */
392
+ readonly handleAssistantStream: (...args: unknown[]) => void;
1072
393
  /**
1073
394
  * Apply the TUI's subagent model selection to every child-agent request.
1074
395
  * The parent request is left untouched (its own `/model` waterfall already
@@ -1119,8 +440,16 @@ export declare class SshTui {
1119
440
  * 'allow' | 'deny', or undefined when the reviewer is unavailable or its
1120
441
  * output was unusable (caller falls back to prompt/reject).
1121
442
  */
443
+ private latestUserAuthorizationText;
1122
444
  private reviewUnknownWithModel;
1123
445
  private recordAutoApproval;
446
+ /**
447
+ * Host ApprovalOutcome cannot carry a reason, so the model only sees
448
+ * `the user rejected tool "bash"`. Steer a plugin notice with the real
449
+ * classifier/reviewer reason. Summary matches the workspace decision row
450
+ * so the handler does not paint the body twice.
451
+ */
452
+ private tellModelApprovalDenied;
1124
453
  readonly handleApproval: (request: ApprovalRequest, _next: () => Promise<ApprovalOutcome>) => Promise<ApprovalOutcome>;
1125
454
  readonly handleUserQuestions: (request: AskUserQuestionRequest) => Promise<AskUserQuestionAnswer>;
1126
455
  /** Queue one dialog behind an already-open one instead of overwriting it. */
@@ -1249,8 +578,10 @@ export declare class SshTui {
1249
578
  /** Idempotently source $DSH_HOME/env.sh from the user's POSIX shell rc files. */
1250
579
  private ensurePosixEnvHook;
1251
580
  private handleEscape;
1252
- /** Toggle the collapsible row under a click on the transcript area. */
1253
- handleMouseClick(y: number): void;
581
+ /** Toggle the collapsible row under a click, or copy an OSC-8 link. */
582
+ handleMouseClick(y: number, x?: number): void;
583
+ private copyPlainText;
584
+ copyFocusedCard(): boolean;
1254
585
  private scrollInspectOrTranscript;
1255
586
  private handleCtrlC;
1256
587
  private submit;
@@ -1275,4 +606,3 @@ export declare class SshTui {
1275
606
  * @returns lifecycle controller used by the Cordis effect disposer.
1276
607
  */
1277
608
  export declare function mountTui(ctx: Context, config: TuiConfig): TuiController;
1278
- export {};