@north-light/crouter 0.3.169 → 0.3.171

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 (122) hide show
  1. package/dist/build-root.d.ts +1 -16
  2. package/dist/build-root.js +1 -25
  3. package/dist/builtin-memory/insights/capture.md +66 -0
  4. package/dist/builtin-memory/insights/init.md +45 -0
  5. package/dist/builtin-memory/internal/plugins.md +12 -11
  6. package/dist/cli.js +2 -6
  7. package/dist/clients/attach/__tests__/attach-keybindings.test.js +7 -1
  8. package/dist/clients/attach/__tests__/context-message.test.js +50 -3
  9. package/dist/clients/attach/__tests__/diagram.test.js +1 -1
  10. package/dist/clients/attach/chrome/header.d.ts +10 -0
  11. package/dist/clients/attach/chrome/header.js +35 -0
  12. package/dist/clients/attach/chrome/status-line.d.ts +3 -0
  13. package/dist/clients/attach/chrome/status-line.js +5 -0
  14. package/dist/clients/attach/chrome/widgets.js +7 -18
  15. package/dist/clients/attach/input/controller.d.ts +10 -0
  16. package/dist/clients/attach/input/controller.js +3 -0
  17. package/dist/clients/attach/overlays/help.d.ts +33 -0
  18. package/dist/clients/attach/overlays/help.js +204 -0
  19. package/dist/clients/attach/render/chat-view.d.ts +81 -9
  20. package/dist/clients/attach/render/chat-view.js +434 -90
  21. package/dist/clients/attach/render/context-message.js +3 -3
  22. package/dist/clients/attach/render/diagram.js +2 -2
  23. package/dist/clients/attach/render/measured-container.d.ts +94 -0
  24. package/dist/clients/attach/render/measured-container.js +307 -0
  25. package/dist/clients/attach/render/transcript-copy.d.ts +9 -0
  26. package/dist/clients/attach/render/transcript-copy.js +81 -0
  27. package/dist/clients/attach/render/viewport.d.ts +29 -0
  28. package/dist/clients/attach/render/viewport.js +160 -0
  29. package/dist/clients/attach/session/bindings.d.ts +5 -1
  30. package/dist/clients/attach/session/bindings.js +16 -0
  31. package/dist/clients/attach/session/context.d.ts +5 -3
  32. package/dist/clients/attach/session/frame.d.ts +34 -0
  33. package/dist/clients/attach/session/frame.js +173 -0
  34. package/dist/clients/attach/session/frames.d.ts +2 -0
  35. package/dist/clients/attach/session/frames.js +3 -0
  36. package/dist/clients/attach/session/input-wiring.d.ts +10 -4
  37. package/dist/clients/attach/session/input-wiring.js +13 -13
  38. package/dist/clients/attach/session/keys.d.ts +4 -1
  39. package/dist/clients/attach/session/keys.js +9 -4
  40. package/dist/clients/attach/session/layout.d.ts +10 -8
  41. package/dist/clients/attach/session/layout.js +32 -31
  42. package/dist/clients/attach/session/mouse.d.ts +15 -0
  43. package/dist/clients/attach/session/mouse.js +61 -0
  44. package/dist/clients/attach/slash/dispatch.d.ts +10 -0
  45. package/dist/clients/attach/slash/dispatch.js +35 -2
  46. package/dist/clients/attach/viewer.js +605 -581
  47. package/dist/clients/web/dev-server.d.ts +4 -1
  48. package/dist/clients/web/dev-server.js +8 -19
  49. package/dist/clients/web/web-cmd.js +1 -1
  50. package/dist/commands/human/prompts.js +16 -6
  51. package/dist/commands/pkg/plugin-manage.js +143 -68
  52. package/dist/commands/sys/__tests__/setup-core.test.js +4 -2
  53. package/dist/commands/sys/setup-core.js +21 -16
  54. package/dist/core/__tests__/human-deliver.test.js +62 -2
  55. package/dist/core/clipboard-text.d.ts +8 -0
  56. package/dist/core/clipboard-text.js +18 -0
  57. package/dist/core/command-manifests/registry.d.ts +1 -3
  58. package/dist/core/command-plugins/bundle.d.ts +29 -0
  59. package/dist/core/command-plugins/bundle.js +198 -0
  60. package/dist/core/command-plugins/discovery.d.ts +0 -8
  61. package/dist/core/command-plugins/discovery.js +2 -39
  62. package/dist/core/command-plugins/transport/http-fetch.d.ts +4 -30
  63. package/dist/core/command-plugins/transport/http-fetch.js +17 -95
  64. package/dist/core/command.d.ts +9 -13
  65. package/dist/core/command.js +12 -17
  66. package/dist/core/config.js +2 -1
  67. package/dist/core/keybindings/__tests__/resolve.test.js +4 -0
  68. package/dist/core/keybindings/attach-control.d.ts +3 -0
  69. package/dist/core/keybindings/attach-control.js +1 -0
  70. package/dist/core/keybindings/catalog.d.ts +2 -2
  71. package/dist/core/keybindings/catalog.js +8 -0
  72. package/dist/core/runtime/broker-protocol.d.ts +13 -1
  73. package/dist/core/runtime/broker.js +213 -5
  74. package/dist/core/runtime/canvas-extensions.d.ts +3 -0
  75. package/dist/core/runtime/canvas-extensions.js +3 -0
  76. package/dist/core/runtime/front-door.js +4 -4
  77. package/dist/core/runtime/kickoff.d.ts +1 -5
  78. package/dist/core/runtime/kickoff.js +1 -5
  79. package/dist/core/runtime/naming.d.ts +7 -0
  80. package/dist/core/runtime/naming.js +5 -2
  81. package/dist/core/runtime/node-read.js +15 -0
  82. package/dist/core/runtime/package-health.d.ts +6 -21
  83. package/dist/core/runtime/package-health.js +12 -123
  84. package/dist/core/runtime/stop-guard.d.ts +1 -1
  85. package/dist/core/runtime/stop-guard.js +3 -3
  86. package/dist/core/runtime/tool-group-summary.d.ts +19 -0
  87. package/dist/core/runtime/tool-group-summary.js +117 -0
  88. package/dist/core/termrender/code-doc.d.ts +12 -0
  89. package/dist/core/termrender/code-doc.js +91 -0
  90. package/dist/core/termrender/display.d.ts +12 -0
  91. package/dist/core/termrender/display.js +19 -0
  92. package/dist/core/termrender/termrender.d.ts +73 -0
  93. package/dist/core/termrender/termrender.js +795 -0
  94. package/dist/core/termrender/version.d.ts +1 -0
  95. package/dist/core/termrender/version.js +1 -0
  96. package/dist/core/user-settings.d.ts +14 -0
  97. package/dist/core/user-settings.js +20 -0
  98. package/dist/daemon/crtrd.js +5 -6
  99. package/dist/index.d.ts +2 -0
  100. package/dist/index.js +1 -0
  101. package/dist/pi-extensions/__tests__/canvas-structured-output.test.d.ts +1 -0
  102. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +63 -0
  103. package/dist/pi-extensions/canvas-stophook.js +2 -2
  104. package/dist/pi-extensions/canvas-structured-output.js +52 -8
  105. package/dist/pi-extensions/summary-tool.d.ts +33 -0
  106. package/dist/pi-extensions/summary-tool.js +62 -0
  107. package/dist/shared/generated-context.d.ts +19 -4
  108. package/dist/shared/generated-context.js +98 -7
  109. package/dist/shared/tool-groups.d.ts +23 -0
  110. package/dist/shared/tool-groups.js +60 -0
  111. package/dist/types.d.ts +11 -0
  112. package/dist/types.js +1 -0
  113. package/dist/web-client/assets/{index-U_NZ66VE.js → index-D7I3gHCL.js} +26 -19
  114. package/dist/web-client/index.html +1 -1
  115. package/dist/web-client/sw.js +1 -1
  116. package/package.json +3 -1
  117. package/runtime.lock.json +62 -3
  118. package/scripts/postinstall.mjs +14 -1
  119. package/dist/core/command-plugins/store.d.ts +0 -16
  120. package/dist/core/command-plugins/store.js +0 -64
  121. package/dist/core/runtime/fault-recovery-nudge.d.ts +0 -3
  122. package/dist/core/runtime/fault-recovery-nudge.js +0 -3
