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