@pi-kaush/pi-tool-call-markers 0.2.11 → 0.3.1
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 +18 -0
- package/README.md +19 -4
- package/package.json +1 -1
- package/src/index.ts +97 -58
- package/src/muted.ts +80 -0
- package/src/thinking-block-merger.ts +36 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Restyle subagent rows: the subagent marker is now `↪` (was `&`), and single
|
|
6
|
+
calls drop the `subagent` tool label — `↪ 🤖 [coder][c3po] Implement the…` —
|
|
7
|
+
with chain/parallel step lines reordered to emoji, profile badge, name badge
|
|
8
|
+
(`1. 🐝 [workhorse][bee] …`). Single-call headings are now scraped too, so
|
|
9
|
+
agent emojis appear on single rows, not just chain/parallel steps.
|
|
10
|
+
|
|
11
|
+
- Color collapsed rows (tool calls, `+ Thought`, subagents) from the
|
|
12
|
+
theme's `syntaxComment` token — which ships with every theme — instead of
|
|
13
|
+
the louder muted/toolTitle/toolOutput split. The bolded tool name and the
|
|
14
|
+
call content share the single tone, so collapsed blocks read as one
|
|
15
|
+
muted unit under any theme without per-theme tuning. Failures stay
|
|
16
|
+
error-colored and live spinners keep their thinking-level tint.
|
|
17
|
+
- Add optional `collapsedToolCall` and `collapsedThinkingCall` theme color
|
|
18
|
+
tokens to override each collapsed kind independently; themes without
|
|
19
|
+
them are unchanged.
|
|
20
|
+
- Fix settled "+ Thought" labels keeping stale colors after a mid-session
|
|
21
|
+
theme switch: label styles now recompute whenever the theme object
|
|
22
|
+
changes instead of only on session start and thinking-level selects.
|
|
5
23
|
- Add `/toggle-info`: hide tool calls and thinking entirely, or restore them
|
|
6
24
|
collapsed. Execution rows (including user-run `!` blocks) are lifted out of
|
|
7
25
|
the render pass via the shared chat-container hook; thinking blocks are
|
package/README.md
CHANGED
|
@@ -34,19 +34,19 @@ Collapsed tool rows use semantic theme colors with no gear, background fill, box
|
|
|
34
34
|
|
|
35
35
|
## Subagent plans
|
|
36
36
|
|
|
37
|
-
A recognized `subagent` call renders as an unboxed plan in the shared tool aesthetic, marked with
|
|
37
|
+
A recognized `subagent` call renders as an unboxed plan in the shared tool aesthetic, marked with `↪` instead of the ordinary `%` tool marker:
|
|
38
38
|
|
|
39
39
|
```text
|
|
40
|
-
|
|
40
|
+
↪ subagent chain (3 steps) [repo-review]
|
|
41
41
|
1. 🐝 bee [workhorse] Challenge the compatibility conclusion…
|
|
42
42
|
2. 🐝 bee …
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
Single calls stay on one
|
|
45
|
+
Single calls stay on one `↪ [<emoji>] [<profile>][<agent>] <task preview>` row — no tool label; the `↪` marker identifies it. Chain calls get a heading with the kind, count, and scope followed by numbered steps (parallel tasks list without numbers), with each step ordered emoji, profile badge, name badge. Agent display names (emoji + name) are scraped from the native plan component — including the single-call heading — with an args fallback, and render in `accent`; everything else stays muted, and failed subagents go full red. Subagents never join ordinary tool groups.
|
|
46
46
|
|
|
47
47
|
While a subagent runs, the plan headline's tail shows live progress from the streamed result details — `→ 1 turn · provider/model` in the warning tone — and a settled call keeps the same `→ N turns · provider/model` summary in muted (turns aggregate across tasks; the model shows only when every task used the same one).
|
|
48
48
|
|
|
49
|
-
Malformed, ambiguous, future, or too-narrow shapes fall back to the generic
|
|
49
|
+
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.
|
|
50
50
|
|
|
51
51
|
## Edit diffs
|
|
52
52
|
|
|
@@ -100,6 +100,21 @@ pi
|
|
|
100
100
|
|
|
101
101
|
`0`, `false`, `no`, and `off` disable parallel grouping. `1`, `true`, `yes`, and `on` enable it. The value is read when the extension loads.
|
|
102
102
|
|
|
103
|
+
### Collapsed-row colors
|
|
104
|
+
|
|
105
|
+
Collapsed rows (tool calls, `+ Thought`, subagents) default to the theme's `syntaxComment` color — it ships with every Pi theme and reads as a muted tone — with the bolded tool name and the call content sharing it. Failures stay error-colored and live spinners keep their existing tints.
|
|
106
|
+
|
|
107
|
+
A theme can override each collapsed kind independently with optional color tokens:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"colors": {
|
|
112
|
+
"collapsedToolCall": "#6272a4",
|
|
113
|
+
"collapsedThinkingCall": "#6272a4"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
103
118
|
## Compatibility and fallback policy
|
|
104
119
|
|
|
105
120
|
**Compatible Pi version:** `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui` `>=0.80.6`.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -16,10 +16,11 @@ import {
|
|
|
16
16
|
} from "./bash-block.ts";
|
|
17
17
|
import { runChatContainerHooks } from "./container-hooks.ts";
|
|
18
18
|
import { installInfoVisibility } from "./info-visibility.ts";
|
|
19
|
+
import { fgCollapsed } from "./muted.ts";
|
|
19
20
|
|
|
20
21
|
const OUTER_INSET = 2;
|
|
21
22
|
const GROUP_MARKER = "%";
|
|
22
|
-
const SUBAGENT_MARKER = "
|
|
23
|
+
const SUBAGENT_MARKER = "↪";
|
|
23
24
|
const PRESENTATION_PATCHED = Symbol.for("kg.pi.toolPresentation.v3");
|
|
24
25
|
const LEGACY_PRESENTATION_PATCHED = Symbol.for("kg.pi.toolPresentation.v2");
|
|
25
26
|
const GROUPING_PATCHED = Symbol.for("kg.pi.toolGrouping.v1");
|
|
@@ -29,6 +30,8 @@ const COLLAPSE_PARALLEL_ENV = "PI_TOOL_CALL_MARKERS_COLLAPSE_PARALLEL";
|
|
|
29
30
|
type ThemeLike = {
|
|
30
31
|
bold(text: string): string;
|
|
31
32
|
fg(color: string, text: string): string;
|
|
33
|
+
getFgAnsi?(color: string): string;
|
|
34
|
+
getColorMode?(): "truecolor" | "256color" | string;
|
|
32
35
|
};
|
|
33
36
|
|
|
34
37
|
type ComponentLike = {
|
|
@@ -252,7 +255,7 @@ function capFailureTail(tail: string, cap: number, theme: ThemeLike): string {
|
|
|
252
255
|
}
|
|
253
256
|
|
|
254
257
|
function renderedGenericOutcome(theme: ThemeLike): string {
|
|
255
|
-
return theme
|
|
258
|
+
return fgCollapsed(theme, "toolOutput", "→ done");
|
|
256
259
|
}
|
|
257
260
|
|
|
258
261
|
// The MCP adapter stashes its call's first line in renderer state before
|
|
@@ -637,7 +640,7 @@ function renderedEditDiffLines(
|
|
|
637
640
|
for (const line of raw.slice(0, MAX_EDIT_DIFF_LINES)) {
|
|
638
641
|
const clean = sanitizeInline(line).trimEnd();
|
|
639
642
|
if (clean.trim() === "...") {
|
|
640
|
-
lines.push(theme
|
|
643
|
+
lines.push(fgCollapsed(theme, "muted", " ..."));
|
|
641
644
|
continue;
|
|
642
645
|
}
|
|
643
646
|
const match = EDIT_DIFF_LINE_RE.exec(clean);
|
|
@@ -652,7 +655,11 @@ function renderedEditDiffLines(
|
|
|
652
655
|
}
|
|
653
656
|
if (raw.length > MAX_EDIT_DIFF_LINES) {
|
|
654
657
|
lines.push(
|
|
655
|
-
|
|
658
|
+
fgCollapsed(
|
|
659
|
+
theme,
|
|
660
|
+
"muted",
|
|
661
|
+
` ... +${raw.length - MAX_EDIT_DIFF_LINES} more`,
|
|
662
|
+
),
|
|
656
663
|
);
|
|
657
664
|
}
|
|
658
665
|
return lines;
|
|
@@ -706,7 +713,7 @@ function renderedOutcome(
|
|
|
706
713
|
): string | undefined {
|
|
707
714
|
const summary = outcomeSummary(row);
|
|
708
715
|
if (!summary) return undefined;
|
|
709
|
-
return theme
|
|
716
|
+
return fgCollapsed(theme, "toolOutput", `→ ${summary}`);
|
|
710
717
|
}
|
|
711
718
|
|
|
712
719
|
function renderedGroupedOutcome(
|
|
@@ -835,10 +842,10 @@ function renderedCallSummary(
|
|
|
835
842
|
.map(trimRenderedLine)
|
|
836
843
|
.filter(hasVisibleContent);
|
|
837
844
|
if (visibleLines.length > rendered.length)
|
|
838
|
-
continuations.push(theme
|
|
845
|
+
continuations.push(fgCollapsed(theme, "muted", "…"));
|
|
839
846
|
const compact = [firstSummary, ...continuations]
|
|
840
847
|
.filter(hasVisibleContent)
|
|
841
|
-
.join(theme
|
|
848
|
+
.join(fgCollapsed(theme, "muted", " · "));
|
|
842
849
|
if (hasVisibleContent(compact)) return compact;
|
|
843
850
|
}
|
|
844
851
|
}
|
|
@@ -846,8 +853,8 @@ function renderedCallSummary(
|
|
|
846
853
|
|
|
847
854
|
const fallback = compactArgs(row.args);
|
|
848
855
|
return fallback
|
|
849
|
-
? theme
|
|
850
|
-
: theme
|
|
856
|
+
? fgCollapsed(theme, "toolOutput", fallback)
|
|
857
|
+
: fgCollapsed(theme, "muted", "(no arguments)");
|
|
851
858
|
}
|
|
852
859
|
|
|
853
860
|
function styledCallLabel(
|
|
@@ -857,15 +864,14 @@ function styledCallLabel(
|
|
|
857
864
|
): string {
|
|
858
865
|
const plain = sanitizeInline(stripAnsi(label)).trim();
|
|
859
866
|
const match = /^(\S+)(.*)$/s.exec(plain);
|
|
860
|
-
if (!match) return theme
|
|
867
|
+
if (!match) return fgCollapsed(theme, color, plain);
|
|
861
868
|
const rest = match[2] ?? "";
|
|
862
|
-
//
|
|
863
|
-
//
|
|
864
|
-
|
|
865
|
-
const restColor = color === "muted" ? "toolOutput" : color;
|
|
869
|
+
// The colon joins the bolded name to its content. Failed rows stay
|
|
870
|
+
// uniformly error-colored; settled rows use the collapsed mute — same
|
|
871
|
+
// color for the bolded name and the call content.
|
|
866
872
|
return (
|
|
867
|
-
theme
|
|
868
|
-
(rest ? theme
|
|
873
|
+
fgCollapsed(theme, color, match[1] ?? "", true) +
|
|
874
|
+
(rest ? fgCollapsed(theme, color, `:${rest}`) : "")
|
|
869
875
|
);
|
|
870
876
|
}
|
|
871
877
|
|
|
@@ -904,7 +910,7 @@ function collapsedOutcome(
|
|
|
904
910
|
}
|
|
905
911
|
if (row.getRenderShell?.() === "self") {
|
|
906
912
|
const editStat = renderedEditDiffStat(row);
|
|
907
|
-
if (editStat) return theme
|
|
913
|
+
if (editStat) return fgCollapsed(theme, "toolOutput", `→ ${editStat}`);
|
|
908
914
|
return renderedGenericOutcome(theme);
|
|
909
915
|
}
|
|
910
916
|
return renderedOutcome(row, theme);
|
|
@@ -917,18 +923,19 @@ function collapsedHeadline(
|
|
|
917
923
|
): string {
|
|
918
924
|
// Failed rows render entirely in error so the row reads as the one that
|
|
919
925
|
// failed, not just its outcome tail. Only the tool name is bold.
|
|
920
|
-
const
|
|
921
|
-
const marker = `${
|
|
922
|
-
|
|
926
|
+
const tone = rowHasFailed(row) ? "error" : "muted";
|
|
927
|
+
const marker = `${fgCollapsed(
|
|
928
|
+
theme,
|
|
929
|
+
tone,
|
|
923
930
|
row.toolName === "subagent" ? SUBAGENT_MARKER : GROUP_MARKER,
|
|
924
931
|
)} `;
|
|
925
932
|
const budget = Math.max(1, width - visibleWidth(marker));
|
|
926
|
-
const label = collapsedCallLabel(row, budget, theme,
|
|
933
|
+
const label = collapsedCallLabel(row, budget, theme, tone);
|
|
927
934
|
const outcome = collapsedOutcome(row, budget, theme);
|
|
928
935
|
// The truncation suffix inherits the row tone; pi-tui's truncation resets
|
|
929
936
|
// around a plain suffix, which would render it in the terminal default
|
|
930
937
|
// foreground instead of the row color.
|
|
931
|
-
const suffix = theme
|
|
938
|
+
const suffix = fgCollapsed(theme, tone, "…");
|
|
932
939
|
return (
|
|
933
940
|
marker +
|
|
934
941
|
(outcome
|
|
@@ -1021,6 +1028,19 @@ function parseStepContent(content: string): {
|
|
|
1021
1028
|
};
|
|
1022
1029
|
}
|
|
1023
1030
|
|
|
1031
|
+
// Splits a scraped "emoji name" display into its emoji prefix and bare name;
|
|
1032
|
+
// plain names pass through with no emoji.
|
|
1033
|
+
function splitEmojiName(
|
|
1034
|
+
displayName: string,
|
|
1035
|
+
bareName: string,
|
|
1036
|
+
): { emoji: string; name: string } {
|
|
1037
|
+
const tokens = displayName.trim().split(/\s+/).filter(Boolean);
|
|
1038
|
+
if (tokens.length > 1 && tokens[tokens.length - 1] === bareName) {
|
|
1039
|
+
return { emoji: tokens.slice(0, -1).join(" "), name: bareName };
|
|
1040
|
+
}
|
|
1041
|
+
return { emoji: "", name: bareName };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1024
1044
|
// Agent display names (emoji + name) come from the subagent extension's own
|
|
1025
1045
|
// call component when available; args only carry the bare agent name.
|
|
1026
1046
|
function scrapeSubagentDisplayNames(
|
|
@@ -1035,10 +1055,16 @@ function scrapeSubagentDisplayNames(
|
|
|
1035
1055
|
try {
|
|
1036
1056
|
for (const raw of component.render(120)) {
|
|
1037
1057
|
const line = sanitizeInline(stripAnsi(raw)).trim();
|
|
1038
|
-
if (!line
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1058
|
+
if (!line) continue;
|
|
1059
|
+
// Single calls carry the agent name on their "subagent <name>" heading
|
|
1060
|
+
// line; stripping the heading word (and step numbers) lets both shapes
|
|
1061
|
+
// flow through the same parse. Heading remainders without an emoji
|
|
1062
|
+
// (e.g. "chain (2 steps)") degrade to displayName === bareName and are
|
|
1063
|
+
// skipped by the guard below.
|
|
1064
|
+
const content = line
|
|
1065
|
+
.replace(/^subagent\b\s*/, "")
|
|
1066
|
+
.replace(/^\d+\.\s*/, "");
|
|
1067
|
+
const { displayName, bareName } = parseStepContent(content);
|
|
1042
1068
|
if (displayName && bareName && displayName !== bareName) {
|
|
1043
1069
|
names.set(bareName, displayName);
|
|
1044
1070
|
}
|
|
@@ -1084,10 +1110,11 @@ function subagentProgressText(row: ToolExecutionRow): string | undefined {
|
|
|
1084
1110
|
return parts.length > 0 ? parts.join(" · ") : undefined;
|
|
1085
1111
|
}
|
|
1086
1112
|
|
|
1087
|
-
// Subagents render as an ordinary unboxed tool block: a
|
|
1113
|
+
// Subagents render as an ordinary unboxed tool block: a `↪` heading with the
|
|
1088
1114
|
// plan kind/count/scope, then the numbered chain steps or parallel tasks with
|
|
1089
|
-
// agent
|
|
1090
|
-
//
|
|
1115
|
+
// agent identities (emoji, profile badge, name badge) in accent. Single calls
|
|
1116
|
+
// collapse to one row: `↪ <emoji> [<profile>][<name>] <task preview>`. Everything
|
|
1117
|
+
// else stays muted (or error on failure), matching the shared tool-row aesthetic.
|
|
1091
1118
|
function renderSubagentPlan(
|
|
1092
1119
|
row: ToolExecutionRow,
|
|
1093
1120
|
width: number,
|
|
@@ -1099,20 +1126,27 @@ function renderSubagentPlan(
|
|
|
1099
1126
|
const failed = rowHasFailed(row);
|
|
1100
1127
|
const color = failed ? "error" : "muted";
|
|
1101
1128
|
const nameColor = failed ? "error" : "accent";
|
|
1102
|
-
const suffix = theme
|
|
1129
|
+
const suffix = fgCollapsed(theme, color, "…");
|
|
1103
1130
|
const displayNames = scrapeSubagentDisplayNames(row);
|
|
1104
1131
|
// Agent/profile/task values are model-supplied; sanitize them like every
|
|
1105
1132
|
// other collapsed-row text so control bytes cannot reach the terminal.
|
|
1106
|
-
|
|
1107
|
-
|
|
1133
|
+
// Identity order: emoji, then the profile and agent-name badges.
|
|
1134
|
+
const identityOf = (step: SubagentStep) => {
|
|
1135
|
+
const displayName =
|
|
1136
|
+
displayNames.get(step.agent) ?? sanitizeInline(step.agent);
|
|
1137
|
+
const { emoji, name } = splitEmojiName(displayName, step.agent);
|
|
1138
|
+
const accentPart = (text: string) => fgCollapsed(theme, nameColor, text);
|
|
1139
|
+
const emojiPart = emoji ? `${accentPart(emoji)} ` : "";
|
|
1140
|
+
const profilePart = step.profile
|
|
1141
|
+
? accentPart(`[${sanitizeInline(step.profile)}]`)
|
|
1142
|
+
: "";
|
|
1143
|
+
return `${emojiPart}${profilePart}${accentPart(`[${sanitizeInline(name)}]`)}`;
|
|
1144
|
+
};
|
|
1108
1145
|
const detailColor = failed ? "error" : "toolOutput";
|
|
1109
|
-
const
|
|
1110
|
-
theme.
|
|
1111
|
-
detailColor,
|
|
1112
|
-
`${step.profile ? ` [${sanitizeInline(step.profile)}]` : ""} ${subagentStepPreview(step.task)}`,
|
|
1113
|
-
);
|
|
1146
|
+
const previewOf = (step: SubagentStep) =>
|
|
1147
|
+
fgCollapsed(theme, detailColor, ` ${subagentStepPreview(step.task)}`);
|
|
1114
1148
|
|
|
1115
|
-
const marker = `${theme
|
|
1149
|
+
const marker = `${fgCollapsed(theme, color, SUBAGENT_MARKER, true)} `;
|
|
1116
1150
|
const budget = Math.max(1, width - visibleWidth(marker));
|
|
1117
1151
|
const progress = subagentProgressText(row);
|
|
1118
1152
|
const outcome = failed
|
|
@@ -1120,7 +1154,11 @@ function renderSubagentPlan(
|
|
|
1120
1154
|
: isLiveRow(row)
|
|
1121
1155
|
? theme.fg("warning", progress ? `→ ${progress}` : "…")
|
|
1122
1156
|
: row.result
|
|
1123
|
-
?
|
|
1157
|
+
? fgCollapsed(
|
|
1158
|
+
theme,
|
|
1159
|
+
"toolOutput",
|
|
1160
|
+
progress ? `→ ${progress}` : "→ done",
|
|
1161
|
+
)
|
|
1124
1162
|
: theme.fg("warning", "…");
|
|
1125
1163
|
const headline = (label: string) =>
|
|
1126
1164
|
marker +
|
|
@@ -1130,14 +1168,7 @@ function renderSubagentPlan(
|
|
|
1130
1168
|
|
|
1131
1169
|
if (plan.kind === "single") {
|
|
1132
1170
|
const step = plan.steps[0]!;
|
|
1133
|
-
return [
|
|
1134
|
-
headline(
|
|
1135
|
-
theme.fg(failed ? "error" : "toolTitle", theme.bold("subagent")) +
|
|
1136
|
-
" " +
|
|
1137
|
-
displayOf(step.agent) +
|
|
1138
|
-
detailOf(step),
|
|
1139
|
-
),
|
|
1140
|
-
];
|
|
1171
|
+
return [headline(identityOf(step) + previewOf(step))];
|
|
1141
1172
|
}
|
|
1142
1173
|
|
|
1143
1174
|
const kindLabel =
|
|
@@ -1146,22 +1177,24 @@ function renderSubagentPlan(
|
|
|
1146
1177
|
: `parallel (${plan.steps.length} tasks)`;
|
|
1147
1178
|
const lines = [
|
|
1148
1179
|
headline(
|
|
1149
|
-
theme
|
|
1180
|
+
fgCollapsed(theme, failed ? "error" : "toolTitle", "subagent", true) +
|
|
1150
1181
|
" " +
|
|
1151
|
-
theme
|
|
1152
|
-
theme
|
|
1182
|
+
fgCollapsed(theme, failed ? "error" : "toolOutput", kindLabel) +
|
|
1183
|
+
fgCollapsed(theme, failed ? "error" : "toolOutput", ` [${plan.scope}]`),
|
|
1153
1184
|
),
|
|
1154
1185
|
];
|
|
1155
1186
|
const shown = plan.steps.slice(0, 3);
|
|
1156
1187
|
for (let index = 0; index < shown.length; index++) {
|
|
1157
1188
|
const step = shown[index]!;
|
|
1158
1189
|
const number =
|
|
1159
|
-
plan.kind === "chain"
|
|
1160
|
-
|
|
1190
|
+
plan.kind === "chain"
|
|
1191
|
+
? `${fgCollapsed(theme, color, `${index + 1}.`)} `
|
|
1192
|
+
: "";
|
|
1193
|
+
lines.push(` ${number}${identityOf(step)}${previewOf(step)}`);
|
|
1161
1194
|
}
|
|
1162
1195
|
if (plan.steps.length > shown.length) {
|
|
1163
1196
|
lines.push(
|
|
1164
|
-
` ${theme
|
|
1197
|
+
` ${fgCollapsed(theme, color, `... +${plan.steps.length - shown.length} more`)}`,
|
|
1165
1198
|
);
|
|
1166
1199
|
}
|
|
1167
1200
|
return lines;
|
|
@@ -1182,7 +1215,7 @@ function imageResultLines(
|
|
|
1182
1215
|
.join("\n");
|
|
1183
1216
|
lines.push(
|
|
1184
1217
|
...wrapTextWithAnsi(
|
|
1185
|
-
theme
|
|
1218
|
+
fgCollapsed(theme, "toolOutput", sanitized),
|
|
1186
1219
|
Math.max(1, width),
|
|
1187
1220
|
),
|
|
1188
1221
|
);
|
|
@@ -1238,7 +1271,7 @@ function groupedChildLabel(
|
|
|
1238
1271
|
Math.max(0, visibleWidth(label) - prefixWidth),
|
|
1239
1272
|
true,
|
|
1240
1273
|
);
|
|
1241
|
-
return child || theme
|
|
1274
|
+
return child || fgCollapsed(theme, color, "(no arguments)");
|
|
1242
1275
|
}
|
|
1243
1276
|
|
|
1244
1277
|
function renderGroupedCallLines(
|
|
@@ -1252,17 +1285,17 @@ function renderGroupedCallLines(
|
|
|
1252
1285
|
const toolName = row.toolName ?? "tool";
|
|
1253
1286
|
if (toolName !== previousToolName) {
|
|
1254
1287
|
lines.push(
|
|
1255
|
-
`${theme
|
|
1288
|
+
`${fgCollapsed(theme, "muted", GROUP_MARKER)} ${fgCollapsed(theme, "toolTitle", toolName, true)}`,
|
|
1256
1289
|
);
|
|
1257
1290
|
previousToolName = toolName;
|
|
1258
1291
|
}
|
|
1259
1292
|
|
|
1260
1293
|
const color = rowHasFailed(row) ? "error" : "muted";
|
|
1261
|
-
const prefix = ` ${theme
|
|
1294
|
+
const prefix = ` ${fgCollapsed(theme, color, "•")} `;
|
|
1262
1295
|
const budget = Math.max(1, width - visibleWidth(prefix));
|
|
1263
1296
|
const label = groupedChildLabel(row, budget, theme, color);
|
|
1264
1297
|
const outcome = collapsedOutcome(row, budget, theme);
|
|
1265
|
-
const suffix = theme
|
|
1298
|
+
const suffix = fgCollapsed(theme, color, "…");
|
|
1266
1299
|
lines.push(
|
|
1267
1300
|
prefix +
|
|
1268
1301
|
(outcome
|
|
@@ -1681,6 +1714,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1681
1714
|
ctx.ui.setToolsExpanded(false);
|
|
1682
1715
|
});
|
|
1683
1716
|
|
|
1717
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1718
|
+
if (patch) patch.theme = ctx.ui.theme;
|
|
1719
|
+
if (bashPatch) bashPatch.theme = ctx.ui.theme;
|
|
1720
|
+
ctx.ui.setToolsExpanded(false);
|
|
1721
|
+
});
|
|
1722
|
+
|
|
1684
1723
|
pi.on("session_shutdown", () => {
|
|
1685
1724
|
if (released) return;
|
|
1686
1725
|
released = true;
|
package/src/muted.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Collapsed-row color policy.
|
|
2
|
+
//
|
|
3
|
+
// Collapsed blocks (tool calls, settled "+ Thought" labels, subagent rows)
|
|
4
|
+
// default to the theme's `syntaxComment` token — it ships with every Pi
|
|
5
|
+
// theme and reads as a muted tone — instead of the louder
|
|
6
|
+
// muted/toolTitle/toolOutput split. A theme can override each collapsed
|
|
7
|
+
// kind independently with a `collapsedToolCall` or `collapsedThinkingCall`
|
|
8
|
+
// color token.
|
|
9
|
+
//
|
|
10
|
+
// Failures always fall back to the historical tokens.
|
|
11
|
+
|
|
12
|
+
const TOOL_OVERRIDE_TOKEN = "collapsedToolCall";
|
|
13
|
+
const THINKING_OVERRIDE_TOKEN = "collapsedThinkingCall";
|
|
14
|
+
const DEFAULT_TOKEN = "syntaxComment";
|
|
15
|
+
|
|
16
|
+
type CollapsedTheme = {
|
|
17
|
+
fg(color: string, text: string): string;
|
|
18
|
+
bold?(text: string): string;
|
|
19
|
+
getFgAnsi?(color: string): string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Resolved per theme object (Pi's Theme throws on unknown tokens; themes
|
|
23
|
+
// without getFgAnsi have no overrides), alongside index.ts's other
|
|
24
|
+
// theme-identity-keyed caches.
|
|
25
|
+
const ansiCache = new WeakMap<object, Map<string, string | null>>();
|
|
26
|
+
|
|
27
|
+
function tokenAnsi(theme: CollapsedTheme, token: string): string | null {
|
|
28
|
+
let cache = ansiCache.get(theme);
|
|
29
|
+
if (!cache) {
|
|
30
|
+
cache = new Map();
|
|
31
|
+
ansiCache.set(theme, cache);
|
|
32
|
+
}
|
|
33
|
+
if (cache.has(token)) return cache.get(token)!;
|
|
34
|
+
let ansi: string | null = null;
|
|
35
|
+
try {
|
|
36
|
+
const resolved = theme.getFgAnsi?.(token);
|
|
37
|
+
if (typeof resolved === "string") ansi = resolved;
|
|
38
|
+
} catch {
|
|
39
|
+
ansi = null;
|
|
40
|
+
}
|
|
41
|
+
cache.set(token, ansi);
|
|
42
|
+
return ansi;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Override token when defined, else the theme's syntaxComment color, else
|
|
46
|
+
// null (caller falls back to the historical tokens).
|
|
47
|
+
function collapsedAnsi(
|
|
48
|
+
theme: CollapsedTheme,
|
|
49
|
+
overrideToken: string,
|
|
50
|
+
): string | null {
|
|
51
|
+
return tokenAnsi(theme, overrideToken) ?? tokenAnsi(theme, DEFAULT_TOKEN);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function collapsedToolAnsi(theme: CollapsedTheme): string | null {
|
|
55
|
+
return collapsedAnsi(theme, TOOL_OVERRIDE_TOKEN);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function collapsedThinkingAnsi(theme: CollapsedTheme): string | null {
|
|
59
|
+
return collapsedAnsi(theme, THINKING_OVERRIDE_TOKEN);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// One styling entry point for collapsed-row text. `color` is the historical
|
|
63
|
+
// token role ("muted", "toolOutput", "toolTitle", "accent", "error"); error
|
|
64
|
+
// keeps its semantic color in all paths.
|
|
65
|
+
export function fgCollapsed(
|
|
66
|
+
theme: CollapsedTheme,
|
|
67
|
+
color: string,
|
|
68
|
+
text: string,
|
|
69
|
+
bold = false,
|
|
70
|
+
): string {
|
|
71
|
+
if (color !== "error") {
|
|
72
|
+
const ansi = collapsedToolAnsi(theme);
|
|
73
|
+
if (ansi) {
|
|
74
|
+
return bold
|
|
75
|
+
? `${ansi}\x1b[1m${text}\x1b[22m\x1b[39m`
|
|
76
|
+
: `${ansi}${text}\x1b[39m`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return theme.fg(color, bold && theme.bold ? theme.bold(text) : text);
|
|
80
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { AssistantMessageComponent } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { infoVisibilityHidden } from "./info-visibility-state.ts";
|
|
4
|
+
import { collapsedThinkingAnsi } from "./muted.ts";
|
|
4
5
|
|
|
5
6
|
const THINKING_GROUPING_PATCHED = Symbol.for("kg.pi.thinkingGrouping.v2");
|
|
6
7
|
const PI_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -44,15 +45,22 @@ type ThemeDetail = {
|
|
|
44
45
|
getColorMode?(): "truecolor" | "256color" | string;
|
|
45
46
|
};
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
// Live reference into the UI context: reading .theme per check keeps label
|
|
49
|
+
// styles current across mid-session theme switches, which fire no event.
|
|
50
|
+
let themeProvider: (() => ThemeDetail | undefined) | undefined;
|
|
48
51
|
let activeLevel: (() => string) | undefined;
|
|
52
|
+
// Theme object the current label styles were computed from.
|
|
53
|
+
let stylesTheme: ThemeDetail | undefined;
|
|
49
54
|
|
|
50
55
|
type LabelStyle = { prefix: string; suffix: string };
|
|
51
56
|
let liveLabelStyle: LabelStyle | undefined;
|
|
52
57
|
let settledLabelStyle: LabelStyle | undefined;
|
|
53
58
|
|
|
54
|
-
function styledWith(
|
|
55
|
-
|
|
59
|
+
function styledWith(
|
|
60
|
+
token: string,
|
|
61
|
+
theme: ThemeDetail | undefined = themeProvider?.(),
|
|
62
|
+
): LabelStyle | undefined {
|
|
63
|
+
const ansi = theme?.getFgAnsi?.(token);
|
|
56
64
|
if (!ansi) return undefined;
|
|
57
65
|
// The styled label replaces Pi's italicized Text node wholesale (see
|
|
58
66
|
// restyleHiddenThinkingLabel). The leading italic-off still matters: the
|
|
@@ -61,19 +69,36 @@ function styledWith(token: string): LabelStyle | undefined {
|
|
|
61
69
|
return { prefix: `\x1b[23m${ansi}`, suffix: "\x1b[39m" };
|
|
62
70
|
}
|
|
63
71
|
|
|
72
|
+
// The theme's collapsedThinkingCall override when defined, else its comment
|
|
73
|
+
// color, else the muted token.
|
|
74
|
+
function settledMutedStyle(
|
|
75
|
+
theme: ThemeDetail | undefined,
|
|
76
|
+
): LabelStyle | undefined {
|
|
77
|
+
const ansi = theme ? collapsedThinkingAnsi(theme) : null;
|
|
78
|
+
if (ansi) return { prefix: `\x1b[23m${ansi}`, suffix: "\x1b[39m" };
|
|
79
|
+
return styledWith("muted", theme);
|
|
80
|
+
}
|
|
64
81
|
function updateThoughtLabelStyle(): void {
|
|
65
82
|
liveLabelStyle = undefined;
|
|
66
83
|
settledLabelStyle = undefined;
|
|
84
|
+
const theme = themeProvider?.();
|
|
85
|
+
stylesTheme = theme;
|
|
67
86
|
const choice = thoughtLabelColorChoice();
|
|
68
|
-
if (choice === "inherit" || !
|
|
87
|
+
if (choice === "inherit" || !theme) return;
|
|
69
88
|
|
|
70
89
|
const token =
|
|
71
90
|
choice === "mdheading"
|
|
72
91
|
? "mdHeading"
|
|
73
92
|
: (LEVEL_TOKEN[activeLevel?.() ?? "off"] ?? "thinkingOff");
|
|
74
|
-
liveLabelStyle = styledWith(token);
|
|
93
|
+
liveLabelStyle = styledWith(token, theme);
|
|
75
94
|
settledLabelStyle =
|
|
76
|
-
choice === "mdheading" ? liveLabelStyle :
|
|
95
|
+
choice === "mdheading" ? liveLabelStyle : settledMutedStyle(theme);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Recompute label styles whenever the theme object has swapped (theme switch
|
|
99
|
+
// or /reload); those transitions fire no extension event.
|
|
100
|
+
function ensureLabelStyles(): void {
|
|
101
|
+
if (themeProvider?.() !== stylesTheme) updateThoughtLabelStyle();
|
|
77
102
|
}
|
|
78
103
|
|
|
79
104
|
// The settled label is the finalized "+ Thought" row; every other label is
|
|
@@ -83,6 +108,7 @@ function isSettledThoughtLabel(label: string): boolean {
|
|
|
83
108
|
}
|
|
84
109
|
|
|
85
110
|
export function visibleThoughtLabel(label: string): string {
|
|
111
|
+
ensureLabelStyles();
|
|
86
112
|
const style = isSettledThoughtLabel(label)
|
|
87
113
|
? settledLabelStyle
|
|
88
114
|
: liveLabelStyle;
|
|
@@ -418,7 +444,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
418
444
|
} catch {
|
|
419
445
|
// Best effort; stale rows would only linger until the process exits.
|
|
420
446
|
}
|
|
421
|
-
|
|
447
|
+
const uiCtx = ctx;
|
|
448
|
+
themeProvider = () => uiCtx.ui?.theme as unknown as ThemeDetail;
|
|
422
449
|
activeLevel = () => {
|
|
423
450
|
try {
|
|
424
451
|
return pi.getThinkingLevel();
|
|
@@ -433,8 +460,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
433
460
|
if (released) return;
|
|
434
461
|
released = true;
|
|
435
462
|
if (patch && !uninstallThinkingGroupingPatch(patch)) return;
|
|
436
|
-
|
|
463
|
+
themeProvider = undefined;
|
|
437
464
|
activeLevel = undefined;
|
|
465
|
+
stylesTheme = undefined;
|
|
438
466
|
liveLabelStyle = undefined;
|
|
439
467
|
settledLabelStyle = undefined;
|
|
440
468
|
});
|