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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pum-agent",
3
- "version": "0.2.11-beta.1",
3
+ "version": "0.2.13-beta.1",
4
4
  "description": "A compact terminal coding agent powered by pi and OpenTUI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
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 "tool_execution_end":
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 lines = txRef.current.lines;
1510
- const answerIndex = lines.findIndex((line) =>
1511
- line.kind === "text" && line.role === "assistant" && line.newsId === item.id,
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
- const promptText = item.prompts?.find((prompt) => !prompt.steer)?.text
1516
- ?? item.prompts?.[0]?.text;
1517
- targetIndex = -1;
1518
- if (promptText) {
1519
- const end = answerIndex >= 0 ? answerIndex : lines.length;
1520
- for (let index = end - 1; index >= 0; index--) {
1521
- const line = lines[index];
1522
- if (line?.kind === "text" && line.role === "user" && line.text === promptText) {
1523
- targetIndex = index;
1524
- break;
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 !== null && !selectAgentView(null)) return;
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
- viewDrafts.current.set("main", quote);
1594
- if (activeAgentIdRef.current !== null) selectAgentView(null);
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}