@itookit/dsht 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +30 -15
  3. package/README.zh.md +30 -15
  4. package/dist/catalog/controller.d.ts +32 -0
  5. package/dist/catalog/controller.js +88 -0
  6. package/dist/catalog/index.d.ts +2 -0
  7. package/dist/catalog/index.js +2 -0
  8. package/dist/{cli.js → cli/index.js} +40 -24
  9. package/dist/controller/connection.d.ts +80 -0
  10. package/dist/controller/connection.js +190 -0
  11. package/dist/controller/controller.d.ts +260 -0
  12. package/dist/controller/controller.js +362 -0
  13. package/dist/controller/index.d.ts +5 -0
  14. package/dist/controller/index.js +3 -0
  15. package/dist/controller/memory-log.d.ts +35 -0
  16. package/dist/controller/memory-log.js +95 -0
  17. package/dist/cost/controller.d.ts +34 -0
  18. package/dist/cost/controller.js +107 -0
  19. package/dist/cost/index.d.ts +9 -0
  20. package/dist/cost/index.js +7 -0
  21. package/dist/cost/ledger-files.d.ts +20 -0
  22. package/dist/cost/ledger-files.js +128 -0
  23. package/dist/cost/ledger.d.ts +65 -0
  24. package/dist/cost/ledger.js +136 -0
  25. package/dist/cost/pricing.d.ts +48 -0
  26. package/dist/cost/pricing.js +141 -0
  27. package/dist/cost/records.d.ts +17 -0
  28. package/dist/cost/records.js +66 -0
  29. package/dist/cost/scanner.d.ts +22 -0
  30. package/dist/cost/scanner.js +95 -0
  31. package/dist/cost/types.d.ts +69 -0
  32. package/dist/cost/types.js +3 -0
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.js +2 -0
  35. package/dist/session/connection-view.d.ts +18 -0
  36. package/dist/session/connection-view.js +1 -0
  37. package/dist/{controller.d.ts → session/controller.d.ts} +102 -136
  38. package/dist/session/controller.js +616 -0
  39. package/dist/session/export-html.d.ts +9 -0
  40. package/dist/session/export-html.js +39 -0
  41. package/dist/{export.d.ts → session/export.d.ts} +1 -1
  42. package/dist/{export.js → session/export.js} +6 -20
  43. package/dist/{history.d.ts → session/history.d.ts} +17 -0
  44. package/dist/{history.js → session/history.js} +163 -2
  45. package/dist/session/index.d.ts +17 -0
  46. package/dist/session/index.js +10 -0
  47. package/dist/session/markdown.d.ts +39 -0
  48. package/dist/session/markdown.js +255 -0
  49. package/dist/session/math.d.ts +11 -0
  50. package/dist/session/math.js +82 -0
  51. package/dist/{navigation.d.ts → session/navigation.d.ts} +1 -1
  52. package/dist/{navigation.js → session/navigation.js} +1 -1
  53. package/dist/{references.js → session/references.js} +1 -1
  54. package/dist/{telemetry.d.ts → session/telemetry.d.ts} +1 -1
  55. package/dist/{telemetry.js → session/telemetry.js} +1 -1
  56. package/dist/{transcript.d.ts → session/transcript.d.ts} +23 -1
  57. package/dist/{transcript.js → session/transcript.js} +36 -18
  58. package/dist/session/types.d.ts +18 -0
  59. package/dist/session/types.js +2 -0
  60. package/dist/state.d.ts +41 -0
  61. package/dist/state.js +9 -0
  62. package/dist/storage/directories.d.ts +14 -0
  63. package/dist/storage/directories.js +24 -0
  64. package/dist/storage/files.d.ts +43 -0
  65. package/dist/storage/files.js +135 -0
  66. package/dist/storage/index.d.ts +3 -0
  67. package/dist/storage/index.js +3 -0
  68. package/dist/transport/auth.js +65 -0
  69. package/dist/transport/host.d.ts +13 -0
  70. package/dist/transport/host.js +1 -0
  71. package/dist/ui/app.d.ts +11 -0
  72. package/dist/ui/app.js +726 -0
  73. package/dist/ui/chat/header.d.ts +11 -0
  74. package/dist/ui/chat/header.js +14 -0
  75. package/dist/{history-view.d.ts → ui/chat/history-view.d.ts} +1 -1
  76. package/dist/{history-view.js → ui/chat/history-view.js} +3 -3
  77. package/dist/ui/chat/status.d.ts +79 -0
  78. package/dist/ui/chat/status.js +311 -0
  79. package/dist/ui/chat/viewport.d.ts +17 -0
  80. package/dist/ui/chat/viewport.js +14 -0
  81. package/dist/ui/commands/parse.d.ts +96 -0
  82. package/dist/ui/commands/parse.js +121 -0
  83. package/dist/ui/commands/registry.d.ts +33 -0
  84. package/dist/ui/commands/registry.js +72 -0
  85. package/dist/ui/copy-mode.d.ts +4 -0
  86. package/dist/ui/copy-mode.js +6 -0
  87. package/dist/{cost-view.d.ts → ui/dialogs/cost.d.ts} +1 -1
  88. package/dist/{cost-view.js → ui/dialogs/cost.js} +3 -3
  89. package/dist/ui/dialogs/index.d.ts +120 -0
  90. package/dist/ui/dialogs/index.js +113 -0
  91. package/dist/ui/dialogs/picker.d.ts +18 -0
  92. package/dist/ui/dialogs/picker.js +38 -0
  93. package/dist/ui/frozen.d.ts +8 -0
  94. package/dist/ui/frozen.js +7 -0
  95. package/dist/ui/input/references.d.ts +12 -0
  96. package/dist/ui/input/references.js +15 -0
  97. package/dist/ui/mount.d.ts +6 -0
  98. package/dist/ui/mount.js +11 -0
  99. package/dist/{theme.d.ts → ui/theme/index.d.ts} +1 -1
  100. package/package.json +19 -13
  101. package/dist/app.d.ts +0 -21
  102. package/dist/app.js +0 -805
  103. package/dist/auth.js +0 -108
  104. package/dist/controller.js +0 -961
  105. package/dist/cost.d.ts +0 -119
  106. package/dist/cost.js +0 -313
  107. package/dist/status.d.ts +0 -28
  108. package/dist/status.js +0 -157
  109. /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
  110. /package/dist/{memory.d.ts → session/memory.d.ts} +0 -0
  111. /package/dist/{memory.js → session/memory.js} +0 -0
  112. /package/dist/{references.d.ts → session/references.d.ts} +0 -0
  113. /package/dist/{auth.d.ts → transport/auth.d.ts} +0 -0
  114. /package/dist/{client.d.ts → transport/client.d.ts} +0 -0
  115. /package/dist/{client.js → transport/client.js} +0 -0
  116. /package/dist/{endpoint.d.ts → transport/endpoint.d.ts} +0 -0
  117. /package/dist/{endpoint.js → transport/endpoint.js} +0 -0
  118. /package/dist/{wire.d.ts → transport/wire.d.ts} +0 -0
  119. /package/dist/{wire.js → transport/wire.js} +0 -0
  120. /package/dist/{input-history.d.ts → ui/input/history.d.ts} +0 -0
  121. /package/dist/{input-history.js → ui/input/history.js} +0 -0
  122. /package/dist/{input.d.ts → ui/input/input.d.ts} +0 -0
  123. /package/dist/{input.js → ui/input/input.js} +0 -0
  124. /package/dist/{mouse.d.ts → ui/input/mouse.d.ts} +0 -0
  125. /package/dist/{mouse.js → ui/input/mouse.js} +0 -0
  126. /package/dist/{theme.js → ui/theme/index.js} +0 -0
