pi-my-compact-ui 0.2.0 → 0.2.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/index.ts +135 -35
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -632,23 +632,38 @@ class ToolGroupComponent extends Container {
|
|
|
632
632
|
if ((tool as any)?.[PARENT_KEY] === this) delete (tool as any)[PARENT_KEY];
|
|
633
633
|
}
|
|
634
634
|
|
|
635
|
+
/** Return only entries that should be displayed (tools, or thinking with non-empty text). */
|
|
636
|
+
getVisibleEntries(): GroupEntry[] {
|
|
637
|
+
return this.entries.filter((entry) => {
|
|
638
|
+
if (entry.kind === "tool") return true;
|
|
639
|
+
if (entry.kind === "thinking") {
|
|
640
|
+
return entry.text.trim().length > 0;
|
|
641
|
+
}
|
|
642
|
+
return false;
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
hasVisibleEntries(): boolean {
|
|
647
|
+
return this.getVisibleEntries().length > 0;
|
|
648
|
+
}
|
|
649
|
+
|
|
635
650
|
hasPending(): boolean {
|
|
636
|
-
return this.
|
|
651
|
+
return this.getVisibleEntries().some((entry) => entry.kind === "tool" && toolStatus(entry.tool) === "pending");
|
|
637
652
|
}
|
|
638
653
|
|
|
639
654
|
/** True while a thinking run in this group is still streaming. */
|
|
640
655
|
hasActiveThinking(): boolean {
|
|
641
|
-
return this.
|
|
656
|
+
return this.getVisibleEntries().some((entry) => entry.kind === "thinking" && entry.active);
|
|
642
657
|
}
|
|
643
658
|
|
|
644
659
|
/** True once any thinking run has been added to this group. */
|
|
645
660
|
hasThinking(): boolean {
|
|
646
|
-
return this.
|
|
661
|
+
return this.getVisibleEntries().some((entry) => entry.kind === "thinking");
|
|
647
662
|
}
|
|
648
663
|
|
|
649
664
|
/** True while this group should keep its spinner animating. */
|
|
650
665
|
needsAnimation(): boolean {
|
|
651
|
-
return this.hasPending() || this.hasActiveThinking();
|
|
666
|
+
return this.hasVisibleEntries() && (this.hasPending() || this.hasActiveThinking());
|
|
652
667
|
}
|
|
653
668
|
|
|
654
669
|
override invalidate(): void {
|
|
@@ -711,10 +726,8 @@ class ToolGroupComponent extends Container {
|
|
|
711
726
|
const bold = theme?.bold ? theme.bold : (t: string) => t;
|
|
712
727
|
const st = toolStatus(tool);
|
|
713
728
|
const s = toolSummary(tool.toolName, tool.args);
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
private thinkingActiveInGroup(): boolean {
|
|
717
|
-
return !this.sealed && this.hasActiveThinking();
|
|
729
|
+
const prefix = rail ? fg("dim", rail) : "";
|
|
730
|
+
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
731
|
}
|
|
719
732
|
|
|
720
733
|
// One row per thinking run. The spinner marks the run that is still streaming;
|
|
@@ -724,28 +737,37 @@ class ToolGroupComponent extends Container {
|
|
|
724
737
|
const fg = (color: string, text: string) => theme?.fg?.(color, text) ?? text;
|
|
725
738
|
const icon = entry.active ? frame : "✓";
|
|
726
739
|
const preview = entry.text.trim().replace(/[*_#`>]+/g, "");
|
|
740
|
+
const prefix = rail ? fg("dim", rail) : "";
|
|
727
741
|
if (!preview) {
|
|
728
|
-
|
|
742
|
+
const label = entry.active ? "thinking..." : "thinking";
|
|
743
|
+
return `${prefix}${fg("thinkingText", icon)} ${fg("thinkingText", label)}`;
|
|
729
744
|
}
|
|
730
745
|
const tokenLabel = thinkingTokenLabel(entry);
|
|
731
|
-
const previewLimit = Math.max(1, Math.min(60, width - GROUP_PADDING_X - 18 - tokenLabel.length));
|
|
732
|
-
return `${
|
|
746
|
+
const previewLimit = Math.max(1, Math.min(60, width - GROUP_PADDING_X - rail.length - 18 - tokenLabel.length));
|
|
747
|
+
return `${prefix}${fg("thinkingText", icon)} ${fg("thinkingText", `thinking: ${oneLine(preview, previewLimit)}`)} ${fg("muted", `· ${tokenLabel}`)}`;
|
|
733
748
|
}
|
|
734
749
|
|
|
735
750
|
// Folded: header + one line per entry, in stream order (oldest first).
|
|
736
|
-
|
|
751
|
+
// When there is only 1 visible entry, do not show hierarchy (no header, no tree rail).
|
|
752
|
+
private renderCollapsed(width: number, visible: GroupEntry[]): string[] {
|
|
737
753
|
const theme = currentTheme;
|
|
738
754
|
const fg = (color: string, text: string) => theme?.fg?.(color, text) ?? text;
|
|
739
755
|
const frame = SPINNER[Math.floor((Date.now() - spinnerStart) / SPINNER_MS) % SPINNER.length]!;
|
|
756
|
+
|
|
757
|
+
if (visible.length === 1) {
|
|
758
|
+
const entry = visible[0]!;
|
|
759
|
+
if (entry.kind === "tool") {
|
|
760
|
+
return [this.toolRow("", entry.tool, frame)];
|
|
761
|
+
}
|
|
762
|
+
return [this.thinkingRow("", entry, frame, width)];
|
|
763
|
+
}
|
|
764
|
+
|
|
740
765
|
const lines: string[] = [];
|
|
741
766
|
|
|
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();
|
|
767
|
+
const hasPendingTool = visible.some((e) => e.kind === "tool" && toolStatus(e.tool) === "pending");
|
|
768
|
+
const isThinking = !this.sealed && visible.some((e) => e.kind === "thinking" && e.active);
|
|
769
|
+
const hasTools = visible.some((e) => e.kind === "tool");
|
|
770
|
+
const hasThinking = visible.some((e) => e.kind === "thinking");
|
|
749
771
|
const openNoTools = !this.sealed && !hasTools;
|
|
750
772
|
const working = hasPendingTool || isThinking || openNoTools;
|
|
751
773
|
const state = hasPendingTool
|
|
@@ -765,9 +787,9 @@ class ToolGroupComponent extends Container {
|
|
|
765
787
|
lines.push(`${fg(stateColor, leftIcon)} ${fg(stateColor, state)}`);
|
|
766
788
|
|
|
767
789
|
// Every entry is shown, one line each, in the order the model produced it.
|
|
768
|
-
const total =
|
|
790
|
+
const total = visible.length;
|
|
769
791
|
for (let index = 0; index < total; index++) {
|
|
770
|
-
const entry =
|
|
792
|
+
const entry = visible[index]!;
|
|
771
793
|
const rail = index === total - 1 ? "└ " : "│ ";
|
|
772
794
|
lines.push(
|
|
773
795
|
entry.kind === "thinking"
|
|
@@ -776,23 +798,73 @@ class ToolGroupComponent extends Container {
|
|
|
776
798
|
);
|
|
777
799
|
}
|
|
778
800
|
|
|
779
|
-
if (this.needsAnimation()) scheduleAnimation();
|
|
780
801
|
return lines;
|
|
781
802
|
}
|
|
782
803
|
|
|
783
804
|
// Expanded: per-tool detail + every thinking run, in stream order.
|
|
784
|
-
|
|
805
|
+
// When there is only 1 visible entry, do not show hierarchy (no header, no tree rail).
|
|
806
|
+
private renderExpanded(width: number, visible: GroupEntry[]): string[] {
|
|
785
807
|
const theme = currentTheme;
|
|
786
808
|
const fg = (color: string, text: string) => theme?.fg?.(color, text) ?? text;
|
|
787
809
|
const frame = SPINNER[Math.floor((Date.now() - spinnerStart) / SPINNER_MS) % SPINNER.length]!;
|
|
788
810
|
const lines: string[] = [];
|
|
789
811
|
|
|
790
|
-
|
|
791
|
-
|
|
812
|
+
if (visible.length === 1) {
|
|
813
|
+
const entry = visible[0]!;
|
|
814
|
+
const sub = " ";
|
|
815
|
+
if (entry.kind === "tool") {
|
|
816
|
+
const tool = entry.tool;
|
|
817
|
+
lines.push(this.toolRow("", tool, frame));
|
|
818
|
+
const result = toolResultText(tool);
|
|
819
|
+
if (result) {
|
|
820
|
+
const markdownWidth = Math.max(1, width - GROUP_PADDING_X - sub.length);
|
|
821
|
+
const preview = this.renderMarkdownPreview(
|
|
822
|
+
`tool:${tool.toolCallId ?? 0}`,
|
|
823
|
+
result,
|
|
824
|
+
markdownWidth,
|
|
825
|
+
config.expandedToolLines,
|
|
826
|
+
{ color: (text) => currentTheme?.fg?.("toolOutput", text) ?? text },
|
|
827
|
+
);
|
|
828
|
+
for (const row of preview.lines) {
|
|
829
|
+
lines.push(`${fg("dim", sub)}${row}`);
|
|
830
|
+
}
|
|
831
|
+
if (preview.truncated) {
|
|
832
|
+
lines.push(`${fg("dim", sub)}${fg("muted", "…")}`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return lines;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const icon = entry.active ? frame : "✓";
|
|
839
|
+
lines.push(
|
|
840
|
+
`${fg("thinkingText", icon)} ${fg("thinkingText", "thinking")} ${fg("muted", `· ${thinkingTokenLabel(entry)}`)}`,
|
|
841
|
+
);
|
|
842
|
+
const tText = entry.text.trim();
|
|
843
|
+
if (tText) {
|
|
844
|
+
const markdownWidth = Math.max(1, width - GROUP_PADDING_X - sub.length);
|
|
845
|
+
const preview = this.renderMarkdownPreview(
|
|
846
|
+
`thinking:${entry.id}`,
|
|
847
|
+
tText,
|
|
848
|
+
markdownWidth,
|
|
849
|
+
config.expandedThinkingLines,
|
|
850
|
+
{ color: (text) => currentTheme?.fg?.("thinkingText", text) ?? text, italic: true },
|
|
851
|
+
);
|
|
852
|
+
for (const row of preview.lines) {
|
|
853
|
+
lines.push(`${fg("dim", sub)}${row}`);
|
|
854
|
+
}
|
|
855
|
+
if (preview.truncated) {
|
|
856
|
+
lines.push(`${fg("dim", sub)}${fg("muted", "…")}`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return lines;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const hasPendingTool = visible.some((e) => e.kind === "tool" && toolStatus(e.tool) === "pending");
|
|
863
|
+
const isThinking = !this.sealed && visible.some((e) => e.kind === "thinking" && e.active);
|
|
792
864
|
// See renderCollapsed: a block without tools keeps the thinking label, and a
|
|
793
865
|
// block with reasoning must not claim "tools done" alone.
|
|
794
|
-
const hasTools =
|
|
795
|
-
const hasThinking =
|
|
866
|
+
const hasTools = visible.some((e) => e.kind === "tool");
|
|
867
|
+
const hasThinking = visible.some((e) => e.kind === "thinking");
|
|
796
868
|
const openNoTools = !this.sealed && !hasTools;
|
|
797
869
|
const working = hasPendingTool || isThinking || openNoTools;
|
|
798
870
|
const state = hasPendingTool
|
|
@@ -811,9 +883,9 @@ class ToolGroupComponent extends Container {
|
|
|
811
883
|
const leftIcon = working ? frame : "✓";
|
|
812
884
|
lines.push(`${fg(stateColor, leftIcon)} ${fg(stateColor, state)}`);
|
|
813
885
|
|
|
814
|
-
const total =
|
|
886
|
+
const total = visible.length;
|
|
815
887
|
for (let index = 0; index < total; index++) {
|
|
816
|
-
const entry =
|
|
888
|
+
const entry = visible[index]!;
|
|
817
889
|
const isLast = index === total - 1;
|
|
818
890
|
const rail = isLast ? "└─ " : "├─ ";
|
|
819
891
|
const sub = isLast ? " " : "│ ";
|
|
@@ -862,12 +934,17 @@ class ToolGroupComponent extends Container {
|
|
|
862
934
|
}
|
|
863
935
|
}
|
|
864
936
|
|
|
865
|
-
if (this.needsAnimation()) scheduleAnimation();
|
|
866
937
|
return lines;
|
|
867
938
|
}
|
|
868
939
|
|
|
869
940
|
override render(width: number): string[] {
|
|
870
|
-
const
|
|
941
|
+
const visible = this.getVisibleEntries();
|
|
942
|
+
if (visible.length === 0) return [];
|
|
943
|
+
|
|
944
|
+
if (this.needsAnimation()) scheduleAnimation();
|
|
945
|
+
|
|
946
|
+
const lines = this._expanded ? this.renderExpanded(width, visible) : this.renderCollapsed(width, visible);
|
|
947
|
+
if (lines.length === 0) return [];
|
|
871
948
|
// Indent compact blocks from the transcript edge while keeping every line
|
|
872
949
|
// within the terminal width (including mobile / narrow terminals).
|
|
873
950
|
const padding = " ".repeat(Math.min(GROUP_PADDING_X, Math.max(0, width - 1)));
|
|
@@ -966,8 +1043,13 @@ function flushPendingTextSeal(): void {
|
|
|
966
1043
|
// visible text. Anchor it immediately before this text block so the visual
|
|
967
1044
|
// component order matches the stream order:
|
|
968
1045
|
// group -> visible text -> next group -> next visible text
|
|
969
|
-
if (
|
|
970
|
-
|
|
1046
|
+
if (lastActiveGroup.hasVisibleEntries()) {
|
|
1047
|
+
if (pendingTextOrdinal !== null) {
|
|
1048
|
+
anchorGroupBeforeCurrentText(lastActiveGroup, pendingTextOrdinal);
|
|
1049
|
+
}
|
|
1050
|
+
} else {
|
|
1051
|
+
if (lastChatContainer) removeGroupFromContainer(lastChatContainer, lastActiveGroup);
|
|
1052
|
+
groups.delete(lastActiveGroup);
|
|
971
1053
|
}
|
|
972
1054
|
lastActiveGroup.sealed = true;
|
|
973
1055
|
finalizeActiveThinking();
|
|
@@ -1139,7 +1221,7 @@ function insertTurnDivider(elapsedMs: number): void {
|
|
|
1139
1221
|
// group). A plain text-only answer gets no divider.
|
|
1140
1222
|
const hasPriorContent = contentContainer.children
|
|
1141
1223
|
.slice(0, finalIndex)
|
|
1142
|
-
.some((child) => child instanceof ToolGroupComponent);
|
|
1224
|
+
.some((child) => child instanceof ToolGroupComponent && child.hasVisibleEntries());
|
|
1143
1225
|
if (!hasPriorContent) return;
|
|
1144
1226
|
|
|
1145
1227
|
const state = getAssistantContentState(contentContainer);
|
|
@@ -1183,6 +1265,7 @@ function placeAnchoredGroupBeforeText(container: Container, target: Markdown, gr
|
|
|
1183
1265
|
}
|
|
1184
1266
|
|
|
1185
1267
|
function insertAnchoredGroup(container: Container, ordinal: number, group: ToolGroupComponent): void {
|
|
1268
|
+
if (!group.hasVisibleEntries()) return;
|
|
1186
1269
|
const markdowns = container.children.filter(isVisibleTextMarkdown);
|
|
1187
1270
|
const target = markdowns[ordinal];
|
|
1188
1271
|
if (!target) return;
|
|
@@ -1220,6 +1303,11 @@ function anchorGroupBeforeCurrentText(group: ToolGroupComponent, ordinal: number
|
|
|
1220
1303
|
removeGroupFromContainer(previousAnchor.container, group);
|
|
1221
1304
|
}
|
|
1222
1305
|
|
|
1306
|
+
if (!group.hasVisibleEntries()) {
|
|
1307
|
+
groups.delete(group);
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1223
1311
|
const state = getAssistantContentState(contentContainer);
|
|
1224
1312
|
const replaced = state.anchors.get(ordinal);
|
|
1225
1313
|
if (replaced && replaced !== group) {
|
|
@@ -1240,7 +1328,7 @@ function restoreAssistantAnchor(parent: any, component: any): void {
|
|
|
1240
1328
|
const state = getAssistantContentState(parent);
|
|
1241
1329
|
const ordinal = state.nextTextOrdinal++;
|
|
1242
1330
|
const group = state.anchors.get(ordinal);
|
|
1243
|
-
if (group) {
|
|
1331
|
+
if (group && group.hasVisibleEntries()) {
|
|
1244
1332
|
removeGroupFromContainer(parent, group);
|
|
1245
1333
|
placeAnchoredGroupBeforeText(parent, component, group);
|
|
1246
1334
|
}
|
|
@@ -1464,6 +1552,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1464
1552
|
lastActiveGroup.invalidate();
|
|
1465
1553
|
}
|
|
1466
1554
|
if (role === "user") {
|
|
1555
|
+
for (const g of [...groups]) {
|
|
1556
|
+
if (!g.hasVisibleEntries()) {
|
|
1557
|
+
if (lastChatContainer) removeGroupFromContainer(lastChatContainer, g);
|
|
1558
|
+
groups.delete(g);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1467
1561
|
turnStartMs = Date.now();
|
|
1468
1562
|
activeThinking = null;
|
|
1469
1563
|
activeThinkingIndex = null;
|
|
@@ -1567,6 +1661,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1567
1661
|
finalizeActiveThinking();
|
|
1568
1662
|
lastActiveGroup.invalidate();
|
|
1569
1663
|
}
|
|
1664
|
+
for (const g of [...groups]) {
|
|
1665
|
+
if (!g.hasVisibleEntries()) {
|
|
1666
|
+
if (lastChatContainer) removeGroupFromContainer(lastChatContainer, g);
|
|
1667
|
+
groups.delete(g);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1570
1670
|
refreshSealedThinkingTokens();
|
|
1571
1671
|
// Separate the final visible text from the preceding work with a divider
|
|
1572
1672
|
// that reports how long this turn ran.
|