@@ -1,7 +1,7 @@
1
1
  // render/context-message.ts — the terminal viewer's foldable renderer for
2
- // crouter-generated context. Session bearings, situational updates, fresh-revive
3
- // kickoffs, context-size nudges, and node inbox digests retain their full model-visible body and
4
- // runtime role, while this display-only component presents them consistently:
2
+ // crouter-generated context. Session bearings, situational updates, lifecycle
3
+ // steering, recovery prompts, and inbox digests retain their full model-visible
4
+ // body and runtime role, while this display-only component presents them consistently:
5
5
  // collapsed → one dim line; expanded (Ctrl+O) → the ordinary user-message
6
6
  // surface, regardless of the message's runtime role. It implements
7
7
  // `setExpanded`, so the global tool-display cycle folds these blocks alongside
@@ -11,14 +11,14 @@
11
11
  // byte-identical to `--color off`). So the rendered lines are safe to splice
12
12
  // back into markdown inside a plain ``` code fence — pi's Markdown then shows
13
13
  // them verbatim in monospace, no escape-code leakage, no re-wrapping.
14
- // • `renderMarkdown` (humanloop — the sole org-wide termrender caller) is
14
+ // • `renderMarkdown` (crouter's sole org-wide termrender binding) is
15
15
  // synchronous and memoized, so this is a plain string→string transform with