@@ -0,0 +1,82 @@
1
+ /** MathJax compiles TeX locally; terminal output keeps explicit grouping when Unicode cannot express it. */
2
+ import { mathjax } from '@mathjax/src/js/mathjax.js';
3
+ import { TeX } from '@mathjax/src/js/input/tex.js';
4
+ import { liteAdaptor } from '@mathjax/src/js/adaptors/liteAdaptor.js';
5
+ import { RegisterHTMLHandler } from '@mathjax/src/js/handlers/html.js';
6
+ import { STATE } from '@mathjax/src/js/core/MathItem.js';
7
+ import { TextNode } from '@mathjax/src/js/core/MmlTree/MmlNode.js';
8
+ import { SerializedMmlVisitor } from '@mathjax/src/js/core/MmlTree/SerializedMmlVisitor.js';
9
+ import '@mathjax/src/js/input/tex/base/BaseConfiguration.js';
10
+ import '@mathjax/src/js/input/tex/ams/AmsConfiguration.js';
11
+ import { safeText } from "../transport/wire.js";
12
+ RegisterHTMLHandler(liteAdaptor());
13
+ const superscripts = Object.fromEntries([...('0123456789+-=()ni')].map((c, i) => [c, [...'⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾ⁿⁱ'][i]]));
14
+ const subscripts = Object.fromEntries([...'0123456789+-=()aehijklmnoprstuvx'].map((c, i) => [c, [...'₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓ'][i]]));
15
+ function script(text, sub) {
16
+ const alphabet = sub ? subscripts : superscripts;
17
+ return [...text].every(c => alphabet[c]) ? [...text].map(c => alphabet[c]).join('') : `${sub ? '_' : '^'}(${text})`;
18
+ }
19
+ function readable(node) {
20
+ if (node instanceof TextNode)
21
+ return node.getText();
22
+ const children = node.childNodes.map(readable);
23
+ const [a = '', b = '', c = ''] = children;
24
+ let base = node.childNodes[0];
25
+ while (base && ['TeXAtom', 'mrow', 'inferredMrow', 'mstyle'].includes(base.kind) && base.childNodes.length === 1)
26
+ base = base.childNodes[0];
27
+ const grouped = base && !base.isToken && base.childNodes.length > 1 ? `(${a})` : a;
28
+ switch (node.kind) {
29
+ case 'mfrac': return `(${a})/(${b})`;
30
+ case 'msqrt': return `√(${children.join('')})`;
31
+ case 'mroot': return `root(${b}, ${a})`;
32
+ case 'msup': return `${grouped}${script(b, false)}`;
33
+ case 'msub': return `${grouped}${script(b, true)}`;
34
+ case 'msubsup': return `${grouped}${script(b, true)}${script(c, false)}`;
35
+ case 'munder': return `${a}_(${b})`;
36
+ case 'mover': return `${a}^(${b})`;
37
+ case 'munderover': return `${a}_(${b})^(${c})`;
38
+ case 'mtable': return `[${children.join('; ')}]`;
39
+ case 'mtr':
40
+ case 'mlabeledtr': return children.join(', ');
41
+ case 'mspace': return ' ';
42
+ case 'mphantom': return '';
43
+ case 'math':
44
+ case 'mrow':
45
+ case 'inferredMrow':
46
+ case 'TeXAtom':
47
+ case 'mstyle':
48
+ case 'mi':
49
+ case 'mn':
50
+ case 'mo':
51
+ case 'mtext':
52
+ case 'mtd': return children.join('');
53
+ default: throw new Error(`Unsupported terminal math node: ${node.kind}`);
54
+ }
55
+ }
56
+ /** Convert one TeX expression, without extension autoloads, network access or shared macro state.
57
+ * @param source - TeX without delimiters.
58
+ * @param display - Block rather than inline mathematics.
59
+ * @returns Unicode approximation and MathJax-generated MathML, or undefined for invalid TeX.
60
+ */
61
+ export function renderMath(source, display) {
62
+ if (source.length > 16 * 1024)
63
+ return undefined;
64
+ try {
65
+ const tex = new TeX({ packages: ['base', 'ams'], maxBuffer: 16 * 1024,
66
+ formatError(_jax, error) { throw error; } });
67
+ const document = mathjax.document('', { InputJax: tex });
68
+ const node = document.convert(source, { display, end: STATE.CONVERT });
69
+ const html = new SerializedMmlVisitor().visitTree(node);
70
+ let text;
71
+ try {
72
+ text = readable(node);
73
+ }
74
+ catch {
75
+ text = source; /* Valid MathML with no faithful terminal notation retains its TeX. */
76
+ }
77
+ return { text: safeText(text), html };
78
+ }
79
+ catch {
80
+ return undefined; /* Incomplete or invalid model-authored TeX remains visible as source. */
81
+ }
82
+ }
@@ -1,5 +1,5 @@
1
1
  /** Shared display names and unambiguous slash-command target resolution. */
