@nathapp/nax 0.68.8 → 0.69.0

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.
Files changed (2) hide show
  1. package/dist/nax.js +253 -126
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -20944,12 +20944,21 @@ function parseAcpxJsonLine(line, state) {
20944
20944
  }
20945
20945
  if (update.sessionUpdate === "usage_update") {
20946
20946
  const activity = { kind: "usage_update" };
20947
- if (typeof update.used === "number") {
20947
+ const metaUsage = update._meta != null && typeof update._meta === "object" ? update._meta.usage : undefined;
20948
+ if (metaUsage != null && typeof metaUsage === "object") {
20949
+ const inp = metaUsage.inputTokens ?? metaUsage.input_tokens;
20950
+ if (typeof inp === "number")
20951
+ activity.inputTokens = inp;
20952
+ const out = metaUsage.outputTokens ?? metaUsage.output_tokens;
20953
+ if (typeof out === "number")
20954
+ activity.outputTokens = out;
20955
+ }
20956
+ if (activity.outputTokens == null && typeof update.used === "number") {
20948
20957
  activity.outputTokens = update.used;
20949
20958
  }
20950
20959
  if (typeof update.cost?.amount === "number") {
20951
20960
  activity.costUsd = update.cost.amount;
20952
- state.exactCostUsd = update.cost.amount;
20961
+ state.exactCostUsd = activity.costUsd;
20953
20962
  }
20954
20963
  return activity;
20955
20964
  }
@@ -34337,7 +34346,25 @@ async function readSpecDriftViolations(input) {
34337
34346
  return [];
34338
34347
  }
34339
34348
  }
