dsh-ssh-tui 0.8.1 → 0.8.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/README.en.md +118 -15
  3. package/README.md +78 -13
  4. package/docs/release-notes-0.7.4.md +91 -0
  5. package/docs/release-notes-0.8.0.md +56 -0
  6. package/docs/release-notes-0.8.1.md +79 -0
  7. package/docs/release-notes-0.8.2-rc.1.md +159 -0
  8. package/docs/release-notes-0.8.2.md +62 -0
  9. package/docs/screenshots/reconnect.gif +0 -0
  10. package/docs/terminals.md +23 -0
  11. package/docs/windows.md +13 -0
  12. package/lib/approval-state.js +148 -0
  13. package/lib/approval-state.js.map +1 -0
  14. package/lib/auth-failure.js +38 -0
  15. package/lib/auth-failure.js.map +1 -1
  16. package/lib/color-depth.js +26 -0
  17. package/lib/color-depth.js.map +1 -1
  18. package/lib/copy-text.js +30 -8
  19. package/lib/copy-text.js.map +1 -1
  20. package/lib/dialogs.js +81 -0
  21. package/lib/dialogs.js.map +1 -1
  22. package/lib/display-sock.js +138 -23
  23. package/lib/display-sock.js.map +1 -1
  24. package/lib/footer-accents.js +28 -0
  25. package/lib/footer-accents.js.map +1 -0
  26. package/lib/footer-budget.js +328 -0
  27. package/lib/footer-budget.js.map +1 -0
  28. package/lib/footer-format.js +433 -0
  29. package/lib/footer-format.js.map +1 -0
  30. package/lib/footer-layout.js +407 -0
  31. package/lib/footer-layout.js.map +1 -0
  32. package/lib/footer-meters.js +185 -0
  33. package/lib/footer-meters.js.map +1 -0
  34. package/lib/footer.js +19 -558
  35. package/lib/footer.js.map +1 -1
  36. package/lib/i18n/en.js +65 -5
  37. package/lib/i18n/en.js.map +1 -1
  38. package/lib/i18n/zh.js +67 -5
  39. package/lib/i18n/zh.js.map +1 -1
  40. package/lib/paint.js +53 -8
  41. package/lib/paint.js.map +1 -1
  42. package/lib/plan-projection.js +291 -0
  43. package/lib/plan-projection.js.map +1 -0
  44. package/lib/plan.js +50 -0
  45. package/lib/plan.js.map +1 -1
  46. package/lib/question-state.js +83 -0
  47. package/lib/question-state.js.map +1 -0
  48. package/lib/representation.js +283 -0
  49. package/lib/representation.js.map +1 -0
  50. package/lib/screen.js +105 -0
  51. package/lib/screen.js.map +1 -0
  52. package/lib/selection.js +5 -3
  53. package/lib/selection.js.map +1 -1
  54. package/lib/startup.js +49 -0
  55. package/lib/startup.js.map +1 -1
  56. package/lib/stats.js +101 -3
  57. package/lib/stats.js.map +1 -1
  58. package/lib/term-text.js +154 -55
  59. package/lib/term-text.js.map +1 -1
  60. package/lib/theme.js +20 -3
  61. package/lib/theme.js.map +1 -1
  62. package/lib/throughput.js +305 -0
  63. package/lib/throughput.js.map +1 -0
  64. package/lib/tool-present.js +57 -7
  65. package/lib/tool-present.js.map +1 -1
  66. package/lib/tui.js +3776 -1105
  67. package/lib/tui.js.map +1 -1
  68. package/lib/types/approval-state.d.ts +75 -0
  69. package/lib/types/auth-failure.d.ts +25 -0
  70. package/lib/types/color-depth.d.ts +21 -0
  71. package/lib/types/copy-text.d.ts +18 -1
  72. package/lib/types/dialogs.d.ts +111 -1
  73. package/lib/types/display-sock.d.ts +15 -0
  74. package/lib/types/footer-accents.d.ts +22 -0
  75. package/lib/types/footer-budget.d.ts +204 -0
  76. package/lib/types/footer-format.d.ts +221 -0
  77. package/lib/types/footer-layout.d.ts +292 -0
  78. package/lib/types/footer-meters.d.ts +151 -0
  79. package/lib/types/footer.d.ts +19 -269
  80. package/lib/types/paint.d.ts +48 -0
  81. package/lib/types/plan-projection.d.ts +162 -0
  82. package/lib/types/plan.d.ts +13 -0
  83. package/lib/types/question-state.d.ts +96 -0
  84. package/lib/types/representation.d.ts +498 -0
  85. package/lib/types/screen.d.ts +126 -0
  86. package/lib/types/startup.d.ts +24 -0
  87. package/lib/types/stats.d.ts +64 -1
  88. package/lib/types/term-text.d.ts +45 -1
  89. package/lib/types/throughput.d.ts +163 -0
  90. package/lib/types/tool-present.d.ts +41 -0
  91. package/lib/types/transcript-types.d.ts +63 -2
  92. package/lib/types/tui.d.ts +862 -11
  93. package/package.json +16 -6