2
- import { type ObjectValue } from './wire.ts';
2
+ import { type ObjectValue } from '../transport/wire.ts';
3
3
  /** Parse workspace and resume navigation, including their long aliases. */
4
4
  export declare function navigationCommand(value: string): {
5
5
  kind: 'workspace' | 'session';
@@ -1,5 +1,5 @@
1
1
  /** Shared display names and unambiguous slash-command target resolution. */
2
- import { safeText, string } from "./wire.js";
2
+ import { safeText, string } from "../transport/wire.js";
3
3
  /** Parse workspace and resume navigation, including their long aliases. */
4
4
  export function navigationCommand(value) {
5
5
  const match = /^\/(ws|workspace|workspaces|resume|session|sessions)(?:\s+(.+))?$/.exec(value);
@@ -1,5 +1,5 @@
1
1
  /** Independent adapter for Harness path-only @ references; no file bytes are read. */
2
- import { array, object, string } from "./wire.js";
2
+ import { array, object, string } from "../transport/wire.js";
3
3
  /** Find the unfinished reference at the end of the draft; email addresses do not trigger it.
4
4
  * @param text - Complete composer draft.
5
5
  * @returns The replaceable token and host query, or undefined outside a reference.
@@ -1,5 +1,5 @@
1
1
  /** Host projection values with per-key watermarks; snapshots never roll back newer updates. */
2
- import { type ObjectValue } from './wire.ts';
2
+ import { type ObjectValue } from '../transport/wire.ts';
3
3
  /** One host-owned pending input, removable only while its occurrence remains queued. */
4
4
  export interface QueuedInput {
5
5
  id: string;
@@ -1,5 +1,5 @@
1
1
  /** Host projection values with per-key watermarks; snapshots never roll back newer updates. */
2
- import { array, object, string } from "./wire.js";
2
+ import { array, object, string } from "../transport/wire.js";
3
3
  function queuedInputs(value) {
4
4
  return array(value).map(value => {
5
5
  const item = object(value);
@@ -1,5 +1,5 @@
1
1
  import type { HistoryLimits } from './memory.ts';
2
- import { type Json } from './wire.ts';
2
+ import { type Json } from '../transport/wire.ts';
3
3
  interface ToolSummary {
4
4
  name: string;
5
5
  operation?: string;
@@ -25,6 +25,17 @@ export interface MessagePart {
25
25
  text: string;
26
26
  /** Closed live reasoning can fold before the assistant message is committed. */
27
27
  closed?: boolean;
28
+ /** Stable identity of a live part, so its rows can be wrapped incrementally as it grows. */
29
+ key?: string;
30
+ }
31
+ /** The stripe of live work the unfinished attempt is in. */
32
+ export interface LivePhase {
33
+ /** Which kind of delta last moved the attempt forward. */
34
+ kind: 'thinking' | 'tool' | 'output';
35
+ /** Tool name when `kind` is `tool`, so the bar can name what is running. */
36
+ name?: string;
37
+ /** Epoch milliseconds when this phase began. */
38
+ startedAt: number;
28
39
  }
29
40
  /** A displayed message retains the durable sequence for stable reconciliation. */
30
41
  export interface Message {
@@ -61,6 +72,7 @@ export declare class Transcript {
61
72
  private oldestSeq;
62
73
  private turnMarker;
63
74
  private attempt;
75
+ private phase;
64
76
  private nextIndex;
65
77
  private revision;
66
78
  private legacyStream;
@@ -97,6 +109,12 @@ export declare class Transcript {
97
109
  private dropProjections;
98
110
  private storeEvent;
99
111
  private deleteEvent;
112
+ /** The stripe of work the live attempt is in: the phase of the block that last took a delta.
113
+ *
114
+ * Its age answers "what is it doing, and for how long" without inferring anything from silence.
115
+ * @returns Phase kind, the tool name when the phase is a tool call, and when the phase began.
116
+ */
117
+ get livePhase(): LivePhase | undefined;
100
118
  /** Start of the open durable turn, when its timestamp is present in the retained window. */
101
119
  get activeTurnStartedAt(): number | undefined;
102
120
  /** Cached prompt/reasoning summaries, oldest first; stream deltas never invalidate this index. */
@@ -144,6 +162,10 @@ export declare class Transcript {
144
162
  */
145
163
  private foldLegacy;
146
164
  private validatePackedTiming;
165
+ /** Drop the live attempt's blocks and the phase derived from them. */
166
+ private clearBlocks;
167
+ /** Record the phase a delta moved the attempt into; the status bar shows its age. */
168
+ private notePhase;
147
169
  private chunk;
148
170
  }
149
171
  export {};
@@ -1,6 +1,6 @@
1
1
  /** Human transcript projection from durable events and ephemeral assistant chunks. */
2
2
  import sliceAnsi from 'slice-ansi';
3
- import { array, object, safeText, string } from "./wire.js";
3
+ import { array, object, safeText, string } from "../transport/wire.js";
4
4
  function toolSummary(block) {
5
5
  let args = {};
6
6
  if (typeof block.arguments === 'string' && block.arguments.trimStart().startsWith('{')) {
@@ -77,6 +77,7 @@ export class Transcript {
77
77
  oldestSeq;
78
78
  turnMarker;
79
79
  attempt;
80
+ phase;
80
81
  nextIndex = 0;
81
82
  revision = 0;
82
83
  legacyStream = false;
@@ -114,7 +115,7 @@ export class Transcript {
114
115
  this.closedBlocks.clear();
115
116
  this.oldestSeq = undefined;
116
117
  this.turnMarker = undefined;
117
- this.blocks.clear();
118
+ this.clearBlocks();
118
119
  this.keysDirty = true;
119
120
  this.projectionRevision++;
120
121
  this.legacyDirty = true;
@@ -157,8 +158,7 @@ export class Transcript {
157
158
  throw new Error('Assistant stream revision gap');
158
159
  this.revision = number(live.revision);
159
160
  if (live.type === 'start') {
160
- this.blocks.clear();
161
- this.closedBlocks.clear();
161
+ this.clearBlocks();
162
162
  this.attempt = string(live.attemptId);
163
163
  this.nextIndex = 0;
164
164
  }
@@ -172,7 +172,7 @@ export class Transcript {
172
172
  }
173
173
  else if (live.type === 'end') {
174
174
  this.attempt = undefined;
175
- this.blocks.clear();
175
+ this.clearBlocks();
176
176
  }
177
177
  else
178
178
  throw new Error('Unknown assistant stream frame');
@@ -257,8 +257,7 @@ export class Transcript {
257
257
  this.events.clear();
258
258
  this.sizes.clear();
259
259
  this.storedBytes = 0;
260
- this.blocks.clear();
261
- this.closedBlocks.clear();
260
+ this.clearBlocks();
262
261
  this.transientSeqs.clear();
263
262
  this.sortedSeqs = [];
264
263
  this.displayed = [];
@@ -296,6 +295,17 @@ export class Transcript {
296
295
  this.sizes.delete(seq);
297
296
  this.events.delete(seq);
298
297
  }
298
+ /** The stripe of work the live attempt is in: the phase of the block that last took a delta.
299
+ *
300
+ * Its age answers "what is it doing, and for how long" without inferring anything from silence.
301
+ * @returns Phase kind, the tool name when the phase is a tool call, and when the phase began.
302
+ */
303
+ get livePhase() {
304
+ if (this.phase === undefined || this.blocks.size === 0)
305
+ return undefined;
306
+ const { kind, name, startedAt } = this.phase;
307
+ return { kind, ...(name === undefined ? {} : { name }), startedAt };
308
+ }
299
309
  /** Start of the open durable turn, when its timestamp is present in the retained window. */
300
310
  get activeTurnStartedAt() {
301
311
  return this.turnMarker?.start;
@@ -449,6 +459,7 @@ export class Transcript {
449
459
  * @returns Ordered blocks without terminal style escapes.
450
460
  */
451
461
  liveParts(width) {
462
+ const attempt = this.liveAttemptKey ?? 'live';
452
463
  return [...this.blocks].sort(([a], [b]) => a - b).map(([index, block]) => {
453
464
  const closed = this.closedBlocks.has(index);
454
465
  const cached = this.liveProjection.get(block);
@@ -457,6 +468,7 @@ export class Transcript {
457
468
  const part = {
458
469
  kind: block.type === 'reasoning' ? 'reasoning' : block.type === 'tool-call' ? 'tool' : 'text',
459
470
  text: cached?.width === width ? cached.part.text : contentText([block], undefined, width), closed,
471
+ key: `${attempt}:${index}`,
460
472
  };
461
473
  this.liveProjection.set(block, { width, part });
462
474
  return part;
@@ -520,8 +532,7 @@ export class Transcript {
520
532
  foldLegacy(added) {
521
533
  let events;
522
534
  if (this.legacyDirty) {
523
- this.blocks.clear();
524
- this.closedBlocks.clear();
535
+ this.clearBlocks();
525
536
  this.legacyPosition = '';
526
537
  this.legacySeq = -1;
527
538
  this.legacyDirty = false;
@@ -533,17 +544,14 @@ export class Transcript {
533
544
  for (const event of events) {
534
545
  this.legacySeq = Math.max(this.legacySeq, number(event.seq));
535
546
  if (['step/start', 'step/end', 'turn/end', 'assistant/message', 'assistant/attempt'].includes(string(event.type))) {
536
- this.blocks.clear();
537
- this.closedBlocks.clear();
547
+ this.clearBlocks();
538
548
  this.legacyPosition = '';
539
549
  }
540
550
  else if (event.type === 'assistant/chunk' || string(event.type).startsWith('chunkrow/')) {
541
551
  const data = object(event.data);
542
552
  const nextPosition = `${number(data.turn)}:${number(data.step)}`;
543
- if (this.legacyPosition !== nextPosition) {
544
- this.blocks.clear();
545
- this.closedBlocks.clear();
546
- }
553
+ if (this.legacyPosition !== nextPosition)
554
+ this.clearBlocks();
547
555
  this.legacyPosition = nextPosition;
548
556
  switch (event.type) {
549
557
  case 'assistant/chunk':
@@ -572,6 +580,15 @@ export class Transcript {
572
580
  if (count === 0 || array(data.dt).length !== count - 1)
573
581
  throw new Error('Invalid packed history member count');
574
582
  }
583
+ /** Drop the live attempt's blocks and the phase derived from them. */
584
+ clearBlocks() { this.blocks.clear(); this.closedBlocks.clear(); this.phase = undefined; }
585
+ /** Record the phase a delta moved the attempt into; the status bar shows its age. */
586
+ notePhase(kind, name) {
587
+ const key = kind === 'tool' ? `tool:${name ?? ''}` : kind;
588
+ if (this.phase?.key === key)
589
+ return;
590
+ this.phase = { key, kind, ...(name === undefined ? {} : { name }), startedAt: Date.now() };
591
+ }
575
592
  chunk(chunk) {
576
593
  if (chunk.type === 'text-delta' || chunk.type === 'tool-call-delta') {
577
594
  for (const [index, block] of this.blocks)
@@ -583,13 +600,14 @@ export class Transcript {
583
600
  const block = this.blocks.get(index);
584
601
  this.blocks.set(index, { type: chunk.type === 'text-delta' ? 'text' : 'reasoning',
585
602
  text: (block ? string(block.text) : '') + string(chunk.text) });
603
+ this.notePhase(chunk.type === 'text-delta' ? 'output' : 'thinking');
586
604
  }
587
605
  else if (chunk.type === 'tool-call-delta') {
588
606
  const index = number(chunk.index);
589
607
  const block = this.blocks.get(index);
590
- this.blocks.set(index, { type: 'tool-call', id: string(chunk.id),
591
- name: typeof chunk.name === 'string' ? chunk.name : block?.name ?? '',
592
- arguments: '' });
608
+ const name = typeof chunk.name === 'string' ? chunk.name : typeof block?.name === 'string' ? block.name : '';
609
+ this.blocks.set(index, { type: 'tool-call', id: string(chunk.id), name, arguments: '' });
610
+ this.notePhase('tool', name === '' ? undefined : name);
593
611
  }
594
612
  else if (chunk.type === 'block-end') {
595
613
  this.blocks.set(number(chunk.index), object(chunk.block));
@@ -0,0 +1,18 @@
1
+ /** Session-domain result types shared by the controller facade and the terminal UI. */
2
+ /** Resolved navigation-removal identity; empty marks a fresh blank, idle session eligible for immediate archival. */
3
+ export interface RemovalTarget {
4
+ kind: 'workspace' | 'session';
5
+ id: string;
6
+ name: string;
7
+ path?: string;
8
+ empty?: boolean;
9
+ }
10
+ /** Bounded search results contain navigation summaries, never complete message bodies. */
11
+ export interface HistorySearch {
12
+ items: {
13
+ seq: number;
14
+ role: string;
15
+ preview: string;
16
+ }[];
17
+ truncated: boolean;
18
+ }
@@ -0,0 +1,2 @@
1
+ /** Session-domain result types shared by the controller facade and the terminal UI. */
2
+ export {};
@@ -0,0 +1,41 @@
1
+ /** Application state shared by the controller facade and every domain controller. */
2
+ import { Transcript } from './session/transcript.ts';
3
+ import type { HistorySearch, RemovalTarget } from './session/types.ts';
4
+ import type { ObjectValue } from './transport/wire.ts';
5
+ /** State shared by the picker and conversation view. */
6
+ export interface State {
7
+ version: number;
8
+ online: boolean;
9
+ busy: boolean;
10
+ screen: 'workspaces' | 'sessions' | 'chat' | 'path';
11
+ status: string;
12
+ error: string;
13
+ workspaces: ObjectValue[];
14
+ sessions: ObjectValue[];
15
+ showAllSessions: boolean;
16
+ workspaceId?: string;
17
+ sessionId?: string;
18
+ pending: ObjectValue[];
19
+ controlError?: string;
20
+ modelError?: string;
21
+ presetError?: string;
22
+ presets?: ObjectValue[];
23
+ defaultModel?: ObjectValue;
24
+ transcript: Transcript;
25
+ }
26
+ /** The state contract every domain controller writes through. */
27
+ export interface ControllerStore {
28
+ /** Current immutable state snapshot. */
29
+ readonly state: State;
30
+ /** Publish a state patch and notify observers. */
31
+ update(patch: Partial<State>): void;
32
+ /** Monotonic selector generation; a changed value discards in-flight session work. */
33
+ selection(): number;
34
+ /** Advance the selector generation when the selected workspace or session changes. */
35
+ bumpSelection(): void;
36
+ }
37
+ /** Build the initial state before any connection exists.
38
+ * @returns A fresh state whose transcript is empty and disconnected.
39
+ */
40
+ export declare function initialState(): State;
41
+ export type { HistorySearch, RemovalTarget };
package/dist/state.js ADDED
@@ -0,0 +1,9 @@
1
+ /** Application state shared by the controller facade and every domain controller. */
2
+ import { Transcript } from "./session/transcript.js";
3
+ /** Build the initial state before any connection exists.
4
+ * @returns A fresh state whose transcript is empty and disconnected.
5
+ */
6
+ export function initialState() {
7
+ return { version: 0, online: false, busy: false, screen: 'workspaces', status: 'Connecting…',
8
+ error: '', workspaces: [], sessions: [], showAllSessions: false, pending: [], transcript: new Transcript() };
9
+ }
@@ -0,0 +1,14 @@
1
+ /** Create a directory tree with owner-only permissions, leaving an existing directory as it is.
2
+ * @param path - Directory path.
3
+ */
4
+ export declare function ensureDirectory(path: string): Promise<void>;
5
+ /** Create a directory tree and require it to stay owner-only.
6
+ * @param path - Directory path.
7
+ * @param label - Name used in the validation error, such as `Cookie directory`.
8
+ */
9
+ export declare function ensurePrivateDirectory(path: string, label: string): Promise<void>;
10
+ /** List the entry names of one directory.
11
+ * @param path - Directory path.
12
+ * @returns Entry names in filesystem order.
13
+ */
14
+ export declare function listEntries(path: string): Promise<string[]>;
@@ -0,0 +1,24 @@
1
+ /** Private directory creation and listing for the client's configuration and state. */
2
+ import { lstat, mkdir, readdir } from 'node:fs/promises';
3
+ /** Create a directory tree with owner-only permissions, leaving an existing directory as it is.
4
+ * @param path - Directory path.
5
+ */
6
+ export async function ensureDirectory(path) {
7
+ await mkdir(path, { recursive: true, mode: 0o700 });
8
+ }
9
+ /** Create a directory tree and require it to stay owner-only.
10
+ * @param path - Directory path.
11
+ * @param label - Name used in the validation error, such as `Cookie directory`.
12
+ */
13
+ export async function ensurePrivateDirectory(path, label) {
14
+ await ensureDirectory(path);
15
+ const stat = await lstat(path);
16
+ if (!stat.isDirectory() || process.platform !== 'win32' && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())) {
17
+ throw new Error(`${label} must be owned by the current user with permissions 0700`);
18
+ }
19
+ }
20
+ /** List the entry names of one directory.
21
+ * @param path - Directory path.
22
+ * @returns Entry names in filesystem order.
23
+ */
24
+ export async function listEntries(path) { return readdir(path); }
@@ -0,0 +1,43 @@
1
+ /** Read a UTF-8 file; an absent file is an ordinary result, not a failure.
2
+ * @param path - Absolute file path.
3
+ * @returns File contents, or undefined when the file does not exist.
4
+ */
5
+ export declare function readText(path: string): Promise<string | undefined>;
6
+ /** Read a user-private file, rejecting a symlink, a foreign owner, or group/other access.
7
+ * @param path - Absolute file path.
8
+ * @param label - Name used in the validation error, such as `Cookie file`.
9
+ * @returns File contents, or undefined when the file does not exist.
10
+ */
11
+ export declare function readPrivateFile(path: string, label: string): Promise<string | undefined>;
12
+ /** Replace a file atomically with owner-only contents, leaving no partial file behind.
13
+ * @param path - Destination path.
14
+ * @param contents - Complete file contents.
15
+ */
16
+ export declare function writePrivateFile(path: string, contents: string): Promise<void>;
17
+ /** Create an owner-only file only when it does not exist.
18
+ * @param path - Destination path.
19
+ * @param contents - Complete file contents.
20
+ * @returns False when the file already existed, leaving it untouched.
21
+ */
22
+ export declare function createPrivateFile(path: string, contents: string): Promise<boolean>;
23
+ /** Stream bytes into a new owner-only file, removing it unless the write completed.
24
+ *
25
+ * The destination is created before the source is requested, so an existing file fails without
26
+ * contacting the host, and a source or streaming failure removes the partial file.
27
+ * @param path - Destination path; an existing file is never replaced.
28
+ * @param source - Produces the byte stream once the destination exists.
29
+ * @param signal - Cancels the write and removes the partial file.
30
+ */
31
+ export declare function writeExclusiveStream(path: string, source: () => Promise<AsyncIterable<Uint8Array>>, signal?: AbortSignal): Promise<void>;
32
+ /** Append to an owner-only file, creating it when absent.
33
+ *
34
+ * Appending keeps a long-lived diagnostic file cheap to extend; callers that need a bounded file
35
+ * rewrite it periodically with `writePrivateFile`.
36
+ * @param path - Destination path.
37
+ * @param contents - Text to append.
38
+ */
39
+ export declare function appendPrivateFile(path: string, contents: string): Promise<void>;
40
+ /** Remove a file, treating an already absent file as success.
41
+ * @param path - File to remove.
42
+ */
43
+ export declare function removeFile(path: string): Promise<void>;
@@ -0,0 +1,135 @@
1
+ /** Every filesystem read and write the client performs; no other module opens a file. */
2
+ import { constants } from 'node:fs';
3
+ import { open, readFile, rename, unlink, writeFile } from 'node:fs/promises';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { dirname, join } from 'node:path';
6
+ /** Read a UTF-8 file; an absent file is an ordinary result, not a failure.
7
+ * @param path - Absolute file path.
8
+ * @returns File contents, or undefined when the file does not exist.
9
+ */
10
+ export async function readText(path) {
11
+ try {
12
+ return await readFile(path, 'utf8');
13
+ }
14
+ catch (error) {
15
+ if (isMissing(error))
16
+ return undefined;
17
+ throw error;
18
+ }
19
+ }
20
+ /** Read a user-private file, rejecting a symlink, a foreign owner, or group/other access.
21
+ * @param path - Absolute file path.
22
+ * @param label - Name used in the validation error, such as `Cookie file`.
23
+ * @returns File contents, or undefined when the file does not exist.
24
+ */
25
+ export async function readPrivateFile(path, label) {
26
+ let handle;
27
+ try {
28
+ handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
29
+ }
30
+ catch (error) {
31
+ if (isMissing(error))
32
+ return undefined;
33
+ throw error;
34
+ }
35
+ try {
36
+ const stat = await handle.stat();
37
+ if (!stat.isFile() || process.platform !== 'win32' && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())) {
38
+ throw new Error(`${label} must be owned by the current user with permissions 0600`);
39
+ }
40
+ return await handle.readFile('utf8');
41
+ }
42
+ finally {
43
+ await handle.close();
44
+ }
45
+ }
46
+ /** Replace a file atomically with owner-only contents, leaving no partial file behind.
47
+ * @param path - Destination path.
48
+ * @param contents - Complete file contents.
49
+ */
50
+ export async function writePrivateFile(path, contents) {
51
+ const temporary = join(dirname(path), `${randomUUID()}.tmp`);
52
+ try {
53
+ await writeFile(temporary, contents, { mode: 0o600, flag: 'wx' });
54
+ await rename(temporary, path);
55
+ }
56
+ finally {
57
+ await removeFile(temporary);
58
+ }
59
+ }
60
+ /** Create an owner-only file only when it does not exist.
61
+ * @param path - Destination path.
62
+ * @param contents - Complete file contents.
63
+ * @returns False when the file already existed, leaving it untouched.
64
+ */
65
+ export async function createPrivateFile(path, contents) {
66
+ try {
67
+ await writeFile(path, contents, { mode: 0o600, flag: 'wx' });
68
+ return true;
69
+ }
70
+ catch (error) {
71
+ if (isCode(error, 'EEXIST'))
72
+ return false;
73
+ throw error;
74
+ }
75
+ }
76
+ /** Stream bytes into a new owner-only file, removing it unless the write completed.
77
+ *
78
+ * The destination is created before the source is requested, so an existing file fails without
79
+ * contacting the host, and a source or streaming failure removes the partial file.
80
+ * @param path - Destination path; an existing file is never replaced.
81
+ * @param source - Produces the byte stream once the destination exists.
82
+ * @param signal - Cancels the write and removes the partial file.
83
+ */
84
+ export async function writeExclusiveStream(path, source, signal) {
85
+ const file = await open(path, 'wx', 0o600);
86
+ let complete = false;
87
+ try {
88
+ await file.writeFile(await source(), { signal });
89
+ signal?.throwIfAborted();
90
+ complete = true;
91
+ }
92
+ finally {
93
+ try {
94
+ await file.close();
95
+ }
96
+ finally {
97
+ if (!complete)
98
+ await removeFile(path);
99
+ }
100
+ }
101
+ }
102
+ /** Append to an owner-only file, creating it when absent.
103
+ *
104
+ * Appending keeps a long-lived diagnostic file cheap to extend; callers that need a bounded file
105
+ * rewrite it periodically with `writePrivateFile`.
106
+ * @param path - Destination path.
107
+ * @param contents - Text to append.
108
+ */
109
+ export async function appendPrivateFile(path, contents) {
110
+ const handle = await open(path, 'a', 0o600);
111
+ try {
112
+ await handle.writeFile(contents);
113
+ }
114
+ finally {
115
+ await handle.close();
116
+ }
117
+ }
118
+ /** Remove a file, treating an already absent file as success.
119
+ * @param path - File to remove.
120
+ */
121
+ export async function removeFile(path) {
122
+ try {
123
+ await unlink(path);
124
+ }
125
+ catch (error) {
126
+ if (!isMissing(error))
127
+ throw error;
128
+ }
129
+ }
130
+ /** Recognize one error code without assuming the error's prototype. */
131
+ function isCode(error, code) {
132
+ return error instanceof Error && 'code' in error && error.code === code;
133
+ }
134
+ /** Whether an operation failed because the path does not exist. */
135
+ function isMissing(error) { return isCode(error, 'ENOENT'); }
@@ -0,0 +1,3 @@
1
+ /** Storage domain: every filesystem operation the client performs. */
2
+ export { appendPrivateFile, createPrivateFile, readPrivateFile, readText, removeFile, writeExclusiveStream, writePrivateFile } from './files.ts';
3
+ export { ensureDirectory, ensurePrivateDirectory, listEntries } from './directories.ts';
@@ -0,0 +1,3 @@
1
+ /** Storage domain: every filesystem operation the client performs. */
2
+ export { appendPrivateFile, createPrivateFile, readPrivateFile, readText, removeFile, writeExclusiveStream, writePrivateFile } from "./files.js";
3
+ export { ensureDirectory, ensurePrivateDirectory, listEntries } from "./directories.js";