@sagmans/dsh-tui 0.5.2 → 0.7.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/.agents/skills/dsh-tui-dogfood/SKILL.md +83 -0
- package/.agents/skills/dsh-tui-dogfood/references/home-state.md +78 -0
- package/.agents/skills/dsh-tui-dogfood/scripts/clone-links.mjs +133 -0
- package/.agents/skills/dsh-tui-dogfood/scripts/run-plugin-from-worktree.sh +417 -0
- package/.agents/skills/dsh-tui-update-models/SKILL.md +121 -0
- package/.agents/skills/dsh-tui-update-models/references/model-wiring.md +162 -0
- package/.agents/skills/dsh-tui-update-models/scripts/dump-model-catalog.mjs +178 -0
- package/README.md +131 -17
- package/lib/agent/host.d.ts +11 -0
- package/lib/agent/host.d.ts.map +1 -1
- package/lib/agent/host.js +16 -2
- package/lib/agent/host.js.map +1 -1
- package/lib/agent/present.d.ts.map +1 -1
- package/lib/agent/present.js +2 -1
- package/lib/agent/present.js.map +1 -1
- package/lib/agent/undo.d.ts +52 -0
- package/lib/agent/undo.d.ts.map +1 -0
- package/lib/agent/undo.js +106 -0
- package/lib/agent/undo.js.map +1 -0
- package/lib/cards/composition.d.ts +68 -0
- package/lib/cards/composition.d.ts.map +1 -0
- package/lib/cards/composition.js +117 -0
- package/lib/cards/composition.js.map +1 -0
- package/lib/cards/presenter.d.ts +23 -0
- package/lib/cards/presenter.d.ts.map +1 -0
- package/lib/cards/presenter.js +394 -0
- package/lib/cards/presenter.js.map +1 -0
- package/lib/cards/preview.d.ts +42 -0
- package/lib/cards/preview.d.ts.map +1 -0
- package/lib/cards/preview.js +51 -0
- package/lib/cards/preview.js.map +1 -0
- package/lib/cards.d.ts +42 -95
- package/lib/cards.d.ts.map +1 -1
- package/lib/cards.js +4 -526
- package/lib/cards.js.map +1 -1
- package/lib/compat/section.d.ts +29 -0
- package/lib/compat/section.d.ts.map +1 -0
- package/lib/compat/section.js +82 -0
- package/lib/compat/section.js.map +1 -0
- package/lib/config.d.ts +27 -2
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +61 -1
- package/lib/config.js.map +1 -1
- package/lib/contracts.d.ts +28 -0
- package/lib/contracts.d.ts.map +1 -1
- package/lib/fold-cursor.d.ts +8 -0
- package/lib/fold-cursor.d.ts.map +1 -1
- package/lib/fold-cursor.js +18 -0
- package/lib/fold-cursor.js.map +1 -1
- package/lib/gates/question-card.d.ts +77 -0
- package/lib/gates/question-card.d.ts.map +1 -0
- package/lib/gates/question-card.js +113 -0
- package/lib/gates/question-card.js.map +1 -0
- package/lib/gates/questions.d.ts +127 -0
- package/lib/gates/questions.d.ts.map +1 -0
- package/lib/gates/questions.js +418 -0
- package/lib/gates/questions.js.map +1 -0
- package/lib/gates.d.ts +1 -113
- package/lib/gates.d.ts.map +1 -1
- package/lib/gates.js +1 -484
- package/lib/gates.js.map +1 -1
- package/lib/herdr/reporter.d.ts +9 -4
- package/lib/herdr/reporter.d.ts.map +1 -1
- package/lib/herdr/reporter.js +5 -9
- package/lib/herdr/reporter.js.map +1 -1
- package/lib/herdr/state.d.ts +35 -4
- package/lib/herdr/state.d.ts.map +1 -1
- package/lib/herdr/state.js +37 -4
- package/lib/herdr/state.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +343 -2267
- package/lib/index.js.map +1 -1
- package/lib/input/action-catalog.d.ts +74 -0
- package/lib/input/action-catalog.d.ts.map +1 -0
- package/lib/input/action-catalog.js +137 -0
- package/lib/input/action-catalog.js.map +1 -0
- package/lib/input/actions.d.ts +5 -81
- package/lib/input/actions.d.ts.map +1 -1
- package/lib/input/actions.js +4 -621
- package/lib/input/actions.js.map +1 -1
- package/lib/input/completion.d.ts +11 -1
- package/lib/input/completion.d.ts.map +1 -1
- package/lib/input/completion.js +17 -1
- package/lib/input/completion.js.map +1 -1
- package/lib/input/file-index.d.ts +40 -0
- package/lib/input/file-index.d.ts.map +1 -0
- package/lib/input/file-index.js +141 -0
- package/lib/input/file-index.js.map +1 -0
- package/lib/input/file-search.d.ts +1 -38
- package/lib/input/file-search.d.ts.map +1 -1
- package/lib/input/file-search.js +1 -487
- package/lib/input/file-search.js.map +1 -1
- package/lib/input/key-press.d.ts +57 -0
- package/lib/input/key-press.d.ts.map +1 -0
- package/lib/input/key-press.js +218 -0
- package/lib/input/key-press.js.map +1 -0
- package/lib/input/keymap-conflicts.d.ts +59 -0
- package/lib/input/keymap-conflicts.d.ts.map +1 -0
- package/lib/input/keymap-conflicts.js +292 -0
- package/lib/input/keymap-conflicts.js.map +1 -0
- package/lib/input/keymap-settings.js +1 -1
- package/lib/input/keymap-settings.js.map +1 -1
- package/lib/input/keymap.d.ts.map +1 -1
- package/lib/input/keymap.js +5 -1
- package/lib/input/keymap.js.map +1 -1
- package/lib/input/submission.d.ts +11 -1
- package/lib/input/submission.d.ts.map +1 -1
- package/lib/input/submission.js +10 -1
- package/lib/input/submission.js.map +1 -1
- package/lib/input/workspace-files.d.ts +10 -0
- package/lib/input/workspace-files.d.ts.map +1 -0
- package/lib/input/workspace-files.js +359 -0
- package/lib/input/workspace-files.js.map +1 -0
- package/lib/install-skills.d.ts +20 -0
- package/lib/install-skills.d.ts.map +1 -0
- package/lib/install-skills.js +198 -0
- package/lib/install-skills.js.map +1 -0
- package/lib/keys-command.d.ts +2 -1
- package/lib/keys-command.d.ts.map +1 -1
- package/lib/keys-command.js +2 -1
- package/lib/keys-command.js.map +1 -1
- package/lib/model-list.d.ts +37 -0
- package/lib/model-list.d.ts.map +1 -0
- package/lib/model-list.js +60 -0
- package/lib/model-list.js.map +1 -0
- package/lib/startup.d.ts +8 -0
- package/lib/startup.d.ts.map +1 -1
- package/lib/startup.js +120 -24
- package/lib/startup.js.map +1 -1
- package/lib/subagents.d.ts +7 -3
- package/lib/subagents.d.ts.map +1 -1
- package/lib/subagents.js +28 -4
- package/lib/subagents.js.map +1 -1
- package/lib/surface/appearance-preferences.d.ts +26 -0
- package/lib/surface/appearance-preferences.d.ts.map +1 -0
- package/lib/surface/appearance-preferences.js +139 -0
- package/lib/surface/appearance-preferences.js.map +1 -0
- package/lib/surface/appearance.d.ts +87 -0
- package/lib/surface/appearance.d.ts.map +1 -0
- package/lib/surface/appearance.js +353 -0
- package/lib/surface/appearance.js.map +1 -0
- package/lib/surface/background-work.d.ts +49 -0
- package/lib/surface/background-work.d.ts.map +1 -0
- package/lib/surface/background-work.js +146 -0
- package/lib/surface/background-work.js.map +1 -0
- package/lib/surface/commands.d.ts +80 -0
- package/lib/surface/commands.d.ts.map +1 -0
- package/lib/surface/commands.js +364 -0
- package/lib/surface/commands.js.map +1 -0
- package/lib/surface/modal-input.d.ts +58 -0
- package/lib/surface/modal-input.d.ts.map +1 -0
- package/lib/surface/modal-input.js +237 -0
- package/lib/surface/modal-input.js.map +1 -0
- package/lib/surface/model-choice.d.ts +54 -0
- package/lib/surface/model-choice.d.ts.map +1 -0
- package/lib/surface/model-choice.js +325 -0
- package/lib/surface/model-choice.js.map +1 -0
- package/lib/surface/preset-choice.d.ts +78 -0
- package/lib/surface/preset-choice.d.ts.map +1 -0
- package/lib/surface/preset-choice.js +163 -0
- package/lib/surface/preset-choice.js.map +1 -0
- package/lib/surface/prompt-input.d.ts +97 -0
- package/lib/surface/prompt-input.d.ts.map +1 -0
- package/lib/surface/prompt-input.js +215 -0
- package/lib/surface/prompt-input.js.map +1 -0
- package/lib/surface/prompt-memory.d.ts +76 -0
- package/lib/surface/prompt-memory.d.ts.map +1 -0
- package/lib/surface/prompt-memory.js +167 -0
- package/lib/surface/prompt-memory.js.map +1 -0
- package/lib/surface/session-lifecycle.d.ts +120 -0
- package/lib/surface/session-lifecycle.d.ts.map +1 -0
- package/lib/surface/session-lifecycle.js +321 -0
- package/lib/surface/session-lifecycle.js.map +1 -0
- package/lib/surface/session-picker.d.ts +39 -0
- package/lib/surface/session-picker.d.ts.map +1 -0
- package/lib/surface/session-picker.js +107 -0
- package/lib/surface/session-picker.js.map +1 -0
- package/lib/surface/session-view.d.ts +86 -0
- package/lib/surface/session-view.d.ts.map +1 -0
- package/lib/surface/session-view.js +215 -0
- package/lib/surface/session-view.js.map +1 -0
- package/lib/surface/staged-turns.d.ts +81 -0
- package/lib/surface/staged-turns.d.ts.map +1 -0
- package/lib/surface/staged-turns.js +206 -0
- package/lib/surface/staged-turns.js.map +1 -0
- package/lib/surface/terminal-lifecycle.d.ts +76 -0
- package/lib/surface/terminal-lifecycle.d.ts.map +1 -0
- package/lib/surface/terminal-lifecycle.js +222 -0
- package/lib/surface/terminal-lifecycle.js.map +1 -0
- package/lib/terminal/warning-screen.d.ts +8 -1
- package/lib/terminal/warning-screen.d.ts.map +1 -1
- package/lib/terminal/warning-screen.js +9 -0
- package/lib/terminal/warning-screen.js.map +1 -1
- package/lib/terminal-text/scan.d.ts +44 -0
- package/lib/terminal-text/scan.d.ts.map +1 -0
- package/lib/terminal-text/scan.js +195 -0
- package/lib/terminal-text/scan.js.map +1 -0
- package/lib/terminal-text/sgr.d.ts +32 -0
- package/lib/terminal-text/sgr.d.ts.map +1 -0
- package/lib/terminal-text/sgr.js +203 -0
- package/lib/terminal-text/sgr.js.map +1 -0
- package/lib/terminal-text.d.ts +1 -1
- package/lib/terminal-text.d.ts.map +1 -1
- package/lib/terminal-text.js +2 -379
- package/lib/terminal-text.js.map +1 -1
- package/lib/theme-command.js +2 -1
- package/lib/theme-command.js.map +1 -1
- package/lib/theme-defaults.d.ts +64 -0
- package/lib/theme-defaults.d.ts.map +1 -0
- package/lib/theme-defaults.js +248 -0
- package/lib/theme-defaults.js.map +1 -0
- package/lib/theme-resolver.d.ts +36 -0
- package/lib/theme-resolver.d.ts.map +1 -0
- package/lib/theme-resolver.js +112 -0
- package/lib/theme-resolver.js.map +1 -0
- package/lib/theme-schema.d.ts +6 -6
- package/lib/theme-schema.d.ts.map +1 -1
- package/lib/theme-schema.js +3 -2
- package/lib/theme-schema.js.map +1 -1
- package/lib/theme-settings.d.ts +13 -15
- package/lib/theme-settings.d.ts.map +1 -1
- package/lib/theme-settings.js +27 -19
- package/lib/theme-settings.js.map +1 -1
- package/lib/theme-tokens.d.ts +1 -90
- package/lib/theme-tokens.d.ts.map +1 -1
- package/lib/theme-tokens.js +11 -341
- package/lib/theme-tokens.js.map +1 -1
- package/lib/theme.d.ts +1 -1
- package/lib/theme.d.ts.map +1 -1
- package/lib/theme.js +2 -1
- package/lib/theme.js.map +1 -1
- package/lib/tool-display.d.ts.map +1 -1
- package/lib/tool-display.js +2 -1
- package/lib/tool-display.js.map +1 -1
- package/lib/transcript/message-content.d.ts +21 -0
- package/lib/transcript/message-content.d.ts.map +1 -0
- package/lib/transcript/message-content.js +61 -0
- package/lib/transcript/message-content.js.map +1 -0
- package/lib/transcript/tool-calls.d.ts +106 -0
- package/lib/transcript/tool-calls.d.ts.map +1 -0
- package/lib/transcript/tool-calls.js +254 -0
- package/lib/transcript/tool-calls.js.map +1 -0
- package/lib/transcript.d.ts +38 -43
- package/lib/transcript.d.ts.map +1 -1
- package/lib/transcript.js +58 -245
- package/lib/transcript.js.map +1 -1
- package/lib/ui/copy.d.ts +17 -0
- package/lib/ui/copy.d.ts.map +1 -0
- package/lib/ui/copy.js +104 -0
- package/lib/ui/copy.js.map +1 -0
- package/lib/ui/dock.d.ts +35 -3
- package/lib/ui/dock.d.ts.map +1 -1
- package/lib/ui/dock.js +124 -23
- package/lib/ui/dock.js.map +1 -1
- package/lib/ui/editor.d.ts.map +1 -1
- package/lib/ui/editor.js +2 -1
- package/lib/ui/editor.js.map +1 -1
- package/lib/ui/frame.d.ts +31 -0
- package/lib/ui/frame.d.ts.map +1 -1
- package/lib/ui/frame.js +27 -4
- package/lib/ui/frame.js.map +1 -1
- package/lib/ui/keymap-picker.d.ts +2 -1
- package/lib/ui/keymap-picker.d.ts.map +1 -1
- package/lib/ui/keymap-picker.js +1 -0
- package/lib/ui/keymap-picker.js.map +1 -1
- package/lib/ui/view/gate-card.d.ts +21 -0
- package/lib/ui/view/gate-card.d.ts.map +1 -0
- package/lib/ui/view/gate-card.js +104 -0
- package/lib/ui/view/gate-card.js.map +1 -0
- package/lib/ui/view/tool-card.d.ts +138 -0
- package/lib/ui/view/tool-card.d.ts.map +1 -0
- package/lib/ui/view/tool-card.js +413 -0
- package/lib/ui/view/tool-card.js.map +1 -0
- package/lib/ui/view/transcript-message.d.ts +62 -0
- package/lib/ui/view/transcript-message.d.ts.map +1 -0
- package/lib/ui/view/transcript-message.js +158 -0
- package/lib/ui/view/transcript-message.js.map +1 -0
- package/lib/ui/view.d.ts +87 -156
- package/lib/ui/view.d.ts.map +1 -1
- package/lib/ui/view.js +122 -556
- package/lib/ui/view.js.map +1 -1
- package/package.json +3 -1
- package/themes/deepseek-blue.yaml +9 -0
- package/themes/violet-orbit.yaml +7 -0
package/lib/ui/view.js
CHANGED
|
@@ -1,92 +1,22 @@
|
|
|
1
|
-
import { visibleWidth, wrapTextWithAnsi, } from '@earendil-works/pi-tui';
|
|
2
|
-
import { cardDetailRows, clip, oneLine, shellFoldHint, shellRetentionHint } from "../cards.js";
|
|
3
|
-
import { defaultKeymap, hintKeys } from "../input/actions.js";
|
|
4
|
-
import { CUSTOM_ROW_NUMBER } from "../gates.js";
|
|
5
|
-
import { DEFAULT_TOOL_DISPLAY } from "../tool-display.js";
|
|
6
|
-
import { CARD_ROW_TOKEN } from "../theme-tokens.js";
|
|
7
|
-
import { codeBlockLines } from "./diff.js";
|
|
8
|
-
import { canFrame, frameLines, FRAME_COLUMNS, textWidth } from "./frame.js";
|
|
9
|
-
import { ANSWER_FACE } from "./markdown.js";
|
|
10
|
-
import { pickerCardLines } from "./picker-card.js";
|
|
11
1
|
import { RowCache } from "./rows.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const QUESTION_MARK = '?';
|
|
28
|
-
const CHECKBOX_ON = '[x]';
|
|
29
|
-
const CHECKBOX_OFF = '[ ]';
|
|
30
|
-
/** A row that a card's kind does not map draws as generic detail. */
|
|
31
|
-
const FALLBACK_ROW_TOKEN = 'tool.detail';
|
|
32
|
-
/** An unselected row has no cursor, and a blank column is not a value to configure. */
|
|
33
|
-
const NO_CURSOR = ' ';
|
|
34
|
-
/** The words an opened card uses when retention, not the fold, dropped rows. */
|
|
35
|
-
const CARD_HINT_RETAINED = 'more lines not shown';
|
|
2
|
+
import {} from "./picker.js";
|
|
3
|
+
import { pickerCardLines } from "./picker-card.js";
|
|
4
|
+
import { ANSWER_FACE } from "./markdown.js";
|
|
5
|
+
import {} from "./frame.js";
|
|
6
|
+
import {} from "../theme.js";
|
|
7
|
+
import {} from "../theme-tokens.js";
|
|
8
|
+
import { DEFAULT_TOOL_DISPLAY } from "../tool-display.js";
|
|
9
|
+
import { SECOND_MS } from "../transcript/tool-calls.js";
|
|
10
|
+
import {} from "../transcript.js";
|
|
11
|
+
import {} from "../gates.js";
|
|
12
|
+
import { defaultKeymap, hintKeys } from "../input/actions.js";
|
|
13
|
+
import { visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
|
|
14
|
+
import { GateCards } from "./view/gate-card.js";
|
|
15
|
+
import { Messages } from "./view/transcript-message.js";
|
|
16
|
+
import { ToolCards } from "./view/tool-card.js";
|
|
36
17
|
/** What a hint names when the reader has unbound the key it would advertise. */
|
|
37
18
|
const REASONING_FOLD_FALLBACK = 'shift+tab';
|
|
38
19
|
const CARD_OPEN_FALLBACK = 'ctrl+o';
|
|
39
|
-
/** What separates a card's header from its measured facts, and the facts from each other. */
|
|
40
|
-
const STAT_LEAD = ' ';
|
|
41
|
-
const STAT_SEPARATOR = ' · ';
|
|
42
|
-
/** The symbol that says what a fact counts; a size needs none. */
|
|
43
|
-
const STAT_SYMBOL = {
|
|
44
|
-
added: '+',
|
|
45
|
-
changed: '~',
|
|
46
|
-
removed: '-',
|
|
47
|
-
size: '',
|
|
48
|
-
};
|
|
49
|
-
/** Which colour draws each fact, so added, changed, and removed never share one. */
|
|
50
|
-
const STAT_TOKEN = {
|
|
51
|
-
added: 'tool.stat.added',
|
|
52
|
-
changed: 'tool.stat.changed',
|
|
53
|
-
removed: 'tool.stat.removed',
|
|
54
|
-
size: 'tool.stat.size',
|
|
55
|
-
};
|
|
56
|
-
/**
|
|
57
|
-
* A markdown theme that keeps every element in one token.
|
|
58
|
-
*
|
|
59
|
-
* A thought is deliberately recessive, and the answer's theme would let a
|
|
60
|
-
* heading or a link inside it outshine the answer it produced. The structure
|
|
61
|
-
* still shows because markdown draws it around the text — bullets, fences,
|
|
62
|
-
* indents, table rules — rather than in the text's own colour. A fenced diff is
|
|
63
|
-
* the one block that does not recede: red and green are what the fence means
|
|
64
|
-
* rather than how loudly it is drawn, and a thought showing a change is the
|
|
65
|
-
* place the reader most needs to see which side of it moved.
|
|
66
|
-
*/
|
|
67
|
-
function recessiveMarkdownTheme(style, theme) {
|
|
68
|
-
return {
|
|
69
|
-
heading: style,
|
|
70
|
-
link: style,
|
|
71
|
-
linkUrl: style,
|
|
72
|
-
code: style,
|
|
73
|
-
codeBlock: style,
|
|
74
|
-
codeBlockBorder: style,
|
|
75
|
-
quote: style,
|
|
76
|
-
quoteBorder: style,
|
|
77
|
-
hr: style,
|
|
78
|
-
listBullet: style,
|
|
79
|
-
bold: style,
|
|
80
|
-
italic: style,
|
|
81
|
-
strikethrough: style,
|
|
82
|
-
underline: style,
|
|
83
|
-
highlightCode: (code, lang) => codeBlockLines(code, lang, {
|
|
84
|
-
style: (token, text) => theme.style(token, text),
|
|
85
|
-
visible: token => theme.visible(token),
|
|
86
|
-
plain: style,
|
|
87
|
-
}),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
20
|
/**
|
|
91
21
|
* The state a reader gets before opening anything, and the view's own fallback.
|
|
92
22
|
*
|
|
@@ -100,8 +30,6 @@ export const DEFAULT_VIEW_STATE = { expandCards: false, expandReasoning: false,
|
|
|
100
30
|
const toolClickKey = (id) => (id === '' ? undefined : `tool:${id}`);
|
|
101
31
|
const reasoningClickKey = (id) => (id === '' ? undefined : `reason:${id}`);
|
|
102
32
|
const subCallClickKey = (parentId, id) => `sub:${parentId}/${id}`;
|
|
103
|
-
/** The column a one-line tool row must not pass, so a cut never reaches the edge. */
|
|
104
|
-
const collapsedEdge = (width) => Math.max(1, width - COLLAPSED_EDGE_PADDING);
|
|
105
33
|
export class TranscriptView {
|
|
106
34
|
model;
|
|
107
35
|
theme;
|
|
@@ -109,23 +37,45 @@ export class TranscriptView {
|
|
|
109
37
|
options;
|
|
110
38
|
rows;
|
|
111
39
|
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
40
|
+
* Fold choices the reader made by clicking, per message.
|
|
41
|
+
*
|
|
42
|
+
* Absent means the key or the tool's own policy decides, so a settings edit
|
|
43
|
+
* stays in charge of messages nobody clicked. Keyed by the call or thought id
|
|
44
|
+
* rather than by the entry, because a result replaces the entry a click was
|
|
45
|
+
* made on and a thought outlives the live row that streamed it.
|
|
46
|
+
*/
|
|
119
47
|
clicked = new Map();
|
|
120
48
|
/** The rows each clickable message drew last render, for mapping a click back to it. */
|
|
121
49
|
spans = [];
|
|
122
50
|
/** The same spans relative to their entry, so a cached entry still answers clicks. */
|
|
123
51
|
entrySpans = new WeakMap();
|
|
52
|
+
/**
|
|
53
|
+
* The framed rows each entry drew last render, for reading a copy back out.
|
|
54
|
+
*
|
|
55
|
+
* Relative to the entry for the same reason the spans are: a cached entry draws
|
|
56
|
+
* no rows this frame and still has to answer for what it drew before.
|
|
57
|
+
*/
|
|
58
|
+
entryCopy = new WeakMap();
|
|
59
|
+
/**
|
|
60
|
+
* The framed rows the in-flight entries drew this frame.
|
|
61
|
+
*
|
|
62
|
+
* They cannot be kept by entry the way the settled ones are: every frame builds
|
|
63
|
+
* a live entry as a fresh object, so a map keyed by it would never answer. A copy
|
|
64
|
+
* is read against the last frame, so it is the render that keeps them.
|
|
65
|
+
*/
|
|
66
|
+
liveCopy = [];
|
|
67
|
+
cards;
|
|
68
|
+
messages;
|
|
69
|
+
gates;
|
|
70
|
+
/** The row renderers, each told only what it draws, so the view stays the one owner of view state and row caches. */
|
|
124
71
|
constructor(model, theme, markdown, options = {}) {
|
|
125
72
|
this.model = model;
|
|
126
73
|
this.theme = theme;
|
|
127
74
|
this.markdown = markdown;
|
|
128
75
|
this.options = options;
|
|
76
|
+
this.cards = new ToolCards({ theme: this.theme, state: () => this.viewState, keymap: () => this.keymap(), toolDisplay: tool => this.toolDisplay(tool), expansionOf: entry => this.expansionOf(entry), liveCall: callId => this.model.liveCall(callId), cardOpenHint: () => hintKeys(this.keymap(), 'surface.toolDetail') || CARD_OPEN_FALLBACK, toolKey: id => toolClickKey(id), subCallKey: (parentId, id) => subCallClickKey(parentId, id), subCallOpen: (parentId, id) => this.subCallOpen(parentId, id) });
|
|
77
|
+
this.messages = new Messages({ theme: this.theme, markdown: this.markdown, reasoningOpen: entry => this.reasoningOpen(entry), reasoningFoldHint: () => this.reasoningFoldHint(), reasoningKey: id => reasoningClickKey(id), pushWrapped: (lines, text, width, prefix, token) => this.pushWrapped(lines, text, width, prefix, token) });
|
|
78
|
+
this.gates = new GateCards({ theme: this.theme, pushWrapped: (lines, text, width, prefix, token) => this.pushWrapped(lines, text, width, prefix, token) });
|
|
129
79
|
this.rows = options.rows ?? new RowCache();
|
|
130
80
|
}
|
|
131
81
|
get viewState() {
|
|
@@ -136,12 +86,12 @@ export class TranscriptView {
|
|
|
136
86
|
return this.options.toolDisplay?.(tool) ?? DEFAULT_TOOL_DISPLAY;
|
|
137
87
|
}
|
|
138
88
|
/**
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
89
|
+
* Whether one tool message draws open.
|
|
90
|
+
*
|
|
91
|
+
* A click outranks the key, which outranks the tool's start state: Ctrl+O
|
|
92
|
+
* stays "show me everything", while one clicked message keeps the state the
|
|
93
|
+
* reader gave it.
|
|
94
|
+
*/
|
|
145
95
|
expansionOf(entry) {
|
|
146
96
|
const key = toolClickKey(entry.id);
|
|
147
97
|
const clicked = key === undefined ? undefined : this.clicked.get(key);
|
|
@@ -158,14 +108,14 @@ export class TranscriptView {
|
|
|
158
108
|
return this.clicked.get(subCallClickKey(parentId, id)) ?? false;
|
|
159
109
|
}
|
|
160
110
|
/**
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
111
|
+
* Answer a click on a message by folding or unfolding that one row.
|
|
112
|
+
*
|
|
113
|
+
* The tightest span under the point wins, so a click on a dispatched call
|
|
114
|
+
* opens that call rather than the card around it. Only a left click is
|
|
115
|
+
* consumed: a press, a drag, and a wheel belong to the surface's own
|
|
116
|
+
* selection and scrolling, and a row that swallowed them would cost the
|
|
117
|
+
* reader the ability to copy the command it just drew.
|
|
118
|
+
*/
|
|
169
119
|
handleMouse(event) {
|
|
170
120
|
if (event.type !== 'click' || event.button !== 'left')
|
|
171
121
|
return undefined;
|
|
@@ -191,13 +141,13 @@ export class TranscriptView {
|
|
|
191
141
|
this.rows.clear();
|
|
192
142
|
}
|
|
193
143
|
/**
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
144
|
+
* Wrap one text block under a prefix, keeping the prefix's column budget.
|
|
145
|
+
*
|
|
146
|
+
* The block is drawn before it is wrapped: a sequence a terminal would have
|
|
147
|
+
* acted on has to be gone before pi-tui measures the row, and a tab has to
|
|
148
|
+
* land on the stop the writer saw rather than one a wrapper would guess at.
|
|
149
|
+
* The prefix seeds that stop, because it is already on the row.
|
|
150
|
+
*/
|
|
201
151
|
pushWrapped(lines, text, width, prefix, token) {
|
|
202
152
|
const lead = visibleWidth(prefix);
|
|
203
153
|
const indent = ' '.repeat(lead);
|
|
@@ -206,85 +156,6 @@ export class TranscriptView {
|
|
|
206
156
|
lines.push(this.theme.cut(`${index === 0 ? prefix : indent}${line}`, width, ''));
|
|
207
157
|
});
|
|
208
158
|
}
|
|
209
|
-
/**
|
|
210
|
-
* Wrap one already-styled block under a prefix.
|
|
211
|
-
*
|
|
212
|
-
* A card header mixes tokens — title, argument, stats — in a single string, so
|
|
213
|
-
* it cannot go through {@link pushWrapped}, which escapes and restyles plain
|
|
214
|
-
* text. Its width still has to fold at the screen edge rather than be cut,
|
|
215
|
-
* because the argument is the part a reader scans for and a silently dropped
|
|
216
|
-
* command is worse than a taller card.
|
|
217
|
-
*/
|
|
218
|
-
pushStyledWrapped(lines, content, width, prefix) {
|
|
219
|
-
const lead = visibleWidth(prefix);
|
|
220
|
-
const indent = ' '.repeat(lead);
|
|
221
|
-
const wrapped = wrapTextWithAnsi(content, Math.max(1, width - lead));
|
|
222
|
-
wrapped.forEach((line, index) => {
|
|
223
|
-
lines.push(this.theme.cut(`${index === 0 ? prefix : indent}${line}`, width, ''));
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* The rows one markdown message draws, with the padding pi-tui adds for background styling removed.
|
|
228
|
-
*
|
|
229
|
-
* The source is drawn first so the markdown renderer never measures a sequence
|
|
230
|
-
* it cannot see; its own spans are applied around the result afterwards.
|
|
231
|
-
*/
|
|
232
|
-
markdownLines(text, width, live, face, column = 0) {
|
|
233
|
-
return this.markdown
|
|
234
|
-
.render(this.theme.rich(text, { column }), Math.max(1, width), live, face)
|
|
235
|
-
.map(line => line.replace(/[ \t]+$/u, ''));
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Render one message's text as markdown.
|
|
239
|
-
*
|
|
240
|
-
* A thought passes its own face and an indent, so the row stays visibly a
|
|
241
|
-
* detail of the step that produced it rather than a second answer.
|
|
242
|
-
*/
|
|
243
|
-
pushMarkdown(lines, text, width, live, face = ANSWER_FACE, indent = '') {
|
|
244
|
-
const lead = visibleWidth(indent);
|
|
245
|
-
for (const line of this.markdownLines(text, Math.max(1, width - lead), live, face, lead)) {
|
|
246
|
-
// A blank markdown line stays blank: it shows nothing, so it holds nothing.
|
|
247
|
-
if (line === '') {
|
|
248
|
-
lines.push('');
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
lines.push(this.theme.cut(`${indent}${line}`, width, '…'));
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* One message closed into a frame, with its markdown laid out to what is left inside.
|
|
256
|
-
*
|
|
257
|
-
* A prompt and a reply are the two objects of an exchange, so both are drawn as
|
|
258
|
-
* bars rather than as one more stretch of rows; the face, the border's element,
|
|
259
|
-
* and whether the text is still arriving are the only differences. Markdown lays
|
|
260
|
-
* itself out to the frame's text width, so the frame may only place the rows:
|
|
261
|
-
* wrapping them again would break what it drew.
|
|
262
|
-
*/
|
|
263
|
-
pushFramed(lines, text, width, live, face, borderToken) {
|
|
264
|
-
const framed = canFrame(width, this.theme.visible(borderToken));
|
|
265
|
-
const inside = framed ? width - FRAME_COLUMNS : width;
|
|
266
|
-
const body = this.markdownLines(text, textWidth(inside), live, face);
|
|
267
|
-
lines.push(...frameLines(body, width, {
|
|
268
|
-
text: line => line,
|
|
269
|
-
border: rule => this.theme.style(borderToken, rule),
|
|
270
|
-
framed,
|
|
271
|
-
}));
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* The face a thought is drawn in.
|
|
275
|
-
*
|
|
276
|
-
* Its shade stays the thought body's and it asks for no drawing: a diagram in
|
|
277
|
-
* the middle of a thought would carry the answer's weight, and the answer's
|
|
278
|
-
* colours would make the thinking compete with it.
|
|
279
|
-
*/
|
|
280
|
-
reasoningFace() {
|
|
281
|
-
const body = (text) => this.theme.style('transcript.reasoning.body', text);
|
|
282
|
-
return { name: 'reasoning', base: { color: body }, theme: recessiveMarkdownTheme(body, this.theme), transform: false };
|
|
283
|
-
}
|
|
284
|
-
/** The face a submitted prompt is drawn in: its structure is markdown's, its shade stays the prompt's. */
|
|
285
|
-
userFace() {
|
|
286
|
-
return { name: 'user', base: { color: text => this.theme.style('transcript.user', text) } };
|
|
287
|
-
}
|
|
288
159
|
/** The live map, or the shipped one for a caller that did not lend one. */
|
|
289
160
|
keymap() {
|
|
290
161
|
return this.options.keys?.() ?? defaultKeymap();
|
|
@@ -292,277 +163,21 @@ export class TranscriptView {
|
|
|
292
163
|
reasoningFoldHint() {
|
|
293
164
|
return hintKeys(this.keymap(), 'surface.reasoning') || REASONING_FOLD_FALLBACK;
|
|
294
165
|
}
|
|
295
|
-
pushReasoning(lines, entry, width, spans) {
|
|
296
|
-
if (!this.theme.visible('transcript.reasoning.summary'))
|
|
297
|
-
return;
|
|
298
|
-
const start = lines.length;
|
|
299
|
-
const open = this.reasoningOpen(entry);
|
|
300
|
-
const glyph = this.theme.glyph('transcript.reasoning.summary');
|
|
301
|
-
const lead = glyph === '' ? '' : `${glyph} `;
|
|
302
|
-
// A folded row carries the key that opens it, because a count with no way to
|
|
303
|
-
// reach the text reads the same as the text never having arrived. The key
|
|
304
|
-
// rides the row rather than a line of its own, so naming the hidden body
|
|
305
|
-
// costs no vertical space.
|
|
306
|
-
const suffix = !open && entry.body !== '' && this.theme.visible('transcript.reasoning.hint')
|
|
307
|
-
? ` (${this.reasoningFoldHint()})`
|
|
308
|
-
: '';
|
|
309
|
-
// The key is kept whole: the summary is the part that gives up room.
|
|
310
|
-
const room = Math.max(1, width - visibleWidth(suffix));
|
|
311
|
-
const summary = this.theme.cut(this.theme.rich(`${lead}${entry.summary}`, { token: 'transcript.reasoning.summary', column: visibleWidth(lead) }), room, '');
|
|
312
|
-
const lined = suffix === '' ? summary : `${summary}${this.theme.style('transcript.reasoning.hint', suffix)}`;
|
|
313
|
-
// The key is kept whole only while the row has room for it: a row wider than
|
|
314
|
-
// the surface loses its tail to the terminal, and the terminal's clamp is not
|
|
315
|
-
// one this component can count on.
|
|
316
|
-
lines.push(this.theme.cut(lined, width, '…'));
|
|
317
|
-
if (open && this.theme.visible('transcript.reasoning.body')) {
|
|
318
|
-
this.pushMarkdown(lines, entry.body, width, entry.live, this.reasoningFace(), DETAIL_INDENT);
|
|
319
|
-
}
|
|
320
|
-
const key = reasoningClickKey(entry.id);
|
|
321
|
-
if (key !== undefined)
|
|
322
|
-
spans.push({ key, start, end: lines.length, expanded: open });
|
|
323
|
-
}
|
|
324
|
-
pushCard(lines, entry, width, spans) {
|
|
325
|
-
const card = entry.card;
|
|
326
|
-
const spec = this.toolDisplay(card.tool);
|
|
327
|
-
const expanded = this.expansionOf(entry);
|
|
328
|
-
const start = lines.length;
|
|
329
|
-
// What a folded card shows of its rows is the reader's choice, so a shell
|
|
330
|
-
// run no longer spends twenty rows on screen just because its output is the
|
|
331
|
-
// answer: the answer is one click away, and the row it costs is one.
|
|
332
|
-
const preview = expanded
|
|
333
|
-
? { expanded: true }
|
|
334
|
-
: spec.output === 'tail'
|
|
335
|
-
? { expanded: false, preview: 'tail', rows: spec.tail }
|
|
336
|
-
: { expanded: false, preview: 'title' };
|
|
337
|
-
const { lines: detail, hidden } = cardDetailRows(card, preview);
|
|
338
|
-
const titleToken = card.failed ? 'tool.failed.title' : 'tool.title';
|
|
339
|
-
const glyphToken = card.failed ? 'tool.failed.glyph' : 'tool.glyph';
|
|
340
|
-
if (expanded) {
|
|
341
|
-
const { lead, body } = this.renderHead(card, titleToken, glyphToken);
|
|
342
|
-
// A header that folds keeps the argument and its stats; a terminal command
|
|
343
|
-
// can be longer than the screen, so it wraps under its own indent.
|
|
344
|
-
if (body !== '')
|
|
345
|
-
this.pushStyledWrapped(lines, body, width, lead);
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
const head = this.renderCollapsedHead(card, hidden, titleToken, glyphToken, width);
|
|
349
|
-
if (head !== '')
|
|
350
|
-
lines.push(this.theme.cut(head, collapsedEdge(width), '…'));
|
|
351
|
-
}
|
|
352
|
-
// The calls a program dispatched sit under the header whether the card is
|
|
353
|
-
// open or folded: one line per call is what the card stands for, and hiding
|
|
354
|
-
// them behind the card's own fold made a program's work invisible.
|
|
355
|
-
if (this.viewState.expandSubCalls)
|
|
356
|
-
this.pushSubCalls(lines, entry, width, spans);
|
|
357
|
-
if (expanded && card.kind === 'terminal' && card.argument !== undefined && card.argument !== '' && this.theme.visible('tool.args')) {
|
|
358
|
-
this.pushStyledWrapped(lines, this.theme.rich(card.argument, { token: 'tool.args', column: visibleWidth(DETAIL_INDENT) }), width, DETAIL_INDENT);
|
|
359
|
-
}
|
|
360
|
-
for (const row of detail) {
|
|
361
|
-
const drawn = this.detailRow(row, card.kind);
|
|
362
|
-
// A row whose every part is hidden draws nothing, and nothing must not
|
|
363
|
-
// cost a line: the indent would read as an empty row the card does not have.
|
|
364
|
-
if (drawn === '')
|
|
365
|
-
continue;
|
|
366
|
-
lines.push(this.theme.cut(`${DETAIL_INDENT}${drawn}`, width, ''));
|
|
367
|
-
}
|
|
368
|
-
// The pill is not output, so it draws after the preview window rather than
|
|
369
|
-
// inside it: a run bounded to its tail still reports how it ended. Folded,
|
|
370
|
-
// it rides the header instead, where it costs no row of its own.
|
|
371
|
-
if (expanded && card.kind === 'terminal' && card.status !== undefined && this.theme.visible('tool.terminal.status')) {
|
|
372
|
-
lines.push(this.theme.cut(`${DETAIL_INDENT}${this.theme.rich(card.status, { token: 'tool.terminal.status', column: visibleWidth(DETAIL_INDENT) })}`, width, ''));
|
|
373
|
-
}
|
|
374
|
-
// A shell card's rows are kept from the end, so a hidden count always names
|
|
375
|
-
// the rows *before* what is on screen and the hint has to say so; every
|
|
376
|
-
// other card keeps its head, where a neutral count is enough. A folded card
|
|
377
|
-
// that shows a tail is bounded by that window, an opened one by retention,
|
|
378
|
-
// so the opened hint promises no more than memory kept.
|
|
379
|
-
if (hidden > 0 && this.theme.visible('tool.hint')) {
|
|
380
|
-
const hint = preview.expanded
|
|
381
|
-
? card.kind === 'terminal' ? shellRetentionHint(hidden) : `${hidden} ${CARD_HINT_RETAINED}`
|
|
382
|
-
: preview.preview === 'tail'
|
|
383
|
-
? shellFoldHint(hidden, hintKeys(this.keymap(), 'surface.toolDetail') || CARD_OPEN_FALLBACK)
|
|
384
|
-
: undefined;
|
|
385
|
-
if (hint !== undefined) {
|
|
386
|
-
lines.push(this.theme.style('tool.hint', this.theme.cut(`${DETAIL_INDENT}${hint}`, width, '')));
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
const key = toolClickKey(entry.id);
|
|
390
|
-
if (key !== undefined)
|
|
391
|
-
spans.push({ key, start, end: lines.length, expanded });
|
|
392
|
-
}
|
|
393
|
-
/**
|
|
394
|
-
* The calls one card dispatched, one entry each.
|
|
395
|
-
*
|
|
396
|
-
* A nested call is a signpost rather than a card of its own, so it keeps one
|
|
397
|
-
* entry per call, clipped to that entry until the reader clicks it: the
|
|
398
|
-
* argument is the part a reader scans for, and one that always wrapped pushed
|
|
399
|
-
* the rest of the program off the screen.
|
|
400
|
-
*/
|
|
401
|
-
pushSubCalls(lines, entry, width, spans) {
|
|
402
|
-
const card = entry.card;
|
|
403
|
-
const subCalls = card.subCalls ?? [];
|
|
404
|
-
for (const call of subCalls) {
|
|
405
|
-
const start = lines.length;
|
|
406
|
-
const open = this.subCallOpen(entry.id, call.id);
|
|
407
|
-
const titleToken = call.failed ? 'tool.failed.title' : 'tool.subcall.title';
|
|
408
|
-
const column = visibleWidth(SUBCALL_INDENT);
|
|
409
|
-
const title = this.theme.visible(titleToken) ? this.theme.rich(call.title, { token: titleToken, column }) : '';
|
|
410
|
-
const argument = call.argument === undefined || !this.theme.visible('tool.subcall.args')
|
|
411
|
-
? ''
|
|
412
|
-
: ` ${this.theme.rich(call.argument, { token: 'tool.subcall.args', column })}`;
|
|
413
|
-
const drawn = `${title}${argument}`;
|
|
414
|
-
// A row whose every part is hidden draws nothing, and nothing must not
|
|
415
|
-
// cost a line the card does not have.
|
|
416
|
-
if (drawn === '')
|
|
417
|
-
continue;
|
|
418
|
-
if (open) {
|
|
419
|
-
this.pushStyledWrapped(lines, drawn, width, SUBCALL_INDENT);
|
|
420
|
-
this.pushSubCallOutput(lines, call, width);
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
lines.push(this.theme.cut(`${SUBCALL_INDENT}${drawn}`, collapsedEdge(width), '…'));
|
|
424
|
-
}
|
|
425
|
-
spans.push({ key: subCallClickKey(entry.id, call.id), start, end: lines.length, expanded: open });
|
|
426
|
-
}
|
|
427
|
-
const hidden = (card.subCallsTotal ?? subCalls.length) - subCalls.length;
|
|
428
|
-
if (hidden > 0 && this.theme.visible('tool.hint')) {
|
|
429
|
-
lines.push(this.theme.style('tool.hint', this.theme.cut(`${SUBCALL_INDENT}… ${hidden} more calls`, width, '')));
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
166
|
/**
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
continue;
|
|
448
|
-
lines.push(this.theme.cut(`${DETAIL_INDENT}${drawn}`, width, ''));
|
|
449
|
-
}
|
|
450
|
-
const hidden = section.totalLines - section.rows.length;
|
|
451
|
-
if (hidden > 0 && this.theme.visible('tool.hint')) {
|
|
452
|
-
// A shell's rows are kept from the end, so its hidden count names the
|
|
453
|
-
// earlier lines; every other kind keeps its head, where the count is of
|
|
454
|
-
// what follows and a neutral wording is what the card uses too.
|
|
455
|
-
const hint = section.kind === 'terminal' ? shellRetentionHint(hidden) : `${hidden} ${CARD_HINT_RETAINED}`;
|
|
456
|
-
lines.push(this.theme.style('tool.hint', this.theme.cut(`${DETAIL_INDENT}${hint}`, width, '')));
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
/**
|
|
461
|
-
* A folded card's one row: label, clipped argument, measured facts, outcome.
|
|
462
|
-
*
|
|
463
|
-
* The outcome rides this row rather than a row of its own, because one line
|
|
464
|
-
* per call is what folding promises. A shell card's hidden rows are counted
|
|
465
|
-
* here too: its output is the answer the reader asked for, and a fold that
|
|
466
|
-
* left no trace of it would read as a call that produced nothing.
|
|
467
|
-
*
|
|
468
|
-
* The argument is the part that gives up room: it is clipped to whatever the
|
|
469
|
-
* label, the facts, and the outcome leave before the edge, so a wide terminal
|
|
470
|
-
* shows more of the call while a narrow one still shows how it ended.
|
|
471
|
-
*/
|
|
472
|
-
renderCollapsedHead(card, hidden, titleToken, glyphToken, width) {
|
|
473
|
-
const edge = collapsedEdge(width);
|
|
474
|
-
const lead = this.cardLead(titleToken, glyphToken);
|
|
475
|
-
const title = this.cardTitle(card, titleToken);
|
|
476
|
-
const separator = this.statSeparator();
|
|
477
|
-
// The facts carry their own lead; the outcome and the count join whatever
|
|
478
|
-
// precedes them, so the folded line reads as one sentence about the run.
|
|
479
|
-
let tail = this.renderStats(card.stats);
|
|
480
|
-
const status = card.kind === 'terminal' && card.status !== undefined && this.theme.visible('tool.terminal.status')
|
|
481
|
-
? this.theme.rich(card.status, { token: 'tool.terminal.status' })
|
|
482
|
-
: '';
|
|
483
|
-
if (status !== '')
|
|
484
|
-
tail += `${separator}${status}`;
|
|
485
|
-
if (card.kind === 'terminal' && hidden > 0) {
|
|
486
|
-
const count = this.renderStats([{ kind: 'size', text: `${hidden} line${hidden === 1 ? '' : 's'}` }], '');
|
|
487
|
-
if (count !== '')
|
|
488
|
-
tail += `${separator}${count}`;
|
|
489
|
-
}
|
|
490
|
-
const fixed = visibleWidth(lead) + visibleWidth(title) + visibleWidth(tail);
|
|
491
|
-
const room = Math.max(0, edge - fixed - (title === '' ? 0 : 1));
|
|
492
|
-
const argument = room > 0 ? this.cardArgument(card, room) : '';
|
|
493
|
-
const head = [title, argument].filter(part => part !== '').join(' ');
|
|
494
|
-
// A row with no visible label or argument opens with the tail, and a line
|
|
495
|
-
// that begins with a separator reads as a row that lost its first word.
|
|
496
|
-
if (head === '' && tail.startsWith(separator))
|
|
497
|
-
tail = tail.slice(separator.length);
|
|
498
|
-
return `${lead}${head}${tail}`;
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* One detail row, drawn with its own kind's colours or nothing at all.
|
|
502
|
-
*
|
|
503
|
-
* The row says what it is, so the renderer never guesses from the text: a diff
|
|
504
|
-
* line beginning with "+" is an addition because the presenter said so, not
|
|
505
|
-
* because of its first character.
|
|
506
|
-
*/
|
|
507
|
-
detailRow(row, kind) {
|
|
508
|
-
return row.parts
|
|
509
|
-
.map(part => {
|
|
510
|
-
const token = CARD_ROW_TOKEN[kind]?.[part.class] ?? FALLBACK_ROW_TOKEN;
|
|
511
|
-
return this.theme.visible(token) ? this.theme.rich(part.text, { token, column: visibleWidth(DETAIL_INDENT) }) : '';
|
|
512
|
-
})
|
|
513
|
-
.join('');
|
|
514
|
-
}
|
|
515
|
-
/** The mark that introduces a card, empty when the theme hides its label. */
|
|
516
|
-
cardLead(titleToken, glyphToken) {
|
|
517
|
-
const glyph = this.theme.visible(titleToken) ? this.theme.glyph(glyphToken) : '';
|
|
518
|
-
return glyph === '' ? '' : `${glyph} `;
|
|
519
|
-
}
|
|
520
|
-
/** A card's label, styled, or nothing when the theme hides it. */
|
|
521
|
-
cardTitle(card, titleToken) {
|
|
522
|
-
return this.theme.visible(titleToken) ? this.theme.rich(card.title, { token: titleToken }) : '';
|
|
523
|
-
}
|
|
524
|
-
/** A card's argument, styled and flattened, clipped to `limit` when one is given. */
|
|
525
|
-
cardArgument(card, limit) {
|
|
526
|
-
if (card.argument === undefined || card.argument === '' || !this.theme.visible('tool.args'))
|
|
527
|
-
return '';
|
|
528
|
-
const shown = limit === undefined ? card.argument : clip(oneLine(card.argument), limit);
|
|
529
|
-
return this.theme.rich(shown, { token: 'tool.args' });
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
532
|
-
* A card's header: its label, its argument, and its measured facts.
|
|
533
|
-
*
|
|
534
|
-
* The glyph is returned apart from the body so a wrapped continuation can
|
|
535
|
-
* align under the label rather than under the mark. An opened terminal's
|
|
536
|
-
* argument is left out because it needs a row of its own — it is the one
|
|
537
|
-
* argument that can be a whole command rather than a word.
|
|
538
|
-
*/
|
|
539
|
-
renderHead(card, titleToken, glyphToken) {
|
|
540
|
-
const lead = this.cardLead(titleToken, glyphToken);
|
|
541
|
-
const title = this.cardTitle(card, titleToken);
|
|
542
|
-
const argument = card.kind === 'terminal' ? '' : this.cardArgument(card);
|
|
543
|
-
const head = [title, argument].filter(part => part !== '').join(' ');
|
|
544
|
-
return { lead, body: head + this.renderStats(card.stats) };
|
|
545
|
-
}
|
|
546
|
-
/** The separator a header uses between its label, its facts, and its outcome. */
|
|
547
|
-
statSeparator() {
|
|
548
|
-
return this.theme.style('tool.stat.separator', STAT_SEPARATOR);
|
|
549
|
-
}
|
|
550
|
-
/**
|
|
551
|
-
* The measured facts, each in its own colour, or nothing when none is visible.
|
|
552
|
-
*
|
|
553
|
-
* The lead is a parameter because a header that already carries a title and an
|
|
554
|
-
* argument separates its facts with the same mark as its outcome, while facts
|
|
555
|
-
* on a row of their own stand apart with blank columns.
|
|
556
|
-
*/
|
|
557
|
-
renderStats(stats, lead = STAT_LEAD) {
|
|
558
|
-
if (stats === undefined || stats.length === 0)
|
|
559
|
-
return '';
|
|
560
|
-
const drawn = stats
|
|
561
|
-
.filter(stat => this.theme.visible(STAT_TOKEN[stat.kind]))
|
|
562
|
-
.map(stat => this.theme.rich(`${STAT_SYMBOL[stat.kind]}${stat.text}`, { token: STAT_TOKEN[stat.kind] }));
|
|
563
|
-
if (drawn.length === 0)
|
|
564
|
-
return '';
|
|
565
|
-
return `${lead}${drawn.join(this.statSeparator())}`;
|
|
167
|
+
* Whether any part of one card is still in flight.
|
|
168
|
+
*
|
|
169
|
+
* Only a card with work outstanding has a clock attached to its cache, so the
|
|
170
|
+
* moment the last call settles the row stops being rebuilt: a program that
|
|
171
|
+
* came back must not keep spending the cache its result is now stored in.
|
|
172
|
+
*/
|
|
173
|
+
isLive(entry) {
|
|
174
|
+
if (entry.kind !== 'tool')
|
|
175
|
+
return false;
|
|
176
|
+
// The card's own call is live only while the fold still has it pending: the
|
|
177
|
+
// flag is put on a copy at draw time, so the entry alone cannot answer this.
|
|
178
|
+
// A dispatched call carries its flag on the fold itself, where a settle
|
|
179
|
+
// clears it.
|
|
180
|
+
return this.model.liveCall(entry.id).running || (entry.card.subCalls ?? []).some(call => call.running);
|
|
566
181
|
}
|
|
567
182
|
pushPicker(lines, picker, width) {
|
|
568
183
|
lines.push('');
|
|
@@ -571,120 +186,40 @@ export class TranscriptView {
|
|
|
571
186
|
// over it, and a field added to a card reaches both at once.
|
|
572
187
|
lines.push(...pickerCardLines(picker, width, this.theme));
|
|
573
188
|
}
|
|
574
|
-
pushGate(lines, gate, width) {
|
|
575
|
-
lines.push('');
|
|
576
|
-
if (this.theme.visible('gate.title')) {
|
|
577
|
-
const glyphToken = gate.kind === 'approval' ? 'gate.glyphApproval' : 'gate.glyphQuestion';
|
|
578
|
-
const glyph = this.theme.glyph(glyphToken) || (gate.kind === 'approval' ? APPROVAL_MARK : QUESTION_MARK);
|
|
579
|
-
// The question is the thing being decided, so it wraps rather than being
|
|
580
|
-
// cut: a reader cannot answer a sentence they were not shown.
|
|
581
|
-
this.pushWrapped(lines, gate.title, width, `${glyph} `, 'gate.title');
|
|
582
|
-
}
|
|
583
|
-
if (this.theme.visible('gate.detail')) {
|
|
584
|
-
for (const detail of gate.detail) {
|
|
585
|
-
this.pushWrapped(lines, detail, width, DETAIL_INDENT, 'gate.detail');
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
gate.options.forEach((option, position) => {
|
|
589
|
-
this.pushGateRow(lines, width, {
|
|
590
|
-
number: gate.optionOffset + position + 1,
|
|
591
|
-
label: option.label,
|
|
592
|
-
description: option.description,
|
|
593
|
-
current: option.current,
|
|
594
|
-
selected: option.selected,
|
|
595
|
-
});
|
|
596
|
-
});
|
|
597
|
-
// The free-text row is drawn under the window rather than inside it: it is
|
|
598
|
-
// the one row that must never scroll out of reach, and the window's own
|
|
599
|
-
// numbering is left running 1..n above it.
|
|
600
|
-
if (gate.custom !== undefined) {
|
|
601
|
-
this.pushGateRow(lines, width, {
|
|
602
|
-
number: CUSTOM_ROW_NUMBER,
|
|
603
|
-
label: gate.custom.label,
|
|
604
|
-
description: gate.custom.description,
|
|
605
|
-
current: gate.custom.current,
|
|
606
|
-
selected: gate.custom.selected,
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
// The answer belongs to the row it fills: the free-text row, or the question
|
|
610
|
-
// itself when typing is the only way to answer it. The rows come from the
|
|
611
|
-
// surface's own editor, so they are placed rather than restyled — it draws
|
|
612
|
-
// its frame, its padding, and its cursor for the width it is given.
|
|
613
|
-
if (gate.answerInput !== undefined) {
|
|
614
|
-
const room = Math.max(1, width - visibleWidth(OPTION_INDENT));
|
|
615
|
-
// A surface narrower than the indent cannot place the answer: that row is cut
|
|
616
|
-
// here so what is handed out already fits, rather than being trimmed at the
|
|
617
|
-
// terminal past the point where the view could count it. A row that fits is
|
|
618
|
-
// left exactly as the editor drew it, cursor styling included.
|
|
619
|
-
for (const row of gate.answerInput.render(room)) {
|
|
620
|
-
const placed = `${OPTION_INDENT}${row}`;
|
|
621
|
-
lines.push(visibleWidth(placed) <= width ? placed : this.theme.cut(placed, width, ''));
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
// The keys are how the gate is answered at all, so they wrap rather than
|
|
625
|
-
// lose their tail at a narrow edge.
|
|
626
|
-
if (this.theme.visible('gate.hint')) {
|
|
627
|
-
this.pushWrapped(lines, gate.hint, width, OPTION_INDENT, 'gate.hint');
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
/**
|
|
631
|
-
* One row a reader can choose: the cursor, the box, the number, the label.
|
|
632
|
-
*
|
|
633
|
-
* The row wraps under the label it belongs to rather than at the screen edge,
|
|
634
|
-
* because the label and its description together are what tells two rows
|
|
635
|
-
* apart, and a mark on a continuation line reads as another row.
|
|
636
|
-
*/
|
|
637
|
-
pushGateRow(lines, width, row) {
|
|
638
|
-
const token = row.current ? 'gate.optionCurrent' : 'gate.option';
|
|
639
|
-
if (!this.theme.visible(token))
|
|
640
|
-
return;
|
|
641
|
-
const cursor = row.current ? this.theme.glyph('gate.cursor') || CURSOR_MARK : NO_CURSOR;
|
|
642
|
-
const box = row.selected ? CHECKBOX_ON : CHECKBOX_OFF;
|
|
643
|
-
const lead = `${OPTION_INDENT}${cursor} ${box} ${row.number}. `;
|
|
644
|
-
const text = row.description === undefined ? row.label : `${row.label} — ${row.description}`;
|
|
645
|
-
// The text reaches pushWrapped undrawn: it is read once there, and reading
|
|
646
|
-
// it twice would show the reader the escape instead of the character.
|
|
647
|
-
this.pushWrapped(lines, text, width, lead, token);
|
|
648
|
-
}
|
|
649
189
|
/** The rows one transcript entry becomes; `live` marks the entry the turn is still writing. */
|
|
650
|
-
renderEntry(entry, lines, width, live, spans) {
|
|
190
|
+
renderEntry(entry, lines, width, live, spans, copy) {
|
|
651
191
|
switch (entry.kind) {
|
|
652
192
|
case 'tool':
|
|
653
|
-
this.pushCard(lines, entry, width, spans);
|
|
193
|
+
this.cards.pushCard(lines, entry, width, spans);
|
|
654
194
|
return;
|
|
655
195
|
case 'reasoning':
|
|
656
|
-
this.pushReasoning(lines, entry, width, spans);
|
|
196
|
+
this.messages.pushReasoning(lines, entry, width, spans);
|
|
657
197
|
return;
|
|
658
198
|
case 'assistant':
|
|
659
199
|
// The reply is boxed the way the prompt that asked for it is, so one
|
|
660
200
|
// exchange reads as two objects rather than as a box and then a stream.
|
|
661
|
-
this.pushFramed(lines, entry.text, width, live, ANSWER_FACE, 'transcript.assistant.border');
|
|
201
|
+
this.messages.pushFramed(lines, copy, entry.text, width, live, ANSWER_FACE, 'transcript.assistant.border');
|
|
662
202
|
return;
|
|
663
203
|
case 'user': {
|
|
664
204
|
if (!this.theme.visible('transcript.user'))
|
|
665
205
|
return;
|
|
666
206
|
// A prompt is boxed wherever it is read, so the row it left in the queue
|
|
667
207
|
// and the row it becomes here are recognisably the same object.
|
|
668
|
-
this.pushFramed(lines, entry.text, width, false, this.userFace(), 'editor.border');
|
|
208
|
+
this.messages.pushFramed(lines, copy, entry.text, width, false, this.messages.userFace(), 'editor.border');
|
|
669
209
|
return;
|
|
670
210
|
}
|
|
671
211
|
case 'notice':
|
|
672
212
|
if (!this.theme.visible('transcript.notice'))
|
|
673
213
|
return;
|
|
674
|
-
this.pushWrapped(lines, entry.text, width, this.elementLead('transcript.notice'), 'transcript.notice');
|
|
214
|
+
this.pushWrapped(lines, entry.text, width, this.messages.elementLead('transcript.notice'), 'transcript.notice');
|
|
675
215
|
return;
|
|
676
216
|
case 'marker':
|
|
677
217
|
if (!this.theme.visible('transcript.marker'))
|
|
678
218
|
return;
|
|
679
|
-
this.pushWrapped(lines, entry.text, width, this.elementLead('transcript.marker'), 'transcript.marker');
|
|
219
|
+
this.pushWrapped(lines, entry.text, width, this.messages.elementLead('transcript.marker'), 'transcript.marker');
|
|
680
220
|
return;
|
|
681
221
|
}
|
|
682
222
|
}
|
|
683
|
-
/** The mark and the space that introduce an element, empty when it has none. */
|
|
684
|
-
elementLead(token) {
|
|
685
|
-
const glyph = this.theme.visible(token) ? this.theme.glyph(token) : '';
|
|
686
|
-
return glyph === '' ? '' : `${glyph} `;
|
|
687
|
-
}
|
|
688
223
|
render(width) {
|
|
689
224
|
if (width <= 0)
|
|
690
225
|
return [];
|
|
@@ -695,6 +230,7 @@ export class TranscriptView {
|
|
|
695
230
|
const baseTag = `${width}|${state.expandCards ? 'c' : '-'}${state.expandReasoning ? 'r' : '-'}${state.expandSubCalls ? 'p' : '-'}|${this.theme.revision}`;
|
|
696
231
|
const lines = [];
|
|
697
232
|
const spans = [];
|
|
233
|
+
const liveCopy = [];
|
|
698
234
|
const settled = this.model.settledCount();
|
|
699
235
|
const entries = this.model.entries();
|
|
700
236
|
for (const [index, entry] of entries.entries()) {
|
|
@@ -706,12 +242,20 @@ export class TranscriptView {
|
|
|
706
242
|
const marks = entry.kind === 'tool'
|
|
707
243
|
? `${open === true ? '+' : '-'}${(entry.card.subCalls ?? []).map(call => (this.subCallOpen(entry.id, call.id) ? '1' : '0')).join('')}`
|
|
708
244
|
: open === true ? '+' : '-';
|
|
709
|
-
|
|
245
|
+
// A running row is drawn from a clock the rest of the row is not, so its
|
|
246
|
+
// cache is spent every second: the entries after it stay put, and a card
|
|
247
|
+
// waiting out a two-minute run keeps saying so instead of freezing at the
|
|
248
|
+
// first second it was drawn.
|
|
249
|
+
const tag = `${baseTag}|${marks}${this.isLive(entry) ? `|${Math.floor(this.model.now() / SECOND_MS)}` : ''}`;
|
|
710
250
|
const local = [];
|
|
251
|
+
const copy = [];
|
|
711
252
|
// The in-flight rows change on every frame, so caching them would only
|
|
712
253
|
// fill the cache with objects nobody will ask for again.
|
|
713
254
|
if (index >= settled) {
|
|
714
|
-
this.renderEntry(entry, lines, width, true, local);
|
|
255
|
+
this.renderEntry(entry, lines, width, true, local, copy);
|
|
256
|
+
// A row still arriving is drawn every frame, so it is never cached: its
|
|
257
|
+
// copy account goes to the frame rather than to an entry nobody can name.
|
|
258
|
+
liveCopy.push(...copy);
|
|
715
259
|
// An in-flight entry draws straight into the transcript, so the spans it
|
|
716
260
|
// recorded already name transcript rows; offsetting them again would move
|
|
717
261
|
// every hit target as many rows down as the entry's own start.
|
|
@@ -720,15 +264,17 @@ export class TranscriptView {
|
|
|
720
264
|
else {
|
|
721
265
|
const cached = this.rows.lookup(entry, tag);
|
|
722
266
|
const saved = this.entrySpans.get(entry);
|
|
723
|
-
|
|
267
|
+
const savedCopy = this.entryCopy.get(entry);
|
|
268
|
+
if (cached !== undefined && saved !== undefined && savedCopy !== undefined) {
|
|
724
269
|
lines.push(...cached);
|
|
725
270
|
local.push(...saved);
|
|
726
271
|
}
|
|
727
272
|
else {
|
|
728
273
|
const rendered = [];
|
|
729
|
-
this.renderEntry(entry, rendered, width, false, local);
|
|
274
|
+
this.renderEntry(entry, rendered, width, false, local, copy);
|
|
730
275
|
this.rows.store(entry, tag, rendered);
|
|
731
276
|
this.entrySpans.set(entry, local);
|
|
277
|
+
this.entryCopy.set(entry, copy);
|
|
732
278
|
lines.push(...rendered);
|
|
733
279
|
}
|
|
734
280
|
// A cached span is kept relative to its entry so the entry can hand it
|
|
@@ -739,13 +285,33 @@ export class TranscriptView {
|
|
|
739
285
|
}
|
|
740
286
|
}
|
|
741
287
|
this.spans = spans;
|
|
288
|
+
this.liveCopy = liveCopy;
|
|
742
289
|
const picker = this.options.picker?.();
|
|
743
290
|
if (picker !== undefined)
|
|
744
291
|
this.pushPicker(lines, picker, width);
|
|
745
292
|
const gate = this.options.gate?.();
|
|
746
293
|
if (gate !== undefined)
|
|
747
|
-
this.pushGate(lines, gate, width);
|
|
294
|
+
this.gates.pushGate(lines, gate, width);
|
|
748
295
|
return lines;
|
|
749
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* The framed rows the last render drew, in the order the transcript reads.
|
|
299
|
+
*
|
|
300
|
+
* A copy of a selection is read off the screen, so it carries the frame the
|
|
301
|
+
* surface drew around a message — the sides, the padding beside them, and the
|
|
302
|
+
* rules above and below. This is the account that lets the surface take its own
|
|
303
|
+
* frame back out of a copy without ever touching a character the reader wrote.
|
|
304
|
+
*/
|
|
305
|
+
copyRows() {
|
|
306
|
+
const rows = [];
|
|
307
|
+
for (const entry of this.model.entries()) {
|
|
308
|
+
const saved = this.entryCopy.get(entry);
|
|
309
|
+
if (saved !== undefined)
|
|
310
|
+
rows.push(...saved);
|
|
311
|
+
}
|
|
312
|
+
// The rows still arriving are not kept by entry, so they come from the frame.
|
|
313
|
+
rows.push(...this.liveCopy);
|
|
314
|
+
return rows;
|
|
315
|
+
}
|
|
750
316
|
}
|
|
751
317
|
//# sourceMappingURL=view.js.map
|