pum-agent 0.2.11-beta.1 → 0.2.13-beta.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/README.md +2 -0
- package/package.json +1 -1
- package/src/app.tsx +139 -22
- package/src/bash-output.ts +688 -0
- package/src/check-mode.ts +34 -14
- package/src/check-policy.ts +53 -3
- package/src/cli.ts +38 -5
- package/src/commands.ts +4 -0
- package/src/filesystem-sandbox.ts +20 -2
- package/src/headless-stats.ts +58 -0
- package/src/headless.ts +71 -1
- package/src/help-popup.tsx +1 -0
- package/src/index.tsx +3 -0
- package/src/main.tsx +26 -1
- package/src/news-popup.tsx +9 -0
- package/src/news.ts +90 -2
- package/src/replay.ts +6 -1
- package/src/sandbox/index.ts +37 -8
- package/src/session-stats.ts +702 -0
- package/src/settings.ts +5 -0
- package/src/stats-popup.tsx +182 -0
- package/src/subagents/manager.ts +168 -3
- package/src/subagents/readonly.ts +19 -3
- package/src/subagents/types.ts +5 -0
- package/src/theme.ts +32 -0
- package/src/tool-line.ts +54 -0
- package/src/transcript.tsx +60 -2
package/README.md
CHANGED
|
@@ -152,6 +152,8 @@ Set `PUM_DIR` to override PUM's complete configuration and data directory. Run `
|
|
|
152
152
|
|
|
153
153
|
Useful commands include `/login`, `/history`, `/news`, `/triggers`, `/check-path`, `/clear`, `/compress`, and `/worktree`.
|
|
154
154
|
|
|
155
|
+
For automated benchmarks, add `--statsFile <path>` to a headless `-p` run. PUM writes a versioned JSON artifact with run metadata and all `/stats` data. PUM creates missing parent directories. PUM rejects an existing file before startup unless `--override` is present. The alias `--stats-file` is also accepted.
|
|
156
|
+
|
|
155
157
|
### Copy transcript text
|
|
156
158
|
|
|
157
159
|
Drag across transcript text with the left mouse button. PUM copies the completed selection when you release the button.
|
package/package.json
CHANGED
package/src/app.tsx
CHANGED
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
type PendingLine,
|
|
48
48
|
type Role,
|
|
49
49
|
} from "./transcript";
|
|
50
|
-
import { editCounts, toolArg, type ToolCall } from "./tool-line";
|
|
50
|
+
import { bashOutput, bashResultDisplay, editCounts, toolArg, type ToolCall } from "./tool-line";
|
|
51
51
|
import { readBranch, watchBranch } from "./git-branch";
|
|
52
52
|
import { HelpPopup, maxHelpScrollOffset } from "./help-popup";
|
|
53
53
|
import { appendHistory, loadHistory, removeHistory } from "./history";
|
|
@@ -135,6 +135,8 @@ import {
|
|
|
135
135
|
type NewsItem,
|
|
136
136
|
type NewsPrompt,
|
|
137
137
|
} from "./news";
|
|
138
|
+
import { statsFromEntries, type SessionStatsManager } from "./session-stats";
|
|
139
|
+
import { maxStatsScrollOffset, StatsPopup } from "./stats-popup";
|
|
138
140
|
|
|
139
141
|
type Stream = { kind: "assistant" | "thinking"; text: string } | null;
|
|
140
142
|
type Transcript = { lines: Line[]; stream: Stream; pending: PendingLine[] };
|
|
@@ -391,6 +393,7 @@ export function App({
|
|
|
391
393
|
settings: initial,
|
|
392
394
|
searchProviders,
|
|
393
395
|
subagentManager,
|
|
396
|
+
statsManager,
|
|
394
397
|
questionnaireManager,
|
|
395
398
|
spawnPreviewManager,
|
|
396
399
|
loginRequired = false,
|
|
@@ -417,6 +420,7 @@ export function App({
|
|
|
417
420
|
/** Provider ids that carry the hosted web-search tool; empty means none. */
|
|
418
421
|
searchProviders: string[];
|
|
419
422
|
subagentManager: SubagentManager;
|
|
423
|
+
statsManager?: SessionStatsManager;
|
|
420
424
|
questionnaireManager?: QuestionnaireManager;
|
|
421
425
|
spawnPreviewManager?: SpawnPreviewManager;
|
|
422
426
|
loginRequired?: boolean;
|
|
@@ -476,6 +480,9 @@ export function App({
|
|
|
476
480
|
const [helpOpen, setHelpOpen] = useState(false);
|
|
477
481
|
const [helpScrollOffset, setHelpScrollOffset] = useState(0);
|
|
478
482
|
const [historyOpen, setHistoryOpen] = useState(false);
|
|
483
|
+
const [statsOpen, setStatsOpen] = useState(false);
|
|
484
|
+
const [statsScrollOffset, setStatsScrollOffset] = useState(0);
|
|
485
|
+
const [statsRevision, setStatsRevision] = useState(0);
|
|
479
486
|
const [historySessions, setHistorySessions] = useState<SessionHistoryItem[]>([]);
|
|
480
487
|
const [page, setPage] = useState<"main" | "models" | "checkModels">("main");
|
|
481
488
|
const [settingsQuery, setSettingsQuery] = useState("");
|
|
@@ -528,6 +535,7 @@ export function App({
|
|
|
528
535
|
const newsOpenRef = useRef(false);
|
|
529
536
|
const [newsCursor, setNewsCursor] = useState(0);
|
|
530
537
|
const newsCursorRef = useRef(0);
|
|
538
|
+
const statsOpenRef = useRef(false);
|
|
531
539
|
|
|
532
540
|
const theme = useMemo(() => loadTheme(settings.theme), [settings.theme]);
|
|
533
541
|
const { width, height } = useTerminalDimensions();
|
|
@@ -555,6 +563,10 @@ export function App({
|
|
|
555
563
|
const visibleUsage = activeAgent?.usage ?? usage;
|
|
556
564
|
const agentTreeRows = buildAgentTree(agents);
|
|
557
565
|
const triggers = sortTriggers(triggerManager?.getTriggers() ?? []);
|
|
566
|
+
const statsSnapshot = useMemo(() => statsManager?.snapshot() ?? statsFromEntries(
|
|
567
|
+
(session.sessionManager as any).getEntries?.() ?? session.sessionManager.buildContextEntries(),
|
|
568
|
+
`${session.agent.state.model.provider}/${session.agent.state.model.id}`,
|
|
569
|
+
), [statsManager, session, statsRevision]);
|
|
558
570
|
const inputHint = cancelArmed
|
|
559
571
|
? " esc again to cancel "
|
|
560
572
|
: quitArmed
|
|
@@ -663,6 +675,8 @@ export function App({
|
|
|
663
675
|
setAgentSelectorOpen(false);
|
|
664
676
|
setNewsOpen(false);
|
|
665
677
|
newsOpenRef.current = false;
|
|
678
|
+
setStatsOpen(false);
|
|
679
|
+
statsOpenRef.current = false;
|
|
666
680
|
setStashMode(false);
|
|
667
681
|
const nextCursor = Math.min(triggerCursorRef.current, Math.max(0, triggers.length - 1));
|
|
668
682
|
triggerCursorRef.current = nextCursor;
|
|
@@ -967,6 +981,7 @@ export function App({
|
|
|
967
981
|
helpOpen ||
|
|
968
982
|
historyOpen ||
|
|
969
983
|
newsOpenRef.current ||
|
|
984
|
+
statsOpenRef.current ||
|
|
970
985
|
settingsOpenRef.current
|
|
971
986
|
) return;
|
|
972
987
|
const input = inputRef.current;
|
|
@@ -1022,6 +1037,11 @@ export function App({
|
|
|
1022
1037
|
append({ kind: "text", role: "system", text: warning });
|
|
1023
1038
|
}), [sandboxWarningSource]);
|
|
1024
1039
|
|
|
1040
|
+
useEffect(() => {
|
|
1041
|
+
setStatsRevision((revision) => revision + 1);
|
|
1042
|
+
return statsManager?.subscribe(() => setStatsRevision((revision) => revision + 1));
|
|
1043
|
+
}, [statsManager, session]);
|
|
1044
|
+
|
|
1025
1045
|
|
|
1026
1046
|
useEffect(() => triggerManager?.subscribe(() => {
|
|
1027
1047
|
setTriggerRevision((revision) => revision + 1);
|
|
@@ -1037,9 +1057,15 @@ export function App({
|
|
|
1037
1057
|
else if (event.type === "main-pending-add") addPending(event.pending);
|
|
1038
1058
|
else if (event.type === "main-pending-resolve") resolvePending(event.id);
|
|
1039
1059
|
else if (event.type === "main-pending-drop") dropPending(event.id);
|
|
1060
|
+
else if (event.type === "news-changed") {
|
|
1061
|
+
const loaded = loadNewsItems(session.sessionFile);
|
|
1062
|
+
newsRef.current = loaded;
|
|
1063
|
+
setNews(loaded);
|
|
1064
|
+
setTx((value) => ({ ...value, lines: tagNewsLines(value.lines, loaded) }));
|
|
1065
|
+
}
|
|
1040
1066
|
setAgentRevision((revision) => revision + 1);
|
|
1041
1067
|
}),
|
|
1042
|
-
[subagentManager],
|
|
1068
|
+
[subagentManager, session],
|
|
1043
1069
|
);
|
|
1044
1070
|
|
|
1045
1071
|
useEffect(() => {
|
|
@@ -1066,6 +1092,8 @@ export function App({
|
|
|
1066
1092
|
setLoginOpen(false);
|
|
1067
1093
|
setNewsOpen(false);
|
|
1068
1094
|
newsOpenRef.current = false;
|
|
1095
|
+
setStatsOpen(false);
|
|
1096
|
+
statsOpenRef.current = false;
|
|
1069
1097
|
}
|
|
1070
1098
|
setQuestionnaireRevision((revision) => revision + 1);
|
|
1071
1099
|
});
|
|
@@ -1155,10 +1183,17 @@ export function App({
|
|
|
1155
1183
|
name: event.toolName,
|
|
1156
1184
|
arg: toolArg(event.toolName, event.args, cwd),
|
|
1157
1185
|
state: "running",
|
|
1186
|
+
startedAt: Date.now(),
|
|
1158
1187
|
},
|
|
1159
1188
|
});
|
|
1160
1189
|
break;
|
|
1161
|
-
case "
|
|
1190
|
+
case "tool_execution_update":
|
|
1191
|
+
if (event.toolName === "bash") {
|
|
1192
|
+
patchTool(event.toolCallId, { output: bashOutput(event.partialResult) });
|
|
1193
|
+
}
|
|
1194
|
+
break;
|
|
1195
|
+
case "tool_execution_end": {
|
|
1196
|
+
const bashResult = event.toolName === "bash" ? bashResultDisplay(event.result) : {};
|
|
1162
1197
|
patchTool(event.toolCallId, {
|
|
1163
1198
|
state: isRejectedToolResult(event.result, event.toolCallId)
|
|
1164
1199
|
? "rejected"
|
|
@@ -1174,8 +1209,11 @@ export function App({
|
|
|
1174
1209
|
: event.toolName.startsWith("message_cache_")
|
|
1175
1210
|
? messageCacheDetail(event.result)
|
|
1176
1211
|
: undefined,
|
|
1212
|
+
output: undefined,
|
|
1213
|
+
exitCode: bashResult.exitCode,
|
|
1177
1214
|
});
|
|
1178
1215
|
break;
|
|
1216
|
+
}
|
|
1179
1217
|
case "agent_start":
|
|
1180
1218
|
setWorking(true);
|
|
1181
1219
|
break;
|
|
@@ -1407,6 +1445,8 @@ export function App({
|
|
|
1407
1445
|
setTriggerPopup(false, false);
|
|
1408
1446
|
setNewsOpen(false);
|
|
1409
1447
|
newsOpenRef.current = false;
|
|
1448
|
+
setStatsOpen(false);
|
|
1449
|
+
statsOpenRef.current = false;
|
|
1410
1450
|
setLoginOpen(true);
|
|
1411
1451
|
loginControllerRef.current?.open();
|
|
1412
1452
|
};
|
|
@@ -1461,6 +1501,8 @@ export function App({
|
|
|
1461
1501
|
setTriggerPopup(false, false);
|
|
1462
1502
|
setNewsOpen(false);
|
|
1463
1503
|
newsOpenRef.current = false;
|
|
1504
|
+
setStatsOpen(false);
|
|
1505
|
+
statsOpenRef.current = false;
|
|
1464
1506
|
loadSessions()
|
|
1465
1507
|
.then((sessions) => {
|
|
1466
1508
|
setHistorySessions(sessions);
|
|
@@ -1483,6 +1525,8 @@ export function App({
|
|
|
1483
1525
|
setAgentSelectorOpen(false);
|
|
1484
1526
|
setTriggerPopup(false, false);
|
|
1485
1527
|
setLoginOpen(false);
|
|
1528
|
+
setStatsOpen(false);
|
|
1529
|
+
statsOpenRef.current = false;
|
|
1486
1530
|
newsCursorRef.current = 0;
|
|
1487
1531
|
setNewsCursor(0);
|
|
1488
1532
|
newsOpenRef.current = true;
|
|
@@ -1495,6 +1539,27 @@ export function App({
|
|
|
1495
1539
|
queueMicrotask(() => inputRef.current?.focus());
|
|
1496
1540
|
};
|
|
1497
1541
|
|
|
1542
|
+
const openStats = () => {
|
|
1543
|
+
settingsOpenRef.current = false;
|
|
1544
|
+
setSettingsOpen(false);
|
|
1545
|
+
setHelpOpen(false);
|
|
1546
|
+
setHistoryOpen(false);
|
|
1547
|
+
setAgentSelectorOpen(false);
|
|
1548
|
+
setTriggerPopup(false, false);
|
|
1549
|
+
setLoginOpen(false);
|
|
1550
|
+
setNewsOpen(false);
|
|
1551
|
+
newsOpenRef.current = false;
|
|
1552
|
+
setStatsScrollOffset(0);
|
|
1553
|
+
statsOpenRef.current = true;
|
|
1554
|
+
setStatsOpen(true);
|
|
1555
|
+
};
|
|
1556
|
+
|
|
1557
|
+
const closeStats = () => {
|
|
1558
|
+
statsOpenRef.current = false;
|
|
1559
|
+
setStatsOpen(false);
|
|
1560
|
+
queueMicrotask(() => inputRef.current?.focus());
|
|
1561
|
+
};
|
|
1562
|
+
|
|
1498
1563
|
const moveNewsCursor = (direction: number) => {
|
|
1499
1564
|
const count = newsRef.current.length;
|
|
1500
1565
|
if (count === 0) return;
|
|
@@ -1506,29 +1571,47 @@ export function App({
|
|
|
1506
1571
|
const jumpFromNews = (target: "answer" | "prompt") => {
|
|
1507
1572
|
const item = newsRef.current[newsCursorRef.current];
|
|
1508
1573
|
if (!item) return;
|
|
1509
|
-
const
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
|
|
1574
|
+
const requesterAgentId = item.completion?.requesterAgentId ?? null;
|
|
1575
|
+
const lines = requesterAgentId === null
|
|
1576
|
+
? txRef.current.lines
|
|
1577
|
+
: subagentManager.getAgent(requesterAgentId)?.transcript.lines;
|
|
1578
|
+
if (!lines) return;
|
|
1579
|
+
const completionPromptIndex = item.completion
|
|
1580
|
+
? lines.findIndex((line) =>
|
|
1581
|
+
line.kind === "agent-message" && line.messageId === item.completion?.messageId,
|
|
1582
|
+
)
|
|
1583
|
+
: -1;
|
|
1584
|
+
const answerIndex = item.completion
|
|
1585
|
+
? lines.findIndex((line, index) =>
|
|
1586
|
+
index > completionPromptIndex
|
|
1587
|
+
&& line.kind === "text"
|
|
1588
|
+
&& line.role === "assistant"
|
|
1589
|
+
&& line.text === item.text,
|
|
1590
|
+
)
|
|
1591
|
+
: lines.findIndex((line) =>
|
|
1592
|
+
line.kind === "text" && line.role === "assistant" && line.newsId === item.id,
|
|
1593
|
+
);
|
|
1513
1594
|
let targetIndex = answerIndex;
|
|
1514
1595
|
if (target === "prompt") {
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1596
|
+
targetIndex = completionPromptIndex;
|
|
1597
|
+
if (!item.completion) {
|
|
1598
|
+
const promptText = item.prompts?.find((prompt) => !prompt.steer)?.text
|
|
1599
|
+
?? item.prompts?.[0]?.text;
|
|
1600
|
+
if (promptText) {
|
|
1601
|
+
const end = answerIndex >= 0 ? answerIndex : lines.length;
|
|
1602
|
+
for (let index = end - 1; index >= 0; index--) {
|
|
1603
|
+
const line = lines[index];
|
|
1604
|
+
if (line?.kind === "text" && line.role === "user" && line.text === promptText) {
|
|
1605
|
+
targetIndex = index;
|
|
1606
|
+
break;
|
|
1607
|
+
}
|
|
1525
1608
|
}
|
|
1526
1609
|
}
|
|
1527
1610
|
}
|
|
1528
1611
|
}
|
|
1529
1612
|
if (targetIndex < 0) return;
|
|
1530
1613
|
|
|
1531
|
-
if (activeAgentIdRef.current !==
|
|
1614
|
+
if (activeAgentIdRef.current !== requesterAgentId && !selectAgentView(requesterAgentId)) return;
|
|
1532
1615
|
newsOpenRef.current = false;
|
|
1533
1616
|
setNewsOpen(false);
|
|
1534
1617
|
const scrollToTarget = () => {
|
|
@@ -1590,8 +1673,12 @@ export function App({
|
|
|
1590
1673
|
const firstLine = item.text.split("\n")[0] ?? item.text;
|
|
1591
1674
|
const preview = firstLine.length > 240 ? `${firstLine.slice(0, 240)} …` : firstLine;
|
|
1592
1675
|
const quote = `> ${preview}\n\n`;
|
|
1593
|
-
|
|
1594
|
-
|
|
1676
|
+
const requesterAgentId = item.completion?.requesterAgentId ?? null;
|
|
1677
|
+
const targetAgentId = requesterAgentId && subagentManager.getAgent(requesterAgentId)
|
|
1678
|
+
? requesterAgentId
|
|
1679
|
+
: null;
|
|
1680
|
+
viewDrafts.current.set(targetAgentId ?? "main", quote);
|
|
1681
|
+
if (activeAgentIdRef.current !== targetAgentId) selectAgentView(targetAgentId);
|
|
1595
1682
|
else setEditorText(quote);
|
|
1596
1683
|
queueMicrotask(() => {
|
|
1597
1684
|
inputRef.current?.focus();
|
|
@@ -1732,8 +1819,9 @@ export function App({
|
|
|
1732
1819
|
const checkPathCommand = /^\/check-path(?:\s|$)/.test(trimmed);
|
|
1733
1820
|
const triggersCommand = trimmed === "/triggers";
|
|
1734
1821
|
const newsCommand = trimmed === "/news";
|
|
1822
|
+
const statsCommand = trimmed === "/stats";
|
|
1735
1823
|
const worktreeCommand = /^\/worktree(?:\s+([a-zA-Z0-9_-]+))?$/.exec(trimmed);
|
|
1736
|
-
if (!compress && !clear && !historyCommand && !loginCommand && !checkPathCommand && !triggersCommand && !newsCommand && !worktreeCommand) return false;
|
|
1824
|
+
if (!compress && !clear && !historyCommand && !loginCommand && !checkPathCommand && !triggersCommand && !newsCommand && !statsCommand && !worktreeCommand) return false;
|
|
1737
1825
|
editingStashIndex.current = null;
|
|
1738
1826
|
|
|
1739
1827
|
if (historyCommand) {
|
|
@@ -1756,6 +1844,11 @@ export function App({
|
|
|
1756
1844
|
openNews();
|
|
1757
1845
|
return true;
|
|
1758
1846
|
}
|
|
1847
|
+
if (statsCommand) {
|
|
1848
|
+
setEditorText("");
|
|
1849
|
+
openStats();
|
|
1850
|
+
return true;
|
|
1851
|
+
}
|
|
1759
1852
|
|
|
1760
1853
|
setEditorText("");
|
|
1761
1854
|
histCursor.current = null;
|
|
@@ -2202,6 +2295,7 @@ export function App({
|
|
|
2202
2295
|
!agentSelectorOpen &&
|
|
2203
2296
|
!helpOpen &&
|
|
2204
2297
|
!historyOpen &&
|
|
2298
|
+
!statsOpenRef.current &&
|
|
2205
2299
|
!settingsOpenRef.current;
|
|
2206
2300
|
const inputValue = inputRef.current?.plainText ?? "";
|
|
2207
2301
|
if (promptOwnsInput && (inputValue.length > 0 || pendingImages.current.length > 0)) {
|
|
@@ -2277,6 +2371,20 @@ export function App({
|
|
|
2277
2371
|
return;
|
|
2278
2372
|
}
|
|
2279
2373
|
|
|
2374
|
+
if (statsOpenRef.current || statsOpen) {
|
|
2375
|
+
key.stopPropagation();
|
|
2376
|
+
const maxOffset = maxStatsScrollOffset(statsSnapshot, width, height);
|
|
2377
|
+
if (key.name === "escape") closeStats();
|
|
2378
|
+
else if (key.name === "home") setStatsScrollOffset(0);
|
|
2379
|
+
else if (key.name === "end") setStatsScrollOffset(maxOffset);
|
|
2380
|
+
else if (key.name === "up" || key.name === "down" || key.name === "pageup" || key.name === "pagedown") {
|
|
2381
|
+
const amount = key.name === "pageup" || key.name === "pagedown" ? 5 : 1;
|
|
2382
|
+
const direction = key.name === "up" || key.name === "pageup" ? -1 : 1;
|
|
2383
|
+
setStatsScrollOffset((offset) => Math.max(0, Math.min(maxOffset, offset + direction * amount)));
|
|
2384
|
+
}
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2280
2388
|
if (key.ctrl && key.name === "t") {
|
|
2281
2389
|
key.stopPropagation();
|
|
2282
2390
|
if (triggersOpenRef.current) setTriggerPopup(false);
|
|
@@ -2961,7 +3069,7 @@ export function App({
|
|
|
2961
3069
|
selectionBg={theme.selectionBg}
|
|
2962
3070
|
wrapMode="word"
|
|
2963
3071
|
scrollMargin={1}
|
|
2964
|
-
focused={!settingsOpen && !helpOpen && !historyOpen && !agentSelectorOpen && !triggersOpen && !loginOpen && !questionnaire && !spawnPreview && !newsOpen}
|
|
3072
|
+
focused={!settingsOpen && !helpOpen && !historyOpen && !statsOpen && !agentSelectorOpen && !triggersOpen && !loginOpen && !questionnaire && !spawnPreview && !newsOpen}
|
|
2965
3073
|
onContentChange={handleTextareaChange}
|
|
2966
3074
|
onCursorChange={scheduleInputMetrics}
|
|
2967
3075
|
onSubmit={() => submitPrompt()}
|
|
@@ -3051,6 +3159,15 @@ export function App({
|
|
|
3051
3159
|
terminalHeight={height}
|
|
3052
3160
|
/>
|
|
3053
3161
|
) : null}
|
|
3162
|
+
{statsOpen ? (
|
|
3163
|
+
<StatsPopup
|
|
3164
|
+
theme={theme}
|
|
3165
|
+
snapshot={statsSnapshot}
|
|
3166
|
+
terminalWidth={width}
|
|
3167
|
+
terminalHeight={height}
|
|
3168
|
+
scrollOffset={statsScrollOffset}
|
|
3169
|
+
/>
|
|
3170
|
+
) : null}
|
|
3054
3171
|
{settingsOpen ? (
|
|
3055
3172
|
<SettingsPopup
|
|
3056
3173
|
theme={theme}
|