34340
- async function normalizeStoryFiles(story, workdir, fileExists) {
34349
+ function collectUpstreamProducedFiles(story, byId) {
34350
+ const produced = new Set;
34351
+ const seen = new Set;
34352
+ const stack = [...story.dependencies ?? []];
34353
+ while (stack.length > 0) {
34354
+ const depId = stack.pop();
34355
+ if (!depId || seen.has(depId))
34356
+ continue;
34357
+ seen.add(depId);
34358
+ const dep = byId.get(depId);
34359
+ if (!dep)
34360
+ continue;
34361
+ for (const filePath of getExpectedFiles(dep))
34362
+ produced.add(filePath);
34363
+ stack.push(...dep.dependencies ?? []);
34364
+ }
34365
+ return produced;
34366
+ }
34367
+ async function normalizeStoryFiles(story, workdir, fileExists, upstreamProduced) {
34341
34368
  const contextFiles = story.contextFiles ?? [];
34342
34369
  if (contextFiles.length === 0)
34343
34370
  return { story, changed: false };
@@ -34352,6 +34379,14 @@ async function normalizeStoryFiles(story, workdir, fileExists) {
34352
34379
  kept.push(entry);
34353
34380
  continue;
34354
34381
  }
34382
+ if (upstreamProduced.has(filePath)) {
34383
+ kept.push(entry);
34384
+ logger?.debug("plan", "Kept cross-story produced file in contextFiles (upstream dependency creates it)", {
34385
+ storyId: story.id,
34386
+ filePath
34387
+ });
34388
+ continue;
34389
+ }
34355
34390
  if (factId) {
34356
34391
  logger?.warn("plan", "Context file cites a manifest fact but is absent on disk", {
34357
34392
  storyId: story.id,
@@ -34379,7 +34414,8 @@ async function normalizeStoryFiles(story, workdir, fileExists) {
34379
34414
  async function normalizeCreatedContextFiles(prd, workdir, fileExists) {
34380
34415
  if (!workdir)
34381
34416
  return prd;
34382
- const results = await Promise.all(prd.userStories.map((story) => normalizeStoryFiles(story, workdir, fileExists)));
34417
+ const byId = new Map(prd.userStories.map((story) => [story.id, story]));
34418
+ const results = await Promise.all(prd.userStories.map((story) => normalizeStoryFiles(story, workdir, fileExists, collectUpstreamProducedFiles(story, byId))));
34383
34419
  if (!results.some((r) => r.changed))
34384
34420
  return prd;
34385
34421
  return { ...prd, userStories: results.map((r) => r.story) };
@@ -42566,9 +42602,9 @@ ${rows.join(`
42566
42602
  `)}
42567
42603
  `;
42568
42604
  }
42569
- var CONTEXT_VS_EXPECTED_FILES_RULE = `**\`contextFiles\` rule \u2014 existing files only.** Only list paths that already exist in the repo today. The pipeline verifies every \`contextFiles\` entry against the filesystem; a path that does not exist is treated as a missing-context warning.
42605
+ var CONTEXT_VS_EXPECTED_FILES_RULE = `**\`contextFiles\` rule \u2014 files readable when this story runs.** List paths that already exist in the repo today, PLUS any file an UPSTREAM dependency story creates (it does not exist now but will exist by the time this story runs, because dependencies execute first). The pipeline verifies every \`contextFiles\` entry against the filesystem; a path that exists neither on disk nor in an upstream dependency's outputs is treated as a missing-context warning.
42570
42606
 
42571
- **\`expectedFiles\` rule \u2014 files this story CREATES.** List every NEW file the story authors (relative paths). A file the story will create belongs here, NEVER in \`contextFiles\` \u2014 these are the story's outputs, not files to read first. A single path may appear in \`contextFiles\` (an existing sibling to mirror) AND \`expectedFiles\` (the new file itself), but the same path must never be in both.`, EXPECTED_FILES_SCHEMA_FIELD = `"expectedFiles": ["string \u2014 NEW files this story creates (relative paths, omit if none)"],`;
42607
+ **\`expectedFiles\` rule \u2014 files THIS story CREATES.** List every NEW file this story authors (relative paths). A file this story creates belongs here, NEVER in \`contextFiles\` \u2014 these are the story's outputs, not files to read first. A file created by an upstream dependency and only read/modified here belongs in \`contextFiles\`, NOT here (this story does not author it). A single path may appear in \`contextFiles\` (an existing sibling to mirror) AND \`expectedFiles\` (the new file itself), but the same path must never be in both.`, EXPECTED_FILES_SCHEMA_FIELD = `"expectedFiles": ["string \u2014 NEW files this story creates (relative paths, omit if none)"],`;
42572
42608
  var init_plan_builder = __esm(() => {
42573
42609
  init_config();
42574
42610
  });
@@ -58668,7 +58704,7 @@ var package_default;
58668
58704
  var init_package = __esm(() => {
58669
58705
  package_default = {
58670
58706
  name: "@nathapp/nax",
58671
- version: "0.68.8",
58707
+ version: "0.69.0",
58672
58708
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
58673
58709
  type: "module",
58674
58710
  bin: {
@@ -58763,8 +58799,8 @@ var init_version = __esm(() => {
58763
58799
  NAX_VERSION = package_default.version;
58764
58800
  NAX_COMMIT = (() => {
58765
58801
  try {
58766
- if (/^[0-9a-f]{6,10}$/.test("00515ea8"))
58767
- return "00515ea8";
58802
+ if (/^[0-9a-f]{6,10}$/.test("ce4d8f0e"))
58803
+ return "ce4d8f0e";
58768
58804
  } catch {}
58769
58805
  try {
58770
58806
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -103850,7 +103886,8 @@ function LiveActivityPanel({
103850
103886
  storySteps,
103851
103887
  runSummary,
103852
103888
  runErrored,
103853
- escalationLog = []
103889
+ escalationLog = [],
103890
+ currentStage
103854
103891
  }) {
103855
103892
  const borderColor = focused ? "cyan" : "gray";
103856
103893
  const activeCallList = activeCalls ? Array.from(activeCalls.values()) : [];
@@ -103907,7 +103944,8 @@ function LiveActivityPanel({
103907
103944
  paddingY: 1,
103908
103945
  children: activeCallList.map((call) => /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(ActiveCallRow, {
103909
103946
  call,
103910
- step: call.storyId ? storySteps?.[call.storyId] : undefined
103947
+ step: call.storyId ? storySteps?.[call.storyId] : undefined,
103948
+ currentStage
103911
103949
  }, call.callId, false, undefined, this))
103912
103950
  }, undefined, false, undefined, this),
103913
103951
  hasEscalations && /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
@@ -103948,7 +103986,24 @@ function LiveActivityPanel({
103948
103986
  !hasActiveCalls && !hasSummary && !hasError && (!storySteps || Object.keys(storySteps).length === 0) && /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
103949
103987
  paddingX: 1,
103950
103988
  paddingY: 1,
103951
- children: /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
103989
+ children: currentStage ? /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
103990
+ flexDirection: "row",
103991
+ gap: 1,
103992
+ children: [
103993
+ /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
103994
+ color: "yellow",
103995
+ children: [
103996
+ "[",
103997
+ currentStage,
103998
+ "]"
103999
+ ]
104000
+ }, undefined, true, undefined, this),
104001
+ /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
104002
+ dimColor: true,
104003
+ children: "preparing..."
104004
+ }, undefined, false, undefined, this)
104005
+ ]
104006
+ }, undefined, true, undefined, this) : /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
103952
104007
  dimColor: true,
103953
104008
  children: "Waiting for agent..."
103954
104009
  }, undefined, false, undefined, this)
@@ -103956,7 +104011,29 @@ function LiveActivityPanel({
103956
104011
  ]
103957
104012
  }, undefined, true, undefined, this);
103958
104013
  }
103959
- function ActiveCallRow({ call, step }) {
104014
+ function ActiveCallRow({ call, step, currentStage }) {
104015
+ const stageLabel = step ? /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
104016
+ color: "yellow",
104017
+ children: [
104018
+ "[",
104019
+ step,
104020
+ "]"
104021
+ ]
104022
+ }, undefined, true, undefined, this) : call.stage ? /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
104023
+ dimColor: true,
104024
+ children: [
104025
+ "[",
104026
+ call.stage,
104027
+ "]"
104028
+ ]
104029
+ }, undefined, true, undefined, this) : currentStage ? /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
104030
+ dimColor: true,
104031
+ children: [
104032
+ "[",
104033
+ currentStage,
104034
+ "]"
104035
+ ]
104036
+ }, undefined, true, undefined, this) : null;
103960
104037
  return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
103961
104038
  flexDirection: "column",
103962
104039
  marginBottom: 1,
@@ -103972,21 +104049,7 @@ function ActiveCallRow({ call, step }) {
103972
104049
  call.storyId && /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
103973
104050
  children: call.storyId
103974
104051
  }, undefined, false, undefined, this),
103975
- step ? /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
103976
- color: "yellow",
103977
- children: [
103978
- "[",
103979
- step,
103980
- "]"
103981
- ]
103982
- }, undefined, true, undefined, this) : call.stage ? /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
103983
- dimColor: true,
103984
- children: [
103985
- "[",
103986
- call.stage,
103987
- "]"
103988
- ]
103989
- }, undefined, true, undefined, this) : null
104052
+ stageLabel
103990
104053
  ]
