pum-agent 0.2.12-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.12-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);
@@ -1072,6 +1092,8 @@ export function App({
1072
1092
  setLoginOpen(false);
1073
1093
  setNewsOpen(false);
1074
1094
  newsOpenRef.current = false;
1095
+ setStatsOpen(false);
1096
+ statsOpenRef.current = false;
1075
1097
  }
1076
1098
  setQuestionnaireRevision((revision) => revision + 1);
1077
1099
  });
@@ -1161,10 +1183,17 @@ export function App({
1161
1183
  name: event.toolName,
1162
1184
  arg: toolArg(event.toolName, event.args, cwd),
1163
1185
  state: "running",
1186
+ startedAt: Date.now(),
1164
1187
  },
1165
1188
  });
1166
1189
  break;
1167
- 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) : {};
1168
1197
  patchTool(event.toolCallId, {
1169
1198
  state: isRejectedToolResult(event.result, event.toolCallId)
1170
1199
  ? "rejected"
@@ -1180,8 +1209,11 @@ export function App({
1180
1209
  : event.toolName.startsWith("message_cache_")
1181
1210
  ? messageCacheDetail(event.result)
1182
1211
  : undefined,
1212
+ output: undefined,
1213
+ exitCode: bashResult.exitCode,
1183
1214
  });
1184
1215
  break;
1216
+ }
1185
1217
  case "agent_start":
1186
1218
  setWorking(true);
1187
1219
  break;
@@ -1413,6 +1445,8 @@ export function App({
1413
1445
  setTriggerPopup(false, false);
1414
1446
  setNewsOpen(false);
1415
1447
  newsOpenRef.current = false;
1448
+ setStatsOpen(false);
1449
+ statsOpenRef.current = false;
1416
1450
  setLoginOpen(true);
1417
1451
  loginControllerRef.current?.open();
1418
1452
  };
@@ -1467,6 +1501,8 @@ export function App({
1467
1501
  setTriggerPopup(false, false);
1468
1502
  setNewsOpen(false);
1469
1503
  newsOpenRef.current = false;
1504
+ setStatsOpen(false);
1505
+ statsOpenRef.current = false;
1470
1506
  loadSessions()
1471
1507
  .then((sessions) => {
1472
1508
  setHistorySessions(sessions);
@@ -1489,6 +1525,8 @@ export function App({
1489
1525
  setAgentSelectorOpen(false);
1490
1526
  setTriggerPopup(false, false);
1491
1527
  setLoginOpen(false);
1528
+ setStatsOpen(false);
1529
+ statsOpenRef.current = false;
1492
1530
  newsCursorRef.current = 0;
1493
1531
  setNewsCursor(0);
1494
1532
  newsOpenRef.current = true;
@@ -1501,6 +1539,27 @@ export function App({
1501
1539
  queueMicrotask(() => inputRef.current?.focus());
1502
1540
  };
1503
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
+
1504
1563
  const moveNewsCursor = (direction: number) => {
1505
1564
  const count = newsRef.current.length;
1506
1565
  if (count === 0) return;
@@ -1760,8 +1819,9 @@ export function App({
1760
1819
  const checkPathCommand = /^\/check-path(?:\s|$)/.test(trimmed);
1761
1820
  const triggersCommand = trimmed === "/triggers";
1762
1821
  const newsCommand = trimmed === "/news";
1822
+ const statsCommand = trimmed === "/stats";
1763
1823
  const worktreeCommand = /^\/worktree(?:\s+([a-zA-Z0-9_-]+))?$/.exec(trimmed);
1764
- if (!compress && !clear && !historyCommand && !loginCommand && !checkPathCommand && !triggersCommand && !newsCommand && !worktreeCommand) return false;
1824
+ if (!compress && !clear && !historyCommand && !loginCommand && !checkPathCommand && !triggersCommand && !newsCommand && !statsCommand && !worktreeCommand) return false;
1765
1825
  editingStashIndex.current = null;
1766
1826
 
1767
1827
  if (historyCommand) {
@@ -1784,6 +1844,11 @@ export function App({
1784
1844
  openNews();
1785
1845
  return true;
1786
1846
  }
1847
+ if (statsCommand) {
1848
+ setEditorText("");
1849
+ openStats();
1850
+ return true;
1851
+ }
1787
1852
 
1788
1853
  setEditorText("");
1789
1854
  histCursor.current = null;
@@ -2230,6 +2295,7 @@ export function App({
2230
2295
  !agentSelectorOpen &&
2231
2296
  !helpOpen &&
2232
2297
  !historyOpen &&
2298
+ !statsOpenRef.current &&
2233
2299
  !settingsOpenRef.current;
2234
2300
  const inputValue = inputRef.current?.plainText ?? "";
2235
2301
  if (promptOwnsInput && (inputValue.length > 0 || pendingImages.current.length > 0)) {
@@ -2305,6 +2371,20 @@ export function App({
2305
2371
  return;
2306
2372
  }
2307
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
+
2308
2388
  if (key.ctrl && key.name === "t") {
2309
2389
  key.stopPropagation();
2310
2390
  if (triggersOpenRef.current) setTriggerPopup(false);
@@ -2989,7 +3069,7 @@ export function App({
2989
3069
  selectionBg={theme.selectionBg}
2990
3070
  wrapMode="word"
2991
3071
  scrollMargin={1}
2992
- focused={!settingsOpen && !helpOpen && !historyOpen && !agentSelectorOpen && !triggersOpen && !loginOpen && !questionnaire && !spawnPreview && !newsOpen}
3072
+ focused={!settingsOpen && !helpOpen && !historyOpen && !statsOpen && !agentSelectorOpen && !triggersOpen && !loginOpen && !questionnaire && !spawnPreview && !newsOpen}
2993
3073
  onContentChange={handleTextareaChange}
2994
3074
  onCursorChange={scheduleInputMetrics}
2995
3075
  onSubmit={() => submitPrompt()}
@@ -3079,6 +3159,15 @@ export function App({
3079
3159
  terminalHeight={height}
3080
3160
  />
3081
3161
  ) : null}
3162
+ {statsOpen ? (
3163
+ <StatsPopup
3164
+ theme={theme}
3165
+ snapshot={statsSnapshot}
3166
+ terminalWidth={width}
3167
+ terminalHeight={height}
3168
+ scrollOffset={statsScrollOffset}
3169
+ />
3170
+ ) : null}
3082
3171
  {settingsOpen ? (
3083
3172
  <SettingsPopup
3084
3173
  theme={theme}