@nanhara/hara 0.111.0 → 0.112.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.
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.0 — /jobs: see (and manage) what's running in the background
9
+
10
+ - **`/jobs` — a user-facing view of the agent's background shell jobs** (dev servers, watchers, long
11
+ builds started via `bash {background:true}`). hara already tracked these for the *agent* (the `job`
12
+ tool), but the *user* had no way to glance and see "what's running back there" — the way codex and
13
+ Claude Code surface it. Now: `/jobs` lists them (id · status · age · command), `/jobs tail <id>`
14
+ shows recent output, `/jobs kill <id>` stops one. Works in the TUI and the readline REPL.
15
+ - **Status row shows a `⚙ N bg` indicator** when background jobs are running, so a preview server /
16
+ watcher humming along is visible at a glance (live while working; `/jobs` is the on-demand truth).
17
+
8
18
  ## 0.111.0 — an interactive /model picker: ↑↓ a model, ←→ its thinking
9
19
 
10
20
  - **`/model` (no argument) now opens an interactive picker** built on the provider registry — the
package/dist/index.js CHANGED
@@ -44,6 +44,21 @@ import { createAnthropicProvider } from "./providers/anthropic.js";
44
44
  import { createOpenAIProvider } from "./providers/openai.js";
45
45
  import { resolvePlatform } from "./providers/registry.js";
46
46
  import { listModels } from "./providers/models.js";
47
+ import { listJobs, tailJob, killJob } from "./exec/jobs.js";
48
+ /** Render the background-job list for /jobs (user-facing view of what the agent has running in the
49
+ * background — dev servers, watchers, long tasks). Mirrors codex/Claude-Code process visibility. */
50
+ function renderBgJobs() {
51
+ const js = listJobs();
52
+ if (!js.length)
53
+ return "(no background jobs — the agent starts them with bash {background:true})";
54
+ const age = (ms) => (ms < 60_000 ? `${Math.round(ms / 1000)}s` : `${Math.round(ms / 60_000)}m`);
55
+ const rows = js.map((j) => {
56
+ const st = j.status === "running" ? "▶ running" : j.status === "killed" ? "✕ killed" : `● exited${j.code != null ? ` (${j.code})` : ""}`;
57
+ const cmd = j.command.length > 64 ? j.command.slice(0, 64) + "…" : j.command;
58
+ return ` ${j.id} ${st} ${age(j.ageMs).padStart(4)} ${cmd}`;
59
+ });
60
+ return `Background jobs — /jobs tail <id> · /jobs kill <id>:\n${rows.join("\n")}`;
61
+ }
47
62
  import { qwenDeviceLogin, getValidQwenAuth } from "./providers/qwen-oauth.js";
48
63
  import { loadAgentsMd, hasAgentsMd, INIT_PROMPT, findProjectRoot } from "./context/agents-md.js";
49
64
  import { getEmbedder } from "./search/embed.js";
@@ -2517,6 +2532,14 @@ program.action(async (opts) => {
2517
2532
  },
2518
2533
  },
2519
2534
  { name: "usage", desc: "show token usage this session", run: () => void out(statusLine(cfg.model, stats.input, stats.output) + "\n") },
2535
+ { name: "jobs", desc: "list/tail/kill background shell jobs (dev servers, watchers)", run: (a) => {
2536
+ const [sub, jid] = (a || "").trim().split(/\s+/);
2537
+ if (sub === "kill" && jid)
2538
+ return void out((killJob(jid) ? `✕ killed ${jid}` : `no running job ${jid}`) + "\n");
2539
+ if (sub === "tail" && jid)
2540
+ return void out((tailJob(jid) ?? `no job ${jid}`) + "\n");
2541
+ out(renderBgJobs() + "\n");
2542
+ } },
2520
2543
  { name: "doctor", desc: "check your hara setup", run: () => void out(runDoctor(cfg) + "\n") },
2521
2544
  {
2522
2545
  name: "roles",
@@ -2990,6 +3013,14 @@ program.action(async (opts) => {
2990
3013
  }
2991
3014
  if (nm === "usage")
2992
3015
  return void h.sink.notice(`tokens — ↑${stats.input} ↓${stats.output}`);
3016
+ if (nm === "jobs") {
3017
+ const [sub, jid] = (arg || "").trim().split(/\s+/);
3018
+ if (sub === "kill" && jid)
3019
+ return void h.sink.notice(killJob(jid) ? `✕ killed ${jid}` : `no running job ${jid}`);
3020
+ if (sub === "tail" && jid)
3021
+ return void h.sink.notice(tailJob(jid) ?? `no job ${jid}`);
3022
+ return void h.sink.notice(renderBgJobs());
3023
+ }
2993
3024
  if (nm === "doctor")
2994
3025
  return void h.sink.notice(runDoctor(cfg).replace(/\[[0-9;]*m/g, ""));
2995
3026
  if (nm === "vision")
package/dist/tui/App.js CHANGED
@@ -18,6 +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
22
  import { ModelPicker } from "./model-picker.js";
22
23
  let _id = 0;
23
24
  const nid = () => ++_id;
@@ -268,14 +269,19 @@ function StatusRow({ working, todos, queued }) {
268
269
  const id = setInterval(() => setFrame((x) => x + 1), SPINNER_FRAME_MS);
269
270
  return () => clearInterval(id);
270
271
  }, [working]);
272
+ // Background-job indicator — so the user can SEE what's running in the background (a preview server, a
273
+ // watcher) without asking. Live while working (spinner ticks re-render); best-effort at idle (/jobs is
274
+ // the authoritative on-demand view). Re-read each render.
275
+ const bg = listJobs().filter((j) => j.status === "running").length;
276
+ const bgTag = bg ? ` · ⚙ ${bg} bg (/jobs)` : "";
271
277
  if (!working) {
272
- return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: ` ${IDLE_HINTS}` }) }));
278
+ return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: ` ${IDLE_HINTS}${bgTag}` }) }));
273
279
  }
274
280
  const frames = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
275
281
  const elapsedSec = Math.floor((Date.now() - startRef.current) / 1000);
276
282
  // Pre-first-token honesty (codex-parity): "waiting for the model" reads very differently from a
277
283
  // generic "working" when the network is slow — the user knows the request is out, not dead.
278
- const verb = phase === "waiting" ? `waiting for the model… ${elapsedSec}s · esc to interrupt` : spinnerVerb(todos, elapsedSec);
284
+ const verb = (phase === "waiting" ? `waiting for the model… ${elapsedSec}s · esc to interrupt` : spinnerVerb(todos, elapsedSec)) + bgTag;
279
285
  return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "yellow", children: frames[frame % frames.length] }), _jsx(Text, { dimColor: true, children: ` ${verb} · ⏎ queues${queued ? ` (${queued})` : ""}` })] }));
280
286
  }
281
287
  // Short per-mode descriptions for the ONE-ROW mode line (the old two-row ModeBar's long sentences
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanhara/hara",
3
- "version": "0.111.0",
3
+ "version": "0.112.0",
4
4
  "description": "hara — a coding agent CLI that runs like an engineering org.",
5
5
  "bin": {
6
6
  "hara": "dist/index.js"