pi-claude-code-ui 1.0.92 → 1.0.95
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 +34 -0
- package/README.md +11 -7
- package/config/config.example.json +1 -1
- package/extensions/bash-file-edit.ts +301 -0
- package/extensions/index.ts +639 -154
- package/extensions/spinner.ts +152 -49
- package/package.json +2 -1
package/extensions/spinner.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import net from "node:net";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { basename, join } from "node:path";
|
|
5
5
|
|
|
6
6
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { Loader } from "@earendil-works/pi-tui";
|
|
@@ -476,6 +476,61 @@ function pickVerb(): string {
|
|
|
476
476
|
}
|
|
477
477
|
|
|
478
478
|
/** Format elapsed ms as compact duration: 5s, 1m 23s, 1h 2m 3s */
|
|
479
|
+
// What a running tool is doing, for the working line ("Running npm test…",
|
|
480
|
+
// "Reading stats.py…"). Random verbs stay for the time the model is thinking.
|
|
481
|
+
function clip(text: string, max: number): string {
|
|
482
|
+
const oneLine = text.split("\n")[0]?.trim() ?? "";
|
|
483
|
+
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function humanizeToolName(name: string): string {
|
|
487
|
+
const direct = /^mcp__.+?__(.+)$/.exec(name);
|
|
488
|
+
return (direct ? direct[1] : name)
|
|
489
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
490
|
+
.replace(/[_-]+/g, " ")
|
|
491
|
+
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function describeToolActivity(name: string, args: any): string {
|
|
495
|
+
const str = (...keys: string[]): string => {
|
|
496
|
+
for (const key of keys) {
|
|
497
|
+
const value = args?.[key];
|
|
498
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
499
|
+
}
|
|
500
|
+
return "";
|
|
501
|
+
};
|
|
502
|
+
const file = basename(str("path", "file_path"));
|
|
503
|
+
switch (name) {
|
|
504
|
+
case "bash": {
|
|
505
|
+
const command = clip(str("command"), 50);
|
|
506
|
+
return command ? `Running ${command}` : "Running a shell command";
|
|
507
|
+
}
|
|
508
|
+
case "read":
|
|
509
|
+
return file ? `Reading ${file}` : "Reading a file";
|
|
510
|
+
case "edit":
|
|
511
|
+
return file ? `Editing ${file}` : "Editing a file";
|
|
512
|
+
case "write":
|
|
513
|
+
return file ? `Writing ${file}` : "Writing a file";
|
|
514
|
+
case "grep":
|
|
515
|
+
case "find": {
|
|
516
|
+
const pattern = clip(str("pattern"), 40);
|
|
517
|
+
return pattern ? `Searching for ${pattern}` : "Searching";
|
|
518
|
+
}
|
|
519
|
+
case "ls":
|
|
520
|
+
return `Listing ${str("path") || "."}`;
|
|
521
|
+
case "Agent": {
|
|
522
|
+
const description = clip(str("description"), 50);
|
|
523
|
+
return description ? `Waiting for agent: ${description}` : "Waiting for an agent";
|
|
524
|
+
}
|
|
525
|
+
case "ask_user_question":
|
|
526
|
+
case "questionnaire":
|
|
527
|
+
case "question":
|
|
528
|
+
return "Waiting for your answer";
|
|
529
|
+
default:
|
|
530
|
+
return `Running ${humanizeToolName(name)}`;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
479
534
|
function formatDuration(ms: number): string {
|
|
480
535
|
const totalSec = Math.floor(ms / 1000);
|
|
481
536
|
const h = Math.floor(totalSec / 3600);
|
|
@@ -490,22 +545,26 @@ function formatCount(value: number): string {
|
|
|
490
545
|
return new Intl.NumberFormat("en-US").format(value);
|
|
491
546
|
}
|
|
492
547
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
for (let i = 0; i < message.content.length; i++) {
|
|
503
|
-
const block = message.content[i];
|
|
504
|
-
if (block?.type === "text" && typeof block.text === "string") {
|
|
505
|
-
lengths[i] = block.text.length;
|
|
548
|
+
/** Characters the model produced in one content block: text, thinking, or tool-call arguments. */
|
|
549
|
+
function contentBlockLength(block: any): number {
|
|
550
|
+
if (block?.type === "text" && typeof block.text === "string") return block.text.length;
|
|
551
|
+
if (block?.type === "thinking" && typeof block.thinking === "string") return block.thinking.length;
|
|
552
|
+
if (block?.type === "toolCall") {
|
|
553
|
+
try {
|
|
554
|
+
return JSON.stringify(block.arguments ?? {}).length;
|
|
555
|
+
} catch {
|
|
556
|
+
return 0;
|
|
506
557
|
}
|
|
507
558
|
}
|
|
508
|
-
return
|
|
559
|
+
return 0;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** Output tokens of a finished assistant message: the provider's count, or an estimate (chars / 4). */
|
|
563
|
+
function messageOutputTokens(message: any): number {
|
|
564
|
+
const reported = message?.usage?.output;
|
|
565
|
+
if (typeof reported === "number" && Number.isFinite(reported) && reported > 0) return reported;
|
|
566
|
+
if (!Array.isArray(message?.content)) return 0;
|
|
567
|
+
return Math.round(message.content.reduce((sum: number, block: any) => sum + contentBlockLength(block), 0) / 4);
|
|
509
568
|
}
|
|
510
569
|
|
|
511
570
|
function statusText(text: string): string {
|
|
@@ -721,8 +780,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
721
780
|
let completionTimer: ReturnType<typeof setTimeout> | null = null;
|
|
722
781
|
let thoughtStatusTimer: ReturnType<typeof setTimeout> | null = null;
|
|
723
782
|
let currentVerb = "";
|
|
724
|
-
|
|
725
|
-
|
|
783
|
+
// Output tokens for the whole run, like pi-polish: finished messages add their real
|
|
784
|
+
// usage.output; the message still streaming adds an estimate (chars / 4) of its text,
|
|
785
|
+
// thinking and tool-call arguments, so the count climbs as the model writes.
|
|
786
|
+
let runOutputTokens = 0;
|
|
787
|
+
let streamedChars = 0;
|
|
788
|
+
let streamedBlockLengths: number[] = [];
|
|
726
789
|
let thinkingStatus: "thinking" | number /* duration ms */ | null = null;
|
|
727
790
|
let thinkingStartTime = 0;
|
|
728
791
|
let thoughtForSetAt = 0;
|
|
@@ -730,6 +793,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
730
793
|
let turnActive = false;
|
|
731
794
|
let lastWorkingMessage: string | null = null;
|
|
732
795
|
let activeCtx: { ui: any; hasUI: boolean } | null = null;
|
|
796
|
+
// Tools running right now, in start order; the latest one names the working line.
|
|
797
|
+
const runningTools = new Map<string, { name: string; args: unknown }>();
|
|
733
798
|
// Root interactive session only — nested/non-UI contexts must not fight
|
|
734
799
|
// herdr state authority for this pane.
|
|
735
800
|
let herdrRootSession = false;
|
|
@@ -746,7 +811,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
746
811
|
|
|
747
812
|
function buildWorkingMessage(): string {
|
|
748
813
|
const elapsed = Date.now() - (agentStartTime || turnStartTime);
|
|
749
|
-
const tokenCount =
|
|
814
|
+
const tokenCount = outputTokenCount();
|
|
750
815
|
const statusParts: string[] = [];
|
|
751
816
|
|
|
752
817
|
if (thinkingStatus === "thinking") {
|
|
@@ -763,22 +828,39 @@ export default function (pi: ExtensionAPI) {
|
|
|
763
828
|
statusParts.push(formatDuration(elapsed));
|
|
764
829
|
}
|
|
765
830
|
|
|
766
|
-
|
|
831
|
+
const running = [...runningTools.values()];
|
|
832
|
+
const latest = running[running.length - 1];
|
|
833
|
+
const verb = latest
|
|
834
|
+
? `${describeToolActivity(latest.name, latest.args)}${running.length > 1 ? ` (+${running.length - 1} more)` : ""}`
|
|
835
|
+
: currentVerb;
|
|
836
|
+
let message = `${CLAUDE_ORANGE}${verb}…${RESET}`;
|
|
767
837
|
if (statusParts.length > 0) {
|
|
768
838
|
message += statusText(` (${statusParts.join(" · ")})`);
|
|
769
839
|
}
|
|
770
840
|
return message;
|
|
771
841
|
}
|
|
772
842
|
|
|
773
|
-
function
|
|
774
|
-
const previous =
|
|
775
|
-
|
|
776
|
-
|
|
843
|
+
function setStreamedBlockLength(index: number, length: number): void {
|
|
844
|
+
const previous = streamedBlockLengths[index] ?? 0;
|
|
845
|
+
streamedBlockLengths[index] = Math.max(0, length);
|
|
846
|
+
streamedChars = Math.max(0, streamedChars + streamedBlockLengths[index] - previous);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function resetStreamed(): void {
|
|
850
|
+
streamedBlockLengths = [];
|
|
851
|
+
streamedChars = 0;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function outputTokenCount(): number {
|
|
855
|
+
return runOutputTokens + Math.round(streamedChars / 4);
|
|
777
856
|
}
|
|
778
857
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
858
|
+
/** Start of a top-level request (not steering or follow-ups inside it). */
|
|
859
|
+
function beginRun(now = Date.now()): void {
|
|
860
|
+
if (agentStartTime) return;
|
|
861
|
+
agentStartTime = now;
|
|
862
|
+
runOutputTokens = 0;
|
|
863
|
+
resetStreamed();
|
|
782
864
|
}
|
|
783
865
|
|
|
784
866
|
function syncWorkingMessage(force = false): void {
|
|
@@ -815,7 +897,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
815
897
|
|
|
816
898
|
function getWorkingMessageIntervalMs(): number {
|
|
817
899
|
const elapsed = Date.now() - (agentStartTime || turnStartTime);
|
|
818
|
-
const tokenCount =
|
|
900
|
+
const tokenCount = outputTokenCount();
|
|
819
901
|
// Keep ticking once per second even when idle so /cc-spinner changes
|
|
820
902
|
// take effect within ~1s and elapsed-time crossover into the timer-on
|
|
821
903
|
// state still fires close to 30s. syncWorkingMessage short-circuits
|
|
@@ -902,7 +984,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
902
984
|
turnStartTime = 0;
|
|
903
985
|
thinkingStatus = null;
|
|
904
986
|
thoughtForSetAt = 0;
|
|
905
|
-
|
|
987
|
+
runOutputTokens = 0;
|
|
988
|
+
resetStreamed();
|
|
906
989
|
restoreDefaultWorkingMessage();
|
|
907
990
|
}
|
|
908
991
|
|
|
@@ -946,12 +1029,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
946
1029
|
|
|
947
1030
|
pi.on("before_agent_start", async () => {
|
|
948
1031
|
// Start once per top-level request. Steering/follow-up messages while the
|
|
949
|
-
// agent is active must not reset the timer.
|
|
950
|
-
|
|
1032
|
+
// agent is active must not reset the timer or the token count.
|
|
1033
|
+
beginRun();
|
|
951
1034
|
});
|
|
952
1035
|
|
|
953
1036
|
pi.on("agent_start", async (_event, ctx) => {
|
|
954
|
-
|
|
1037
|
+
beginRun();
|
|
955
1038
|
herdrMarkWorking(ctx);
|
|
956
1039
|
});
|
|
957
1040
|
|
|
@@ -961,9 +1044,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
961
1044
|
activeCtx = ctx;
|
|
962
1045
|
applyThemeColors(ctx.ui?.theme);
|
|
963
1046
|
turnStartTime = Date.now();
|
|
964
|
-
|
|
1047
|
+
beginRun(turnStartTime);
|
|
965
1048
|
currentVerb = pickVerb();
|
|
966
|
-
|
|
1049
|
+
resetStreamed();
|
|
967
1050
|
clearCompletionTimer();
|
|
968
1051
|
if (typeof thinkingStatus !== "number" || Date.now() - thoughtForSetAt >= THOUGHT_DISPLAY_MS) {
|
|
969
1052
|
thinkingStatus = null;
|
|
@@ -982,21 +1065,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
982
1065
|
applyThemeColors(ctx.ui?.theme);
|
|
983
1066
|
const evt = event.assistantMessageEvent;
|
|
984
1067
|
let statusChanged = false;
|
|
985
|
-
const previousTokenCount =
|
|
1068
|
+
const previousTokenCount = outputTokenCount();
|
|
986
1069
|
|
|
987
1070
|
if (evt.type === "start") {
|
|
988
|
-
|
|
989
|
-
} else if (evt.type === "text_start") {
|
|
990
|
-
|
|
991
|
-
} else if (evt.type === "text_delta") {
|
|
992
|
-
const previous =
|
|
993
|
-
|
|
994
|
-
} else if (evt.type === "text_end") {
|
|
995
|
-
|
|
996
|
-
} else if (evt.type === "
|
|
997
|
-
|
|
998
|
-
} else if (evt.type === "error") {
|
|
999
|
-
resetResponseTracking(evt.error);
|
|
1071
|
+
resetStreamed();
|
|
1072
|
+
} else if (evt.type === "text_start" || evt.type === "thinking_start" || evt.type === "toolcall_start") {
|
|
1073
|
+
setStreamedBlockLength(evt.contentIndex, 0);
|
|
1074
|
+
} else if (evt.type === "text_delta" || evt.type === "thinking_delta" || evt.type === "toolcall_delta") {
|
|
1075
|
+
const previous = streamedBlockLengths[evt.contentIndex] ?? 0;
|
|
1076
|
+
setStreamedBlockLength(evt.contentIndex, previous + (typeof evt.delta === "string" ? evt.delta.length : 0));
|
|
1077
|
+
} else if (evt.type === "text_end" || evt.type === "thinking_end") {
|
|
1078
|
+
setStreamedBlockLength(evt.contentIndex, typeof evt.content === "string" ? evt.content.length : 0);
|
|
1079
|
+
} else if (evt.type === "toolcall_end") {
|
|
1080
|
+
setStreamedBlockLength(evt.contentIndex, contentBlockLength({ type: "toolCall", arguments: evt.toolCall?.arguments }));
|
|
1000
1081
|
}
|
|
1001
1082
|
|
|
1002
1083
|
if (evt.type === "thinking_start") {
|
|
@@ -1019,12 +1100,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
1019
1100
|
return;
|
|
1020
1101
|
}
|
|
1021
1102
|
|
|
1022
|
-
const nextTokenCount =
|
|
1103
|
+
const nextTokenCount = outputTokenCount();
|
|
1023
1104
|
if (previousTokenCount === 0 && nextTokenCount > 0) {
|
|
1024
1105
|
rescheduleRefreshLoop();
|
|
1025
1106
|
}
|
|
1026
1107
|
});
|
|
1027
1108
|
|
|
1109
|
+
// A finished message swaps its estimate for the provider's real output-token count.
|
|
1110
|
+
pi.on("message_end", async (event: any) => {
|
|
1111
|
+
if (event?.message?.role !== "assistant") return;
|
|
1112
|
+
runOutputTokens += messageOutputTokens(event.message);
|
|
1113
|
+
resetStreamed();
|
|
1114
|
+
if (turnActive) syncWorkingMessage();
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
pi.on("tool_execution_start", async (event: any, ctx) => {
|
|
1118
|
+
if (typeof event?.toolCallId !== "string") return;
|
|
1119
|
+
activeCtx = ctx;
|
|
1120
|
+
runningTools.set(event.toolCallId, { name: String(event.toolName ?? ""), args: event.args });
|
|
1121
|
+
if (turnActive) syncWorkingMessage(true);
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
pi.on("tool_execution_end", async (event: any, ctx) => {
|
|
1125
|
+
if (typeof event?.toolCallId !== "string" || !runningTools.delete(event.toolCallId)) return;
|
|
1126
|
+
activeCtx = ctx;
|
|
1127
|
+
if (turnActive) syncWorkingMessage(true);
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1028
1130
|
pi.on("turn_end", async (_event, ctx) => {
|
|
1029
1131
|
turnActive = false;
|
|
1030
1132
|
activeCtx = ctx;
|
|
@@ -1060,13 +1162,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1060
1162
|
return;
|
|
1061
1163
|
}
|
|
1062
1164
|
|
|
1063
|
-
|
|
1064
|
-
responseTextBlockLengths = [];
|
|
1165
|
+
resetStreamed();
|
|
1065
1166
|
});
|
|
1066
1167
|
|
|
1067
1168
|
pi.on("agent_end", async () => {
|
|
1068
1169
|
turnActive = false;
|
|
1069
1170
|
agentStartTime = 0;
|
|
1171
|
+
runningTools.clear();
|
|
1070
1172
|
// Preserve the just-finished "Turn took …" line. Pi emits agent_end
|
|
1071
1173
|
// immediately after the final turn, so clearing here made the completion
|
|
1072
1174
|
// status disappear before users could see it.
|
|
@@ -1088,6 +1190,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1088
1190
|
|
|
1089
1191
|
pi.on("session_shutdown", async () => {
|
|
1090
1192
|
turnActive = false;
|
|
1193
|
+
runningTools.clear();
|
|
1091
1194
|
clearDisplay();
|
|
1092
1195
|
herdrMarkIdle(undefined, true);
|
|
1093
1196
|
herdrRootSession = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-claude-code-ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.95",
|
|
4
4
|
"description": "Claude Code-style grouped tool rows for pi with Ctrl+O/Ctrl+Shift+O previews, math rendering, and consistent built-in, MCP, and custom tool rendering",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"extensions/spinner.ts",
|
|
33
33
|
"extensions/settings-path.ts",
|
|
34
34
|
"extensions/bash-command.ts",
|
|
35
|
+
"extensions/bash-file-edit.ts",
|
|
35
36
|
"extensions/theme-colors.ts",
|
|
36
37
|
"extensions/terminal-theme.ts",
|
|
37
38
|
"config/config.example.json",
|