@nanhara/hara 0.112.0 → 0.112.2

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,26 @@ 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.2 — moving/resizing the window no longer garbles the UI
9
+
10
+ - **Terminal resize no longer stacks the status row + input box into a garble.** ink 6.8's resize
11
+ handler only clears the screen when the terminal gets *narrower*; on a *widen* (or a resize it doesn't
12
+ classify as narrowing) it just re-renders, so the old frame — reflowed at the new width — is never
13
+ erased, and the ~125ms spinner tick stacks a fresh copy each time (the "I moved the window and the UI
14
+ filled up with repeated `waiting for the model…` lines"). hara now hooks the resize event and, on ANY
15
+ resize, resets ink's tracked output (debounced across a drag's burst of events) so the next render
16
+ starts clean and subsequent ticks erase correctly. Not "just the terminal" — a real repaint fix.
17
+
18
+ ## 0.112.1 — the background-job indicator is now LIVE (even at idle)
19
+
20
+ - **`⚙ N bg running` updates in real time — including when hara is idle.** In 0.112.0 the indicator only
21
+ refreshed while a turn was running, so once the agent finished but a background task (a preview server,
22
+ a watcher, a long render) was still going, the idle prompt looked exactly like "nothing running" — and
23
+ the user reads that as *"why did it just stop?"*. Now `jobs.ts` emits on every start / self-exit / kill
24
+ and the status row subscribes, so the indicator appears the moment a task starts, updates as tasks
25
+ finish on their own, and clears when the last one ends — event-driven, no polling. (`/jobs` remains the
26
+ on-demand detailed view.)
27
+
8
28
  ## 0.112.0 — /jobs: see (and manage) what's running in the background
9
29
 
10
30
  - **`/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/dist/tui/run.js CHANGED
@@ -6,7 +6,35 @@ import { createElement } from "react";
6
6
  import { App } from "./App.js";
7
7
  export async function runTui(props) {
8
8
  const instance = render(createElement(App, props));
9
- await instance.waitUntilExit();
9
+ // Resize repaint fix. ink 6.8's own resize handler only clears the screen when the terminal gets
10
+ // NARROWER; on a WIDEN (or a resize it doesn't classify as narrowing) it just re-renders, so the old
11
+ // frame — reflowed at the new width — is never erased, and the ~125ms spinner tick stacks a fresh copy
12
+ // each time (the "moved the window and the UI stacked up" garble). We complement it: on ANY resize,
13
+ // clear ink's tracked output so the next render starts clean. Debounced by a microtask so a burst of
14
+ // resize events during a window drag collapses to one clear.
15
+ const out = process.stdout;
16
+ let pending = false;
17
+ const onResize = () => {
18
+ if (pending)
19
+ return;
20
+ pending = true;
21
+ queueMicrotask(() => {
22
+ pending = false;
23
+ try {
24
+ instance.clear();
25
+ }
26
+ catch {
27
+ /* best-effort — never let a repaint fix crash the session */
28
+ }
29
+ });
30
+ };
31
+ out.on("resize", onResize);
32
+ try {
33
+ await instance.waitUntilExit();
34
+ }
35
+ finally {
36
+ out.off("resize", onResize);
37
+ }
10
38
  }
11
39
  // A tiny ink yes/no prompt for pre-TUI confirms (e.g. the first-run "create AGENTS.md?" offer).
12
40
  // MUST be ink, NOT readline: a readline question before the main TUI leaves stdin in a state ink
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanhara/hara",
3
- "version": "0.112.0",
3
+ "version": "0.112.2",
4
4
  "description": "hara — a coding agent CLI that runs like an engineering org.",
5
5
  "bin": {
6
6
  "hara": "dist/index.js"