@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.
- package/dist/build-root.d.ts +1 -16
- package/dist/build-root.js +1 -25
- package/dist/builtin-memory/insights/capture.md +66 -0
- package/dist/builtin-memory/insights/init.md +45 -0
- package/dist/builtin-memory/internal/plugins.md +12 -11
- package/dist/cli.js +2 -6
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +7 -1
- package/dist/clients/attach/__tests__/context-message.test.js +50 -3
- package/dist/clients/attach/__tests__/diagram.test.js +1 -1
- package/dist/clients/attach/chrome/header.d.ts +10 -0
- package/dist/clients/attach/chrome/header.js +35 -0
- package/dist/clients/attach/chrome/status-line.d.ts +3 -0
- package/dist/clients/attach/chrome/status-line.js +5 -0
- package/dist/clients/attach/chrome/widgets.js +7 -18
- package/dist/clients/attach/input/controller.d.ts +10 -0
- package/dist/clients/attach/input/controller.js +3 -0
- package/dist/clients/attach/overlays/help.d.ts +33 -0
- package/dist/clients/attach/overlays/help.js +204 -0
- package/dist/clients/attach/render/chat-view.d.ts +81 -9
- package/dist/clients/attach/render/chat-view.js +434 -90
- package/dist/clients/attach/render/context-message.js +3 -3
- package/dist/clients/attach/render/diagram.js +2 -2
- package/dist/clients/attach/render/measured-container.d.ts +94 -0
- package/dist/clients/attach/render/measured-container.js +307 -0
- package/dist/clients/attach/render/transcript-copy.d.ts +9 -0
- package/dist/clients/attach/render/transcript-copy.js +81 -0
- package/dist/clients/attach/render/viewport.d.ts +29 -0
- package/dist/clients/attach/render/viewport.js +160 -0
- package/dist/clients/attach/session/bindings.d.ts +5 -1
- package/dist/clients/attach/session/bindings.js +16 -0
- package/dist/clients/attach/session/context.d.ts +5 -3
- package/dist/clients/attach/session/frame.d.ts +34 -0
- package/dist/clients/attach/session/frame.js +173 -0
- package/dist/clients/attach/session/frames.d.ts +2 -0
- package/dist/clients/attach/session/frames.js +3 -0
- package/dist/clients/attach/session/input-wiring.d.ts +10 -4
- package/dist/clients/attach/session/input-wiring.js +13 -13
- package/dist/clients/attach/session/keys.d.ts +4 -1
- package/dist/clients/attach/session/keys.js +9 -4
- package/dist/clients/attach/session/layout.d.ts +10 -8
- package/dist/clients/attach/session/layout.js +32 -31
- package/dist/clients/attach/session/mouse.d.ts +15 -0
- package/dist/clients/attach/session/mouse.js +61 -0
- package/dist/clients/attach/slash/dispatch.d.ts +10 -0
- package/dist/clients/attach/slash/dispatch.js +35 -2
- package/dist/clients/attach/viewer.js +605 -581
- package/dist/clients/web/dev-server.d.ts +4 -1
- package/dist/clients/web/dev-server.js +8 -19
- package/dist/clients/web/web-cmd.js +1 -1
- package/dist/commands/human/prompts.js +16 -6
- package/dist/commands/pkg/plugin-manage.js +143 -68
- package/dist/commands/sys/__tests__/setup-core.test.js +4 -2
- package/dist/commands/sys/setup-core.js +21 -16
- package/dist/core/__tests__/human-deliver.test.js +62 -2
- package/dist/core/clipboard-text.d.ts +8 -0
- package/dist/core/clipboard-text.js +18 -0
- package/dist/core/command-manifests/registry.d.ts +1 -3
- package/dist/core/command-plugins/bundle.d.ts +29 -0
- package/dist/core/command-plugins/bundle.js +198 -0
- package/dist/core/command-plugins/discovery.d.ts +0 -8
- package/dist/core/command-plugins/discovery.js +2 -39
- package/dist/core/command-plugins/transport/http-fetch.d.ts +4 -30
- package/dist/core/command-plugins/transport/http-fetch.js +17 -95
- package/dist/core/command.d.ts +9 -13
- package/dist/core/command.js +12 -17
- package/dist/core/config.js +2 -1
- package/dist/core/keybindings/__tests__/resolve.test.js +4 -0
- package/dist/core/keybindings/attach-control.d.ts +3 -0
- package/dist/core/keybindings/attach-control.js +1 -0
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +8 -0
- package/dist/core/runtime/broker-protocol.d.ts +13 -1
- package/dist/core/runtime/broker.js +213 -5
- package/dist/core/runtime/canvas-extensions.d.ts +3 -0
- package/dist/core/runtime/canvas-extensions.js +3 -0
- package/dist/core/runtime/front-door.js +4 -4
- package/dist/core/runtime/kickoff.d.ts +1 -5
- package/dist/core/runtime/kickoff.js +1 -5
- package/dist/core/runtime/naming.d.ts +7 -0
- package/dist/core/runtime/naming.js +5 -2
- package/dist/core/runtime/node-read.js +15 -0
- package/dist/core/runtime/package-health.d.ts +6 -21
- package/dist/core/runtime/package-health.js +12 -123
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +3 -3
- package/dist/core/runtime/tool-group-summary.d.ts +19 -0
- package/dist/core/runtime/tool-group-summary.js +117 -0
- package/dist/core/termrender/code-doc.d.ts +12 -0
- package/dist/core/termrender/code-doc.js +91 -0
- package/dist/core/termrender/display.d.ts +12 -0
- package/dist/core/termrender/display.js +19 -0
- package/dist/core/termrender/termrender.d.ts +73 -0
- package/dist/core/termrender/termrender.js +795 -0
- package/dist/core/termrender/version.d.ts +1 -0
- package/dist/core/termrender/version.js +1 -0
- package/dist/core/user-settings.d.ts +14 -0
- package/dist/core/user-settings.js +20 -0
- package/dist/daemon/crtrd.js +5 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +63 -0
- package/dist/pi-extensions/canvas-stophook.js +2 -2
- package/dist/pi-extensions/canvas-structured-output.js +52 -8
- package/dist/pi-extensions/summary-tool.d.ts +33 -0
- package/dist/pi-extensions/summary-tool.js +62 -0
- package/dist/shared/generated-context.d.ts +19 -4
- package/dist/shared/generated-context.js +98 -7
- package/dist/shared/tool-groups.d.ts +23 -0
- package/dist/shared/tool-groups.js +60 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +1 -0
- package/dist/web-client/assets/{index-U_NZ66VE.js → index-D7I3gHCL.js} +26 -19
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +3 -1
- package/runtime.lock.json +62 -3
- package/scripts/postinstall.mjs +14 -1
- package/dist/core/command-plugins/store.d.ts +0 -16
- package/dist/core/command-plugins/store.js +0 -64
- package/dist/core/runtime/fault-recovery-nudge.d.ts +0 -3
- 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,
|
|
3
|
-
//
|
|
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` (
|
|
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 '
|
|
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 {};
|