@sagmans/dsh-tui 0.1.2 → 0.2.0
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/README.md +82 -8
- package/cordis.patch.yml +4 -2
- package/lib/agent/host.d.ts +14 -0
- package/lib/agent/host.d.ts.map +1 -1
- package/lib/agent/host.js +20 -4
- package/lib/agent/host.js.map +1 -1
- package/lib/agent/model.d.ts +24 -0
- package/lib/agent/model.d.ts.map +1 -1
- package/lib/agent/model.js +46 -1
- package/lib/agent/model.js.map +1 -1
- package/lib/agent/present.d.ts +7 -1
- package/lib/agent/present.d.ts.map +1 -1
- package/lib/agent/present.js +33 -4
- package/lib/agent/present.js.map +1 -1
- package/lib/agent/presets.d.ts +7 -1
- package/lib/agent/presets.d.ts.map +1 -1
- package/lib/agent/presets.js +7 -3
- package/lib/agent/presets.js.map +1 -1
- package/lib/cards.d.ts +127 -7
- package/lib/cards.d.ts.map +1 -1
- package/lib/cards.js +380 -52
- package/lib/cards.js.map +1 -1
- package/lib/export.d.ts +5 -4
- package/lib/export.d.ts.map +1 -1
- package/lib/export.js +62 -13
- package/lib/export.js.map +1 -1
- package/lib/fold-cursor.d.ts +23 -0
- package/lib/fold-cursor.d.ts.map +1 -0
- package/lib/fold-cursor.js +31 -0
- package/lib/fold-cursor.js.map +1 -0
- package/lib/index.d.ts +7 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +310 -32
- package/lib/index.js.map +1 -1
- package/lib/input/submission.d.ts +3 -1
- package/lib/input/submission.d.ts.map +1 -1
- package/lib/input/submission.js +5 -2
- package/lib/input/submission.js.map +1 -1
- package/lib/terminal/warning-screen.d.ts +8 -0
- package/lib/terminal/warning-screen.d.ts.map +1 -0
- package/lib/terminal/warning-screen.js +66 -0
- package/lib/terminal/warning-screen.js.map +1 -0
- package/lib/theme-capability.d.ts +36 -0
- package/lib/theme-capability.d.ts.map +1 -0
- package/lib/theme-capability.js +190 -0
- package/lib/theme-capability.js.map +1 -0
- package/lib/theme-command.d.ts +10 -0
- package/lib/theme-command.d.ts.map +1 -0
- package/lib/theme-command.js +61 -0
- package/lib/theme-command.js.map +1 -0
- package/lib/theme-settings.d.ts +161 -0
- package/lib/theme-settings.d.ts.map +1 -0
- package/lib/theme-settings.js +166 -0
- package/lib/theme-settings.js.map +1 -0
- package/lib/theme-tokens.d.ts +110 -0
- package/lib/theme-tokens.d.ts.map +1 -0
- package/lib/theme-tokens.js +401 -0
- package/lib/theme-tokens.js.map +1 -0
- package/lib/theme.d.ts +32 -35
- package/lib/theme.d.ts.map +1 -1
- package/lib/theme.js +112 -63
- package/lib/theme.js.map +1 -1
- package/lib/tokens.d.ts +15 -0
- package/lib/tokens.d.ts.map +1 -0
- package/lib/tokens.js +29 -0
- package/lib/tokens.js.map +1 -0
- package/lib/transcript.d.ts +26 -1
- package/lib/transcript.d.ts.map +1 -1
- package/lib/transcript.js +153 -25
- package/lib/transcript.js.map +1 -1
- package/lib/ui/dock.d.ts.map +1 -1
- package/lib/ui/dock.js +67 -28
- package/lib/ui/dock.js.map +1 -1
- package/lib/ui/editor.d.ts +34 -0
- package/lib/ui/editor.d.ts.map +1 -0
- package/lib/ui/editor.js +98 -0
- package/lib/ui/editor.js.map +1 -0
- package/lib/ui/markdown.d.ts +8 -0
- package/lib/ui/markdown.d.ts.map +1 -1
- package/lib/ui/markdown.js +12 -0
- package/lib/ui/markdown.js.map +1 -1
- package/lib/ui/picker.d.ts +37 -0
- package/lib/ui/picker.d.ts.map +1 -1
- package/lib/ui/picker.js +53 -0
- package/lib/ui/picker.js.map +1 -1
- package/lib/ui/status.d.ts +3 -1
- package/lib/ui/status.d.ts.map +1 -1
- package/lib/ui/status.js +76 -24
- package/lib/ui/status.js.map +1 -1
- package/lib/ui/view.d.ts +43 -0
- package/lib/ui/view.d.ts.map +1 -1
- package/lib/ui/view.js +254 -54
- package/lib/ui/view.js.map +1 -1
- package/package.json +3 -1
package/lib/theme.js
CHANGED
|
@@ -1,84 +1,133 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { truncateToWidth } from '@earendil-works/pi-tui';
|
|
2
|
+
import { detectColourMode } from "./theme-capability.js";
|
|
3
|
+
import { DEFAULT_PALETTE, DEFAULT_TOKENS, resetSequence, resolveToken } from "./theme-tokens.js";
|
|
4
|
+
/** What a caller gets when it has written no settings at all. */
|
|
5
|
+
const NO_OVERRIDES = { palette: DEFAULT_PALETTE, tokens: new Map() };
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
+
* Distinguishes one theme table from the next.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
* its
|
|
10
|
-
* terminal theme.
|
|
9
|
+
* A renderer caches rows against the theme that produced them, so the revision
|
|
10
|
+
* is how a cache learns its rows were drawn under a table that is now stale.
|
|
11
11
|
*/
|
|
12
|
+
let themeRevision = 0;
|
|
13
|
+
/** Style markdown through the same table as the rest of the surface. */
|
|
12
14
|
function markdownTheme(style) {
|
|
13
15
|
return {
|
|
14
|
-
heading: text => style
|
|
15
|
-
link: text => style
|
|
16
|
-
linkUrl: style.
|
|
17
|
-
code: style.
|
|
18
|
-
codeBlock: text => text,
|
|
19
|
-
codeBlockBorder: style.
|
|
20
|
-
quote: style.
|
|
21
|
-
quoteBorder: style.
|
|
22
|
-
hr: style.
|
|
23
|
-
listBullet: style.
|
|
24
|
-
bold: style.bold,
|
|
25
|
-
italic: style.italic,
|
|
26
|
-
strikethrough: style.
|
|
27
|
-
underline: style.underline,
|
|
16
|
+
heading: text => style('markdown.heading', text),
|
|
17
|
+
link: text => style('markdown.link', text),
|
|
18
|
+
linkUrl: text => style('markdown.linkUrl', text),
|
|
19
|
+
code: text => style('markdown.code', text),
|
|
20
|
+
codeBlock: text => style('markdown.codeBlock', text),
|
|
21
|
+
codeBlockBorder: text => style('markdown.codeBlockBorder', text),
|
|
22
|
+
quote: text => style('markdown.quote', text),
|
|
23
|
+
quoteBorder: text => style('markdown.quoteBorder', text),
|
|
24
|
+
hr: text => style('markdown.hr', text),
|
|
25
|
+
listBullet: text => style('markdown.listBullet', text),
|
|
26
|
+
bold: text => style('markdown.bold', text),
|
|
27
|
+
italic: text => style('markdown.italic', text),
|
|
28
|
+
strikethrough: text => style('markdown.strikethrough', text),
|
|
29
|
+
underline: text => style('markdown.underline', text),
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
|
-
|
|
32
|
+
/** The editor keeps only a border colour and a select list, so both come from tokens. */
|
|
33
|
+
function editorTheme(style, visible) {
|
|
31
34
|
return {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// Hiding the border has to remove it: returning the text unstyled would draw
|
|
36
|
+
// it louder than the muted default the reader was trying to remove.
|
|
37
|
+
borderColor: text => (visible('editor.border') ? style('editor.border', text) : ''),
|
|
38
|
+
// A select-list row is framework-owned text, so a hidden entry can only be
|
|
39
|
+
// unstyled rather than absent; the README records that limit.
|
|
40
|
+
selectList: {
|
|
41
|
+
selectedPrefix: text => style('editor.selectList.selectedPrefix', text),
|
|
42
|
+
selectedText: text => style('editor.selectList.selectedText', text),
|
|
43
|
+
description: text => style('editor.selectList.description', text),
|
|
44
|
+
scrollInfo: text => style('editor.selectList.scrollInfo', text),
|
|
45
|
+
noMatch: text => style('editor.selectList.noMatch', text),
|
|
46
|
+
},
|
|
37
47
|
};
|
|
38
48
|
}
|
|
39
49
|
/**
|
|
40
|
-
*
|
|
50
|
+
* Build the surface theme.
|
|
41
51
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
52
|
+
* Every token is resolved once, when the theme is built, rather than per frame:
|
|
53
|
+
* a repaint walks the whole transcript and re-resolving a palette chain for
|
|
54
|
+
* every row would pay for the same answer thousands of times. A settings change
|
|
55
|
+
* builds a new theme instead, which is also what makes the swap atomic.
|
|
46
56
|
*/
|
|
47
|
-
export function
|
|
48
|
-
const
|
|
49
|
-
|
|
57
|
+
export function createTheme(mode = detectColourMode(process.env), overrides = NO_OVERRIDES) {
|
|
58
|
+
const resolved = new Map();
|
|
59
|
+
const resolve = (token) => {
|
|
60
|
+
const cached = resolved.get(token);
|
|
61
|
+
if (cached !== undefined)
|
|
62
|
+
return cached;
|
|
63
|
+
const style = resolveToken(token, overrides.tokens, overrides.palette, mode);
|
|
64
|
+
resolved.set(token, style);
|
|
65
|
+
return style;
|
|
66
|
+
};
|
|
67
|
+
const style = (token, text) => {
|
|
68
|
+
const { prefix, suffix } = resolve(token);
|
|
69
|
+
return prefix === '' ? text : `${prefix}${text}${suffix}`;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Truncate to a width, honouring the promise that `none` emits nothing.
|
|
73
|
+
*
|
|
74
|
+
* pi-tui closes a cut by writing a reset unconditionally, whether or not the
|
|
75
|
+
* text carried any styling, so a truncated row leaks escapes even when
|
|
76
|
+
* colour is off. Those resets close styles this surface never opened, so
|
|
77
|
+
* dropping them restores the contract without changing what is drawn.
|
|
78
|
+
*/
|
|
79
|
+
const cut = (text, width, ellipsis = '') => {
|
|
80
|
+
const truncated = truncateToWidth(text, width, ellipsis);
|
|
81
|
+
return mode === 'none' ? truncated.replaceAll(resetSequence(), '') : truncated;
|
|
82
|
+
};
|
|
83
|
+
const visible = (token) => !resolve(token).hidden;
|
|
84
|
+
return {
|
|
85
|
+
revision: ++themeRevision,
|
|
86
|
+
color: mode !== 'none',
|
|
87
|
+
style,
|
|
88
|
+
cut,
|
|
89
|
+
glyph: token => resolve(token).glyph,
|
|
90
|
+
visible,
|
|
91
|
+
editor: editorTheme(style, visible),
|
|
92
|
+
markdown: markdownTheme(style),
|
|
93
|
+
};
|
|
50
94
|
}
|
|
51
95
|
/**
|
|
52
|
-
*
|
|
96
|
+
* An editor theme that follows a moving source.
|
|
53
97
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
98
|
+
* The editor keeps the theme it was built with for the life of the session, so
|
|
99
|
+
* a settings change has to reach it through a stable object instead of by
|
|
100
|
+
* replacing the one it captured.
|
|
57
101
|
*/
|
|
58
|
-
export function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
102
|
+
export function forwardEditorTheme(source) {
|
|
103
|
+
return {
|
|
104
|
+
borderColor: text => source().borderColor(text),
|
|
105
|
+
selectList: {
|
|
106
|
+
selectedPrefix: text => source().selectList.selectedPrefix(text),
|
|
107
|
+
selectedText: text => source().selectList.selectedText(text),
|
|
108
|
+
description: text => source().selectList.description(text),
|
|
109
|
+
scrollInfo: text => source().selectList.scrollInfo(text),
|
|
110
|
+
noMatch: text => source().selectList.noMatch(text),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** The markdown counterpart of {@link forwardEditorTheme}. */
|
|
115
|
+
export function forwardMarkdownTheme(source) {
|
|
68
116
|
return {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
117
|
+
heading: text => source().heading(text),
|
|
118
|
+
link: text => source().link(text),
|
|
119
|
+
linkUrl: text => source().linkUrl(text),
|
|
120
|
+
code: text => source().code(text),
|
|
121
|
+
codeBlock: text => source().codeBlock(text),
|
|
122
|
+
codeBlockBorder: text => source().codeBlockBorder(text),
|
|
123
|
+
quote: text => source().quote(text),
|
|
124
|
+
quoteBorder: text => source().quoteBorder(text),
|
|
125
|
+
hr: text => source().hr(text),
|
|
126
|
+
listBullet: text => source().listBullet(text),
|
|
127
|
+
bold: text => source().bold(text),
|
|
128
|
+
italic: text => source().italic(text),
|
|
129
|
+
strikethrough: text => source().strikethrough(text),
|
|
130
|
+
underline: text => source().underline(text),
|
|
82
131
|
};
|
|
83
132
|
}
|
|
84
133
|
//# sourceMappingURL=theme.js.map
|
package/lib/theme.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.js","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"theme.js","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"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"}
|
package/lib/tokens.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token estimates shared by every row that reports a size.
|
|
3
|
+
*
|
|
4
|
+
* The exact count needs the provider's own tokenizer, which a terminal does not
|
|
5
|
+
* have; four characters per token is the estimate a reader can compare against,
|
|
6
|
+
* and one definition keeps a reasoning row, a status row, and a tool card from
|
|
7
|
+
* disagreeing about the same text.
|
|
8
|
+
*/
|
|
9
|
+
/** Characters a provider bills as roughly one token. */
|
|
10
|
+
export declare const CHARS_PER_TOKEN = 4;
|
|
11
|
+
/** A thought's or a payload's size in tokens, rounded up so a non-empty text never reads as zero. */
|
|
12
|
+
export declare function countTokens(text: string): number;
|
|
13
|
+
/** Compact a token count, because the exact number changes nothing a reader decides. */
|
|
14
|
+
export declare function formatTokens(count: number): string;
|
|
15
|
+
//# sourceMappingURL=tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,wDAAwD;AACxD,eAAO,MAAM,eAAe,IAAI,CAAA;AAMhC,qGAAqG;AACrG,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlD"}
|
package/lib/tokens.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token estimates shared by every row that reports a size.
|
|
3
|
+
*
|
|
4
|
+
* The exact count needs the provider's own tokenizer, which a terminal does not
|
|
5
|
+
* have; four characters per token is the estimate a reader can compare against,
|
|
6
|
+
* and one definition keeps a reasoning row, a status row, and a tool card from
|
|
7
|
+
* disagreeing about the same text.
|
|
8
|
+
*/
|
|
9
|
+
/** Characters a provider bills as roughly one token. */
|
|
10
|
+
export const CHARS_PER_TOKEN = 4;
|
|
11
|
+
const THOUSAND = 1000;
|
|
12
|
+
const HUNDRED_THOUSAND = 100 * THOUSAND;
|
|
13
|
+
const MILLION = 1_000_000;
|
|
14
|
+
/** A thought's or a payload's size in tokens, rounded up so a non-empty text never reads as zero. */
|
|
15
|
+
export function countTokens(text) {
|
|
16
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
17
|
+
}
|
|
18
|
+
/** Compact a token count, because the exact number changes nothing a reader decides. */
|
|
19
|
+
export function formatTokens(count) {
|
|
20
|
+
if (count >= MILLION)
|
|
21
|
+
return `${(count / MILLION).toFixed(1)}M`;
|
|
22
|
+
// A decimal on a five-digit count is noise: 128.0k reads worse than 128k.
|
|
23
|
+
if (count >= HUNDRED_THOUSAND)
|
|
24
|
+
return `${Math.round(count / THOUSAND)}k`;
|
|
25
|
+
if (count >= THOUSAND)
|
|
26
|
+
return `${(count / THOUSAND).toFixed(1)}k`;
|
|
27
|
+
return String(count);
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,wDAAwD;AACxD,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAA;AAEhC,MAAM,QAAQ,GAAG,IAAI,CAAA;AACrB,MAAM,gBAAgB,GAAG,GAAG,GAAG,QAAQ,CAAA;AACvC,MAAM,OAAO,GAAG,SAAS,CAAA;AAEzB,qGAAqG;AACrG,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,eAAe,CAAC,CAAA;AACjD,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,IAAI,KAAK,IAAI,OAAO;QAAE,OAAO,GAAG,CAAC,KAAK,GAAG,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAA;IAC/D,0EAA0E;IAC1E,IAAI,KAAK,IAAI,gBAAgB;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAA;IACxE,IAAI,KAAK,IAAI,QAAQ;QAAE,OAAO,GAAG,CAAC,KAAK,GAAG,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAA;IACjE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;AACtB,CAAC"}
|
package/lib/transcript.d.ts
CHANGED
|
@@ -49,9 +49,23 @@ export declare class TranscriptModel {
|
|
|
49
49
|
private readonly now;
|
|
50
50
|
private readonly settled;
|
|
51
51
|
private readonly pending;
|
|
52
|
+
/**
|
|
53
|
+
* Where each nested call landed on its root card.
|
|
54
|
+
*
|
|
55
|
+
* A dispatch logs a start and then a settle, so the second event has to find
|
|
56
|
+
* the row the first one drew instead of adding a second line for one call.
|
|
57
|
+
*/
|
|
58
|
+
private readonly pendingSub;
|
|
52
59
|
private live;
|
|
53
60
|
private liveReasoning;
|
|
54
61
|
private reasoningStartedAt;
|
|
62
|
+
/**
|
|
63
|
+
* Whether this step already painted a thought from the stream.
|
|
64
|
+
*
|
|
65
|
+
* A live turn streams the thinking and then records the same text in the
|
|
66
|
+
* message, so the recorded copy is a repeat rather than a second thought.
|
|
67
|
+
*/
|
|
68
|
+
private reasoningPaintedThisStep;
|
|
55
69
|
constructor(presenter?: ToolPresenter | undefined,
|
|
56
70
|
/** Clock for "how long has this been thinking"; injected so a test can pin it. */
|
|
57
71
|
now?: () => number);
|
|
@@ -79,13 +93,24 @@ export declare class TranscriptModel {
|
|
|
79
93
|
/** Report a live agent failure the durable log never carries as a message. */
|
|
80
94
|
reportError(error: unknown): void;
|
|
81
95
|
/**
|
|
82
|
-
* Settle
|
|
96
|
+
* Settle one thought into a row of its own.
|
|
83
97
|
*
|
|
84
98
|
* The row lands where the reasoning happened, which is before the answer it
|
|
85
99
|
* produced, so reading order still matches the order the model thought in.
|
|
86
100
|
*/
|
|
101
|
+
private paintReasoning;
|
|
102
|
+
/** Settle the reasoning streamed so far into a row of its own. */
|
|
87
103
|
private settleReasoning;
|
|
88
104
|
apply(event: FoldableEvent): void;
|
|
105
|
+
/**
|
|
106
|
+
* Fold one nested PTC call onto the card of the run_code call that dispatched it.
|
|
107
|
+
*
|
|
108
|
+
* Dispatches are logged while the program runs, so the root is still pending;
|
|
109
|
+
* a dispatch whose root is not in this fold belongs to a window that starts
|
|
110
|
+
* mid-run, and attaching it to another card would claim a call that card
|
|
111
|
+
* never made.
|
|
112
|
+
*/
|
|
113
|
+
private applySubCall;
|
|
89
114
|
private settleToolResult;
|
|
90
115
|
}
|
|
91
116
|
//# sourceMappingURL=transcript.d.ts.map
|
package/lib/transcript.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../src/transcript.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../src/transcript.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoE,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAA;AAGhI,qCAAqC;AACrC,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClD;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAA;AAE3G,yGAAyG;AACzG,eAAO,MAAM,oBAAoB,QAAS,CAAA;AAQ1C;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CACxB;AA2FD;;;;;;;;GAQG;AACH,qBAAa,eAAe;IAsBxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;IAC3B,kFAAkF;IAClF,OAAO,CAAC,QAAQ,CAAC,GAAG;IAvBtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiF;IAC5G,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,kBAAkB,CAAoB;IAC9C;;;;;OAKG;IACH,OAAO,CAAC,wBAAwB,CAAQ;gBAGrB,SAAS,CAAC,EAAE,aAAa,YAAA;IAC1C,kFAAkF;IACjE,GAAG,GAAE,MAAM,MAAyB;IAGvD,sFAAsF;IACtF,OAAO,IAAI,SAAS,eAAe,EAAE;IAerC;;;;;;OAMG;IACH,YAAY,IAAI,MAAM;IAItB,yEAAyE;IACzE,OAAO,IAAI,OAAO;IAIlB,KAAK,IAAI,IAAI;IASb,gFAAgF;IAChF,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI1B,mFAAmF;IACnF,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI1B,kDAAkD;IAClD,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAyBtC,2FAA2F;IAC3F,OAAO,CAAC,aAAa;IAiBrB,8EAA8E;IAC9E,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAKjC;;;;;OAKG;IACH,OAAO,CAAC,cAAc;IAYtB,kEAAkE;IAClE,OAAO,CAAC,eAAe;IASvB,KAAK,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IA2FjC;;;;;;;OAOG;IACH,OAAO,CAAC,YAAY;IAkCpB,OAAO,CAAC,gBAAgB;CAwCzB"}
|
package/lib/transcript.js
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
|
-
import { cardFromLines, mergeCards } from "./cards.js";
|
|
1
|
+
import { cardFromLines, carriedFields, mergeCards, subCallOf, SUBCALL_MAX } from "./cards.js";
|
|
2
|
+
import { countTokens } from "./tokens.js";
|
|
2
3
|
/** Reasoning kept per settled block, so one runaway thought cannot grow the transcript without bound. */
|
|
3
4
|
export const REASONING_CHAR_LIMIT = 20_000;
|
|
5
|
+
/** The token count with its noun, because "1 tokens" reads as a bug. */
|
|
6
|
+
function describeTokens(text) {
|
|
7
|
+
const tokens = countTokens(text);
|
|
8
|
+
return `${tokens} token${tokens === 1 ? '' : 's'}`;
|
|
9
|
+
}
|
|
4
10
|
function asRecord(value) {
|
|
5
11
|
return typeof value === 'object' && value !== null ? value : undefined;
|
|
6
12
|
}
|
|
13
|
+
/** The arguments a nested call was made with, as the shape a presenter is asked with. */
|
|
14
|
+
function argumentsJsonOf(value) {
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
return '';
|
|
17
|
+
try {
|
|
18
|
+
return JSON.stringify(value) ?? '';
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
7
24
|
function contentLinesOf(content) {
|
|
8
25
|
if (!Array.isArray(content))
|
|
9
26
|
return [];
|
|
@@ -12,6 +29,10 @@ function contentLinesOf(content) {
|
|
|
12
29
|
const record = asRecord(block);
|
|
13
30
|
if (record === undefined)
|
|
14
31
|
continue;
|
|
32
|
+
// A thought is a row of its own; reading it here would hand the model's
|
|
33
|
+
// private reasoning to the reader as though it had been said out loud.
|
|
34
|
+
if (record.type === 'reasoning')
|
|
35
|
+
continue;
|
|
15
36
|
// Any block that carries text counts; a tool-result block also holds its
|
|
16
37
|
// model-facing content one level deeper.
|
|
17
38
|
if (typeof record.text === 'string')
|
|
@@ -24,12 +45,30 @@ function contentLinesOf(content) {
|
|
|
24
45
|
function textOfContent(content) {
|
|
25
46
|
return contentLinesOf(content).join('\n');
|
|
26
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* The thoughts a durable message carries, in the order they were recorded.
|
|
50
|
+
*
|
|
51
|
+
* A recorded thought is the only path a resumed session has to the model's
|
|
52
|
+
* reasoning, and the live stream does not outlive the turn that produced it.
|
|
53
|
+
*/
|
|
54
|
+
function reasoningTextsOf(content) {
|
|
55
|
+
if (!Array.isArray(content))
|
|
56
|
+
return [];
|
|
57
|
+
const thoughts = [];
|
|
58
|
+
for (const block of content) {
|
|
59
|
+
const record = asRecord(block);
|
|
60
|
+
if (record === undefined)
|
|
61
|
+
continue;
|
|
62
|
+
if (record.type === 'reasoning' && typeof record.text === 'string')
|
|
63
|
+
thoughts.push(record.text);
|
|
64
|
+
if (Array.isArray(record.content))
|
|
65
|
+
thoughts.push(...reasoningTextsOf(record.content));
|
|
66
|
+
}
|
|
67
|
+
return thoughts;
|
|
68
|
+
}
|
|
27
69
|
function sourceKind(data) {
|
|
28
70
|
return asRecord(data.source)?.kind === 'user' ? 'user' : 'plugin';
|
|
29
71
|
}
|
|
30
|
-
function countLines(text) {
|
|
31
|
-
return text === '' ? 0 : text.split('\n').length;
|
|
32
|
-
}
|
|
33
72
|
function messageOf(value) {
|
|
34
73
|
const record = asRecord(value);
|
|
35
74
|
if (record === undefined)
|
|
@@ -69,9 +108,23 @@ export class TranscriptModel {
|
|
|
69
108
|
now;
|
|
70
109
|
settled = [];
|
|
71
110
|
pending = new Map();
|
|
111
|
+
/**
|
|
112
|
+
* Where each nested call landed on its root card.
|
|
113
|
+
*
|
|
114
|
+
* A dispatch logs a start and then a settle, so the second event has to find
|
|
115
|
+
* the row the first one drew instead of adding a second line for one call.
|
|
116
|
+
*/
|
|
117
|
+
pendingSub = new Map();
|
|
72
118
|
live = '';
|
|
73
119
|
liveReasoning = '';
|
|
74
120
|
reasoningStartedAt;
|
|
121
|
+
/**
|
|
122
|
+
* Whether this step already painted a thought from the stream.
|
|
123
|
+
*
|
|
124
|
+
* A live turn streams the thinking and then records the same text in the
|
|
125
|
+
* message, so the recorded copy is a repeat rather than a second thought.
|
|
126
|
+
*/
|
|
127
|
+
reasoningPaintedThisStep = false;
|
|
75
128
|
constructor(presenter,
|
|
76
129
|
/** Clock for "how long has this been thinking"; injected so a test can pin it. */
|
|
77
130
|
now = () => Date.now()) {
|
|
@@ -85,7 +138,7 @@ export class TranscriptModel {
|
|
|
85
138
|
const ranFor = this.reasoningStartedAt === undefined ? undefined : this.now() - this.reasoningStartedAt;
|
|
86
139
|
entries.push({
|
|
87
140
|
kind: 'reasoning',
|
|
88
|
-
summary: `reasoning · ${this.liveReasoning
|
|
141
|
+
summary: `reasoning · ${describeTokens(this.liveReasoning)}${ranFor === undefined ? '' : ` · ${Math.max(1, Math.round(ranFor / 1000))}s`} · streaming`,
|
|
89
142
|
body: this.liveReasoning,
|
|
90
143
|
live: true,
|
|
91
144
|
});
|
|
@@ -114,6 +167,7 @@ export class TranscriptModel {
|
|
|
114
167
|
this.live = '';
|
|
115
168
|
this.liveReasoning = '';
|
|
116
169
|
this.reasoningStartedAt = undefined;
|
|
170
|
+
this.reasoningPaintedThisStep = false;
|
|
117
171
|
}
|
|
118
172
|
/** Append a surface-local line that is not part of the durable conversation. */
|
|
119
173
|
notice(text) {
|
|
@@ -141,9 +195,9 @@ export class TranscriptModel {
|
|
|
141
195
|
return;
|
|
142
196
|
case 'block-end': {
|
|
143
197
|
const block = asRecord(record.block);
|
|
144
|
-
// A settled block
|
|
145
|
-
|
|
146
|
-
|
|
198
|
+
// A settled reasoning block becomes a row now. A settled text block keeps
|
|
199
|
+
// its streamed text on screen, because dropping it would blank the answer
|
|
200
|
+
// on every repaint until the recorded message arrives.
|
|
147
201
|
if (block?.type === 'reasoning')
|
|
148
202
|
this.settleReasoning();
|
|
149
203
|
return;
|
|
@@ -176,11 +230,23 @@ export class TranscriptModel {
|
|
|
176
230
|
this.settled.push({ kind: 'notice', text: `error: ${text === '' ? 'agent failed' : text}` });
|
|
177
231
|
}
|
|
178
232
|
/**
|
|
179
|
-
* Settle
|
|
233
|
+
* Settle one thought into a row of its own.
|
|
180
234
|
*
|
|
181
235
|
* The row lands where the reasoning happened, which is before the answer it
|
|
182
236
|
* produced, so reading order still matches the order the model thought in.
|
|
183
237
|
*/
|
|
238
|
+
paintReasoning(text, ranFor) {
|
|
239
|
+
const timing = ranFor === undefined ? '' : ` · ${Math.max(1, Math.round(ranFor / 1000))}s`;
|
|
240
|
+
const cut = text.length > REASONING_CHAR_LIMIT ? `\n… truncated at ${REASONING_CHAR_LIMIT} chars` : '';
|
|
241
|
+
this.settled.push({
|
|
242
|
+
kind: 'reasoning',
|
|
243
|
+
summary: `reasoning · ${describeTokens(text)}${timing}`,
|
|
244
|
+
body: text.slice(0, REASONING_CHAR_LIMIT) + cut,
|
|
245
|
+
live: false,
|
|
246
|
+
});
|
|
247
|
+
this.reasoningPaintedThisStep = true;
|
|
248
|
+
}
|
|
249
|
+
/** Settle the reasoning streamed so far into a row of its own. */
|
|
184
250
|
settleReasoning() {
|
|
185
251
|
if (this.liveReasoning === '')
|
|
186
252
|
return;
|
|
@@ -188,14 +254,7 @@ export class TranscriptModel {
|
|
|
188
254
|
const ranFor = this.reasoningStartedAt === undefined ? undefined : this.now() - this.reasoningStartedAt;
|
|
189
255
|
this.liveReasoning = '';
|
|
190
256
|
this.reasoningStartedAt = undefined;
|
|
191
|
-
|
|
192
|
-
const cut = text.length > REASONING_CHAR_LIMIT ? `\n… truncated at ${REASONING_CHAR_LIMIT} chars` : '';
|
|
193
|
-
this.settled.push({
|
|
194
|
-
kind: 'reasoning',
|
|
195
|
-
summary: `reasoning · ${lines} line${lines === 1 ? '' : 's'} · ${text.length} chars${ranFor === undefined ? '' : ` · ${Math.max(1, Math.round(ranFor / 1000))}s`}`,
|
|
196
|
-
body: text.slice(0, REASONING_CHAR_LIMIT) + cut,
|
|
197
|
-
live: false,
|
|
198
|
-
});
|
|
257
|
+
this.paintReasoning(text, ranFor);
|
|
199
258
|
}
|
|
200
259
|
apply(event) {
|
|
201
260
|
const data = asRecord(event.data) ?? {};
|
|
@@ -211,6 +270,11 @@ export class TranscriptModel {
|
|
|
211
270
|
}
|
|
212
271
|
case 'turn/end': {
|
|
213
272
|
this.reportTurnEnd(asRecord(data.reason) ?? {});
|
|
273
|
+
// A turn that ended without a recorded message must not leave streamed
|
|
274
|
+
// text on screen as though it had settled.
|
|
275
|
+
this.live = '';
|
|
276
|
+
// A step that ended without a message cannot own the next one's thoughts.
|
|
277
|
+
this.reasoningPaintedThisStep = false;
|
|
214
278
|
return;
|
|
215
279
|
}
|
|
216
280
|
case 'agent-preset/selected': {
|
|
@@ -242,8 +306,17 @@ export class TranscriptModel {
|
|
|
242
306
|
return;
|
|
243
307
|
}
|
|
244
308
|
case 'assistant/message': {
|
|
245
|
-
const
|
|
309
|
+
const content = asRecord(data.message)?.content;
|
|
310
|
+
const text = textOfContent(content);
|
|
311
|
+
// Settle whatever the stream held, then decide whether the recorded
|
|
312
|
+
// thoughts are news or the same step arriving twice.
|
|
246
313
|
this.settleReasoning();
|
|
314
|
+
const painted = this.reasoningPaintedThisStep;
|
|
315
|
+
this.reasoningPaintedThisStep = false;
|
|
316
|
+
if (!painted) {
|
|
317
|
+
for (const thought of reasoningTextsOf(content))
|
|
318
|
+
this.paintReasoning(thought, undefined);
|
|
319
|
+
}
|
|
247
320
|
this.live = '';
|
|
248
321
|
if (text !== '')
|
|
249
322
|
this.settled.push({ kind: 'assistant', text });
|
|
@@ -260,7 +333,15 @@ export class TranscriptModel {
|
|
|
260
333
|
});
|
|
261
334
|
const callId = typeof data.callId === 'string' ? data.callId : '';
|
|
262
335
|
if (callId !== '')
|
|
263
|
-
this.pending.set(callId, { name, argumentsJson, index: this.settled.length - 1 });
|
|
336
|
+
this.pending.set(callId, { name, argumentsJson, index: this.settled.length - 1, subs: [] });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
case 'tool/ptc-dispatch-start': {
|
|
340
|
+
this.applySubCall(data, false);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
case 'tool/ptc-dispatch': {
|
|
344
|
+
this.applySubCall(data, true);
|
|
264
345
|
return;
|
|
265
346
|
}
|
|
266
347
|
case 'tool/result': {
|
|
@@ -271,6 +352,50 @@ export class TranscriptModel {
|
|
|
271
352
|
return;
|
|
272
353
|
}
|
|
273
354
|
}
|
|
355
|
+
/**
|
|
356
|
+
* Fold one nested PTC call onto the card of the run_code call that dispatched it.
|
|
357
|
+
*
|
|
358
|
+
* Dispatches are logged while the program runs, so the root is still pending;
|
|
359
|
+
* a dispatch whose root is not in this fold belongs to a window that starts
|
|
360
|
+
* mid-run, and attaching it to another card would claim a call that card
|
|
361
|
+
* never made.
|
|
362
|
+
*/
|
|
363
|
+
applySubCall(data, settled) {
|
|
364
|
+
const rootCallId = typeof data.rootCallId === 'string' ? data.rootCallId : '';
|
|
365
|
+
const subCallId = typeof data.subCallId === 'string' ? data.subCallId : '';
|
|
366
|
+
const root = rootCallId === '' ? undefined : this.pending.get(rootCallId);
|
|
367
|
+
if (root === undefined || subCallId === '')
|
|
368
|
+
return;
|
|
369
|
+
const entry = this.settled[root.index];
|
|
370
|
+
const card = entry !== undefined && entry.kind === 'tool' ? entry.card : undefined;
|
|
371
|
+
if (card === undefined)
|
|
372
|
+
return;
|
|
373
|
+
const known = this.pendingSub.get(subCallId);
|
|
374
|
+
if (known !== undefined) {
|
|
375
|
+
// A settle only restates the row its start already drew, and only when the
|
|
376
|
+
// call failed; nothing else about the row can change.
|
|
377
|
+
if (!settled || known.childIndex < 0 || data.isError !== true)
|
|
378
|
+
return;
|
|
379
|
+
const subCalls = (card.subCalls ?? []).map((call, at) => (at === known.childIndex ? { ...call, failed: true } : call));
|
|
380
|
+
this.settled[root.index] = { kind: 'tool', card: { ...card, subCalls } };
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const name = typeof data.name === 'string' ? data.name : 'tool';
|
|
384
|
+
const argumentsJson = argumentsJsonOf(data.arguments);
|
|
385
|
+
const call = { ...subCallOf(name, argumentsJson, this.presenter?.call(name, argumentsJson)), failed: settled && data.isError === true };
|
|
386
|
+
const kept = card.subCalls ?? [];
|
|
387
|
+
const total = (card.subCallsTotal ?? 0) + 1;
|
|
388
|
+
if (kept.length >= SUBCALL_MAX) {
|
|
389
|
+
// Retention keeps the head, where the calls that shaped the program are;
|
|
390
|
+
// the count still reports everything it dispatched.
|
|
391
|
+
this.pendingSub.set(subCallId, { rootIndex: root.index, childIndex: -1 });
|
|
392
|
+
this.settled[root.index] = { kind: 'tool', card: { ...card, subCallsTotal: total } };
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
root.subs.push(subCallId);
|
|
396
|
+
this.pendingSub.set(subCallId, { rootIndex: root.index, childIndex: kept.length });
|
|
397
|
+
this.settled[root.index] = { kind: 'tool', card: { ...card, subCalls: [...kept, call], subCallsTotal: total } };
|
|
398
|
+
}
|
|
274
399
|
settleToolResult(data) {
|
|
275
400
|
const message = asRecord(data.message);
|
|
276
401
|
const firstBlock = Array.isArray(message?.content) ? asRecord(message.content[0]) : undefined;
|
|
@@ -278,6 +403,11 @@ export class TranscriptModel {
|
|
|
278
403
|
const pending = callId === '' ? undefined : this.pending.get(callId);
|
|
279
404
|
if (callId !== '')
|
|
280
405
|
this.pending.delete(callId);
|
|
406
|
+
// The root is done dispatching, so its bookkeeping goes with it; the rows it
|
|
407
|
+
// already drew stay on the card.
|
|
408
|
+
if (pending !== undefined)
|
|
409
|
+
for (const sub of pending.subs)
|
|
410
|
+
this.pendingSub.delete(sub);
|
|
281
411
|
const name = pending?.name ?? 'tool';
|
|
282
412
|
const isError = message?.isError === true;
|
|
283
413
|
const result = this.presenter?.result(name, {
|
|
@@ -301,12 +431,10 @@ export class TranscriptModel {
|
|
|
301
431
|
// and a reader without it would see a tool row that never reported back.
|
|
302
432
|
const reported = contentLinesOf(message?.content);
|
|
303
433
|
const base = call ?? cardFromLines('generic', name, [], false);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
: { ...cardFromLines(base.kind, base.title, reported, isError) },
|
|
309
|
-
};
|
|
434
|
+
const rebuilt = reported.length === 0
|
|
435
|
+
? { ...base, failed: isError }
|
|
436
|
+
: { ...cardFromLines(base.kind, base.title, reported, isError), ...carriedFields(base) };
|
|
437
|
+
this.settled[pending.index] = { kind: 'tool', card: rebuilt };
|
|
310
438
|
return;
|
|
311
439
|
}
|
|
312
440
|
this.settled[pending.index] = { kind: 'tool', card: mergeCards(call, { ...result, failed: isError }) };
|