@nanhara/hara 0.112.0 → 0.112.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/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ All notable changes to `@nanhara/hara`.
5
5
  > Versioning (pre-1.0, SemVer-style): the **minor** (middle) number bumps for a **new feature**; the
6
6
  > **patch** (last) number bumps for **optimizations/fixes of existing features**.
7
7
 
8
+ ## 0.112.1 — the background-job indicator is now LIVE (even at idle)
9
+
10
+ - **`⚙ N bg running` updates in real time — including when hara is idle.** In 0.112.0 the indicator only
11
+ refreshed while a turn was running, so once the agent finished but a background task (a preview server,
12
+ a watcher, a long render) was still going, the idle prompt looked exactly like "nothing running" — and
13
+ the user reads that as *"why did it just stop?"*. Now `jobs.ts` emits on every start / self-exit / kill
14
+ and the status row subscribes, so the indicator appears the moment a task starts, updates as tasks
15
+ finish on their own, and clears when the last one ends — event-driven, no polling. (`/jobs` remains the
16
+ on-demand detailed view.)
17
+
8
18
  ## 0.112.0 — /jobs: see (and manage) what's running in the background
9
19
 
10
20
  - **`/jobs` — a user-facing view of the agent's background shell jobs** (dev servers, watchers, long
package/dist/exec/jobs.js CHANGED
@@ -7,6 +7,26 @@ import { shellCommand } from "../sandbox.js";
7
7
  const MAX_BUF = 64 * 1024; // retain only the tail of each job's combined output
8
8
  const jobs = new Map();
9
9
  let seq = 0;
10
+ const jobsListeners = new Set();
11
+ /** Subscribe to job start/exit/kill. Lets the UI show "a background task is running" LIVE — crucially at
12
+ * IDLE too: a turn ending doesn't mean the background work did, and without this the user reads the idle
13
+ * prompt as "it stopped". Returns an unsubscribe. */
14
+ export function onJobsChange(fn) {
15
+ jobsListeners.add(fn);
16
+ return () => {
17
+ jobsListeners.delete(fn);
18
+ };
19
+ }
20
+ function emitJobsChange() {
21
+ for (const fn of jobsListeners) {
22
+ try {
23
+ fn();
24
+ }
25
+ catch {
26
+ /* a listener must never break job bookkeeping */
27
+ }
28
+ }
29
+ }
10
30
  let hooked = false;
11
31
  function ensureExitCleanup() {
12
32
  if (hooked)
@@ -29,6 +49,7 @@ export function startJob(command, cwd, mode) {
29
49
  if (job.status === "running") {
30
50
  job.status = "exited";
31
51
  job.code = code;
52
+ emitJobsChange(); // a job finishing ON ITS OWN (no user action) must update the UI — this is the idle case
32
53
  }
33
54
  });
34
55
  child.on("error", (e) => {
@@ -36,9 +57,11 @@ export function startJob(command, cwd, mode) {
36
57
  job.status = "exited";
37
58
  job.code = -1;
38
59
  job.buf = (job.buf + `\n[spawn error] ${e.message}`).slice(-MAX_BUF);
60
+ emitJobsChange();
39
61
  }
40
62
  });
41
63
  jobs.set(job.id, job);
64
+ emitJobsChange();
42
65
  return job.id;
43
66
  }
44
67
  export function listJobs() {
@@ -64,6 +87,7 @@ export function killJob(id) {
64
87
  /* already gone */
65
88
  }
66
89
  j.status = "killed";
90
+ emitJobsChange();
67
91
  return true;
68
92
  }
69
93
  /** Terminate every running job — registered on process exit so dev servers don't outlive hara. */
package/dist/tui/App.js CHANGED
@@ -18,7 +18,7 @@ import { accent } from "./theme.js";
18
18
  import { renderMarkdown } from "../md.js";
19
19
  import { clearTodos, currentTodos, onTodosChange } from "../tools/todo.js";
20
20
  import { onTurnPhase, turnPhase } from "../agent/phase.js";
21
- import { listJobs } from "../exec/jobs.js";
21
+ import { listJobs, onJobsChange } from "../exec/jobs.js";
22
22
  import { ModelPicker } from "./model-picker.js";
23
23
  let _id = 0;
24
24
  const nid = () => ++_id;
@@ -273,7 +273,7 @@ function StatusRow({ working, todos, queued }) {
273
273
  // watcher) without asking. Live while working (spinner ticks re-render); best-effort at idle (/jobs is
274
274
  // the authoritative on-demand view). Re-read each render.
275
275
  const bg = listJobs().filter((j) => j.status === "running").length;
276
- const bgTag = bg ? ` · ⚙ ${bg} bg (/jobs)` : "";
276
+ const bgTag = bg ? ` · ⚙ ${bg} bg running (/jobs)` : "";
277
277
  if (!working) {
278
278
  return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: ` ${IDLE_HINTS}${bgTag}` }) }));
279
279
  }
@@ -389,6 +389,12 @@ export function App({ initialStatus, model, cwd, header, onSubmit, cycleApproval
389
389
  });
390
390
  return unsub;
391
391
  }, []);
392
+ // Subscribe to background-job start/exit/kill so the `⚙ N bg` indicator is LIVE — crucially at idle.
393
+ // A job finishing on its own, or a preview server still running after a turn ends, now refreshes the
394
+ // status row; without it the idle prompt reads as "it stopped". Bumping a nonce forces the re-render;
395
+ // StatusRow re-reads listJobs().
396
+ const [, setJobsTick] = useState(0);
397
+ useEffect(() => onJobsChange(() => setJobsTick((n) => n + 1)), []);
392
398
  // Reconcile the synchronously-mutated live buffer into React state, at most once per ~33ms. First
393
399
  // append any finalized blocks to <Static> (once, in order), then publish the remaining live tail.
394
400
  // The array identities are fresh each flush so React/ink re-render exactly the minimal live region.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanhara/hara",
3
- "version": "0.112.0",
3
+ "version": "0.112.1",
4
4
  "description": "hara — a coding agent CLI that runs like an engineering org.",
5
5
  "bin": {
6
6
  "hara": "dist/index.js"