pi-my-compact-ui 0.2.0 → 0.2.2
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/README.md +2 -2
- package/index.ts +140 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,8 +53,8 @@ reasoning content:
|
|
|
53
53
|
- Renders fenced code blocks as subtle theme-aware background panels with
|
|
54
54
|
syntax highlighting and one character of horizontal padding instead of
|
|
55
55
|
decorative top and bottom border rows.
|
|
56
|
-
- Preserves Pi's native execution semantics for `read`, `bash`, `
|
|
57
|
-
`find`, `grep`, and `ls`.
|
|
56
|
+
- Preserves Pi's native execution semantics for `read`, `bash`, `powershell`, `edit`,
|
|
57
|
+
`write`, `find`, `grep`, and `ls`.
|
|
58
58
|
- Gives compaction summaries a distinct, compact presentation.
|
|
59
59
|
|
|
60
60
|
## Installation
|
package/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
createFindTool,
|
|
31
31
|
createGrepTool,
|
|
32
32
|
createLsTool,
|
|
33
|
+
createPowerShellTool,
|
|
33
34
|
createReadTool,
|
|
34
35
|
createWriteTool,
|
|
35
36
|
getMarkdownTheme,
|
|
@@ -313,7 +314,8 @@ function refreshSealedThinkingTokens(reportedReasoning?: unknown): void {
|
|
|
313
314
|
function toolSummary(name: string, args: any): { name: string; content: string } {
|
|
314
315
|
switch (name) {
|
|
315
316
|
case "bash":
|
|
316
|
-
|
|
317
|
+
case "powershell":
|
|
318
|
+
return { name, content: oneLine(args?.command || "…") };
|
|
317
319
|
case "read":
|
|
318
320
|
return { name: "read", content: shortenPath(args?.path || "…") };
|
|
319
321
|
case "write":
|
|
@@ -632,23 +634,38 @@ class ToolGroupComponent extends Container {
|
|
|
632
634
|
if ((tool as any)?.[PARENT_KEY] === this) delete (tool as any)[PARENT_KEY];
|
|
633
635
|
}
|
|
634
636
|
|
|
637
|
+
/** Return only entries that should be displayed (tools, or thinking with non-empty text). */
|
|
638
|
+
getVisibleEntries(): GroupEntry[] {
|
|
639
|
+
return this.entries.filter((entry) => {
|
|
640
|
+
if (entry.kind === "tool") return true;
|
|
641
|
+
if (entry.kind === "thinking") {
|
|
642
|
+
return entry.text.trim().length > 0;
|
|
643
|
+
}
|
|
644
|
+
return false;
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
hasVisibleEntries(): boolean {
|
|
649
|
+
return this.getVisibleEntries().length > 0;
|
|
650
|
+
}
|
|
651
|
+
|
|
635
652
|
hasPending(): boolean {
|
|
636
|
-
return this.
|
|
653
|
+
return this.getVisibleEntries().some((entry) => entry.kind === "tool" && toolStatus(entry.tool) === "pending");
|
|
637
654
|
}
|
|
638
655
|
|
|
639
656
|
/** True while a thinking run in this group is still streaming. */
|
|
640
657
|
hasActiveThinking(): boolean {
|
|
641
|
-
return this.
|
|
658
|
+
return this.getVisibleEntries().some((entry) => entry.kind === "thinking" && entry.active);
|
|
642
659
|
}
|
|
643
660
|
|
|
644
661
|
/** True once any thinking run has been added to this group. */
|
|
645
662
|
hasThinking(): boolean {
|
|
646
|
-
return this.
|
|
663
|
+
return this.getVisibleEntries().some((entry) => entry.kind === "thinking");
|
|
647
664
|
}
|
|
648
665
|
|
|
649
666
|
/** True while this group should keep its spinner animating. */
|
|
650
667
|
needsAnimation(): boolean {
|
|
651
|
-
return this.hasPending() || this.hasActiveThinking();
|
|
668
|
+
return this.hasVisibleEntries() && (this.hasPending() || this.hasActiveThinking());
|
|
652
669
|
}
|
|
653
670
|
|
|
654
671
|
override invalidate(): void {
|
|
@@ -711,10 +728,8 @@ class ToolGroupComponent extends Container {
|
|
|
711
728
|
const bold = theme?.bold ? theme.bold : (t: string) => t;
|
|
712
729
|
const st = toolStatus(tool);
|
|
713
730
|
const s = toolSummary(tool.toolName, tool.args);
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
private thinkingActiveInGroup(): boolean {
|
|
717
|
-
return !this.sealed && this.hasActiveThinking();
|
|
731
|
+
const prefix = rail ? fg("dim", rail) : "";
|
|
732
|
+
return `${prefix}${fg(this.colorFor(st), this.iconFor(tool, frame))} ${fg("toolTitle", bold(s.name))} ${fg("dim", s.content)} ${fg("muted", `(${toolElapsed(tool)}s)`)}`;
|
|
718
733
|
}
|
|
719
734
|
|
|
720
735
|
// One row per thinking run. The spinner marks the run that is still streaming;
|
|
@@ -724,28 +739,37 @@ class ToolGroupComponent extends Container {
|
|
|
724
739
|
const fg = (color: string, text: string) => theme?.fg?.(color, text) ?? text;
|
|
725
740
|
const icon = entry.active ? frame : "✓";
|
|
726
741
|
const preview = entry.text.trim().replace(/[*_#`>]+/g, "");
|
|
742
|
+
const prefix = rail ? fg("dim", rail) : "";
|
|
727
743
|
if (!preview) {
|
|
728
|
-
|
|
744
|
+
const label = entry.active ? "thinking..." : "thinking";
|
|
745
|
+
return `${prefix}${fg("thinkingText", icon)} ${fg("thinkingText", label)}`;
|
|
729
746
|
}
|
|
730
747
|
const tokenLabel = thinkingTokenLabel(entry);
|
|
731
|
-
const previewLimit = Math.max(1, Math.min(60, width - GROUP_PADDING_X - 18 - tokenLabel.length));
|
|
732
|
-
return `${
|
|
748
|
+
const previewLimit = Math.max(1, Math.min(60, width - GROUP_PADDING_X - rail.length - 18 - tokenLabel.length));
|
|
749
|
+
return `${prefix}${fg("thinkingText", icon)} ${fg("thinkingText", `thinking: ${oneLine(preview, previewLimit)}`)} ${fg("muted", `· ${tokenLabel}`)}`;
|
|
733
750
|
}
|
|
734
751
|
|
|
735
752
|
// Folded: header + one line per entry, in stream order (oldest first).
|
|
736
|
-
|
|
753
|
+
// When there is only 1 visible entry, do not show hierarchy (no header, no tree rail).
|
|
754
|
+
private renderCollapsed(width: number, visible: GroupEntry[]): string[] {
|
|
737
755
|
const theme = currentTheme;
|
|
738
756
|
const fg = (color: string, text: string) => theme?.fg?.(color, text) ?? text;
|
|
739
757
|
const frame = SPINNER[Math.floor((Date.now() - spinnerStart) / SPINNER_MS) % SPINNER.length]!;
|
|
758
|
+
|
|
759
|
+
if (visible.length === 1) {
|
|
760
|
+
const entry = visible[0]!;
|
|
761
|
+
if (entry.kind === "tool") {
|
|
762
|
+
return [this.toolRow("", entry.tool, frame)];
|
|
763
|
+
}
|
|
764
|
+
return [this.thinkingRow("", entry, frame, width)];
|
|
765
|
+
}
|
|
766
|
+
|
|
740
767
|
const lines: string[] = [];
|
|
741
768
|
|
|
742
|
-
const hasPendingTool =
|
|
743
|
-
const isThinking = this.
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
// block and must not claim "tools done".
|
|
747
|
-
const hasTools = this.children.length > 0;
|
|
748
|
-
const hasThinking = this.hasThinking();
|
|
769
|
+
const hasPendingTool = visible.some((e) => e.kind === "tool" && toolStatus(e.tool) === "pending");
|
|
770
|
+
const isThinking = !this.sealed && visible.some((e) => e.kind === "thinking" && e.active);
|
|
771
|
+
const hasTools = visible.some((e) => e.kind === "tool");
|
|
772
|
+
const hasThinking = visible.some((e) => e.kind === "thinking");
|
|
749
773
|
const openNoTools = !this.sealed && !hasTools;
|
|
750
774
|
const working = hasPendingTool || isThinking || openNoTools;
|
|
751
775
|
const state = hasPendingTool
|
|
@@ -765,9 +789,9 @@ class ToolGroupComponent extends Container {
|
|
|
765
789
|
lines.push(`${fg(stateColor, leftIcon)} ${fg(stateColor, state)}`);
|
|
766
790
|
|
|
767
791
|
// Every entry is shown, one line each, in the order the model produced it.
|
|
768
|
-
const total =
|
|
792
|
+
const total = visible.length;
|
|
769
793
|
for (let index = 0; index < total; index++) {
|
|
770
|
-
const entry =
|
|
794
|
+
const entry = visible[index]!;
|
|
771
795
|
const rail = index === total - 1 ? "└ " : "│ ";
|
|
772
796
|
lines.push(
|
|
773
797
|
entry.kind === "thinking"
|
|
@@ -776,23 +800,73 @@ class ToolGroupComponent extends Container {
|
|
|
776
800
|
);
|
|
777
801
|
}
|
|
778
802
|
|
|
779
|
-
if (this.needsAnimation()) scheduleAnimation();
|
|
780
803
|
return lines;
|
|
781
804
|
}
|
|
782
805
|
|
|
783
806
|
// Expanded: per-tool detail + every thinking run, in stream order.
|
|
784
|
-
|
|
807
|
+
// When there is only 1 visible entry, do not show hierarchy (no header, no tree rail).
|
|
808
|
+
private renderExpanded(width: number, visible: GroupEntry[]): string[] {
|
|
785
809
|
const theme = currentTheme;
|
|
786
810
|
const fg = (color: string, text: string) => theme?.fg?.(color, text) ?? text;
|
|
787
811
|
const frame = SPINNER[Math.floor((Date.now() - spinnerStart) / SPINNER_MS) % SPINNER.length]!;
|
|
788
812
|
const lines: string[] = [];
|
|
789
813
|
|
|
790
|
-
|
|
791
|
-
|
|
814
|
+
if (visible.length === 1) {
|
|
815
|
+
const entry = visible[0]!;
|
|
816
|
+
const sub = " ";
|
|
817
|
+
if (entry.kind === "tool") {
|
|
818
|
+
const tool = entry.tool;
|
|
819
|
+
lines.push(this.toolRow("", tool, frame));
|
|
820
|
+
const result = toolResultText(tool);
|
|
821
|
+
if (result) {
|
|
822
|
+
const markdownWidth = Math.max(1, width - GROUP_PADDING_X - sub.length);
|
|
823
|
+
const preview = this.renderMarkdownPreview(
|
|
824
|
+
`tool:${tool.toolCallId ?? 0}`,
|
|
825
|
+
result,
|
|
826
|
+
markdownWidth,
|
|
827
|
+
config.expandedToolLines,
|
|
828
|
+
{ color: (text) => currentTheme?.fg?.("toolOutput", text) ?? text },
|
|
829
|
+
);
|
|
830
|
+
for (const row of preview.lines) {
|
|
831
|
+
lines.push(`${fg("dim", sub)}${row}`);
|
|
832
|
+
}
|
|
833
|
+
if (preview.truncated) {
|
|
834
|
+
lines.push(`${fg("dim", sub)}${fg("muted", "…")}`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return lines;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const icon = entry.active ? frame : "✓";
|
|
841
|
+
lines.push(
|
|
842
|
+
`${fg("thinkingText", icon)} ${fg("thinkingText", "thinking")} ${fg("muted", `· ${thinkingTokenLabel(entry)}`)}`,
|
|
843
|
+
);
|
|
844
|
+
const tText = entry.text.trim();
|
|
845
|
+
if (tText) {
|
|
846
|
+
const markdownWidth = Math.max(1, width - GROUP_PADDING_X - sub.length);
|
|
847
|
+
const preview = this.renderMarkdownPreview(
|
|
848
|
+
`thinking:${entry.id}`,
|
|
849
|
+
tText,
|
|
850
|
+
markdownWidth,
|
|
851
|
+
config.expandedThinkingLines,
|
|
852
|
+
{ color: (text) => currentTheme?.fg?.("thinkingText", text) ?? text, italic: true },
|
|
853
|
+
);
|
|
854
|
+
for (const row of preview.lines) {
|
|
855
|
+
lines.push(`${fg("dim", sub)}${row}`);
|
|
856
|
+
}
|
|
857
|
+
if (preview.truncated) {
|
|
858
|
+
lines.push(`${fg("dim", sub)}${fg("muted", "…")}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return lines;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const hasPendingTool = visible.some((e) => e.kind === "tool" && toolStatus(e.tool) === "pending");
|
|
865
|
+
const isThinking = !this.sealed && visible.some((e) => e.kind === "thinking" && e.active);
|
|
792
866
|
// See renderCollapsed: a block without tools keeps the thinking label, and a
|
|
793
867
|
// block with reasoning must not claim "tools done" alone.
|
|
794
|
-
const hasTools =
|
|
795
|
-
const hasThinking =
|
|
868
|
+
const hasTools = visible.some((e) => e.kind === "tool");
|
|
869
|
+
const hasThinking = visible.some((e) => e.kind === "thinking");
|
|
796
870
|
const openNoTools = !this.sealed && !hasTools;
|
|
797
871
|
const working = hasPendingTool || isThinking || openNoTools;
|
|
798
872
|
const state = hasPendingTool
|
|
@@ -811,9 +885,9 @@ class ToolGroupComponent extends Container {
|
|
|
811
885
|
const leftIcon = working ? frame : "✓";
|
|
812
886
|
lines.push(`${fg(stateColor, leftIcon)} ${fg(stateColor, state)}`);
|
|
813
887
|
|
|
814
|
-
const total =
|
|
888
|
+
const total = visible.length;
|
|
815
889
|
for (let index = 0; index < total; index++) {
|
|
816
|
-
const entry =
|
|
890
|
+
const entry = visible[index]!;
|
|
817
891
|
const isLast = index === total - 1;
|
|
818
892
|
const rail = isLast ? "└─ " : "├─ ";
|
|
819
893
|
const sub = isLast ? " " : "│ ";
|
|
@@ -862,12 +936,17 @@ class ToolGroupComponent extends Container {
|
|
|
862
936
|
}
|
|
863
937
|
}
|
|
864
938
|
|
|
865
|
-
if (this.needsAnimation()) scheduleAnimation();
|
|
866
939
|
return lines;
|
|
867
940
|
}
|
|
868
941
|
|
|
869
942
|
override render(width: number): string[] {
|
|
870
|
-
const
|
|
943
|
+
const visible = this.getVisibleEntries();
|
|
944
|
+
if (visible.length === 0) return [];
|
|
945
|
+
|
|
946
|
+
if (this.needsAnimation()) scheduleAnimation();
|
|
947
|
+
|
|
948
|
+
const lines = this._expanded ? this.renderExpanded(width, visible) : this.renderCollapsed(width, visible);
|
|
949
|
+
if (lines.length === 0) return [];
|
|
871
950
|
// Indent compact blocks from the transcript edge while keeping every line
|
|
872
951
|
// within the terminal width (including mobile / narrow terminals).
|
|
873
952
|
const padding = " ".repeat(Math.min(GROUP_PADDING_X, Math.max(0, width - 1)));
|
|
@@ -966,8 +1045,13 @@ function flushPendingTextSeal(): void {
|
|
|
966
1045
|
// visible text. Anchor it immediately before this text block so the visual
|
|
967
1046
|
// component order matches the stream order:
|
|
968
1047
|
// group -> visible text -> next group -> next visible text
|
|
969
|
-
if (
|
|
970
|
-
|
|
1048
|
+
if (lastActiveGroup.hasVisibleEntries()) {
|
|
1049
|
+
if (pendingTextOrdinal !== null) {
|
|
1050
|
+
anchorGroupBeforeCurrentText(lastActiveGroup, pendingTextOrdinal);
|
|
1051
|
+
}
|
|
1052
|
+
} else {
|
|
1053
|
+
if (lastChatContainer) removeGroupFromContainer(lastChatContainer, lastActiveGroup);
|
|
1054
|
+
groups.delete(lastActiveGroup);
|
|
971
1055
|
}
|
|
972
1056
|
lastActiveGroup.sealed = true;
|
|
973
1057
|
finalizeActiveThinking();
|
|
@@ -1139,7 +1223,7 @@ function insertTurnDivider(elapsedMs: number): void {
|
|
|
1139
1223
|
// group). A plain text-only answer gets no divider.
|
|
1140
1224
|
const hasPriorContent = contentContainer.children
|
|
1141
1225
|
.slice(0, finalIndex)
|
|
1142
|
-
.some((child) => child instanceof ToolGroupComponent);
|
|
1226
|
+
.some((child) => child instanceof ToolGroupComponent && child.hasVisibleEntries());
|
|
1143
1227
|
if (!hasPriorContent) return;
|
|
1144
1228
|
|
|
1145
1229
|
const state = getAssistantContentState(contentContainer);
|
|
@@ -1183,6 +1267,7 @@ function placeAnchoredGroupBeforeText(container: Container, target: Markdown, gr
|
|
|
1183
1267
|
}
|
|
1184
1268
|
|
|
1185
1269
|
function insertAnchoredGroup(container: Container, ordinal: number, group: ToolGroupComponent): void {
|
|
1270
|
+
if (!group.hasVisibleEntries()) return;
|
|
1186
1271
|
const markdowns = container.children.filter(isVisibleTextMarkdown);
|
|
1187
1272
|
const target = markdowns[ordinal];
|
|
1188
1273
|
if (!target) return;
|
|
@@ -1220,6 +1305,11 @@ function anchorGroupBeforeCurrentText(group: ToolGroupComponent, ordinal: number
|
|
|
1220
1305
|
removeGroupFromContainer(previousAnchor.container, group);
|
|
1221
1306
|
}
|
|
1222
1307
|
|
|
1308
|
+
if (!group.hasVisibleEntries()) {
|
|
1309
|
+
groups.delete(group);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1223
1313
|
const state = getAssistantContentState(contentContainer);
|
|
1224
1314
|
const replaced = state.anchors.get(ordinal);
|
|
1225
1315
|
if (replaced && replaced !== group) {
|
|
@@ -1240,7 +1330,7 @@ function restoreAssistantAnchor(parent: any, component: any): void {
|
|
|
1240
1330
|
const state = getAssistantContentState(parent);
|
|
1241
1331
|
const ordinal = state.nextTextOrdinal++;
|
|
1242
1332
|
const group = state.anchors.get(ordinal);
|
|
1243
|
-
if (group) {
|
|
1333
|
+
if (group && group.hasVisibleEntries()) {
|
|
1244
1334
|
removeGroupFromContainer(parent, group);
|
|
1245
1335
|
placeAnchoredGroupBeforeText(parent, component, group);
|
|
1246
1336
|
}
|
|
@@ -1386,6 +1476,7 @@ function getTools(cwd: string): Record<string, AnyTool> {
|
|
|
1386
1476
|
tools = {
|
|
1387
1477
|
read: createReadTool(cwd),
|
|
1388
1478
|
bash: createBashTool(cwd),
|
|
1479
|
+
powershell: createPowerShellTool(cwd),
|
|
1389
1480
|
edit: createEditTool(cwd),
|
|
1390
1481
|
write: createWriteTool(cwd),
|
|
1391
1482
|
find: createFindTool(cwd),
|
|
@@ -1413,7 +1504,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1413
1504
|
return getTools((ctx as { cwd: string }).cwd)[name].execute(toolCallId, params, signal, onUpdate);
|
|
1414
1505
|
};
|
|
1415
1506
|
|
|
1416
|
-
for (const name of ["read", "bash", "edit", "write", "find", "grep", "ls"] as const) {
|
|
1507
|
+
for (const name of ["read", "bash", "powershell", "edit", "write", "find", "grep", "ls"] as const) {
|
|
1417
1508
|
pi.registerTool({
|
|
1418
1509
|
name,
|
|
1419
1510
|
label: name,
|
|
@@ -1464,6 +1555,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1464
1555
|
lastActiveGroup.invalidate();
|
|
1465
1556
|
}
|
|
1466
1557
|
if (role === "user") {
|
|
1558
|
+
for (const g of [...groups]) {
|
|
1559
|
+
if (!g.hasVisibleEntries()) {
|
|
1560
|
+
if (lastChatContainer) removeGroupFromContainer(lastChatContainer, g);
|
|
1561
|
+
groups.delete(g);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1467
1564
|
turnStartMs = Date.now();
|
|
1468
1565
|
activeThinking = null;
|
|
1469
1566
|
activeThinkingIndex = null;
|
|
@@ -1567,6 +1664,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1567
1664
|
finalizeActiveThinking();
|
|
1568
1665
|
lastActiveGroup.invalidate();
|
|
1569
1666
|
}
|
|
1667
|
+
for (const g of [...groups]) {
|
|
1668
|
+
if (!g.hasVisibleEntries()) {
|
|
1669
|
+
if (lastChatContainer) removeGroupFromContainer(lastChatContainer, g);
|
|
1670
|
+
groups.delete(g);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1570
1673
|
refreshSealedThinkingTokens();
|
|
1571
1674
|
// Separate the final visible text from the preceding work with a divider
|
|
1572
1675
|
// that reports how long this turn ran.
|