@pi-kaush/pi-tool-call-markers 0.3.5 → 0.3.7
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/CHANGELOG.md +80 -0
- package/README.md +35 -8
- package/package.json +1 -1
- package/src/bash-block.ts +33 -137
- package/src/container-hooks.ts +6 -0
- package/src/index.ts +108 -164
- package/src/info-visibility-state.ts +16 -2
- package/src/info-visibility.ts +15 -3
- package/src/muted.ts +31 -16
- package/src/prompt-shell.ts +174 -0
- package/src/question-block.ts +328 -0
- package/src/sanitize.ts +20 -0
- package/src/thinking-block-merger.ts +141 -64
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,86 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Render an asked question as a user-input moment instead of a raw tool row.
|
|
6
|
+
`ask_user_question` used to collapse to `│ * {"questions":…`, which is a tool
|
|
7
|
+
call marker doing the work of a prompt. An asked question now takes the
|
|
8
|
+
submitted-prompt shell: `▎ > <question>` followed by `▎ User: <answer>`, one
|
|
9
|
+
pair per question, with multi-select labels joined, a decline reading
|
|
10
|
+
`User declined to answer questions`, and a live row showing
|
|
11
|
+
`awaiting your answer…`. Recognition is shape-first off
|
|
12
|
+
`details.answers`, so another author's question tool renders the same without
|
|
13
|
+
a change here; the tool name is the fallback for calls that never answered.
|
|
14
|
+
The block reuses pi-content-layout's submitted-prompt geometry — rail,
|
|
15
|
+
surface, inset, one-column left and two-column right body padding, and
|
|
16
|
+
background padding rows — and italicizes the question while the answer stays
|
|
17
|
+
upright. Question blocks never join a tool group, and `Ctrl+O` still restores
|
|
18
|
+
Pi's native expanded rendering.
|
|
19
|
+
|
|
20
|
+
- Make the railed shell's body padding explicit: `renderRailedBlockLines` takes
|
|
21
|
+
a `{ left, right }` padding, so the submitted-prompt shape (1/2) and the
|
|
22
|
+
existing `!`-block shape (1/0) come from one code path instead of the body
|
|
23
|
+
width being hardcoded.
|
|
24
|
+
|
|
25
|
+
- Extract the railed prompt shell into `src/prompt-shell.ts`. User `!` blocks
|
|
26
|
+
and asked-question blocks share one implementation of the inset, rail,
|
|
27
|
+
surface background, and background repaint instead of two.
|
|
28
|
+
|
|
29
|
+
- Read colors from the live palette instead of caching them against the
|
|
30
|
+
theme object. Pi swaps the palette behind a stable theme Proxy, so every
|
|
31
|
+
identity-keyed cache survived a theme switch: collapsed rows, the `+ Thought`
|
|
32
|
+
label, and user `!` blocks kept the old colors until their content changed.
|
|
33
|
+
Probes are map lookups, so the render caches now key on the resolved colors
|
|
34
|
+
(`paletteSample`, including the bash-block line cache) and the label
|
|
35
|
+
resolves its style per call.
|
|
36
|
+
|
|
37
|
+
- Stop grouping from resurrecting a tool that opted out of the transcript. A
|
|
38
|
+
self-rendered tool that draws no lines stays hidden whether it renders alone
|
|
39
|
+
or beside another call: the singleton and grouped paths share one
|
|
40
|
+
draws-nothing rule.
|
|
41
|
+
|
|
42
|
+
- Go inert at the final owner's shutdown. Every prototype wrapper — tool
|
|
43
|
+
presentation, container grouping, user bash blocks, thinking — delegates to
|
|
44
|
+
the original when another extension's wrapper has buried it and removal is
|
|
45
|
+
impossible, so collapsed rows and restyled labels cannot outlive their owner.
|
|
46
|
+
A later install re-enables the same wrappers.
|
|
47
|
+
|
|
48
|
+
- Roll back a container hook that mutates the child list and then throws.
|
|
49
|
+
Its edit used to stick, dropping those rows from every later render. Both
|
|
50
|
+
copies of the shared hook contract changed together.
|
|
51
|
+
|
|
52
|
+
- Own and release `/toggle-info` with the rest of the extension: the
|
|
53
|
+
process-global filter hook and the hidden flag now leave with the last owner
|
|
54
|
+
instead of filtering a transcript this package no longer renders.
|
|
55
|
+
|
|
56
|
+
- Track thinking rows for the `/toggle-info` replay instead of every assistant
|
|
57
|
+
row, capped at 400 entries, so a long session or an abandoned branch cannot
|
|
58
|
+
pin every message the transcript ever showed.
|
|
59
|
+
|
|
60
|
+
- Delete state nothing reads (the row→group map) and the duplicate
|
|
61
|
+
`session_start` handler that applied the collapsed default twice.
|
|
62
|
+
|
|
63
|
+
- Drop the colon after every collapsed-row anchor: glyph rows read
|
|
64
|
+
`│ ● src/a.ts` and bash rows `│ $ npm test`. The anchor runs bold with a
|
|
65
|
+
single space before the call text, so the anchor alone separates the two.
|
|
66
|
+
The whole seam — glyph map, `$` prompt, subagent fallback heading, and the
|
|
67
|
+
tool-token drop — now lives in one function that both the singleton and
|
|
68
|
+
grouped render paths call, and self-rendered labels no longer leak their
|
|
69
|
+
tool name into the row under themes that tag instead of color.
|
|
70
|
+
|
|
71
|
+
- Drop the italics Pi puts on thinking. The hidden `+ Thought` label renders
|
|
72
|
+
plain in every theme — the live spinner keeps its level tint, the settled
|
|
73
|
+
row its muted tone, and a theme that resolves no color still loses the
|
|
74
|
+
italics — and the visible trace stays italic only while it streams, then
|
|
75
|
+
renders as ordinary transcript text once the row settles. The live/settled
|
|
76
|
+
rule is shared with the label, so un-flagged row rebuilds (resize, theme
|
|
77
|
+
switch) keep a live trace italic and a finished one plain.
|
|
78
|
+
`PI_TOOL_CALL_MARKERS_THOUGHT_COLOR=inherit` remains the only opt-out that
|
|
79
|
+
keeps Pi's native italic styling.
|
|
80
|
+
|
|
81
|
+
- Fail open when a theme rejects a thinking token. Pi's Theme throws on
|
|
82
|
+
tokens it does not know, so the label probe now falls back to the plain
|
|
83
|
+
treatment instead of throwing out of the row's update pass.
|
|
84
|
+
|
|
5
85
|
- Restore Pi 0.85's click-to-expand under the custom collapsed rows. Pi routes
|
|
6
86
|
mouse clicks by per-child rendered line heights, and the grouping render
|
|
7
87
|
bypassed the native render that refreshes that cache, so clicks on anything
|
package/README.md
CHANGED
|
@@ -15,13 +15,13 @@ Restart Pi or run `/reload`.
|
|
|
15
15
|
Collapsed tool rows use semantic theme colors with no gear, background fill, box padding, or filled blank rows:
|
|
16
16
|
|
|
17
17
|
```text
|
|
18
|
-
│
|
|
19
|
-
│
|
|
20
|
-
│
|
|
18
|
+
│ ● src/a.ts 42 lines
|
|
19
|
+
│ ● src/b.ts 18 lines
|
|
20
|
+
│ $ npm test → done
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
- **Two-column outer inset.** Tool markers and image output align with an inset conversation surface. Very narrow terminals reduce the decoration before useful content.
|
|
24
|
-
- **`│` tool-call rail.** Every collapsed call anchors on a glyph instead of its tool name
|
|
24
|
+
- **`│` tool-call rail.** Every collapsed call anchors on a bold glyph instead of its tool name — `●` read, `+` write, `±` edit, `○` local search, `≡` ls, `↗` web search and fetch, and `$` bash; unmapped tools fall back to `*` — with a single space between the anchor and the call text, no joining colon. Grouped rows flow directly across tool types.
|
|
25
25
|
- **Semantic, low-contrast status.** The anchor and the call content share the collapsed mute, pending state is warning-colored, and failures remain error-colored. Ordinary tool states have no background.
|
|
26
26
|
- **Width-safe outcome tails.** Long summaries truncate before useful tails such as `→ done`, `→ 42 lines`, `→ +2/-1`, or a `bash` duration.
|
|
27
27
|
- **Stable running groups.** Adjacent calls group as they appear, including settled failures, which stay error-colored inside the group. Pending state and elapsed `bash` time settle into the final outcome without changing the row count.
|
|
@@ -47,6 +47,27 @@ While a subagent runs, the plan headline's tail shows live progress from the str
|
|
|
47
47
|
|
|
48
48
|
Malformed, ambiguous, future, or too-narrow shapes fall back to the generic `↪ subagent …` collapsed row rather than dropping information, and `Ctrl+O` still exposes the native subagent renderer.
|
|
49
49
|
|
|
50
|
+
## Asked questions
|
|
51
|
+
|
|
52
|
+
A call that asks the user something is a user-input moment, not an execution row, so it renders in the submitted-prompt shell instead of the tool-call rail:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
▎
|
|
56
|
+
▎ > Which call-site shape should `notifications:` use?
|
|
57
|
+
▎ User: Nested within subagent (Recommended)
|
|
58
|
+
▎ > Which tier should the flag read at runtime?
|
|
59
|
+
▎ User: Client only, Client and server
|
|
60
|
+
▎
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The block reuses `pi-content-layout`'s submitted-prompt geometry — the same rail, surface, inset, one-column left padding, two-column right padding, and background padding rows — so an asked question and a submitted prompt are the same shape in the transcript.
|
|
64
|
+
|
|
65
|
+
- **Shape-first recognition.** Any tool whose result carries an `answers` array of `{ question, answer | selected }` entries renders as a question block, so another author's question tool works without a change here. The tool name (`ask_user_question` and close variants) is the fallback for a call that failed before answering.
|
|
66
|
+
- **Answers, not options.** Each question shows its answer: an option label, typed custom text, or multi-select labels joined with commas. A question left blank reads `User: (no answer)`, a decline reads `User declined to answer questions`, and a validation failure keeps Pi's native failure row.
|
|
67
|
+
- **Quoted questions.** The question is the model's text read inside a user-input shell, so it renders italic under a `>` marker and the answer stays upright: the answer is the user's own words. A theme without italics renders the question plainly.
|
|
68
|
+
- **Prompt-shell tones.** The rail uses `borderAccent` and the body paints on `userMessageBg`, with the question in `text`, the `User:` label in `muted`, the answer in `userMessageText`, and a live row's `awaiting your answer…` in `warning`.
|
|
69
|
+
- **No grouping.** Question blocks never join an adjacent tool group and carry no tool glyph. `Ctrl+O` still restores Pi's native expanded rendering.
|
|
70
|
+
|
|
50
71
|
## Edit diffs
|
|
51
72
|
|
|
52
73
|
Settled `edit` calls collapse like every other row: the call line keeps its path and a `+added/-removed` outcome stat, and the hunk itself stays out of the collapsed row. `Ctrl+O` restores Pi's full native diff, including line numbers and intra-line word highlights. To keep edits expanded without pressing `Ctrl+O`, list `edit` in `PI_ALWAYS_EXPANDED_TOOL_CALL_MARKERS` (see Configuration).
|
|
@@ -55,9 +76,9 @@ Files and paths in collapsed rows are preserved as displayed by Pi: hyperlink-wr
|
|
|
55
76
|
|
|
56
77
|
## Scope boundary
|
|
57
78
|
|
|
58
|
-
This package owns every **execution row** in the transcript: collapsed tool calls, tool grouping, subagent plans, thinking labels, and user-run `!` bash blocks (reshaped into the railed prompt shell). Transcript _surface_ layout — message insets, system-text and status-rule alignment, the editor surface, and the submitted-prompt shell for user messages — belongs to `@pi-kaush/pi-content-layout`, which never renders tool rows.
|
|
79
|
+
This package owns every **execution row** in the transcript: collapsed tool calls, tool grouping, subagent plans, asked-question blocks, thinking labels, and user-run `!` bash blocks (reshaped into the railed prompt shell). Transcript _surface_ layout — message insets, system-text and status-rule alignment, the editor surface, and the submitted-prompt shell for user messages — belongs to `@pi-kaush/pi-content-layout`, which never renders tool rows.
|
|
59
80
|
|
|
60
|
-
One shared visual contract crosses the line: `!` blocks align with message text, so their inset mirrors `pi-content-layout`'s message `contentInset` (see `src/
|
|
81
|
+
One shared visual contract crosses the line: `!` blocks and asked-question blocks align with message text, so their inset mirrors `pi-content-layout`'s message `contentInset` (see `src/prompt-shell.ts`, which owns the railed shell both use). Change indentation in both packages together.
|
|
61
82
|
|
|
62
83
|
## Hiding info entirely: /toggle-info
|
|
63
84
|
|
|
@@ -78,7 +99,9 @@ When Pi exposes its per-row hidden-thinking and streaming fields, hidden reasoni
|
|
|
78
99
|
+ Thought · 2.5s
|
|
79
100
|
```
|
|
80
101
|
|
|
81
|
-
The live label samples Pi's native braille spinner sequence from the content updates Pi already renders; it does not add a timer. The adapter stores the first local streaming timestamp per assistant row in a `WeakMap`. A restored message or an older runtime with no streaming argument uses `+ Thought`.
|
|
102
|
+
The live label samples Pi's native braille spinner sequence from the content updates Pi already renders; it does not add a timer. The adapter stores the first local streaming timestamp per assistant row in a `WeakMap`. A restored message or an older runtime with no streaming argument uses `+ Thought`. There is no interval, timeout, render request, model call, or network work.
|
|
103
|
+
|
|
104
|
+
Pi renders thinking labels and traces italic. This package drops those italics: both labels read as plain collapsed rows (the live spinner keeps its thinking-level tint, the settled row the muted tone, and a theme that resolves no color still loses the italics), and a visible trace stays italic only while it streams, then settles into ordinary transcript text. `PI_TOOL_CALL_MARKERS_THOUGHT_COLOR=inherit` is the opt-out that keeps Pi's native italic `thinkingText` styling.
|
|
82
105
|
|
|
83
106
|
## Local development
|
|
84
107
|
|
|
@@ -131,10 +154,14 @@ Pi has no public hook for native tool rows, transcript grouping, or per-message
|
|
|
131
154
|
|
|
132
155
|
- `ToolExecutionComponent` for collapsed presentation;
|
|
133
156
|
- `Container` for adjacent grouping; and
|
|
134
|
-
- `AssistantMessageComponent.updateContent` for display-only thinking merging and
|
|
157
|
+
- `AssistantMessageComponent.updateContent` for display-only thinking merging, lifecycle labels, and the settled trace's italics.
|
|
135
158
|
|
|
136
159
|
Each adapter feature-detects the fields and methods it needs, keeps the original method, uses an idempotency symbol, catches cosmetic failures, and restores the original on `session_shutdown` when it still owns the patch. Unsupported shapes fail open to Pi's native rendering. The thinking adapter continues adjacent merging even when the private label shape is unavailable.
|
|
137
160
|
|
|
161
|
+
Teardown is owner-counted and inert: when another extension's wrapper sits above one of these, removal is impossible, so the buried wrapper delegates to the original instead of leaving collapsed rows or restyled labels behind after its owner shuts down. A later install re-enables it. The shared chat-container hook contract applies the same fail-open rule to hooks: a hook that throws after editing the child list has its edit rolled back, so a failed hook cannot drop rows from the transcript.
|
|
162
|
+
|
|
163
|
+
Colors are read live from the theme, so a mid-session theme switch repaints collapsed rows, labels, and `!` blocks; cached render output keys on the resolved palette rather than on the theme object, which Pi keeps stable across a switch.
|
|
164
|
+
|
|
138
165
|
Expanded tools always use Pi's native renderer. The collapsed tool shell owns its two-column inset directly; transcript layout extensions should leave tool rows unchanged, preventing load-order-dependent double padding.
|
|
139
166
|
|
|
140
167
|
> TODO: migrate these adapters to public Pi transcript and tool-rendering APIs when available.
|
package/package.json
CHANGED
package/src/bash-block.ts
CHANGED
|
@@ -1,141 +1,21 @@
|
|
|
1
1
|
import { BashExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { paletteSample } from "./muted.ts";
|
|
4
|
+
import {
|
|
5
|
+
PROMPT_RAIL,
|
|
6
|
+
fitLine,
|
|
7
|
+
promptBlockInset,
|
|
8
|
+
renderRailedBlockLines,
|
|
9
|
+
stripDisplayAnsi,
|
|
10
|
+
type PromptShellTheme,
|
|
11
|
+
} from "./prompt-shell.ts";
|
|
12
|
+
|
|
13
|
+
type ThemeLike = PromptShellTheme & {
|
|
6
14
|
fg(color: string, text: string): string;
|
|
7
|
-
getBgAnsi?(color: string): string;
|
|
8
15
|
};
|
|
9
16
|
|
|
10
17
|
const BASH_BLOCK_PATCHED = Symbol.for("kg.pi.bashBlock.v1");
|
|
11
18
|
|
|
12
|
-
// The prompt surface paints with the theme's userMessageBg token, matching
|
|
13
|
-
// pi-content-layout's surfaces. userMessageBg is a required pi theme token;
|
|
14
|
-
// the legacy hex remains only as a fallback for theme lookalikes without
|
|
15
|
-
// getBgAnsi.
|
|
16
|
-
const PROMPT_SURFACE_BG_FALLBACK = "\x1b[48;2;7;19;18m"; // #071312
|
|
17
|
-
|
|
18
|
-
function promptSurfaceBg(theme: ThemeLike): string {
|
|
19
|
-
try {
|
|
20
|
-
return theme.getBgAnsi?.("userMessageBg") ?? PROMPT_SURFACE_BG_FALLBACK;
|
|
21
|
-
} catch {
|
|
22
|
-
return PROMPT_SURFACE_BG_FALLBACK;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
const PROMPT_RAIL = "▎";
|
|
26
|
-
|
|
27
|
-
// User-run bash blocks align with the message inset pi-content-layout
|
|
28
|
-
// applies to transcript lines. Keep this in sync with that package's
|
|
29
|
-
// contentInset (2 columns outside the rail for any width above 4); the
|
|
30
|
-
// conversation surface has one shared visual contract even though the two
|
|
31
|
-
// packages otherwise own disjoint components.
|
|
32
|
-
function bashBlockInset(width: number): number {
|
|
33
|
-
return width > BASH_BLOCK_INSET * 2 ? BASH_BLOCK_INSET : 0;
|
|
34
|
-
}
|
|
35
|
-
const BASH_BLOCK_INSET = 2;
|
|
36
|
-
|
|
37
|
-
const SGR_PATTERN = /\x1b\[([0-9;]*)m/g;
|
|
38
|
-
const BACKGROUND_PATTERN =
|
|
39
|
-
/\x1b\[(?:4[0-8]|10[0-7]|48;5;\d{1,3}|48;2;\d{1,3};\d{1,3};\d{1,3})m/g;
|
|
40
|
-
const DISPLAY_ANSI_PATTERN = /\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
|
41
|
-
|
|
42
|
-
function semanticControlEnd(text: string, offset: number): number | undefined {
|
|
43
|
-
if (text[offset] !== "\x1b") return undefined;
|
|
44
|
-
const kind = text[offset + 1];
|
|
45
|
-
if (kind !== "]" && kind !== "_" && kind !== "P" && kind !== "^") {
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
for (let index = offset + 2; index < text.length; index++) {
|
|
49
|
-
if (text[index] === "\x07") return index + 1;
|
|
50
|
-
if (text[index] === "\x1b" && text[index + 1] === "\\") {
|
|
51
|
-
return index + 2;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function splitLeadingSemanticControls(line: string): {
|
|
58
|
-
controls: string;
|
|
59
|
-
content: string;
|
|
60
|
-
} {
|
|
61
|
-
let offset = 0;
|
|
62
|
-
while (offset < line.length) {
|
|
63
|
-
const end = semanticControlEnd(line, offset);
|
|
64
|
-
if (end === undefined) break;
|
|
65
|
-
offset = end;
|
|
66
|
-
}
|
|
67
|
-
return { controls: line.slice(0, offset), content: line.slice(offset) };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function fitLine(line: string, width: number): string {
|
|
71
|
-
if (width <= 0) return "";
|
|
72
|
-
const clipped = truncateToWidth(line, width, "");
|
|
73
|
-
return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function reapplyBackgroundAfterReset(
|
|
77
|
-
text: string,
|
|
78
|
-
backgroundAnsi: string,
|
|
79
|
-
): string {
|
|
80
|
-
return text.replace(SGR_PATTERN, (sequence, rawParameters: string) => {
|
|
81
|
-
const parameters =
|
|
82
|
-
rawParameters === "" ? [0] : rawParameters.split(";").map(Number);
|
|
83
|
-
return parameters.includes(0) || parameters.includes(49)
|
|
84
|
-
? `${sequence}${backgroundAnsi}`
|
|
85
|
-
: sequence;
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function paintBackground(
|
|
90
|
-
line: string,
|
|
91
|
-
width: number,
|
|
92
|
-
backgroundAnsi: string,
|
|
93
|
-
): string {
|
|
94
|
-
const fitted = fitLine(line, width);
|
|
95
|
-
// Mirrors Theme.bg: background, content, then a background-only reset.
|
|
96
|
-
return `${backgroundAnsi}${reapplyBackgroundAfterReset(fitted, backgroundAnsi)}\x1b[49m`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function stripDisplayAnsi(text: string): string {
|
|
100
|
-
return text.replace(DISPLAY_ANSI_PATTERN, "");
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function replaceBackground(text: string, backgroundAnsi: string): string {
|
|
104
|
-
return text.replace(BACKGROUND_PATTERN, backgroundAnsi);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Renders lines inside a railed, dark-surfaced block (the submitted-prompt
|
|
108
|
-
// shell): optional leading semantic controls stay outside the inset, the rail
|
|
109
|
-
// is recolored, and content backgrounds are normalized to the prompt surface.
|
|
110
|
-
function renderRailedBlockLines(
|
|
111
|
-
lines: string[],
|
|
112
|
-
width: number,
|
|
113
|
-
theme: ThemeLike,
|
|
114
|
-
inset: number,
|
|
115
|
-
railColor: string,
|
|
116
|
-
): string[] {
|
|
117
|
-
const blockWidth = width - inset * 2;
|
|
118
|
-
if (inset === 0 || blockWidth <= visibleWidth(PROMPT_RAIL)) {
|
|
119
|
-
return lines.map((line) => fitLine(line, width));
|
|
120
|
-
}
|
|
121
|
-
const bodyWidth = blockWidth - visibleWidth(PROMPT_RAIL);
|
|
122
|
-
const margin = " ".repeat(inset);
|
|
123
|
-
const surfaceBg = promptSurfaceBg(theme);
|
|
124
|
-
return lines.map((line) => {
|
|
125
|
-
const { controls, content } = splitLeadingSemanticControls(line);
|
|
126
|
-
const recolored = replaceBackground(content, surfaceBg);
|
|
127
|
-
const rail = theme.fg(railColor, PROMPT_RAIL);
|
|
128
|
-
// One extra leading space inside the body, so submitted text sits two
|
|
129
|
-
// columns right of the rail instead of one.
|
|
130
|
-
const body = paintBackground(
|
|
131
|
-
` ${fitLine(recolored, Math.max(1, bodyWidth - 1))}`,
|
|
132
|
-
bodyWidth,
|
|
133
|
-
surfaceBg,
|
|
134
|
-
);
|
|
135
|
-
return `${controls}${margin}${rail}${body}${margin}`;
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
19
|
const RULE_LINE_RE = /^─+$/;
|
|
140
20
|
|
|
141
21
|
function isRuleLine(line: string | undefined): line is string {
|
|
@@ -178,7 +58,7 @@ function renderBashBlockLines(
|
|
|
178
58
|
width: number,
|
|
179
59
|
theme: ThemeLike,
|
|
180
60
|
status: string | undefined,
|
|
181
|
-
inset =
|
|
61
|
+
inset = promptBlockInset(width),
|
|
182
62
|
): string[] {
|
|
183
63
|
const [spacer, ...rest] = lines;
|
|
184
64
|
const topRule = isRuleLine(rest[0]) ? rest[0] : undefined;
|
|
@@ -199,8 +79,15 @@ type BashRow = { status?: unknown };
|
|
|
199
79
|
|
|
200
80
|
type BashBlockPatchState = {
|
|
201
81
|
owners: number;
|
|
82
|
+
// Set at the final owner's shutdown. A wrapper another extension has buried
|
|
83
|
+
// cannot be uninstalled, so it delegates instead of outliving its owner; a
|
|
84
|
+
// later install re-enables it.
|
|
85
|
+
disabled?: boolean;
|
|
202
86
|
theme?: ThemeLike;
|
|
203
|
-
cache: WeakMap<
|
|
87
|
+
cache: WeakMap<
|
|
88
|
+
object,
|
|
89
|
+
{ width: number; src: string[]; lines: string[]; palette: string }
|
|
90
|
+
>;
|
|
204
91
|
originalRender: (width: number) => string[];
|
|
205
92
|
patchedRender?: (width: number) => string[];
|
|
206
93
|
};
|
|
@@ -222,6 +109,7 @@ function installBashBlockPatch(): BashBlockPatchState | undefined {
|
|
|
222
109
|
const existing = proto[BASH_BLOCK_PATCHED];
|
|
223
110
|
if (existing) {
|
|
224
111
|
existing.owners++;
|
|
112
|
+
existing.disabled = false;
|
|
225
113
|
return existing;
|
|
226
114
|
}
|
|
227
115
|
|
|
@@ -235,17 +123,24 @@ function installBashBlockPatch(): BashBlockPatchState | undefined {
|
|
|
235
123
|
width: number,
|
|
236
124
|
): string[] {
|
|
237
125
|
const theme = state.theme;
|
|
238
|
-
if (!theme)
|
|
239
|
-
|
|
126
|
+
if (state.disabled || !theme) {
|
|
127
|
+
return state.originalRender.call(this, width);
|
|
128
|
+
}
|
|
129
|
+
const inset = promptBlockInset(width);
|
|
240
130
|
const bodyWidth = width - inset * 2 - visibleWidth(PROMPT_RAIL);
|
|
241
131
|
if (inset === 0 || bodyWidth < 8) {
|
|
242
132
|
return state.originalRender.call(this, width);
|
|
243
133
|
}
|
|
244
134
|
const lines = state.originalRender.call(this, Math.max(1, bodyWidth));
|
|
135
|
+
// Decorated lines carry resolved colors, so the cache keys on the
|
|
136
|
+
// palette too: Pi swaps the colors behind a stable theme Proxy, which a
|
|
137
|
+
// theme-identity check would never notice.
|
|
138
|
+
const palette = paletteSample(theme);
|
|
245
139
|
const entry = state.cache.get(this);
|
|
246
140
|
if (
|
|
247
141
|
entry &&
|
|
248
142
|
entry.width === width &&
|
|
143
|
+
entry.palette === palette &&
|
|
249
144
|
entry.src.length === lines.length &&
|
|
250
145
|
entry.src.every((line, index) => line === lines[index])
|
|
251
146
|
) {
|
|
@@ -259,7 +154,7 @@ function installBashBlockPatch(): BashBlockPatchState | undefined {
|
|
|
259
154
|
typeof status === "string" ? status : undefined,
|
|
260
155
|
inset,
|
|
261
156
|
);
|
|
262
|
-
state.cache.set(this, { width, src: lines, lines: decorated });
|
|
157
|
+
state.cache.set(this, { width, src: lines, lines: decorated, palette });
|
|
263
158
|
return decorated;
|
|
264
159
|
};
|
|
265
160
|
|
|
@@ -285,6 +180,7 @@ function uninstallBashBlockPatch(state: BashBlockPatchState | undefined): void {
|
|
|
285
180
|
if (!state || state.owners <= 0) return;
|
|
286
181
|
state.owners--;
|
|
287
182
|
if (state.owners > 0) return;
|
|
183
|
+
state.disabled = true;
|
|
288
184
|
const proto = BashExecutionComponent?.prototype as unknown as BashRow & {
|
|
289
185
|
[BASH_BLOCK_PATCHED]?: BashBlockPatchState;
|
|
290
186
|
render?: (width: number) => string[];
|
package/src/container-hooks.ts
CHANGED
|
@@ -37,11 +37,17 @@ export function runChatContainerHooks(
|
|
|
37
37
|
const restores: Array<() => void> = [];
|
|
38
38
|
if (Array.isArray(children)) {
|
|
39
39
|
for (const hook of chatContainerHooks()) {
|
|
40
|
+
// A hook rewrites `children` and hands back its own undo. Snapshot first:
|
|
41
|
+
// a hook that throws after splicing rows out would otherwise leave them
|
|
42
|
+
// missing from every later render, with nothing left to restore them.
|
|
43
|
+
const before = [...children];
|
|
40
44
|
try {
|
|
41
45
|
const restore = hook(container, children, width);
|
|
42
46
|
if (typeof restore === "function") restores.push(restore);
|
|
43
47
|
} catch {
|
|
44
48
|
// Hooks are cosmetic; one failing hook must not break rendering.
|
|
49
|
+
children.length = 0;
|
|
50
|
+
children.push(...before);
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
53
|
}
|