16
16
  // no async swap or component lifecycle.
17
17
  //
18
18
  // Scope: Mermaid and a single-line `text`/`plain`/untagged Unicode-arrow chain.
19
19
  // The wider termrender directive vocabulary remains out of scope — see the base
20
20
  // prompt's Diagrams note.
21
- import { ensureRenderer, isRendererReady, renderMarkdown } from '@crouton-kit/humanloop';
21
+ import { ensureRenderer, isRendererReady, renderMarkdown } from '../../../core/termrender/termrender.js';
22
22
  /** A complete Mermaid fence, including normal Markdown's optional three-space
23
23
  * indentation. Line-anchored (m flag), non-greedy body (captured). An unclosed
24
24
  * fence (still streaming) does not match, so mid-stream text is left raw and
@@ -0,0 +1,94 @@
1
+ import { Container, type Component } from '@earendil-works/pi-tui';
2
+ /** The block at the viewport's top row, plus the line offset within it. */
3
+ export interface ScrollAnchor {
4
+ block: Component;
5
+ offset: number;
6
+ }
7
+ /** A measurable, sliceable ordered block list. The viewport never touches
8
+ * components directly — it addresses blocks by flat index, and holds scroll
9
+ * position as a block identity. */
10
+ export interface ViewportSource {
11
+ /** Visible blocks, in visual order. */
12
+ blockCount(): number;
13
+ /** Rendered line count of block `i` at `width` (renders + caches on miss). */
14
+ blockHeight(i: number, width: number): number;
15
+ /** Cached rendered lines of block `i` at `width`. */
16
+ blockLines(i: number, width: number): readonly string[];
17
+ /** Total rendered lines at `width`. */
18
+ totalHeight(width: number): number;
19
+ /** Absolute content line at which block `i` starts. */
20
+ blockStart(i: number, width: number): number;
21
+ /** The block component at flat index `i`. */
22
+ blockAt(i: number): Component | undefined;
23
+ /** The anchor for the block containing absolute line `line`. */
24
+ anchorAt(line: number, width: number): ScrollAnchor | undefined;
25
+ /** Absolute top line for `anchor` against current measurements, or undefined
26
+ * when the block is gone entirely. */
27
+ anchorLine(anchor: ScrollAnchor, width: number): number | undefined;
28
+ /** Clear cached measurements after a component-level invalidation. */
29
+ invalidate(): void;
30
+ /** Monotonic; bumped by every structural or content mutation. */
31
+ revision(): number;
32
+ }
33
+ /**
34
+ * A container whose direct children are independently rendered and measured.
35
+ *
36
+ * `children` is public on pi-tui's Container, but it must never be mutated from
37
+ * outside these methods: this class owns the visible-index, height, and prefix
38
+ * invariants that make identity anchors safe across transcript mutations.
39
+ */
40
+ export declare class MeasuredContainer extends Container implements ViewportSource {
41
+ private readonly childCache;
42
+ /** Direct children hidden by a collapsed tool-group summary. Never wrap a
43
+ * child: the cache and pi callback paths both key on this exact identity. */
44
+ readonly hiddenChildren: Set<Component>;
45
+ /** Rendered line counts for visible children at `measureWidth`. */
46
+ private heights;
47
+ /** `prefix[i]` is the start line of visible child `i`. */
48
+ private prefix;
49
+ /** The first visible-child index whose prefix onward must be rebuilt. */
50
+ private prefixValidTo;
51
+ private visibleChildren;
52
+ private measureWidth;
53
+ private mutationRevision;
54
+ addChild(component: Component): void;
55
+ setHidden(component: Component, hidden: boolean): void;
56
+ markDirty(component?: Component): void;
57
+ removeChild(component: Component): void;
58
+ clear(): void;
59
+ invalidate(): void;
60
+ render(width: number): string[];
61
+ blockCount(): number;
62
+ blockHeight(index: number, width: number): number;
63
+ blockLines(index: number, width: number): readonly string[];
64
+ totalHeight(width: number): number;
65
+ blockStart(index: number, width: number): number;
66
+ blockAt(index: number): Component | undefined;
67
+ anchorAt(line: number, width: number): ScrollAnchor | undefined;
68
+ anchorLine(anchor: ScrollAnchor, width: number): number | undefined;
69
+ revision(): number;
70
+ private visible;
71
+ private visibleIndexOf;
72
+ private ensureWidth;
73
+ private dropMeasurements;
74
+ private linesFor;
75
+ /** Rebuild the prefix ending at `endExclusive`; `prefixValidTo` is the first
76
+ * child whose height/prefix may be stale, so `prefix[prefixValidTo]` is valid. */
77
+ private rebuildPrefixThrough;
78
+ }
79
+ /** Concatenates measured containers into the transcript's one flat block space. */
80
+ export declare class CompositeViewportSource implements ViewportSource {
81
+ private readonly parts;
82
+ constructor(parts: readonly MeasuredContainer[]);
83
+ blockCount(): number;
84
+ blockHeight(index: number, width: number): number;
85
+ blockLines(index: number, width: number): readonly string[];
86
+ totalHeight(width: number): number;
87
+ blockStart(index: number, width: number): number;
88
+ blockAt(index: number): Component | undefined;
89
+ anchorAt(line: number, width: number): ScrollAnchor | undefined;
90
+ anchorLine(anchor: ScrollAnchor, width: number): number | undefined;
91
+ invalidate(): void;
92
+ revision(): number;
93
+ private locate;
94
+ }
@@ -0,0 +1,307 @@
1
+ import { Container } from '@earendil-works/pi-tui';
2
+ /**
3
+ * A container whose direct children are independently rendered and measured.
4
+ *
5
+ * `children` is public on pi-tui's Container, but it must never be mutated from
6
+ * outside these methods: this class owns the visible-index, height, and prefix
7
+ * invariants that make identity anchors safe across transcript mutations.
8
+ */
9
+ export class MeasuredContainer extends Container {
10
+ childCache = new Map();
11
+ /** Direct children hidden by a collapsed tool-group summary. Never wrap a
12
+ * child: the cache and pi callback paths both key on this exact identity. */
13
+ hiddenChildren = new Set();
14
+ /** Rendered line counts for visible children at `measureWidth`. */
15
+ heights = [];
16
+ /** `prefix[i]` is the start line of visible child `i`. */
17
+ prefix = [0];
18
+ /** The first visible-child index whose prefix onward must be rebuilt. */
19
+ prefixValidTo = 0;
20
+ visibleChildren;
21
+ measureWidth;
22
+ mutationRevision = 0;
23
+ addChild(component) {
24
+ const visibleCount = this.visible().length;
25
+ super.addChild(component);
26
+ this.visibleChildren = undefined;
27
+ this.heights.push(undefined);
28
+ this.prefixValidTo = Math.min(this.prefixValidTo, visibleCount);
29
+ this.mutationRevision++;
30
+ }
31
+ setHidden(component, hidden) {
32
+ const wasHidden = this.hiddenChildren.has(component);
33
+ const oldVisibleIndex = wasHidden ? undefined : this.visibleIndexOf(component);
34
+ if (hidden)
35
+ this.hiddenChildren.add(component);
36
+ else
37
+ this.hiddenChildren.delete(component);
38
+ this.childCache.delete(component);
39
+ if (!wasHidden && hidden && oldVisibleIndex !== undefined) {
40
+ this.heights.splice(oldVisibleIndex, 1);
41
+ this.prefix.length = this.heights.length + 1;
42
+ this.prefixValidTo = Math.min(this.prefixValidTo, oldVisibleIndex);
43
+ }
44
+ else if (wasHidden && !hidden) {
45
+ this.visibleChildren = undefined;
46
+ const insertionIndex = this.visibleIndexOf(component);
47
+ if (insertionIndex !== undefined) {
48
+ this.heights.splice(insertionIndex, 0, undefined);
49
+ this.prefixValidTo = Math.min(this.prefixValidTo, insertionIndex);
50
+ }
51
+ }
52
+ else if (oldVisibleIndex !== undefined) {
53
+ // Preserve RenderCacheContainer's behavior when this is called with the
54
+ // current state: evict this child and re-measure it on its next render.
55
+ this.heights[oldVisibleIndex] = undefined;
56
+ this.prefixValidTo = Math.min(this.prefixValidTo, oldVisibleIndex);
57
+ }
58
+ this.visibleChildren = undefined;
59
+ this.mutationRevision++;
60
+ }
61
+ markDirty(component) {
62
+ if (component) {
63
+ const visibleIndex = this.visibleIndexOf(component);
64
+ this.childCache.delete(component);
65
+ if (visibleIndex !== undefined) {
66
+ this.heights[visibleIndex] = undefined;
67
+ this.prefixValidTo = Math.min(this.prefixValidTo, visibleIndex);
68
+ }
69
+ }
70
+ else {
71
+ this.dropMeasurements();
72
+ }
73
+ this.mutationRevision++;
74
+ }
75
+ removeChild(component) {
76
+ const visibleIndex = this.visibleIndexOf(component);
77
+ super.removeChild(component);
78
+ this.childCache.delete(component);
79
+ this.hiddenChildren.delete(component);
80
+ if (visibleIndex !== undefined) {
81
+ this.heights.splice(visibleIndex, 1);
82
+ this.prefix.length = this.heights.length + 1;
83
+ this.prefixValidTo = Math.min(this.prefixValidTo, visibleIndex);
84
+ }
85
+ this.visibleChildren = undefined;
86
+ this.mutationRevision++;
87
+ }
88
+ clear() {
89
+ super.clear();
90
+ this.childCache.clear();
91
+ this.hiddenChildren.clear();
92
+ this.heights = [];
93
+ this.prefix = [0];
94
+ this.prefixValidTo = 0;
95
+ this.visibleChildren = undefined;
96
+ this.mutationRevision++;
97
+ }
98
+ invalidate() {
99
+ super.invalidate();
100
+ this.dropMeasurements();
101
+ this.mutationRevision++;
102
+ }
103
+ render(width) {
104
+ this.ensureWidth(width);
105
+ const lines = [];
106
+ for (let i = 0; i < this.visible().length; i++)
107
+ lines.push(...this.linesFor(i, width));
108
+ return lines;
109
+ }
110
+ blockCount() {
111
+ return this.visible().length;
112
+ }
113
+ blockHeight(index, width) {
114
+ this.ensureWidth(width);
115
+ return this.linesFor(index, width).length;
116
+ }
117
+ blockLines(index, width) {
118
+ this.ensureWidth(width);
119
+ return this.linesFor(index, width);
120
+ }
121
+ totalHeight(width) {
122
+ this.ensureWidth(width);
123
+ this.rebuildPrefixThrough(this.visible().length, width);
124
+ return this.prefix[this.visible().length] ?? 0;
125
+ }
126
+ blockStart(index, width) {
127
+ this.ensureWidth(width);
128
+ if (index < 0 || index >= this.visible().length)
129
+ return 0;
130
+ this.rebuildPrefixThrough(index, width);
131
+ return this.prefix[index] ?? 0;
132
+ }
133
+ blockAt(index) {
134
+ return this.visible()[index];
135
+ }
136
+ anchorAt(line, width) {
137
+ this.ensureWidth(width);
138
+ const count = this.visible().length;
139
+ this.rebuildPrefixThrough(count, width);
140
+ const total = this.prefix[count] ?? 0;
141
+ if (line < 0 || line >= total)
142
+ return undefined;
143
+ let low = 0;
144
+ let high = count - 1;
145
+ while (low <= high) {
146
+ const middle = Math.floor((low + high) / 2);
147
+ const start = this.prefix[middle] ?? 0;
148
+ const end = this.prefix[middle + 1] ?? start;
149
+ if (line < start)
150
+ high = middle - 1;
151
+ else if (line >= end)
152
+ low = middle + 1;
153
+ else
154
+ return { block: this.visible()[middle], offset: line - start };
155
+ }
156
+ return undefined;
157
+ }
158
+ anchorLine(anchor, width) {
159
+ this.ensureWidth(width);
160
+ const childIndex = this.children.indexOf(anchor.block);
161
+ if (childIndex === -1)
162
+ return undefined;
163
+ const visibleIndex = this.visibleIndexOf(anchor.block);
164
+ if (visibleIndex !== undefined) {
165
+ const height = this.blockHeight(visibleIndex, width);
166
+ if (height === 0)
167
+ return this.blockStart(visibleIndex, width);
168
+ return this.blockStart(visibleIndex, width) + Math.min(anchor.offset, height - 1);
169
+ }
170
+ // A hidden child resolves to the visible sibling that preceded it, so a fold
171
+ // leaves the viewport on its summary rather than throwing it to live bottom.
172
+ for (let i = childIndex - 1; i >= 0; i--) {
173
+ const preceding = this.children[i];
174
+ const precedingVisibleIndex = this.visibleIndexOf(preceding);
175
+ if (precedingVisibleIndex !== undefined)
176
+ return this.blockStart(precedingVisibleIndex, width);
177
+ }
178
+ return 0;
179
+ }
180
+ revision() {
181
+ return this.mutationRevision;
182
+ }
183
+ visible() {
184
+ if (!this.visibleChildren)
185
+ this.visibleChildren = this.children.filter((child) => !this.hiddenChildren.has(child));
186
+ return this.visibleChildren;
187
+ }
188
+ visibleIndexOf(component) {
189
+ const index = this.visible().indexOf(component);
190
+ return index === -1 ? undefined : index;
191
+ }
192
+ ensureWidth(width) {
193
+ if (this.measureWidth === width)
194
+ return;
195
+ this.measureWidth = width;
196
+ this.dropMeasurements();
197
+ }
198
+ dropMeasurements() {
199
+ this.childCache.clear();
200
+ this.heights = this.visible().map(() => undefined);
201
+ this.prefix = [0];
202
+ this.prefixValidTo = 0;
203
+ }
204
+ linesFor(index, width) {
205
+ const child = this.visible()[index];
206
+ if (!child)
207
+ return [];
208
+ let cached = this.childCache.get(child);
209
+ if (!cached || cached.width !== width) {
210
+ cached = { width, lines: child.render(width) };
211
+ this.childCache.set(child, cached);
212
+ }
213
+ this.heights[index] = cached.lines.length;
214
+ return cached.lines;
215
+ }
216
+ /** Rebuild the prefix ending at `endExclusive`; `prefixValidTo` is the first
217
+ * child whose height/prefix may be stale, so `prefix[prefixValidTo]` is valid. */
218
+ rebuildPrefixThrough(endExclusive, width) {
219
+ const count = this.visible().length;
220
+ const end = Math.min(Math.max(endExclusive, 0), count);
221
+ const start = Math.min(this.prefixValidTo, end);
222
+ if (this.prefix.length === 0)
223
+ this.prefix = [0];
224
+ this.prefix[0] = 0;
225
+ for (let i = start; i < end; i++) {
226
+ const height = this.heights[i] ?? this.linesFor(i, width).length;
227
+ this.prefix[i + 1] = (this.prefix[i] ?? 0) + height;
228
+ }
229
+ if (end > this.prefixValidTo)
230
+ this.prefixValidTo = end;
231
+ }
232
+ }
233
+ /** Concatenates measured containers into the transcript's one flat block space. */
234
+ export class CompositeViewportSource {
235
+ parts;
236
+ constructor(parts) {
237
+ this.parts = parts;
238
+ }
239
+ blockCount() {
240
+ return this.parts.reduce((count, part) => count + part.blockCount(), 0);
241
+ }
242
+ blockHeight(index, width) {
243
+ const located = this.locate(index);
244
+ return located ? located.part.blockHeight(located.index, width) : 0;
245
+ }
246
+ blockLines(index, width) {
247
+ const located = this.locate(index);
248
+ return located ? located.part.blockLines(located.index, width) : [];
249
+ }
250
+ totalHeight(width) {
251
+ return this.parts.reduce((total, part) => total + part.totalHeight(width), 0);
252
+ }
253
+ blockStart(index, width) {
254
+ let offset = 0;
255
+ for (const part of this.parts) {
256
+ const count = part.blockCount();
257
+ if (index < count)
258
+ return offset + part.blockStart(index, width);
259
+ index -= count;
260
+ offset += part.totalHeight(width);
261
+ }
262
+ return 0;
263
+ }
264
+ blockAt(index) {
265
+ const located = this.locate(index);
266
+ return located?.part.blockAt(located.index);
267
+ }
268
+ anchorAt(line, width) {
269
+ if (line < 0)
270
+ return undefined;
271
+ for (const part of this.parts) {
272
+ const height = part.totalHeight(width);
273
+ if (line < height)
274
+ return part.anchorAt(line, width);
275
+ line -= height;
276
+ }
277
+ return undefined;
278
+ }
279
+ anchorLine(anchor, width) {
280
+ let offset = 0;
281
+ for (const part of this.parts) {
282
+ const line = part.anchorLine(anchor, width);
283
+ if (line !== undefined)
284
+ return offset + line;
285
+ offset += part.totalHeight(width);
286
+ }
287
+ return undefined;
288
+ }
289
+ invalidate() {
290
+ for (const part of this.parts)
291
+ part.invalidate();
292
+ }
293
+ revision() {
294
+ return this.parts.reduce((revision, part) => revision + part.revision(), 0);
295
+ }
296
+ locate(index) {
297
+ if (index < 0)
298
+ return undefined;
299
+ for (const part of this.parts) {
300
+ const count = part.blockCount();
301
+ if (index < count)
302
+ return { part, index };
303
+ index -= count;
304
+ }
305
+ return undefined;
306
+ }
307
+ }
@@ -0,0 +1,9 @@
1
+ import type { ChatView } from './chat-view.js';
2
+ import type { ViewportSource } from './measured-container.js';
3
+ import type { TranscriptViewport } from './viewport.js';
4
+ export type CopyTarget = 'message' | 'last-assistant' | 'tool' | 'transcript';
5
+ export interface TranscriptCopy {
6
+ /** Copy `target`; returns the status-region confirmation to show. */
7
+ copy(target: CopyTarget): string;
8
+ }
9
+ export declare function createTranscriptCopy(chat: ChatView, viewport: TranscriptViewport, source: ViewportSource): TranscriptCopy;
@@ -0,0 +1,81 @@
1
+ import { writeClipboardTextEverywhere } from '../../../core/clipboard-text.js';
2
+ function hasText(text) {
3
+ return text !== undefined && text.trim() !== '';
4
+ }
5
+ function lineCount(text) {
6
+ return text.split('\n').length;
7
+ }
8
+ function copiedTo(system, tmux) {
9
+ if (system && tmux)
10
+ return 'clipboard + tmux buffer';
11
+ if (system)
12
+ return 'clipboard';
13
+ if (tmux)
14
+ return 'tmux buffer';
15
+ return undefined;
16
+ }
17
+ /** Resolves the newest copyable block wholly visible before falling back to one
18
+ * merely intersecting the current viewport. */
19
+ function visibleMessageText(chat, viewport, source) {
20
+ const { first, last, lastFullyVisible } = viewport.visibleBlocks();
21
+ const textAt = (index) => {
22
+ const block = source.blockAt(index);
23
+ return block ? chat.blockText(block) : undefined;
24
+ };
25
+ for (let index = Math.min(last, lastFullyVisible); index >= first; index--) {
26
+ const text = textAt(index);
27
+ if (hasText(text))
28
+ return text;
29
+ }
30
+ for (let index = last; index >= first; index--) {
31
+ const text = textAt(index);
32
+ if (hasText(text))
33
+ return text;
34
+ }
35
+ return undefined;
36
+ }
37
+ export function createTranscriptCopy(chat, viewport, source) {
38
+ return {
39
+ copy(target) {
40
+ let resolved;
41
+ let emptyNotice;
42
+ switch (target) {
43
+ case 'message': {
44
+ const text = visibleMessageText(chat, viewport, source);
45
+ if (hasText(text))
46
+ resolved = { text, label: 'message' };
47
+ emptyNotice = 'Nothing to copy — no visible message';
48
+ break;
49
+ }
50
+ case 'last-assistant': {
51
+ const text = chat.getLastAssistantText();
52
+ if (hasText(text))
53
+ resolved = { text, label: 'last assistant message' };
54
+ emptyNotice = 'Nothing to copy — no assistant message yet';
55
+ break;
56
+ }
57
+ case 'tool': {
58
+ const text = chat.getLastToolOutputText();
59
+ if (hasText(text))
60
+ resolved = { text, label: 'tool output' };
61
+ emptyNotice = 'Nothing to copy — no tool output yet';
62
+ break;
63
+ }
64
+ case 'transcript': {
65
+ const text = chat.getTranscriptPlainText();
66
+ if (hasText(text))
67
+ resolved = { text, label: 'transcript' };
68
+ emptyNotice = 'Nothing to copy — transcript is empty';
69
+ break;
70
+ }
71
+ }
72
+ if (!resolved)
73
+ return emptyNotice;
74
+ const { system, tmux } = writeClipboardTextEverywhere(resolved.text);
75
+ const destination = copiedTo(system, tmux);
76
+ if (!destination)
77
+ return 'Copy failed — no clipboard tool found';
78
+ return `Copied ${resolved.label} (${lineCount(resolved.text)} lines) — ${destination}`;
79
+ },
80
+ };
81
+ }
@@ -0,0 +1,29 @@
1
+ import type { ViewportSource } from './measured-container.js';
2
+ /** The flat block range occupied by the most recently rendered viewport. */
3
+ type VisibleBlocks = {
4
+ first: number;
5
+ last: number;
6
+ lastFullyVisible: number;
7
+ };
8
+ /**
9
+ * A virtualized, identity-anchored transcript viewport. It owns only scroll
10
+ * state; all measurement and line caching remain with its ViewportSource.
11
+ */
12
+ export interface TranscriptViewport {
13
+ /** Exactly `rows` lines, bottom-aligned and top-padded with empty rows. */
14
+ renderSlice(width: number, rows: number): string[];
15
+ /** Scroll by `lines` (negative = toward older content). */
16
+ scrollBy(lines: number): boolean;
17
+ /** Scroll by one page in `direction`. */
18
+ scrollPage(direction: 'up' | 'down'): boolean;
19
+ /** Pin back to the live bottom. */
20
+ goLive(): void;
21
+ /** True while the viewport is pinned to the bottom. */
22
+ isLive(): boolean;
23
+ /** Flat blocks intersecting the visible slice and its last whole block. */
24
+ visibleBlocks(): VisibleBlocks;
25
+ /** Forward frame invalidation without exposing the source to the frame. */
26
+ invalidate(): void;
27
+ }
28
+ export declare function createTranscriptViewport(source: ViewportSource): TranscriptViewport;
29
+ export {};