@sagmans/dsh-tui 0.1.1 → 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.
Files changed (94) hide show
  1. package/README.md +84 -10
  2. package/cordis.patch.yml +4 -2
  3. package/lib/agent/host.d.ts +14 -0
  4. package/lib/agent/host.d.ts.map +1 -1
  5. package/lib/agent/host.js +20 -4
  6. package/lib/agent/host.js.map +1 -1
  7. package/lib/agent/model.d.ts +24 -0
  8. package/lib/agent/model.d.ts.map +1 -1
  9. package/lib/agent/model.js +46 -1
  10. package/lib/agent/model.js.map +1 -1
  11. package/lib/agent/present.d.ts +7 -1
  12. package/lib/agent/present.d.ts.map +1 -1
  13. package/lib/agent/present.js +33 -4
  14. package/lib/agent/present.js.map +1 -1
  15. package/lib/agent/presets.d.ts +7 -1
  16. package/lib/agent/presets.d.ts.map +1 -1
  17. package/lib/agent/presets.js +7 -3
  18. package/lib/agent/presets.js.map +1 -1
  19. package/lib/cards.d.ts +127 -7
  20. package/lib/cards.d.ts.map +1 -1
  21. package/lib/cards.js +380 -52
  22. package/lib/cards.js.map +1 -1
  23. package/lib/export.d.ts +5 -4
  24. package/lib/export.d.ts.map +1 -1
  25. package/lib/export.js +62 -13
  26. package/lib/export.js.map +1 -1
  27. package/lib/fold-cursor.d.ts +23 -0
  28. package/lib/fold-cursor.d.ts.map +1 -0
  29. package/lib/fold-cursor.js +31 -0
  30. package/lib/fold-cursor.js.map +1 -0
  31. package/lib/index.d.ts +7 -1
  32. package/lib/index.d.ts.map +1 -1
  33. package/lib/index.js +310 -32
  34. package/lib/index.js.map +1 -1
  35. package/lib/input/submission.d.ts +3 -1
  36. package/lib/input/submission.d.ts.map +1 -1
  37. package/lib/input/submission.js +5 -2
  38. package/lib/input/submission.js.map +1 -1
  39. package/lib/terminal/warning-screen.d.ts +8 -0
  40. package/lib/terminal/warning-screen.d.ts.map +1 -0
  41. package/lib/terminal/warning-screen.js +66 -0
  42. package/lib/terminal/warning-screen.js.map +1 -0
  43. package/lib/theme-capability.d.ts +36 -0
  44. package/lib/theme-capability.d.ts.map +1 -0
  45. package/lib/theme-capability.js +190 -0
  46. package/lib/theme-capability.js.map +1 -0
  47. package/lib/theme-command.d.ts +10 -0
  48. package/lib/theme-command.d.ts.map +1 -0
  49. package/lib/theme-command.js +61 -0
  50. package/lib/theme-command.js.map +1 -0
  51. package/lib/theme-settings.d.ts +161 -0
  52. package/lib/theme-settings.d.ts.map +1 -0
  53. package/lib/theme-settings.js +166 -0
  54. package/lib/theme-settings.js.map +1 -0
  55. package/lib/theme-tokens.d.ts +110 -0
  56. package/lib/theme-tokens.d.ts.map +1 -0
  57. package/lib/theme-tokens.js +401 -0
  58. package/lib/theme-tokens.js.map +1 -0
  59. package/lib/theme.d.ts +32 -35
  60. package/lib/theme.d.ts.map +1 -1
  61. package/lib/theme.js +112 -63
  62. package/lib/theme.js.map +1 -1
  63. package/lib/tokens.d.ts +15 -0
  64. package/lib/tokens.d.ts.map +1 -0
  65. package/lib/tokens.js +29 -0
  66. package/lib/tokens.js.map +1 -0
  67. package/lib/transcript.d.ts +26 -1
  68. package/lib/transcript.d.ts.map +1 -1
  69. package/lib/transcript.js +153 -25
  70. package/lib/transcript.js.map +1 -1
  71. package/lib/ui/dock.d.ts.map +1 -1
  72. package/lib/ui/dock.js +67 -28
  73. package/lib/ui/dock.js.map +1 -1
  74. package/lib/ui/editor.d.ts +34 -0
  75. package/lib/ui/editor.d.ts.map +1 -0
  76. package/lib/ui/editor.js +98 -0
  77. package/lib/ui/editor.js.map +1 -0
  78. package/lib/ui/markdown.d.ts +8 -0
  79. package/lib/ui/markdown.d.ts.map +1 -1
  80. package/lib/ui/markdown.js +12 -0
  81. package/lib/ui/markdown.js.map +1 -1
  82. package/lib/ui/picker.d.ts +37 -0
  83. package/lib/ui/picker.d.ts.map +1 -1
  84. package/lib/ui/picker.js +53 -0
  85. package/lib/ui/picker.js.map +1 -1
  86. package/lib/ui/status.d.ts +3 -1
  87. package/lib/ui/status.d.ts.map +1 -1
  88. package/lib/ui/status.js +76 -24
  89. package/lib/ui/status.js.map +1 -1
  90. package/lib/ui/view.d.ts +43 -0
  91. package/lib/ui/view.d.ts.map +1 -1
  92. package/lib/ui/view.js +254 -54
  93. package/lib/ui/view.js.map +1 -1
  94. package/package.json +3 -1
