@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 +10 -0
- package/dist/index.js +31 -0
- package/dist/tui/App.js +8 -2
- package/package.json +1 -1
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
|