@@ -0,0 +1,498 @@
1
+ /**
2
+ * Representation policy: the one place that decides what a transcript line *is*.
3
+ *
4
+ * AD-10 in `docs/decisions/b2-architecture-decisions.md` froze the shape —
5
+ * `Event / Command → Representation Policy → Representation → Renderer` — and this
6
+ * module is the policy. A renderer never decides whether something exists, and a
7
+ * creation site never decides whether its line is history or an echo: it names its
8
+ * *source*, and the table below says what that source means.
9
+ *
10
+ * The boundary is enforced by the type system rather than by review:
11
+ *
12
+ * - `pushRow` takes a `Representation`, not a `Row`, so a line cannot reach the
13
+ * transcript without passing through `represent()` — a bare row is a type error;
14
+ * - `RepresentationSource` is `keyof` the policy table, so naming a source that has
15
+ * no policy is also a type error: adding a creation site *requires* adding its
16
+ * policy, and there is no default to fall back on;
17
+ * - anything that arrives anyway (a JavaScript caller, a test fixture) is pushed
18
+ * with no metadata and counted as **unclassified** by `auditRepresentations`,
19
+ * which the suite asserts is zero. Nothing is silently assumed durable.
20
+ *
21
+ * Three durabilities (B2.3a):
22
+ *
23
+ * - `durable` — rebuilt from the authoritative session log by replaying it, so it
24
+ * is there again after a resume;
25
+ * - `display` — lives only for this Host/display's lifetime; its absence after a
26
+ * resume is correct, not a loss;
27
+ * - `live` — a runtime projection, recomputed every tick. **It must never be a
28
+ * source row at all** (B2.2 moved the streaming text and the wait card out); a
29
+ * `live` row in the transcript is a bug, and the audit reports it as one.
30
+ *
31
+ * The A/B/C/D classes are orthogonal and describe the *form* of the
32
+ * representation, not its importance:
33
+ *
34
+ * - **A** persistent transcript entry — an ordinary row;
35
+ * - **B** ephemeral transient — never a source row (the live tail, a Surface);
36
+ * - **C** summary entry — a row that summarizes an exchange (`question` cards);
37
+ * - **D** artifact reference — a row that points at an object (plans, later).
38
+ *
39
+ * "Important" is not the same as "durable": a quota alert matters and is still
40
+ * `display`, because no replay can bring it back.
41
+ */
42
+ import type { Row } from './transcript-types.js';
43
+ /** Where a line can come from after a resume. */
44
+ export type Durability = 'durable' | 'display' | 'live';
45
+ /**
46
+ * Where a representation is *shown* (B2.3b's routing boundary).
47
+ *
48
+ * Classification says what a line is; routing says where it goes. The policy owns
49
+ * both, so a control-plane confirmation cannot be a transcript row at one call site
50
+ * and a footer echo at the next: the source decides, once.
51
+ *
52
+ * - `transcript` — a real line in the log's view (narrative, a durable summary, a
53
+ * Screen's line-mode echo, or a local record with causal value);
54
+ * - `echo` — a Host-local footer chip: the confirmation of something the reader just
55
+ * did. Replaced by the next echo, cleared by the next submit, never in the log;
56
+ * - `notice` — a one-row Host-local notice above the composer, for feedback that
57
+ * needs to be read now but is not history (a failed command, an operational
58
+ * warning). Same lifetime rules as the echo, more room for the message.
59
+ *
60
+ * The rule behind the split is AD-13: narrative and causal facts go to the
61
+ * transcript, runtime state belongs to the footer's own facts, and immediate action
62
+ * feedback is an echo or a notice.
63
+ */
64
+ export type Destination = 'transcript' | 'echo' | 'notice';
65
+ /** The form of the representation (see the module comment). */
66
+ export type RepresentationClass = 'A' | 'B' | 'C' | 'D';
67
+ /**
68
+ * Every place that may put a line into the transcript.
69
+ *
70
+ * Adding an entry here is what makes a new creation site possible, and the entry
71
+ * states what the line *is*. The ids name the producer, not the wording: one id
72
+ * covers a family of messages whose answer to "is this history?" is the same.
73
+ */
74
+ export declare const REPRESENTATION_POLICY: {
75
+ readonly 'user-message': {
76
+ readonly durability: "durable";
77
+ readonly class: "A";
78
+ readonly destination: "transcript";
79
+ };
80
+ readonly 'assistant-message': {
81
+ readonly durability: "durable";
82
+ readonly class: "A";
83
+ readonly destination: "transcript";
84
+ };
85
+ readonly 'reasoning-message': {
86
+ readonly durability: "durable";
87
+ readonly class: "A";
88
+ readonly destination: "transcript";
89
+ };
90
+ readonly 'tool-call': {
91
+ readonly durability: "durable";
92
+ readonly class: "A";
93
+ readonly destination: "transcript";
94
+ };
95
+ readonly 'turn-error': {
96
+ readonly durability: "durable";
97
+ readonly class: "A";
98
+ readonly destination: "transcript";
99
+ };
100
+ /** Notices the log itself carries: reminders, context snapshots, interrupted streams. */
101
+ readonly 'session-notice': {
102
+ readonly durability: "durable";
103
+ readonly class: "A";
104
+ readonly destination: "transcript";
105
+ };
106
+ readonly compaction: {
107
+ readonly durability: "durable";
108
+ readonly class: "A";
109
+ readonly destination: "transcript";
110
+ };
111
+ readonly goal: {
112
+ readonly durability: "durable";
113
+ readonly class: "A";
114
+ readonly destination: "transcript";
115
+ };
116
+ readonly 'prompt-injection': {
117
+ readonly durability: "durable";
118
+ readonly class: "A";
119
+ readonly destination: "transcript";
120
+ };
121
+ readonly 'plan-row': {
122
+ readonly durability: "durable";
123
+ readonly class: "D";
124
+ readonly destination: "transcript";
125
+ };
126
+ readonly 'subagent-card': {
127
+ readonly durability: "durable";
128
+ readonly class: "A";
129
+ readonly destination: "transcript";
130
+ };
131
+ readonly 'command-event': {
132
+ readonly durability: "durable";
133
+ readonly class: "A";
134
+ readonly destination: "transcript";
135
+ };
136
+ readonly 'retry-notice': {
137
+ readonly durability: "durable";
138
+ readonly class: "A";
139
+ readonly destination: "transcript";
140
+ };
141
+ readonly 'session-route': {
142
+ readonly durability: "durable";
143
+ readonly class: "A";
144
+ readonly destination: "transcript";
145
+ };
146
+ /** The question card: one row summarizing an exchange, rebuilt from the projection. */
147
+ readonly 'question-card': {
148
+ readonly durability: "durable";
149
+ readonly class: "C";
150
+ readonly destination: "transcript";
151
+ };
152
+ readonly boot: {
153
+ readonly durability: "display";
154
+ readonly class: "A";
155
+ readonly destination: "transcript";
156
+ };
157
+ /** A confirmation of something the reader just did: a footer echo. */
158
+ readonly 'command-feedback': {
159
+ readonly durability: "display";
160
+ readonly class: "A";
161
+ readonly destination: "echo";
162
+ };
163
+ /**
164
+ * A command that *reports* state rather than changing it (`/retryauth` with no
165
+ * argument, `/notify` showing its target).
166
+ *
167
+ * A report is information, not an acknowledgement: the reader may need to read it
168
+ * twice, or compare it with something they scrolled past, so it stays in the
169
+ * transcript. The policy draws that line, not the call site.
170
+ */
171
+ readonly 'command-status': {
172
+ readonly durability: "display";
173
+ readonly class: "A";
174
+ readonly destination: "transcript";
175
+ };
176
+ /** The outcome of a repair that edited the install (`/mode fix`). */
177
+ readonly 'repair-result': {
178
+ readonly durability: "display";
179
+ readonly class: "A";
180
+ readonly destination: "transcript";
181
+ };
182
+ /**
183
+ * A local command that failed.
184
+ *
185
+ * Kept in the transcript: the message is usually an error chain the reader has to
186
+ * be able to scroll back to, and re-running the command is not the same as reading
187
+ * what it said. (A command that was simply *misused* is the notice below.)
188
+ */
189
+ readonly 'command-error': {
190
+ readonly durability: "display";
191
+ readonly class: "A";
192
+ readonly destination: "transcript";
193
+ };
194
+ /**
195
+ * A command that could not be understood at all: a mistyped name, a missing
196
+ * argument, a value outside the table. Immediate feedback, and nothing to keep —
197
+ * the reader fixes the line and moves on.
198
+ */
199
+ readonly 'command-misuse': {
200
+ readonly durability: "display";
201
+ readonly class: "A";
202
+ readonly destination: "notice";
203
+ };
204
+ /** How to use a command: the reader asked for it, and it is not history. */
205
+ readonly 'command-usage': {
206
+ readonly durability: "display";
207
+ readonly class: "A";
208
+ readonly destination: "notice";
209
+ };
210
+ readonly 'auth-notice': {
211
+ readonly durability: "display";
212
+ readonly class: "A";
213
+ readonly destination: "transcript";
214
+ };
215
+ readonly onboarding: {
216
+ readonly durability: "display";
217
+ readonly class: "A";
218
+ readonly destination: "transcript";
219
+ };
220
+ /**
221
+ * An auto approval the TUI *refused*. The transient notice, not a row (B2.4).
222
+ *
223
+ * The durable trace of a refusal is the tool card it guarded — its `approval`
224
+ * field, rebuilt from the Harness's own `approval/asked` + `approval/decided`
225
+ * pair — so a second, permanent sentence saying "rejected" would be a second
226
+ * representation of the same event (and one nothing can rebuild). What the notice
227
+ * adds is *now*: the reader sees why the model changed course while it happens.
228
+ */
229
+ readonly 'approval-notice': {
230
+ readonly durability: "display";
231
+ readonly class: "A";
232
+ readonly destination: "notice";
233
+ };
234
+ /**
235
+ * An auto approval the TUI *granted*.
236
+ *
237
+ * Routed to the footer chip rather than the notice row: an allow is an
238
+ * acknowledgement, and it happens far more often than a refusal — the identity
239
+ * row is for the one the reader must not miss.
240
+ */
241
+ readonly 'approval-allowance': {
242
+ readonly durability: "display";
243
+ readonly class: "A";
244
+ readonly destination: "echo";
245
+ };
246
+ readonly 'doctor-result': {
247
+ readonly durability: "display";
248
+ readonly class: "A";
249
+ readonly destination: "transcript";
250
+ };
251
+ /** Preset wizard/list feedback and its failures. */
252
+ readonly 'preset-feedback': {
253
+ readonly durability: "display";
254
+ readonly class: "A";
255
+ readonly destination: "echo";
256
+ };
257
+ readonly 'preset-error': {
258
+ readonly durability: "display";
259
+ readonly class: "A";
260
+ readonly destination: "transcript";
261
+ };
262
+ /** The self-update path: progress is an echo, a failure is a notice. */
263
+ readonly 'update-feedback': {
264
+ readonly durability: "display";
265
+ readonly class: "A";
266
+ readonly destination: "echo";
267
+ };
268
+ readonly 'update-error': {
269
+ readonly durability: "display";
270
+ readonly class: "A";
271
+ readonly destination: "notice";
272
+ };
273
+ /** A quota or balance reading the reader asked for. */
274
+ readonly 'quota-report': {
275
+ readonly durability: "display";
276
+ readonly class: "A";
277
+ readonly destination: "echo";
278
+ };
279
+ /** The context window filling up: operational, and worth reading now. */
280
+ readonly 'runtime-warning': {
281
+ readonly durability: "display";
282
+ readonly class: "A";
283
+ readonly destination: "transcript";
284
+ };
285
+ /** An approval the TUI could not resolve on its own. */
286
+ readonly 'approval-warning': {
287
+ readonly durability: "display";
288
+ readonly class: "A";
289
+ readonly destination: "transcript";
290
+ };
291
+ /** A question the Session still holds but cannot answer any more. */
292
+ readonly 'question-notice': {
293
+ readonly durability: "display";
294
+ readonly class: "A";
295
+ readonly destination: "transcript";
296
+ };
297
+ /**
298
+ * A child agent starting or ending.
299
+ *
300
+ * Left in the transcript on purpose: it is narrative about work that happened, and
301
+ * the durable child *card* is what a reader scrolls back to.
302
+ */
303
+ readonly 'subagent-notice': {
304
+ readonly durability: "display";
305
+ readonly class: "A";
306
+ readonly destination: "transcript";
307
+ };
308
+ /**
309
+ * What happened while the display was away.
310
+ *
311
+ * AD-13's causal exception: a gap in the work is exactly what a later reader needs
312
+ * explained, so this one stays in the transcript rather than becoming an echo.
313
+ */
314
+ readonly 'away-summary': {
315
+ readonly durability: "display";
316
+ readonly class: "A";
317
+ readonly destination: "transcript";
318
+ };
319
+ readonly 'attach-notice': {
320
+ readonly durability: "display";
321
+ readonly class: "A";
322
+ readonly destination: "transcript";
323
+ };
324
+ readonly 'find-feedback': {
325
+ readonly durability: "display";
326
+ readonly class: "A";
327
+ readonly destination: "echo";
328
+ };
329
+ readonly 'copy-feedback': {
330
+ readonly durability: "display";
331
+ readonly class: "A";
332
+ readonly destination: "echo";
333
+ };
334
+ /**
335
+ * The caveat that a terminal cannot take an OSC 52 write, so the copy may not have
336
+ * landed.
337
+ *
338
+ * An operational warning (AD-13), not an acknowledgement: it goes to the notice row
339
+ * where it can be read, and stays out of the footer chip that says "copied" — the
340
+ * two would otherwise overwrite each other.
341
+ */
342
+ readonly 'copy-caveat': {
343
+ readonly durability: "display";
344
+ readonly class: "A";
345
+ readonly destination: "notice";
346
+ };
347
+ readonly 'plan-notice': {
348
+ readonly durability: "display";
349
+ readonly class: "A";
350
+ readonly destination: "echo";
351
+ };
352
+ /**
353
+ * A plan artifact's lifecycle in the transcript: created, a review's answer, the
354
+ * mode left. The *reference* the reader scrolls back to (`plan-row` points at the
355
+ * same artifact), and the only representation of a transition — the plan body lives
356
+ * with the artifact, not in a second copy.
357
+ */
358
+ readonly 'plan-lifecycle': {
359
+ readonly durability: "durable";
360
+ readonly class: "D";
361
+ readonly destination: "transcript";
362
+ };
363
+ /**
364
+ * A message the reader submitted while a turn was running.
365
+ *
366
+ * The record of it is the `user/message` the Harness appends when the next step
367
+ * claims it — that row is the durable representation. This one is the
368
+ * acknowledgement ("accepted, applies at the next step"), which has to be seen
369
+ * the moment it happens and is worth no geometry.
370
+ */
371
+ readonly 'steer-notice': {
372
+ readonly durability: "display";
373
+ readonly class: "A";
374
+ readonly destination: "notice";
375
+ };
376
+ readonly 'error-surface': {
377
+ readonly durability: "display";
378
+ readonly class: "A";
379
+ readonly destination: "notice";
380
+ };
381
+ /** Line mode's echo of a Screen / Surface — there is no frame there to show it. */
382
+ readonly 'report-echo': {
383
+ readonly durability: "display";
384
+ readonly class: "A";
385
+ readonly destination: "transcript";
386
+ };
387
+ readonly 'surface-echo': {
388
+ readonly durability: "display";
389
+ readonly class: "A";
390
+ readonly destination: "transcript";
391
+ };
392
+ /**
393
+ * The workspace-changes card.
394
+ *
395
+ * The canonical `display` case (B2.3a): it *looks* like history — a card about a
396
+ * turn that happened — but the summary is not in the session log and a restarted
397
+ * Host cannot reopen it, so it must not pretend to survive a resume.
398
+ */
399
+ readonly 'changes-card': {
400
+ readonly durability: "display";
401
+ readonly class: "A";
402
+ readonly destination: "transcript";
403
+ };
404
+ /** Rows a test fixture puts in, so the suite does not have to fabricate a policy. */
405
+ readonly fixture: {
406
+ readonly durability: "display";
407
+ readonly class: "A";
408
+ readonly destination: "transcript";
409
+ };
410
+ };
411
+ /** A source id that has a policy. Its type *is* the table's keys. */
412
+ export type RepresentationSource = keyof typeof REPRESENTATION_POLICY;
413
+ /** What a row carries once it has been through the policy. */
414
+ export interface RepresentationMeta {
415
+ source: RepresentationSource;
416
+ durability: Durability;
417
+ representationClass: RepresentationClass;
418
+ /** Where this representation is shown — a transcript row, an echo or a notice. */
419
+ destination: Destination;
420
+ }
421
+ /** A row plus the policy that produced it. The only thing `pushRow` accepts. */
422
+ export interface Representation {
423
+ readonly meta: RepresentationMeta;
424
+ readonly row: Row;
425
+ }
426
+ /**
427
+ * Classify one row.
428
+ *
429
+ * The only constructor of a `Representation`: it looks the source up in the table,
430
+ * so the durability and class can never disagree with the policy, and a source with
431
+ * no entry cannot be named (the parameter's type is the table's keys).
432
+ */
433
+ export declare function represent(source: RepresentationSource, row: Row): Representation;
434
+ /** Whether a value really is a classified representation (a runtime guard for JS callers). */
435
+ export declare function isRepresentation(value: unknown): value is Representation;
436
+ export interface RepresentationAudit {
437
+ /** Rows carrying a policy, by durability. */
438
+ counts: Record<Durability, number>;
439
+ /** Rows with no `representation` at all: must be 0. */
440
+ unclassified: number;
441
+ /**
442
+ * Rows claiming `live`: must be 0 — a live projection is not a source row.
443
+ *
444
+ * Counted from the row's own metadata, so a row whose source is not even in the
445
+ * table (and is therefore also `unclassified`) still reports the live violation.
446
+ */
447
+ liveRows: number;
448
+ /** Per source: durability, class, destination and how many rows carry it. */
449
+ sources: {
450
+ source: string;
451
+ durability: Durability;
452
+ representationClass: RepresentationClass;
453
+ destination: Destination;
454
+ rows: number;
455
+ }[];
456
+ /** Rows by destination. `unknown` must be 0: every source has a route. */
457
+ destinations: Record<Destination, number>;
458
+ /** Sources with no destination — impossible by construction, and asserted anyway. */
459
+ unknownDestination: number;
460
+ /**
461
+ * Sources the policy knows, with no rows in this transcript. Named so a caller can
462
+ * see the *whole* policy rather than only the part that happened to be exercised.
463
+ */
464
+ unusedSources: string[];
465
+ }
466
+ /**
467
+ * Read a transcript's representations back.
468
+ *
469
+ * This is the audit artifact: it answers, for a real transcript, how many lines are
470
+ * history, how many are this Host's own voice, and how many nobody classified. The
471
+ * `live` and `unclassified` numbers are the two that must be zero, and the suite
472
+ * asserts exactly that rather than trusting the policy table alone.
473
+ */
474
+ export declare function auditRepresentations(rows: readonly unknown[]): RepresentationAudit;
475
+ /** The audit as text: what `scripts/representation-audit.mjs` prints. */
476
+ /**
477
+ * One semantic question, one primary representation — counted from real rows
478
+ * (B2.4).
479
+ *
480
+ * The success criterion of the ask/approval round is a *count*, so it is audited
481
+ * like one: an ordinary `ask_user_question` call is represented by its question
482
+ * card, and a generic tool row for the same call would be a second primary for one
483
+ * semantic event. The tool's name is not read off the row's title but off `name`,
484
+ * because that is the field the policy suppresses.
485
+ * @param rows - the transcript rows to audit.
486
+ * @returns the counts, with the call ids that broke the invariant.
487
+ */
488
+ export declare function auditQuestionPrimaries(rows: readonly {
489
+ kind?: string;
490
+ callId?: string;
491
+ name?: string;
492
+ }[]): {
493
+ questionRows: number;
494
+ genericToolRows: number;
495
+ calls: number;
496
+ duplicateCalls: string[];
497
+ };
498
+ export declare function formatRepresentationAudit(audit: RepresentationAudit): string;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * The Screen contract: a whole-view surface with its own navigation state.
3
+ *
4
+ * A **Surface** (ask-user, a picker) borrows a few rows of the workspace and gives
5
+ * them back when the action is done: the transcript behind it never moves, and
6
+ * the reader returns to the same place. A **Screen** is the other thing entirely —
7
+ * it replaces the workspace, owns its own scrolling, and must be able to redraw
8
+ * itself from its state alone (a reattach after an SSH drop is a normal event, not
9
+ * an edge case). See `docs/decisions/b2-architecture-decisions.md` (AD-1 … AD-3).
10
+ *
11
+ * Depth is exactly one: a Screen may hold tabs, sections and detail modes of its
12
+ * own, but it never pushes another Screen. Nothing here allocates a stack, and
13
+ * nothing here renders — this module is the contract, `tui.ts` is the renderer.
14
+ */
15
+ import type { Dialog, InspectDialog } from './dialogs.js';
16
+ import type { DiffDisplayLine } from './tool-present.js';
17
+ /**
18
+ * Which Screen is up. Adding one here is the only way to add one at all.
19
+ *
20
+ * `setup` is the odd one: it draws no body of its own — the wizard's rows are built
21
+ * from its state machine every frame — but it is a Screen in every other sense. It
22
+ * owns the whole view, it owns its input, it never enters the dialog queue, and it is
23
+ * rebuilt from state after a reattach (B2.6).
24
+ */
25
+ export type ScreenKind = 'inspect' | 'report' | 'setup';
26
+ /** The report a `report` Screen is showing. */
27
+ export type ReportKind = 'status' | 'usage' | 'diag' | 'doctor' | 'help' | 'subagents';
28
+ /**
29
+ * A Screen's whole state.
30
+ *
31
+ * Everything the renderer needs is here on purpose: nothing about the picture
32
+ * lives only in the paint loop, which is what makes a reattach able to rebuild the
33
+ * same screen, and what makes the screen testable without a terminal. `offset` is
34
+ * the first visible body row — the Screen's own scroll position, unrelated to the
35
+ * transcript's `scrollOffset` behind it.
36
+ */
37
+ export interface ScreenState {
38
+ kind: ScreenKind;
39
+ /** Title row. */
40
+ title: string;
41
+ /** Body, already rendered for the width it will be shown at. */
42
+ lines: readonly DiffDisplayLine[];
43
+ /** First visible body row. */
44
+ offset: number;
45
+ /** Which report this is, when `kind === 'report'`. */
46
+ report?: ReportKind;
47
+ /** What the copy key takes while this screen is up. */
48
+ copyText?: string;
49
+ /**
50
+ * A confirmation line shown in place of the navigation hint.
51
+ *
52
+ * Used by an in-screen copy ("copied 412 chars") and by a Screen's own action
53
+ * (the doctor repair result): the row the action would otherwise push is behind
54
+ * this screen, and silence after an action reads as a failure.
55
+ */
56
+ notice?: string;
57
+ /** Child session whose live log refreshes this screen in place. */
58
+ subagentSessionId?: string;
59
+ /** `/find` already scrolled to its hit here; later repaints keep the offset. */
60
+ searchRevealed?: boolean;
61
+ }
62
+ /**
63
+ * The one judgement: is this dialog a Screen?
64
+ *
65
+ * A Screen is *not* a dialog — it never enters `dialogQueue`, is never replaced by
66
+ * a Surface and never blocks one. This function exists so that no future call site
67
+ * has to remember that: a screen-shaped dialog handed to the dialog path is
68
+ * converted here and handed to the Screen path instead, and a test pins it.
69
+ *
70
+ * Returns `undefined` for the surfaces that genuinely are dialogs (questions,
71
+ * confirms, the onboarding wizard).
72
+ */
73
+ export declare function screenFromDialog(dialog: Dialog | InspectDialog): ScreenState | undefined;
74
+ /**
75
+ * The row kind a report falls back to where there is no frame to draw on.
76
+ *
77
+ * Line mode has no Screen (and no way to dismiss one), so `/diag` and `/doctor`
78
+ * keep their own copyable `diag` rows there — which is also where they are read
79
+ * from in a transcript (`/copy error` looks for `error` or `diag`).
80
+ */
81
+ export declare function reportRowKind(report: ReportKind): 'diag' | 'system';
82
+ /** The rows a Screen lays out, top to bottom. */
83
+ export interface ScreenLayout {
84
+ /** 1 when the title row is drawn. */
85
+ titleRows: number;
86
+ /** 1 when the rule under the title is drawn. */
87
+ dividerRows: number;
88
+ /** Rows available to the body; may be 0 on a terminal that short. */
89
+ bodyRows: number;
90
+ /** 1 when the navigation hint row is drawn. */
91
+ hintRows: number;
92
+ /** 1 when the compact runtime strip is drawn. */
93
+ stripRows: number;
94
+ /** 0-based index of the first body row in the painted frame. */
95
+ contentTop: number;
96
+ /** 0-based index of the hint row, or -1 when it is not drawn. */
97
+ hintTop: number;
98
+ /** 0-based index of the strip row, or -1 when it is not drawn. */
99
+ stripTop: number;
100
+ }
101
+ /**
102
+ * Lay a Screen out for the height the terminal has.
103
+ *
104
+ * The strip is the one row that never goes: a Screen is a place the reader can be
105
+ * *while the agent keeps working*, and "is it still running, is anything waiting,
106
+ * is the link alive" has to be answerable without leaving. After that the order
107
+ * gives up the least load-bearing row first — the rule under the title, then the
108
+ * title, then body rows — and the navigation hint goes only when there is nothing
109
+ * left to navigate. Measured at 8 / 12 / 20 rows in the Screen contract tests.
110
+ */
111
+ export declare function screenLayout(height: number): ScreenLayout;
112
+ /**
113
+ * Clamp a Screen's scroll offset to its body.
114
+ *
115
+ * The body can be shorter than the last offset the reader scrolled to (a resize, a
116
+ * shorter report, a live subagent log that was replaced), and an offset past the
117
+ * end paints an empty screen with a position readout that lies.
118
+ */
119
+ export declare function clampScreenOffset(offset: number, lineCount: number, bodyRows: number): number;
120
+ /**
121
+ * `1–18/64`, the position readout a Screen's footer shows.
122
+ *
123
+ * Zero lines reads `0/0` rather than `1–0/0`: the second looks like a bug in the
124
+ * readout instead of an empty report.
125
+ */
126
+ export declare function screenPositionText(offset: number, lineCount: number, bodyRows: number): string;
@@ -38,6 +38,30 @@ declare module '@deepseek-ai/cordis' {
38
38
  }