package/lib/theme.js CHANGED
@@ -1,84 +1,133 @@
1
- const RESET = '\u001B[0m';
2
- function sgr(code, enabled) {
3
- return enabled ? text => `\u001B[${code}m${text}${RESET}` : text => text;
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
- * Style markdown through the same palette as the rest of the surface.
7
+ * Distinguishes one theme table from the next.
7
8
  *
8
- * Code blocks stay unstyled on purpose: a fenced block is already set apart by
9
- * its border and indentation, and coloring it would fight the reader's own
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.bold(style.accent(text)),
15
- link: text => style.underline(style.accent(text)),
16
- linkUrl: style.dim,
17
- code: style.accent,
18
- codeBlock: text => text,
19
- codeBlockBorder: style.dim,
20
- quote: style.dim,
21
- quoteBorder: style.dim,
22
- hr: style.dim,
23
- listBullet: style.accent,
24
- bold: style.bold,
25
- italic: style.italic,
26
- strikethrough: style.strike,
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
- function selectListTheme(dim, accent, bold) {
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
- selectedPrefix: accent,
33
- selectedText: bold,
34
- description: dim,
35
- scrollInfo: dim,
36
- noMatch: dim,
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
- * Whether styling is on, given the launch flag and the environment.
50
+ * Build the surface theme.
41
51
  *
42
- * `NO_COLOR` is a cross-tool convention (no-color.org): when it is present and
43
- * not empty, a program that colors by default must not, regardless of its own
44
- * flags. Honouring it means one environment variable turns styling off for
45
- * every tool in a session, which is how a reader actually wants it to work.
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 colorEnabled(requested, env = process.env) {
48
- const noColor = env.NO_COLOR;
49
- return requested && (noColor === undefined || noColor === '');
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
- * Build the surface theme.
96
+ * An editor theme that follows a moving source.
53
97
  *
54
- * Styling uses the standard 16 ANSI colors and terminal defaults rather than
55
- * true-color values, so a light or dark terminal remaps the interface on its
56
- * own and the surface needs no theme setting.
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 createTheme(color) {
59
- const dim = sgr('2', color);
60
- const bold = sgr('1', color);
61
- const accent = sgr('36', color);
62
- const warn = sgr('33', color);
63
- const added = sgr('32', color);
64
- const removed = sgr('31', color);
65
- const italic = sgr('3', color);
66
- const underline = sgr('4', color);
67
- const strike = sgr('9', color);
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
- color,
70
- glyphs: { user: '›', assistant: '⏺', tool: '⚒', notice: '·', reasoning: '▸', marker: '⧉' },
71
- dim,
72
- bold,
73
- tool: warn,
74
- notice: dim,
75
- added,
76
- removed,
77
- marker: dim,
78
- accent,
79
- italic,
80
- editor: { borderColor: dim, selectList: selectListTheme(dim, accent, bold) },
81
- markdown: markdownTheme({ dim, bold, accent, italic, underline, strike }),
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":"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"}
1
+ {"version":3,"file":"theme.js","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"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"}
@@ -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"}
@@ -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 the reasoning streamed so far into a row of its own.
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
@@ -1 +1 @@
1
- {"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../src/transcript.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAA;AAEzF,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;AAE1C;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CACxB;AA8DD;;;;;;;;GAQG;AACH,qBAAa,eAAe;IAQxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;IAC3B,kFAAkF;IAClF,OAAO,CAAC,QAAQ,CAAC,GAAG;IATtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,kBAAkB,CAAoB;gBAG3B,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;IAQb,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;IAwBtC,2FAA2F;IAC3F,OAAO,CAAC,aAAa;IAiBrB,8EAA8E;IAC9E,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAKjC;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAgBvB,KAAK,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAsEjC,OAAO,CAAC,gBAAgB;CAuCzB"}
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.length} chars${ranFor === undefined ? '' : ` · ${Math.max(1, Math.round(ranFor / 1000))}s`} · streaming`,
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 supersedes the transient text it streamed.
145
- if (block?.type === 'text')
146
- this.live = '';
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 the reasoning streamed so far into a row of its own.
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
- const lines = countLines(text);
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 text = textOfContent(asRecord(data.message)?.content);
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
- this.settled[pending.index] = {
305
- kind: 'tool',
306
- card: reported.length === 0
307
- ? { ...base, failed: isError }
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 }) };