pi-cc-extensions 0.1.5 → 0.3.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/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<a href="#安装"><img alt="Pi package" src="https://img.shields.io/badge/Pi-package-58B7FF?style=flat-square"></a>
|
|
7
|
-
<a href="./package.json"><img alt="Version 0.
|
|
7
|
+
<a href="./package.json"><img alt="Version 0.2.0" src="https://img.shields.io/badge/version-0.2.0-66E3C4?style=flat-square"></a>
|
|
8
8
|
<a href="#兼容性"><img alt="Node.js 22.19 or newer" src="https://img.shields.io/badge/Node.js-%E2%89%A522.19-66E3C4?style=flat-square"></a>
|
|
9
9
|
<a href="./extensions"><img alt="TypeScript extensions" src="https://img.shields.io/badge/TypeScript-extensions-3178C6?style=flat-square"></a>
|
|
10
10
|
</p>
|
|
@@ -388,6 +388,8 @@ export class TerminalSplitCompositor {
|
|
|
388
388
|
private maxScrollOffset = 0;
|
|
389
389
|
private lastRootLineCount = 0;
|
|
390
390
|
private rootLines: string[] = [];
|
|
391
|
+
private rootWindowWidth = 0;
|
|
392
|
+
private rootWindowRawRows = 0;
|
|
391
393
|
private suppressRootGrowthAdjustment = false;
|
|
392
394
|
private readonly toolExpansionStarts = new Map<string, number>();
|
|
393
395
|
private visibleRootStart = 0;
|
|
@@ -399,6 +401,8 @@ export class TerminalSplitCompositor {
|
|
|
399
401
|
private selectionAnchor: SelectionPoint | null = null;
|
|
400
402
|
private selectionFocus: SelectionPoint | null = null;
|
|
401
403
|
private selectionDragging = false;
|
|
404
|
+
private batchingMouseInput = false;
|
|
405
|
+
private selectionRepaintPending = false;
|
|
402
406
|
private preserveSelectionFocusOnRelease = false;
|
|
403
407
|
private lastLeftPress: { area: SelectionArea; line: number; at: number } | null = null;
|
|
404
408
|
private consumedClusterClick = false;
|
|
@@ -554,6 +558,8 @@ export class TerminalSplitCompositor {
|
|
|
554
558
|
this.lastLeftPress = null;
|
|
555
559
|
this.hoveredRootLine = null;
|
|
556
560
|
this.rootLines = [];
|
|
561
|
+
this.rootWindowWidth = 0;
|
|
562
|
+
this.rootWindowRawRows = 0;
|
|
557
563
|
this.visibleRootLines = [];
|
|
558
564
|
this.visibleRootStart = 0;
|
|
559
565
|
this.lastRootLineCount = 0;
|
|
@@ -597,6 +603,17 @@ export class TerminalSplitCompositor {
|
|
|
597
603
|
const rawRows = this.getRawRows();
|
|
598
604
|
const width = Math.max(1, this.terminal.columns || 80);
|
|
599
605
|
const cluster = this.getCluster(width, rawRows);
|
|
606
|
+
const scrollableRows = Math.max(1, rawRows - cluster.lines.length);
|
|
607
|
+
// A cluster-only paint cannot change the split point safely. Fall back to
|
|
608
|
+
// the normal frame when width, terminal height, or cluster height changed.
|
|
609
|
+
if (
|
|
610
|
+
this.rootWindowWidth !== width
|
|
611
|
+
|| this.rootWindowRawRows !== rawRows
|
|
612
|
+
|| (this.visibleScrollableRows > 0 && this.visibleScrollableRows !== scrollableRows)
|
|
613
|
+
) {
|
|
614
|
+
this.requestRender();
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
600
617
|
if (cluster.lines.length === 0) return;
|
|
601
618
|
|
|
602
619
|
this.originalWrite(
|
|
@@ -694,6 +711,8 @@ export class TerminalSplitCompositor {
|
|
|
694
711
|
|
|
695
712
|
const rawRows = this.getRawRows();
|
|
696
713
|
const renderWidth = Math.max(1, width);
|
|
714
|
+
this.rootWindowWidth = renderWidth;
|
|
715
|
+
this.rootWindowRawRows = rawRows;
|
|
697
716
|
const cluster = this.getCluster(renderWidth, rawRows);
|
|
698
717
|
const scrollableRows = Math.max(1, rawRows - cluster.lines.length);
|
|
699
718
|
const renderedLines = this.originalRender(renderWidth);
|
|
@@ -717,6 +736,28 @@ export class TerminalSplitCompositor {
|
|
|
717
736
|
return this.updateVisibleRootWindow(scrollableRows);
|
|
718
737
|
}
|
|
719
738
|
|
|
739
|
+
private ensureRootWindow(width: number): void {
|
|
740
|
+
if (!this.originalRender) return;
|
|
741
|
+
|
|
742
|
+
const renderWidth = Math.max(1, width);
|
|
743
|
+
const rawRows = this.getRawRows();
|
|
744
|
+
const cluster = this.getCluster(renderWidth, rawRows);
|
|
745
|
+
const scrollableRows = Math.max(1, rawRows - cluster.lines.length);
|
|
746
|
+
const sameRootDimensions = this.rootWindowWidth === renderWidth && this.rootWindowRawRows === rawRows;
|
|
747
|
+
|
|
748
|
+
if (sameRootDimensions && this.visibleScrollableRows > 0) {
|
|
749
|
+
if (this.visibleScrollableRows !== scrollableRows) {
|
|
750
|
+
this.maxScrollOffset = Math.max(0, this.rootLines.length - scrollableRows);
|
|
751
|
+
this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, this.maxScrollOffset));
|
|
752
|
+
this.pendingImageCleanup = true;
|
|
753
|
+
this.updateVisibleRootWindow(scrollableRows);
|
|
754
|
+
}
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
this.refreshRootWindow(renderWidth);
|
|
759
|
+
}
|
|
760
|
+
|
|
720
761
|
private handleInput(data: string): { consume?: boolean; data?: string } | undefined {
|
|
721
762
|
if (this.disposed || this.hasVisibleOverlay()) return undefined;
|
|
722
763
|
|
|
@@ -735,8 +776,17 @@ export class TerminalSplitCompositor {
|
|
|
735
776
|
|
|
736
777
|
const mousePackets = this.mouseScroll ? parseSgrMousePackets(data) : null;
|
|
737
778
|
if (mousePackets) {
|
|
738
|
-
|
|
739
|
-
|
|
779
|
+
this.batchingMouseInput = true;
|
|
780
|
+
try {
|
|
781
|
+
for (const packet of mousePackets) {
|
|
782
|
+
this.handleMousePacket(packet);
|
|
783
|
+
}
|
|
784
|
+
} finally {
|
|
785
|
+
this.batchingMouseInput = false;
|
|
786
|
+
}
|
|
787
|
+
if (this.selectionRepaintPending) {
|
|
788
|
+
this.selectionRepaintPending = false;
|
|
789
|
+
this.repaintSelection(Math.max(1, this.terminal.columns || 80));
|
|
740
790
|
}
|
|
741
791
|
return { consume: true };
|
|
742
792
|
}
|
|
@@ -777,7 +827,11 @@ export class TerminalSplitCompositor {
|
|
|
777
827
|
return;
|
|
778
828
|
}
|
|
779
829
|
|
|
780
|
-
|
|
830
|
+
const width = Math.max(1, this.terminal.columns || 80);
|
|
831
|
+
// Mouse selection and clicks operate on the last painted root window. Avoid
|
|
832
|
+
// re-rendering the complete transcript for every mouse packet; refresh only
|
|
833
|
+
// on the first packet or after a terminal resize.
|
|
834
|
+
this.ensureRootWindow(width);
|
|
781
835
|
const location = this.selectionLocationForPacket(packet);
|
|
782
836
|
|
|
783
837
|
if (isRightPress(packet)) {
|
|
@@ -845,6 +899,10 @@ export class TerminalSplitCompositor {
|
|
|
845
899
|
this.clearSelection();
|
|
846
900
|
this.lastLeftPress = null;
|
|
847
901
|
this.consumedClusterClick = true;
|
|
902
|
+
// refreshRootWindow() updated the logical lines above, but the TUI
|
|
903
|
+
// render is scheduled asynchronously. Paint the collapsed/expanded
|
|
904
|
+
// viewport immediately so stale rows cannot remain until the next drag.
|
|
905
|
+
this.repaintScrollableViewport(Math.max(1, this.terminal.columns || 80));
|
|
848
906
|
this.requestRender();
|
|
849
907
|
return;
|
|
850
908
|
}
|
|
@@ -869,7 +927,7 @@ export class TerminalSplitCompositor {
|
|
|
869
927
|
this.lastLeftPress = null;
|
|
870
928
|
this.preserveSelectionFocusOnRelease = false;
|
|
871
929
|
this.selectionFocus = location.point;
|
|
872
|
-
this.
|
|
930
|
+
this.repaintSelection(width);
|
|
873
931
|
return;
|
|
874
932
|
}
|
|
875
933
|
}
|
|
@@ -904,7 +962,7 @@ export class TerminalSplitCompositor {
|
|
|
904
962
|
} else {
|
|
905
963
|
this.clearSelection();
|
|
906
964
|
}
|
|
907
|
-
this.
|
|
965
|
+
this.repaintSelection(Math.max(1, this.terminal.columns || 80));
|
|
908
966
|
}
|
|
909
967
|
|
|
910
968
|
private startSelection(location: SelectionLocation): void {
|
|
@@ -922,7 +980,7 @@ export class TerminalSplitCompositor {
|
|
|
922
980
|
this.selectionDragging = true;
|
|
923
981
|
this.preserveSelectionFocusOnRelease = true;
|
|
924
982
|
this.lastLeftPress = null;
|
|
925
|
-
this.
|
|
983
|
+
this.repaintSelection(Math.max(1, this.terminal.columns || 80));
|
|
926
984
|
return;
|
|
927
985
|
}
|
|
928
986
|
|
|
@@ -932,7 +990,7 @@ export class TerminalSplitCompositor {
|
|
|
932
990
|
this.selectionDragging = true;
|
|
933
991
|
this.preserveSelectionFocusOnRelease = false;
|
|
934
992
|
this.lastLeftPress = { area: location.area, line, at: now };
|
|
935
|
-
this.
|
|
993
|
+
this.repaintSelection(Math.max(1, this.terminal.columns || 80));
|
|
936
994
|
}
|
|
937
995
|
|
|
938
996
|
private selectionLocationForPacket(packet: SgrMousePacket): SelectionLocation | null {
|
|
@@ -974,7 +1032,7 @@ export class TerminalSplitCompositor {
|
|
|
974
1032
|
line: edgeLine,
|
|
975
1033
|
col: Math.max(0, packet.col - 1),
|
|
976
1034
|
};
|
|
977
|
-
this.
|
|
1035
|
+
this.repaintSelection(Math.max(1, this.terminal.columns || 80));
|
|
978
1036
|
return true;
|
|
979
1037
|
}
|
|
980
1038
|
|
|
@@ -1065,7 +1123,7 @@ export class TerminalSplitCompositor {
|
|
|
1065
1123
|
|
|
1066
1124
|
private scrollBy(delta: number): void {
|
|
1067
1125
|
const width = Math.max(1, this.terminal.columns || 80);
|
|
1068
|
-
this.
|
|
1126
|
+
this.ensureRootWindow(width);
|
|
1069
1127
|
|
|
1070
1128
|
const wasScrolled = this.scrollOffset > 0;
|
|
1071
1129
|
const nextOffset = Math.max(0, Math.min(this.scrollOffset + delta, this.maxScrollOffset));
|
|
@@ -1088,12 +1146,24 @@ export class TerminalSplitCompositor {
|
|
|
1088
1146
|
}
|
|
1089
1147
|
}
|
|
1090
1148
|
|
|
1091
|
-
private
|
|
1092
|
-
if (this.
|
|
1149
|
+
private repaintSelection(width: number): void {
|
|
1150
|
+
if (this.batchingMouseInput) {
|
|
1151
|
+
this.selectionRepaintPending = true;
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
if (!this.repaintScrollableViewport(width)) {
|
|
1155
|
+
this.requestRender();
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
private repaintScrollableViewport(width: number): boolean {
|
|
1160
|
+
if (this.disposed || this.writing || this.hasVisibleOverlay()) return false;
|
|
1093
1161
|
|
|
1094
1162
|
const rawRows = this.getRawRows();
|
|
1095
1163
|
const cluster = this.getCluster(width, rawRows);
|
|
1096
1164
|
const scrollableRows = Math.max(1, rawRows - cluster.lines.length);
|
|
1165
|
+
this.maxScrollOffset = Math.max(0, this.rootLines.length - scrollableRows);
|
|
1166
|
+
this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, this.maxScrollOffset));
|
|
1097
1167
|
const start = this.updateVisibleRootWindow(scrollableRows);
|
|
1098
1168
|
let buffer = beginSynchronizedOutput()
|
|
1099
1169
|
+ this.consumePendingImageCleanup()
|
|
@@ -1113,6 +1183,7 @@ export class TerminalSplitCompositor {
|
|
|
1113
1183
|
buffer += this.mouseReportingStateGuard();
|
|
1114
1184
|
buffer += endSynchronizedOutput();
|
|
1115
1185
|
this.originalWrite(buffer);
|
|
1186
|
+
return true;
|
|
1116
1187
|
}
|
|
1117
1188
|
|
|
1118
1189
|
private pauseMouseReportingForContextMenu(textToRestoreToClipboard: string | null = null): void {
|
|
@@ -984,6 +984,7 @@ export default function powerlineFooter(pi: ExtensionAPI): PowerlineController {
|
|
|
984
984
|
let fixedEditorContainer: any = null;
|
|
985
985
|
let fixedWidgetContainerAbove: any = null;
|
|
986
986
|
let fixedWidgetContainerBelow: any = null;
|
|
987
|
+
let statusRenderNeedsFullRender = false;
|
|
987
988
|
let stashShortcutInputUnsubscribe: (() => void) | null = null;
|
|
988
989
|
let dismissWelcomeOverlay: (() => void) | null = null;
|
|
989
990
|
let welcomeHeaderActive = false;
|
|
@@ -1024,7 +1025,12 @@ export default function powerlineFooter(pi: ExtensionAPI): PowerlineController {
|
|
|
1024
1025
|
return;
|
|
1025
1026
|
}
|
|
1026
1027
|
|
|
1027
|
-
|
|
1028
|
+
if (fixedEditorCompositor && !statusRenderNeedsFullRender) {
|
|
1029
|
+
fixedEditorCompositor.requestRepaint();
|
|
1030
|
+
} else {
|
|
1031
|
+
tuiRef?.requestRender();
|
|
1032
|
+
}
|
|
1033
|
+
statusRenderNeedsFullRender = false;
|
|
1028
1034
|
}, STATUS_RENDER_DEBOUNCE_MS);
|
|
1029
1035
|
|
|
1030
1036
|
const resetLayoutCache = () => {
|
|
@@ -1039,6 +1045,7 @@ export default function powerlineFooter(pi: ExtensionAPI): PowerlineController {
|
|
|
1039
1045
|
|
|
1040
1046
|
const requestImmediateStatusRender = (options: { deferDuringTyping?: boolean } = {}) => {
|
|
1041
1047
|
layoutDirty = true;
|
|
1048
|
+
statusRenderNeedsFullRender = true;
|
|
1042
1049
|
if (options.deferDuringTyping !== false && Date.now() - lastEditorInputAt < EDITOR_STATUS_DEFER_MS) {
|
|
1043
1050
|
statusRenderScheduler.schedule();
|
|
1044
1051
|
return;
|
|
@@ -1244,6 +1251,7 @@ export default function powerlineFooter(pi: ExtensionAPI): PowerlineController {
|
|
|
1244
1251
|
// Read from the extension runtime rather than retaining a session context
|
|
1245
1252
|
// method across /reload. The runtime already restored the effective level.
|
|
1246
1253
|
currentThinkingLevel = pi.getThinkingLevel();
|
|
1254
|
+
recalculateUsageFromEvents(ctx);
|
|
1247
1255
|
|
|
1248
1256
|
if (ctx.hasUI) {
|
|
1249
1257
|
ctx.ui.setStatus("stash", undefined);
|
|
@@ -1295,6 +1303,7 @@ export default function powerlineFooter(pi: ExtensionAPI): PowerlineController {
|
|
|
1295
1303
|
welcomeOverlayShouldDismiss = false;
|
|
1296
1304
|
welcomeDismissScheduler.cancel();
|
|
1297
1305
|
statusRenderScheduler.cancel();
|
|
1306
|
+
statusRenderNeedsFullRender = false;
|
|
1298
1307
|
restoreFooterStatusRepaintHook?.();
|
|
1299
1308
|
restoreFooterStatusRepaintHook = null;
|
|
1300
1309
|
teardownFixedEditorCompositor(isTerminalExit ? { resetExtendedKeyboardModes: true } : undefined);
|
|
@@ -1379,6 +1388,7 @@ export default function powerlineFooter(pi: ExtensionAPI): PowerlineController {
|
|
|
1379
1388
|
currentCtx = ctx;
|
|
1380
1389
|
currentThinkingLevel = pi.getThinkingLevel();
|
|
1381
1390
|
liveAssistantUsage = null;
|
|
1391
|
+
recalculateUsageFromEvents(ctx);
|
|
1382
1392
|
requestImmediateStatusRender({ deferDuringTyping: false });
|
|
1383
1393
|
});
|
|
1384
1394
|
|
|
@@ -1388,6 +1398,8 @@ export default function powerlineFooter(pi: ExtensionAPI): PowerlineController {
|
|
|
1388
1398
|
// intentionally unknown until the next model response, so the segment will
|
|
1389
1399
|
// display "?" rather than a stale percentage.
|
|
1390
1400
|
liveAssistantUsage = null;
|
|
1401
|
+
// Recalculate usage after compaction (old events removed, summary added).
|
|
1402
|
+
recalculateUsageFromEvents(ctx);
|
|
1391
1403
|
fixedEditorCompositor?.resetTranscriptLayout();
|
|
1392
1404
|
resetLayoutCache();
|
|
1393
1405
|
requestImmediateStatusRender({ deferDuringTyping: false });
|
|
@@ -1455,6 +1467,13 @@ export default function powerlineFooter(pi: ExtensionAPI): PowerlineController {
|
|
|
1455
1467
|
liveAssistantUsage = null;
|
|
1456
1468
|
} else if (getUsageTokenTotal(event.message.usage) > 0) {
|
|
1457
1469
|
liveAssistantUsage = event.message.usage;
|
|
1470
|
+
// Incrementally update cached usage (O(1) instead of rescanning all events).
|
|
1471
|
+
cachedUsage.input += event.message.usage.input;
|
|
1472
|
+
cachedUsage.output += event.message.usage.output;
|
|
1473
|
+
cachedUsage.cacheRead += event.message.usage.cacheRead;
|
|
1474
|
+
cachedUsage.cacheWrite += event.message.usage.cacheWrite;
|
|
1475
|
+
cachedUsage.cost += event.message.usage.cost.total;
|
|
1476
|
+
cachedLastAssistantUsage = event.message.usage;
|
|
1458
1477
|
}
|
|
1459
1478
|
}
|
|
1460
1479
|
requestImmediateStatusRender({ deferDuringTyping: false });
|
|
@@ -1841,6 +1860,7 @@ export default function powerlineFooter(pi: ExtensionAPI): PowerlineController {
|
|
|
1841
1860
|
tuiRef = null;
|
|
1842
1861
|
currentEditor = null;
|
|
1843
1862
|
statusRenderScheduler.cancel();
|
|
1863
|
+
statusRenderNeedsFullRender = false;
|
|
1844
1864
|
resetLayoutCache();
|
|
1845
1865
|
ctx.ui.notify("Powerline disabled", "info");
|
|
1846
1866
|
}
|
|
@@ -2130,46 +2150,47 @@ export default function powerlineFooter(pi: ExtensionAPI): PowerlineController {
|
|
|
2130
2150
|
},
|
|
2131
2151
|
});
|
|
2132
2152
|
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
// Check for thinking level change entries
|
|
2153
|
+
// ═══ Incremental usage stats cache ═══
|
|
2154
|
+
// Avoids scanning all session events on every render (was O(n) per frame).
|
|
2155
|
+
// Updated incrementally on message_end / session_start / session_compact.
|
|
2156
|
+
let cachedUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
2157
|
+
let cachedLastAssistantUsage: SessionAssistantUsage | null = null;
|
|
2158
|
+
let cachedThinkingLevel: string | null = null;
|
|
2159
|
+
|
|
2160
|
+
function recalculateUsageFromEvents(ctx: any): void {
|
|
2161
|
+
cachedUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
2162
|
+
cachedLastAssistantUsage = null;
|
|
2163
|
+
cachedThinkingLevel = null;
|
|
2164
|
+
const events = ctx?.sessionManager?.getBranch?.() ?? [];
|
|
2165
|
+
for (const e of events) {
|
|
2166
|
+
if (!isRecord(e)) continue;
|
|
2149
2167
|
if (e.type === "thinking_level_change" && typeof e.thinkingLevel === "string") {
|
|
2150
|
-
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
if (e.type !== "message" || !isSessionAssistantMessage(e.message)) {
|
|
2154
|
-
continue;
|
|
2168
|
+
cachedThinkingLevel = e.thinkingLevel;
|
|
2155
2169
|
}
|
|
2156
|
-
|
|
2170
|
+
if (e.type !== "message" || !isSessionAssistantMessage(e.message)) continue;
|
|
2157
2171
|
const m = e.message;
|
|
2158
|
-
if (m.stopReason === "error" || m.stopReason === "aborted")
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
cacheWrite += m.usage.cacheWrite;
|
|
2165
|
-
cost += m.usage.cost.total;
|
|
2172
|
+
if (m.stopReason === "error" || m.stopReason === "aborted") continue;
|
|
2173
|
+
cachedUsage.input += m.usage.input;
|
|
2174
|
+
cachedUsage.output += m.usage.output;
|
|
2175
|
+
cachedUsage.cacheRead += m.usage.cacheRead;
|
|
2176
|
+
cachedUsage.cacheWrite += m.usage.cacheWrite;
|
|
2177
|
+
cachedUsage.cost += m.usage.cost.total;
|
|
2166
2178
|
if (getUsageTokenTotal(m.usage) > 0) {
|
|
2167
|
-
|
|
2179
|
+
cachedLastAssistantUsage = m.usage;
|
|
2168
2180
|
}
|
|
2169
2181
|
}
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
function buildSegmentContext(ctx: any, theme: Theme): SegmentContext {
|
|
2185
|
+
const presetDef = getPreset(config.preset);
|
|
2186
|
+
const colors: ColorScheme = presetDef.colors ?? getDefaultColors();
|
|
2187
|
+
|
|
2188
|
+
// Use cached incremental stats (O(1)) instead of scanning all events.
|
|
2189
|
+
const { input, output, cacheRead, cacheWrite, cost } = cachedUsage;
|
|
2190
|
+
const thinkingLevelFromSession = currentThinkingLevel ?? cachedThinkingLevel ?? "off";
|
|
2170
2191
|
|
|
2171
2192
|
// Calculate context percentage.
|
|
2172
|
-
const latestUsage = isStreaming ? liveAssistantUsage ??
|
|
2193
|
+
const latestUsage = isStreaming ? liveAssistantUsage ?? cachedLastAssistantUsage : cachedLastAssistantUsage;
|
|
2173
2194
|
const coreContextUsage = isStreaming && liveAssistantUsage ? null : readCoreContextUsage(ctx);
|
|
2174
2195
|
const fallbackContextTokens = latestUsage ? getUsageTokenTotal(latestUsage) : 0;
|
|
2175
2196
|
const contextWindow = coreContextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
|
|
@@ -2191,7 +2212,7 @@ export default function powerlineFooter(pi: ExtensionAPI): PowerlineController {
|
|
|
2191
2212
|
? ctx.modelRegistry?.isUsingOAuth?.(ctx.model) ?? false
|
|
2192
2213
|
: false;
|
|
2193
2214
|
|
|
2194
|
-
const thinkingLevel =
|
|
2215
|
+
const thinkingLevel = thinkingLevelFromSession;
|
|
2195
2216
|
|
|
2196
2217
|
return {
|
|
2197
2218
|
model: ctx.model,
|
|
@@ -2511,8 +2532,9 @@ export default function powerlineFooter(pi: ExtensionAPI): PowerlineController {
|
|
|
2511
2532
|
component.setExpanded(!expanded);
|
|
2512
2533
|
} else {
|
|
2513
2534
|
const state = (component.rendererState ??= {});
|
|
2514
|
-
|
|
2515
|
-
|
|
2535
|
+
const nextExpanded = !expanded;
|
|
2536
|
+
state.ccstyleToolExpanded = nextExpanded;
|
|
2537
|
+
component.setExpanded?.(nextExpanded);
|
|
2516
2538
|
component.invalidate?.();
|
|
2517
2539
|
}
|
|
2518
2540
|
// The compositor refreshes and renders once after applying its viewport
|
|
@@ -2523,8 +2545,13 @@ export default function powerlineFooter(pi: ExtensionAPI): PowerlineController {
|
|
|
2523
2545
|
key: expandableRootComponentKey(component),
|
|
2524
2546
|
};
|
|
2525
2547
|
};
|
|
2526
|
-
const isExpandableRootLine = (
|
|
2527
|
-
|
|
2548
|
+
const isExpandableRootLine = (_line: number, _col: number, text: string): boolean => {
|
|
2549
|
+
// O(1) regex check instead of full tree traversal + component.render().
|
|
2550
|
+
// Click handler (onRootClick) still uses findExpandableRootComponentAtText for accurate matching.
|
|
2551
|
+
if (!text) return false;
|
|
2552
|
+
const stripped = stripAnsiForToolClick(text);
|
|
2553
|
+
return stripped ? /(?:expand|\/ click)/.test(stripped) : false;
|
|
2554
|
+
};
|
|
2528
2555
|
const readRenderTheme = (): Theme => {
|
|
2529
2556
|
if (!currentCtx) return fallbackTheme;
|
|
2530
2557
|
try {
|
|
@@ -2547,32 +2574,73 @@ export default function powerlineFooter(pi: ExtensionAPI): PowerlineController {
|
|
|
2547
2574
|
(line.match(/\x1b\[[0-9;]*m/g) ?? []).some((sequence) =>
|
|
2548
2575
|
sequence.slice(2, -1).split(";").includes("3"),
|
|
2549
2576
|
);
|
|
2577
|
+
// Cache expensive per-line ANSI work by rendered line content. The TUI
|
|
2578
|
+
// creates a new array on every render, so caching the array reference does
|
|
2579
|
+
// not help once the transcript grows.
|
|
2580
|
+
const MAX_DECORATION_CACHE_ENTRIES = 4096;
|
|
2581
|
+
let decorationTheme: Theme | null = null;
|
|
2582
|
+
let thinkingColor = "";
|
|
2583
|
+
const thinkingLineCache = new Map<string, boolean>();
|
|
2584
|
+
const nonEmptyLineCache = new Map<string, boolean>();
|
|
2585
|
+
const unboldLineCache = new Map<string, string>();
|
|
2586
|
+
|
|
2587
|
+
const trimDecorationCache = <T>(cache: Map<string, T>): void => {
|
|
2588
|
+
if (cache.size > MAX_DECORATION_CACHE_ENTRIES) cache.clear();
|
|
2589
|
+
};
|
|
2590
|
+
|
|
2550
2591
|
const decorateThinkingLines = (lines: string[]): string[] => {
|
|
2551
2592
|
const theme = readRenderTheme();
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2593
|
+
if (theme !== decorationTheme) {
|
|
2594
|
+
decorationTheme = theme;
|
|
2595
|
+
const marker = "__ccstyle_thinking__";
|
|
2596
|
+
const themedMarker = theme.fg("thinkingText", marker);
|
|
2597
|
+
const markerIndex = themedMarker.indexOf(marker);
|
|
2598
|
+
thinkingColor = markerIndex > 0 ? themedMarker.slice(0, markerIndex) : "";
|
|
2599
|
+
thinkingLineCache.clear();
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
// Fast path: themes without a thinking color need no processing.
|
|
2603
|
+
if (!thinkingColor) return lines;
|
|
2604
|
+
|
|
2605
|
+
const isThinking = lines.map((line) => {
|
|
2606
|
+
const cached = thinkingLineCache.get(line);
|
|
2607
|
+
if (cached !== undefined) return cached;
|
|
2608
|
+
const value = line.includes(thinkingColor) && hasItalicStyle(line);
|
|
2609
|
+
thinkingLineCache.set(line, value);
|
|
2610
|
+
trimDecorationCache(thinkingLineCache);
|
|
2611
|
+
return value;
|
|
2612
|
+
});
|
|
2559
2613
|
const decorated = [...isThinking];
|
|
2614
|
+
const hasText = (line: string): boolean => {
|
|
2615
|
+
const cached = nonEmptyLineCache.get(line);
|
|
2616
|
+
if (cached !== undefined) return cached;
|
|
2617
|
+
const value = Boolean(stripAnsiForToolClick(line));
|
|
2618
|
+
nonEmptyLineCache.set(line, value);
|
|
2619
|
+
trimDecorationCache(nonEmptyLineCache);
|
|
2620
|
+
return value;
|
|
2621
|
+
};
|
|
2560
2622
|
|
|
2561
2623
|
// Continue the guide through paragraph spacing inside one thinking block.
|
|
2562
2624
|
for (let index = 0; index < lines.length; index++) {
|
|
2563
|
-
if (
|
|
2625
|
+
if (hasText(lines[index] ?? "")) continue;
|
|
2564
2626
|
let before = index - 1;
|
|
2565
|
-
while (before >= 0 && !
|
|
2627
|
+
while (before >= 0 && !hasText(lines[before] ?? "")) before--;
|
|
2566
2628
|
let after = index + 1;
|
|
2567
|
-
while (after < lines.length && !
|
|
2629
|
+
while (after < lines.length && !hasText(lines[after] ?? "")) after++;
|
|
2568
2630
|
decorated[index] = before >= 0 && after < lines.length && isThinking[before] && isThinking[after];
|
|
2569
2631
|
}
|
|
2570
2632
|
|
|
2571
2633
|
// Keep the native thinking text styling, but do not add a guide line.
|
|
2572
2634
|
// The guide conflicted visually with the tool status markers.
|
|
2573
|
-
return lines.map((line, index) =>
|
|
2574
|
-
decorated[index]
|
|
2575
|
-
|
|
2635
|
+
return lines.map((line, index) => {
|
|
2636
|
+
if (!decorated[index]) return line;
|
|
2637
|
+
const cached = unboldLineCache.get(line);
|
|
2638
|
+
if (cached !== undefined) return cached;
|
|
2639
|
+
const result = removeThinkingBold(line);
|
|
2640
|
+
unboldLineCache.set(line, result);
|
|
2641
|
+
trimDecorationCache(unboldLineCache);
|
|
2642
|
+
return result;
|
|
2643
|
+
});
|
|
2576
2644
|
};
|
|
2577
2645
|
|
|
2578
2646
|
let compositor: TerminalSplitCompositor;
|
|
@@ -178,31 +178,46 @@ function animationFrame(frames: string[], intervalMs = 120): string {
|
|
|
178
178
|
return frames[Math.floor(Date.now() / intervalMs) % frames.length] ?? frames[0] ?? "";
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
const
|
|
181
|
+
const activeAnimationContexts = new Set<any>();
|
|
182
|
+
let sharedAnimationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
182
183
|
|
|
183
184
|
function clearAnimation(context: any) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
185
|
+
if (!context?.state?.ccstyleAnimationScheduled) return;
|
|
186
|
+
context.state.ccstyleAnimationScheduled = false;
|
|
187
|
+
activeAnimationContexts.delete(context);
|
|
188
|
+
if (activeAnimationContexts.size === 0 && sharedAnimationTimer) {
|
|
189
|
+
clearTimeout(sharedAnimationTimer);
|
|
190
|
+
sharedAnimationTimer = null;
|
|
191
|
+
}
|
|
189
192
|
}
|
|
190
193
|
|
|
191
194
|
function clearAllAnimations() {
|
|
192
|
-
for (const
|
|
193
|
-
|
|
195
|
+
for (const ctx of activeAnimationContexts) {
|
|
196
|
+
ctx.state.ccstyleAnimationScheduled = false;
|
|
197
|
+
}
|
|
198
|
+
activeAnimationContexts.clear();
|
|
199
|
+
if (sharedAnimationTimer) {
|
|
200
|
+
clearTimeout(sharedAnimationTimer);
|
|
201
|
+
sharedAnimationTimer = null;
|
|
202
|
+
}
|
|
194
203
|
}
|
|
195
204
|
|
|
196
205
|
function scheduleAnimation(context: any, intervalMs = 80) {
|
|
197
206
|
const state = (context.state ??= {});
|
|
198
|
-
if (state.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
207
|
+
if (state.ccstyleAnimationScheduled) return;
|
|
208
|
+
state.ccstyleAnimationScheduled = true;
|
|
209
|
+
activeAnimationContexts.add(context);
|
|
210
|
+
if (!sharedAnimationTimer) {
|
|
211
|
+
sharedAnimationTimer = setTimeout(() => {
|
|
212
|
+
sharedAnimationTimer = null;
|
|
213
|
+
const contexts = Array.from(activeAnimationContexts);
|
|
214
|
+
activeAnimationContexts.clear();
|
|
215
|
+
for (const ctx of contexts) {
|
|
216
|
+
ctx.state.ccstyleAnimationScheduled = false;
|
|
217
|
+
ctx.invalidate?.();
|
|
218
|
+
}
|
|
219
|
+
}, intervalMs);
|
|
220
|
+
}
|
|
206
221
|
}
|
|
207
222
|
|
|
208
223
|
function pendingIcon(name: string): string {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cc-extensions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Personal extensions for the Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -23,10 +23,11 @@
|
|
|
23
23
|
"scripts": {
|
|
24
24
|
"test": "node --test tests/**/*.test.ts",
|
|
25
25
|
"typecheck": "node scripts/typecheck.mjs",
|
|
26
|
-
"
|
|
26
|
+
"preversion": "npm run typecheck && npm test",
|
|
27
27
|
"release": "npm version patch && npm publish",
|
|
28
28
|
"release:minor": "npm version minor && npm publish",
|
|
29
|
-
"release:major": "npm version major && npm publish"
|
|
29
|
+
"release:major": "npm version major && npm publish",
|
|
30
|
+
"postpublish": "git push && git push --tags"
|
|
30
31
|
},
|
|
31
32
|
"peerDependencies": {
|
|
32
33
|
"@earendil-works/pi-ai": "*",
|