pum-agent 0.2.12-beta.1 → 0.2.14-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.14-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,10 @@ export function App({
1180
1209
  : event.toolName.startsWith("message_cache_")
1181
1210
  ? messageCacheDetail(event.result)
1182
1211
  : undefined,
1212
+ exitCode: bashResult.exitCode,
1183
1213
  });
1184
1214
  break;
1215
+ }
1185
1216
  case "agent_start":
1186
1217
  setWorking(true);
1187
1218
  break;
@@ -1413,6 +1444,8 @@ export function App({
1413
1444
  setTriggerPopup(false, false);
1414
1445
  setNewsOpen(false);
1415
1446
  newsOpenRef.current = false;
1447
+ setStatsOpen(false);
1448
+ statsOpenRef.current = false;
1416
1449
  setLoginOpen(true);
1417
1450
  loginControllerRef.current?.open();
1418
1451
  };
@@ -1467,6 +1500,8 @@ export function App({
1467
1500
  setTriggerPopup(false, false);
1468
1501
  setNewsOpen(false);
1469
1502
  newsOpenRef.current = false;
1503
+ setStatsOpen(false);
1504
+ statsOpenRef.current = false;
1470
1505
  loadSessions()
1471
1506
  .then((sessions) => {
1472
1507
  setHistorySessions(sessions);
@@ -1489,6 +1524,8 @@ export function App({
1489
1524
  setAgentSelectorOpen(false);
1490
1525
  setTriggerPopup(false, false);
1491
1526
  setLoginOpen(false);
1527
+ setStatsOpen(false);
1528
+ statsOpenRef.current = false;
1492
1529
  newsCursorRef.current = 0;
1493
1530
  setNewsCursor(0);
1494
1531
  newsOpenRef.current = true;
@@ -1501,6 +1538,27 @@ export function App({
1501
1538
  queueMicrotask(() => inputRef.current?.focus());
1502
1539
  };
1503
1540
 
1541
+ const openStats = () => {
1542
+ settingsOpenRef.current = false;
1543
+ setSettingsOpen(false);
1544
+ setHelpOpen(false);
1545
+ setHistoryOpen(false);
1546
+ setAgentSelectorOpen(false);
1547
+ setTriggerPopup(false, false);
1548
+ setLoginOpen(false);
1549
+ setNewsOpen(false);
1550
+ newsOpenRef.current = false;
1551
+ setStatsScrollOffset(0);
1552
+ statsOpenRef.current = true;
1553
+ setStatsOpen(true);
1554
+ };
1555
+
1556
+ const closeStats = () => {
1557
+ statsOpenRef.current = false;
1558
+ setStatsOpen(false);
1559
+ queueMicrotask(() => inputRef.current?.focus());
1560
+ };
1561
+
1504
1562
  const moveNewsCursor = (direction: number) => {
1505
1563
  const count = newsRef.current.length;
1506
1564
  if (count === 0) return;
@@ -1760,8 +1818,9 @@ export function App({
1760
1818
  const checkPathCommand = /^\/check-path(?:\s|$)/.test(trimmed);
1761
1819
  const triggersCommand = trimmed === "/triggers";
1762
1820
  const newsCommand = trimmed === "/news";
1821
+ const statsCommand = trimmed === "/stats";
1763
1822
  const worktreeCommand = /^\/worktree(?:\s+([a-zA-Z0-9_-]+))?$/.exec(trimmed);
1764
- if (!compress && !clear && !historyCommand && !loginCommand && !checkPathCommand && !triggersCommand && !newsCommand && !worktreeCommand) return false;
1823
+ if (!compress && !clear && !historyCommand && !loginCommand && !checkPathCommand && !triggersCommand && !newsCommand && !statsCommand && !worktreeCommand) return false;
1765
1824
  editingStashIndex.current = null;
1766
1825
 
1767
1826
  if (historyCommand) {
@@ -1784,6 +1843,11 @@ export function App({
1784
1843
  openNews();
1785
1844
  return true;
1786
1845
  }
1846
+ if (statsCommand) {
1847
+ setEditorText("");
1848
+ openStats();
1849
+ return true;
1850
+ }
1787
1851
 
1788
1852
  setEditorText("");
1789
1853
  histCursor.current = null;
@@ -2230,6 +2294,7 @@ export function App({
2230
2294
  !agentSelectorOpen &&
2231
2295
  !helpOpen &&
2232
2296
  !historyOpen &&
2297
+ !statsOpenRef.current &&
2233
2298
  !settingsOpenRef.current;
2234
2299
  const inputValue = inputRef.current?.plainText ?? "";
2235
2300
  if (promptOwnsInput && (inputValue.length > 0 || pendingImages.current.length > 0)) {
@@ -2305,6 +2370,20 @@ export function App({
2305
2370
  return;
2306
2371
  }
2307
2372
 
2373
+ if (statsOpenRef.current || statsOpen) {
2374
+ key.stopPropagation();
2375
+ const maxOffset = maxStatsScrollOffset(statsSnapshot, width, height);
2376
+ if (key.name === "escape") closeStats();
2377
+ else if (key.name === "home") setStatsScrollOffset(0);
2378
+ else if (key.name === "end") setStatsScrollOffset(maxOffset);
2379
+ else if (key.name === "up" || key.name === "down" || key.name === "pageup" || key.name === "pagedown") {
2380
+ const amount = key.name === "pageup" || key.name === "pagedown" ? 5 : 1;
2381
+ const direction = key.name === "up" || key.name === "pageup" ? -1 : 1;
2382
+ setStatsScrollOffset((offset) => Math.max(0, Math.min(maxOffset, offset + direction * amount)));
2383
+ }
2384
+ return;
2385
+ }
2386
+
2308
2387
  if (key.ctrl && key.name === "t") {
2309
2388
  key.stopPropagation();
2310
2389
  if (triggersOpenRef.current) setTriggerPopup(false);
@@ -2989,7 +3068,7 @@ export function App({
2989
3068
  selectionBg={theme.selectionBg}
2990
3069
  wrapMode="word"
2991
3070
  scrollMargin={1}
2992
- focused={!settingsOpen && !helpOpen && !historyOpen && !agentSelectorOpen && !triggersOpen && !loginOpen && !questionnaire && !spawnPreview && !newsOpen}
3071
+ focused={!settingsOpen && !helpOpen && !historyOpen && !statsOpen && !agentSelectorOpen && !triggersOpen && !loginOpen && !questionnaire && !spawnPreview && !newsOpen}
2993
3072
  onContentChange={handleTextareaChange}
2994
3073
  onCursorChange={scheduleInputMetrics}
2995
3074
  onSubmit={() => submitPrompt()}
@@ -3079,6 +3158,15 @@ export function App({
3079
3158
  terminalHeight={height}
3080
3159
  />
3081
3160
  ) : null}
3161
+ {statsOpen ? (
3162
+ <StatsPopup
3163
+ theme={theme}
3164
+ snapshot={statsSnapshot}
3165
+ terminalWidth={width}
3166
+ terminalHeight={height}
3167
+ scrollOffset={statsScrollOffset}
3168
+ />
3169
+ ) : null}
3082
3170
  {settingsOpen ? (
3083
3171
  <SettingsPopup
3084
3172
  theme={theme}