39
39
  export declare const name = "ssh-tui-startup";
40
40
  export declare const inject: string[];
41
+ /**
42
+ * Whether another application already owns this profile's command line.
43
+ *
44
+ * `@deepseek-ai/dsh-cmdline` has **no arbitration between apps**: every app plugin is
45
+ * handed the same argv and parses it with its own grammar, and an unknown flag is
46
+ * `program.error`, which exits the process. A profile that hosts two apps therefore
47
+ * requires the second one to understand the first one's flags — and the desktop
48
+ * application passes `--no-open` (the web app's own flag) to its host, so this row's
49
+ * grammar rejected it: the host exited 1, the application reported
50
+ * `dsh desktop host exited with 1: error: unknown option '--no-open'` and died. Only
51
+ * the *startup* row did that; the TUI row never ran, which is why 0.8.1's inert-when-
52
+ * headless fix could not have caught it.
53
+ *
54
+ * The signals are two, and they overlap on purpose:
55
+ *
56
+ * - the launcher facts (`desktopLauncher`): an Electron host, `DeepSeek Harness.exe`,
57
+ * or `app.asar` in argv — true whatever the profile's row order is;
58
+ * - `webStartup`: the web app provides it as soon as it has parsed, and a `web`
59
+ * profile is not a terminal profile in any case.
60
+ *
61
+ * Stepping aside is not a degraded mode: the TUI row `inject`s `sshTuiStartup`, so
62
+ * nothing of this plugin activates and the host keeps its own grammar and its own app.
63
+ */
64
+ export declare function anotherAppOwnsCommandLine(ctx: Context): boolean;
41
65
  /**
42
66
  * Build the TUI command grammar and provide the session identity for the
43
67
  * agent-loop row plus the {@link SshTuiStartup} service. On `--help` or a