103991
104054
  }, undefined, true, undefined, this),
103992
104055
  /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
@@ -104193,7 +104256,14 @@ function getStatusIcon(status) {
104193
104256
  return "\u23F8\uFE0F";
104194
104257
  }
104195
104258
  }
104196
- function StoriesPanel({ stories, postRunPhases, width, compact: compact2 = false, maxHeight }) {
104259
+ function StoriesPanel({
104260
+ stories,
104261
+ preRunPhases,
104262
+ postRunPhases,
104263
+ width,
104264
+ compact: compact2 = false,
104265
+ maxHeight
104266
+ }) {
104197
104267
  const maxVisible = compact2 ? COMPACT_MAX_VISIBLE_STORIES : MAX_VISIBLE_STORIES;
104198
104268
  const needsScrolling = stories.length > maxVisible;
104199
104269
  const [scrollOffset, setScrollOffset] = import_react30.useState(0);
@@ -104225,7 +104295,7 @@ function StoriesPanel({ stories, postRunPhases, width, compact: compact2 = false
104225
104295
  children: [
104226
104296
  /* @__PURE__ */ jsx_dev_runtime5.jsxDEV(Text, {
104227
104297
  bold: true,
104228
- children: "Stories"
104298
+ children: "Progress"
104229
104299
  }, undefined, false, undefined, this),
104230
104300
  needsScrolling && /* @__PURE__ */ jsx_dev_runtime5.jsxDEV(Text, {
104231
104301
  dimColor: true,
@@ -104237,6 +104307,16 @@ function StoriesPanel({ stories, postRunPhases, width, compact: compact2 = false
104237
104307
  }, undefined, true, undefined, this)
104238
104308
  ]
104239
104309
  }, undefined, true, undefined, this),
104310
+ preRunPhases && Object.keys(preRunPhases).length > 0 && /* @__PURE__ */ jsx_dev_runtime5.jsxDEV(Box_default, {
104311
+ flexDirection: "column",
104312
+ paddingX: 1,
104313
+ paddingTop: 1,
104314
+ children: Object.entries(preRunPhases).map(([name, phase]) => /* @__PURE__ */ jsx_dev_runtime5.jsxDEV(PreRunPhaseRow, {
104315
+ label: name,
104316
+ phase,
104317
+ compact: compact2
104318
+ }, name, false, undefined, this))
104319
+ }, undefined, false, undefined, this),
104240
104320
  needsScrolling && canScrollUp && /* @__PURE__ */ jsx_dev_runtime5.jsxDEV(Box_default, {
104241
104321
  paddingX: 1,
104242
104322
  children: /* @__PURE__ */ jsx_dev_runtime5.jsxDEV(Text, {
@@ -104315,11 +104395,6 @@ function StoriesPanel({ stories, postRunPhases, width, compact: compact2 = false
104315
104395
  paddingX: 1,
104316
104396
  paddingTop: 1,
104317
104397
  children: [
104318
- /* @__PURE__ */ jsx_dev_runtime5.jsxDEV(Box_default, {
104319
- borderStyle: "single",
104320
- borderTop: true,
104321
- borderColor: "gray"
104322
- }, undefined, false, undefined, this),
104323
104398
  /* @__PURE__ */ jsx_dev_runtime5.jsxDEV(Text, {
104324
104399
  dimColor: true,
104325
104400
  children: "Post-Run"
@@ -104344,6 +104419,21 @@ function StoriesPanel({ stories, postRunPhases, width, compact: compact2 = false
104344
104419
  ]
104345
104420
  }, undefined, true, undefined, this);
104346
104421
  }
104422
+ function PreRunPhaseRow({ label, phase, compact: compact2 }) {
104423
+ const icon = phase.status === "running" ? "\u25CF" : phase.status === "passed" ? "\u2713" : "\u2717";
104424
+ const color = phase.status === "running" ? "yellow" : phase.status === "passed" ? "green" : "red";
104425
+ const displayLabel = compact2 ? label.slice(0, 6) : label;
104426
+ return /* @__PURE__ */ jsx_dev_runtime5.jsxDEV(Box_default, {
104427
+ children: /* @__PURE__ */ jsx_dev_runtime5.jsxDEV(Text, {
104428
+ color,
104429
+ children: [
104430
+ icon,
104431
+ " ",
104432
+ displayLabel
104433
+ ]
104434
+ }, undefined, true, undefined, this)
104435
+ }, undefined, false, undefined, this);
104436
+ }
104347
104437
  function PostRunPhaseRow({ label, phase, compact: compact2 }) {
104348
104438
  const icon = phase.status === "running" ? ">" : phase.status === "passed" ? "[OK]" : "[X]";
104349
104439
  const color = phase.status === "running" ? "cyan" : phase.status === "passed" ? "green" : "red";
@@ -104361,110 +104451,120 @@ function PostRunPhaseRow({ label, phase, compact: compact2 }) {
104361
104451
 
104362
104452
  // src/tui/hooks/useAgentStreamEvents.ts
104363
104453
  var import_react31 = __toESM(require_react(), 1);
104454
+ var RENDER_INTERVAL_MS = 150;
104364
104455
  function useAgentStreamEvents(bus) {
104456
+ const activeCallsRef = import_react31.useRef(new Map);
104457
+ const inputTokensRef = import_react31.useRef(0);
104458
+ const outputTokensRef = import_react31.useRef(0);
104459
+ const lastTokensRef = import_react31.useRef(new Map);
104460
+ const dirtyRef = import_react31.useRef(false);
104365
104461
  const [activeCalls, setActiveCalls] = import_react31.useState(new Map);
104366
104462
  const [inputTokens, setInputTokens] = import_react31.useState(0);
104367
104463
  const [outputTokens, setOutputTokens] = import_react31.useState(0);
104368
- const lastTokensRef = import_react31.useRef(new Map);
104369
104464
  import_react31.useEffect(() => {
104370
104465
  if (!bus)
104371
104466
  return;
104372
104467
  const unsubscribe = bus.onAgentStream((event) => {
104373
- setActiveCalls((prev) => {
104374
- const next = new Map(prev);
104375
- switch (event.kind) {
104376
- case "agent.call_started": {
104377
- const now3 = event.timestamp;
104468
+ const next = new Map(activeCallsRef.current);
104469
+ switch (event.kind) {
104470
+ case "agent.call_started": {
104471
+ next.set(event.callId, {
104472
+ callId: event.callId,
104473
+ agentName: event.agentName,
104474
+ storyId: event.storyId,
104475
+ stage: event.stage,
104476
+ startedAt: event.timestamp,
104477
+ lastActivityAt: event.timestamp,
104478
+ messageUpdates: 0,
104479
+ thinkingUpdates: 0,
104480
+ usageUpdates: 0,
104481
+ toolCallUpdates: 0,
104482
+ status: "active",
104483
+ model: event.model
104484
+ });
104485
+ break;
104486
+ }
104487
+ case "agent.message_update": {
104488
+ const state = next.get(event.callId);
104489
+ if (state) {
104378
104490
  next.set(event.callId, {
104379
- callId: event.callId,
104380
- agentName: event.agentName,
104381
- storyId: event.storyId,
104382
- stage: event.stage,
104383
- startedAt: now3,
104384
- lastActivityAt: now3,
104385
- messageUpdates: 0,
104386
- thinkingUpdates: 0,
104387
- usageUpdates: 0,
104388
- toolCallUpdates: 0,
104389
- status: "active",
104390
- model: event.model
104491
+ ...state,
104492
+ messageUpdates: state.messageUpdates + 1,
104493
+ lastActivityAt: event.timestamp
104391
104494
  });
104392
- break;
104393
- }
104394
- case "agent.message_update": {
104395
- const state = next.get(event.callId);
104396
- if (state) {
104397
- next.set(event.callId, {
104398
- ...state,
104399
- messageUpdates: state.messageUpdates + 1,
104400
- lastActivityAt: event.timestamp
104401
- });
104402
- }
104403
- break;
104404
104495
  }
104405
- case "agent.thinking_update": {
104406
- const state = next.get(event.callId);
104407
- if (state) {
104408
- next.set(event.callId, {
104409
- ...state,
104410
- thinkingUpdates: state.thinkingUpdates + 1,
104411
- lastActivityAt: event.timestamp
104412
- });
104413
- }
104414
- break;
104496
+ break;
104497
+ }
104498
+ case "agent.thinking_update": {
104499
+ const state = next.get(event.callId);
104500
+ if (state) {
104501
+ next.set(event.callId, {
104502
+ ...state,
104503
+ thinkingUpdates: state.thinkingUpdates + 1,
104504
+ lastActivityAt: event.timestamp
104505
+ });
104415
104506
  }
104416
- case "agent.usage_update": {
104417
- const state = next.get(event.callId);
104418
- if (state) {
104419
- next.set(event.callId, {
104420
- ...state,
104421
- usageUpdates: state.usageUpdates + 1,
104422
- lastActivityAt: event.timestamp
104423
- });
104424
- }
104425
- {
104426
- const last2 = lastTokensRef.current.get(event.callId) ?? { input: 0, output: 0 };
104427
- const newInput = event.inputTokens ?? last2.input;
104428
- const newOutput = event.outputTokens ?? last2.output;
104429
- const deltaIn = newInput - last2.input;
104430
- const deltaOut = newOutput - last2.output;
104431
- lastTokensRef.current.set(event.callId, { input: newInput, output: newOutput });
104432
- if (deltaIn > 0)
104433
- setInputTokens((prev2) => prev2 + deltaIn);
104434
- if (deltaOut > 0)
104435
- setOutputTokens((prev2) => prev2 + deltaOut);
104436
- }
104437
- break;
104507
+ break;
104508
+ }
104509
+ case "agent.usage_update": {
104510
+ const state = next.get(event.callId);
104511
+ if (state) {
104512
+ next.set(event.callId, {
104513
+ ...state,
104514
+ usageUpdates: state.usageUpdates + 1,
104515
+ lastActivityAt: event.timestamp
104516
+ });
104438
104517
  }
104439
- case "agent.tool_call_update": {
104440
- const state = next.get(event.callId);
104441
- if (state) {
104442
- next.set(event.callId, {
104443
- ...state,
104444
- toolCallUpdates: state.toolCallUpdates + 1,
104445
- lastActivityAt: event.timestamp,
104446
- lastToolName: event.toolName
104447
- });
104448
- }
104449
- break;
104518
+ {
104519
+ const last2 = lastTokensRef.current.get(event.callId) ?? { input: 0, output: 0 };
104520
+ const newInput = event.inputTokens ?? last2.input;
104521
+ const newOutput = event.outputTokens ?? last2.output;
104522
+ const deltaIn = newInput - last2.input;
104523
+ const deltaOut = newOutput - last2.output;
104524
+ lastTokensRef.current.set(event.callId, { input: newInput, output: newOutput });
104525
+ if (deltaIn > 0)
104526
+ inputTokensRef.current += deltaIn;
104527
+ if (deltaOut > 0)
104528
+ outputTokensRef.current += deltaOut;
104450
104529
  }
104451
- case "agent.call_ended": {
104452
- const state = next.get(event.callId);
104453
- if (state) {
104454
- next.set(event.callId, { ...state, status: "ended" });
104455
- next.delete(event.callId);
104456
- }
104457
- lastTokensRef.current.delete(event.callId);
104458
- break;
104530
+ break;
104531
+ }
104532
+ case "agent.tool_call_update": {
104533
+ const state = next.get(event.callId);
104534
+ if (state) {
104535
+ next.set(event.callId, {
104536
+ ...state,
104537
+ toolCallUpdates: state.toolCallUpdates + 1,
104538
+ lastActivityAt: event.timestamp,
104539
+ lastToolName: event.toolName
104540
+ });
104459
104541
  }
104460
- default:
104461
- break;
104542
+ break;
104462
104543
  }
104463
- return next;
104464
- });
104544
+ case "agent.call_ended": {
104545
+ next.delete(event.callId);
104546
+ lastTokensRef.current.delete(event.callId);
104547
+ break;
104548
+ }
104549
+ default:
104550
+ break;
104551
+ }
104552
+ activeCallsRef.current = next;
104553
+ dirtyRef.current = true;
104465
104554
  });
104466
104555
  return unsubscribe;
104467
104556
  }, [bus]);
104557
+ import_react31.useEffect(() => {
104558
+ const interval = setInterval(() => {
104559
+ if (!dirtyRef.current)
104560
+ return;
104561
+ dirtyRef.current = false;
104562
+ setActiveCalls(new Map(activeCallsRef.current));
104563
+ setInputTokens(inputTokensRef.current);
104564
+ setOutputTokens(outputTokensRef.current);
104565
+ }, RENDER_INTERVAL_MS);
104566
+ return () => clearInterval(interval);
104567
+ }, []);
104468
104568
  return { activeCalls, inputTokens, outputTokens };
104469
104569
  }
104470
104570
 
@@ -104660,14 +104760,33 @@ function usePipelineBusEvents(initialStories) {
104660
104760
 
104661
104761
  // src/tui/hooks/usePipelineEvents.ts
104662
104762
  var import_react33 = __toESM(require_react(), 1);
104763
+ var PRE_RUN_STAGES = new Set(["acceptance-setup"]);
104663
104764
  function usePipelineEvents(events) {
104664
104765
  const [currentStage, setCurrentStage] = import_react33.useState(undefined);
104766
+ const [preRunPhases, setPreRunPhases] = import_react33.useState({});
104665
104767
  import_react33.useEffect(() => {
104666
- const onStageEnter = (stage) => setCurrentStage(stage);
104768
+ const onStageEnter = (stage) => {
104769
+ setCurrentStage(stage);
104770
+ if (PRE_RUN_STAGES.has(stage)) {
104771
+ setPreRunPhases((prev) => ({ ...prev, [stage]: { status: "running" } }));
104772
+ }
104773
+ };
104774
+ const onStageExit = (stage, result2) => {
104775
+ if (PRE_RUN_STAGES.has(stage)) {
104776
+ setPreRunPhases((prev) => ({
104777
+ ...prev,
104778
+ [stage]: { status: result2.action === "fail" ? "failed" : "passed" }
104779
+ }));
104780
+ }
104781
+ };
104667
104782
  events.on("stage:enter", onStageEnter);
104668
- return () => events.off("stage:enter", onStageEnter);
104783
+ events.on("stage:exit", onStageExit);
104784
+ return () => {
104785
+ events.off("stage:enter", onStageEnter);
104786
+ events.off("stage:exit", onStageExit);
104787
+ };
104669
104788
  }, [events]);
104670
- return { currentStage };
104789
+ return { currentStage, preRunPhases };
104671
104790
  }
104672
104791
 
104673
104792
  // src/tui/hooks/usePty.ts
@@ -104799,7 +104918,7 @@ function App2({
104799
104918
  }) {
104800
104919
  const layout = useLayout();
104801
104920
  const busState = usePipelineBusEvents(initialStories);
104802
- const { currentStage } = usePipelineEvents(events);
104921
+ const { currentStage, preRunPhases } = usePipelineEvents(events);
104803
104922
  const { exit } = use_app_default();
104804
104923
  const startTimeRef = import_react35.useRef(Date.now());
104805
104924
  const [elapsedMs, setElapsedMs] = import_react35.useState(0);
@@ -104820,6 +104939,8 @@ function App2({
104820
104939
  const runningStories = busState.stories.filter((s) => s.status === "running");
104821
104940
  const isParallel = runningStories.length > 1;
104822
104941
  const currentRunningStory = runningStories[0];
104942
+ const runningPostRunPhase = busState.postRunPhases.acceptance?.status === "running" ? "post-run: acceptance" : busState.postRunPhases.regression?.status === "running" ? "post-run: regression" : busState.postRunPhases.review?.status === "running" ? "post-run: review" : undefined;
104943
+ const currentPhaseLabel = runningPostRunPhase ?? currentStage;
104823
104944
  const runErroredForPanel = busState.runErrored ? "Run encountered an error" : undefined;
104824
104945
  const handleKeyboardAction = async (action) => {
104825
104946
  switch (action.type) {
@@ -104903,7 +105024,11 @@ function App2({
104903
105024
  const isTooSmall = layout.width < MIN_TERMINAL_WIDTH;
104904
105025
  const activeCount = runningStories.length;
104905
105026
  const displayElapsed = busState.runSummary ? busState.runSummary.durationMs : elapsedMs;
104906
- const tokensStr = inputTokens > 0 || outputTokens > 0 ? `${formatTokens(inputTokens)} in / ${formatTokens(outputTokens)} out` : null;
105027
+ const tokenParts = [
105028
+ inputTokens > 0 ? `${formatTokens(inputTokens)} in` : null,
105029
+ outputTokens > 0 ? `${formatTokens(outputTokens)} out` : null
105030
+ ].filter(Boolean);
105031
+ const tokensStr = tokenParts.length > 0 ? tokenParts.join(" / ") : null;
104907
105032
  const headerRight = [
104908
105033
  activeCount > 0 ? `${activeCount} running` : null,
104909
105034
  formatCost3(busState.totalCost),
@@ -104964,6 +105089,7 @@ function App2({
104964
105089
  children: [
104965
105090
  /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(MemoStoriesPanel, {
104966
105091
  stories: busState.stories,
105092
+ preRunPhases,
104967
105093
  postRunPhases: busState.postRunPhases,
104968
105094
  width: layout.mode === "single" ? layout.width : layout.storiesPanelWidth,
104969
105095
  compact: layout.mode === "single",
@@ -104975,7 +105101,8 @@ function App2({
104975
105101
  storySteps: busState.storySteps,
104976
105102
  runSummary: busState.runSummary,
104977
105103
  runErrored: runErroredForPanel,
104978
- escalationLog: busState.escalationLog
105104
+ escalationLog: busState.escalationLog,
105105
+ currentStage: currentPhaseLabel
104979
105106
  }, undefined, false, undefined, this)
104980
105107
  ]
104981
105108
  }, undefined, true, undefined, this),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.68.8",
3
+ "version": "0.69.0",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {