@datalabrotterdam/nova-ai-cli 1.2.0-alpha.15 → 1.2.0-alpha.17

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
@@ -1,3 +1,17 @@
1
+ # [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.17](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.16...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.17) (2026-10-25)
2
+
3
+
4
+ ### Features
5
+
6
+ * **cli:** /tasks to monitor and stop background tasks; drop Ctrl+B ([aaa3cb9](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/aaa3cb9dc058ab9a726964414a959084c9237669))
7
+
8
+ # [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.16](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.15...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.16) (2026-10-25)
9
+
10
+
11
+ ### Features
12
+
13
+ * **cli:** Ctrl+C clears the prompt; Esc asks about running background tasks ([a7cb854](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/a7cb854f4494d88851b672af913d7a1dee666684))
14
+
1
15
  # [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.15](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.14...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.15) (2026-10-25)
2
16
 
3
17
 
package/README.md CHANGED
@@ -75,9 +75,9 @@ The UI runs full screen (the terminal's alternate screen, like `less` or
75
75
  `vim`) and gives your shell its screen back on exit. The mouse wheel or
76
76
  PgUp/PgDn scroll the transcript, Esc jumps back to the newest output, and
77
77
  while you read an answer the prompt that asked for it stays pinned on the top
78
- row. To select text, hold Shift (Option in some macOS terminals) while dragging,
79
- or press Ctrl+S to turn the mouse over to your terminal (select mode; Ctrl+S
80
- again gives the wheel back). `/copy` copies the last answer.
78
+ row. Drag over text to select it, then click (or press Enter) to copy it;
79
+ Shift+arrows extend the selection and Esc cancels. `/copy` copies the last
80
+ answer.
81
81
 
82
82
  - **Modes** (Shift+Tab): `agent` does the work; `plan` reads the code, asks
83
83
  questions and builds a task list but changes nothing; `ask` only answers;
@@ -99,17 +99,16 @@ again gives the wheel back). `/copy` copies the last answer.
99
99
  | Key | Action |
100
100
  |---|---|
101
101
  | Enter / Shift+Enter | Send / new line (Shift+Enter where the terminal supports the kitty keyboard protocol: VS Code, kitty, WezTerm, Ghostty, foot, Alacritty, recent iTerm2; Alt+Enter, Ctrl+J or a trailing `\` work everywhere) |
102
- | Esc | Cancel the running turn |
103
- | Ctrl+C twice | Exit; prints `nova-ai --resume <id>` |
102
+ | Esc | Interrupt the running turn (asks whether running background tasks keep going) |
103
+ | Ctrl+C | Clear the prompt; on an empty prompt, twice to exit (prints `nova-ai --resume <id>`) |
104
104
  | Shift+Tab | Cycle agent / ask / plan / autopilot and your own agents |
105
105
  | Ctrl+R / Ctrl+P / Ctrl+K | Sessions / models / permission mode |
106
- | Ctrl+O / Ctrl+T / Ctrl+B | Thinking and verbose tool output / tool inspector / background jobs |
107
- | Ctrl+S | Select mode: the mouse selects text instead of scrolling (again to switch back) |
106
+ | Ctrl+O / Ctrl+T | Thinking and verbose tool output / tool inspector |
108
107
  | Alt+V | Paste a clipboard image |
109
108
 
110
109
  Slash commands: `/help`, `/model`, `/effort` (thinking models), `/mode`, `/permission`, `/session`,
111
110
  `/resume`, `/clear`, `/compact`, `/copy`, `/rewind [n]`, `/queue`, `/steer`, `/usage` (context, and the session's tokens and estimated cost; the status line shows `≈ €0.0123` once a priced model answered),
112
- `/tools`, `/mcp`, `/skills`, `/shell` and `/agent` (background jobs), `/trust`
111
+ `/tools`, `/tasks` (watch or stop background shells and agents), `/mcp`, `/skills`, `/shell` and `/agent` (background jobs), `/trust`
113
112
  and `/exit`. Your own commands are markdown files in `~/.nova-ai/commands/` or
114
113
  the repository's `.nova-ai/commands/` (Claude Code's format, so
115
114
  `.claude/commands` works too): the file is the prompt, `$ARGUMENTS` what you
package/dist/index.js CHANGED
@@ -185,7 +185,7 @@ ${MAIN_HELP}
185
185
  );
186
186
  return 2;
187
187
  }
188
- const { default: runChat } = await import("./tui-W6ZQ2SWR.js");
188
+ const { default: runChat } = await import("./tui-VP2V5DOF.js");
189
189
  return runChat(options);
190
190
  }
191
191
  }
@@ -2175,6 +2175,13 @@ var builtinCommands = [
2175
2175
  ctx.openToolInspector();
2176
2176
  }
2177
2177
  },
2178
+ {
2179
+ name: "tasks",
2180
+ description: "Monitor background shells and agents, and stop one",
2181
+ run(ctx) {
2182
+ return ctx.openTasks();
2183
+ }
2184
+ },
2178
2185
  {
2179
2186
  name: "mcp",
2180
2187
  description: "Inspect MCP servers, or sign in to one: /mcp login|logout <server>",
@@ -4179,7 +4186,7 @@ var TuiController = class {
4179
4186
  if (key.escape) {
4180
4187
  if (this.editor.autocomplete) return false;
4181
4188
  if (this.store.getState().busy) {
4182
- this.runner.cancel();
4189
+ void this.interrupt();
4183
4190
  return true;
4184
4191
  }
4185
4192
  if (this.scroll > 0) {
@@ -4212,9 +4219,6 @@ var TuiController = class {
4212
4219
  case "k":
4213
4220
  this.openPermissionModes();
4214
4221
  return true;
4215
- case "b":
4216
- void this.openBackgroundTasks();
4217
- return true;
4218
4222
  case "o":
4219
4223
  this.toggleToolDetails();
4220
4224
  return true;
@@ -4225,7 +4229,12 @@ var TuiController = class {
4225
4229
  return false;
4226
4230
  }
4227
4231
  }
4232
+ /** Ctrl+C clears a typed prompt first; on an empty one it arms exit. */
4228
4233
  pressCtrlC() {
4234
+ if (this.editor.getText()) {
4235
+ this.editor.setText("");
4236
+ return;
4237
+ }
4229
4238
  if (this.exitArmed) {
4230
4239
  this.requestExit();
4231
4240
  return;
@@ -4369,36 +4378,91 @@ var TuiController = class {
4369
4378
  (value) => this.setPermissionMode(value)
4370
4379
  );
4371
4380
  };
4372
- openBackgroundTasks = async () => {
4381
+ /** Esc during a turn: stop it, then ask whether background tasks it started keep running. */
4382
+ async interrupt() {
4383
+ this.runner.cancel();
4384
+ const running = (await this.runner.listBackgroundJobs().catch(() => [])).filter(
4385
+ (job) => job.status === "running"
4386
+ );
4387
+ if (running.length === 0) return;
4388
+ const titles = running.map((job) => job.title).join(", ");
4389
+ this.showSelection(
4390
+ `Interrupted. ${running.length === 1 ? "1 background task is" : `${running.length} background tasks are`} still running.`,
4391
+ [
4392
+ { value: "keep", label: "Keep running", description: titles },
4393
+ { value: "stop", label: running.length === 1 ? "Stop it" : "Stop them", description: titles }
4394
+ ],
4395
+ "background-tasks",
4396
+ (value) => {
4397
+ if (value !== "stop") return;
4398
+ void Promise.allSettled(running.map((job) => this.runner.killBackgroundJob(job.jobId))).then(
4399
+ (results) => {
4400
+ const stopped = results.filter((result) => result.status === "fulfilled").length;
4401
+ this.store.setState({ statusLine: `Stopped ${stopped} of ${running.length} background tasks.` });
4402
+ }
4403
+ );
4404
+ }
4405
+ );
4406
+ }
4407
+ /** /tasks: background shells and agents; pick one to read its output or stop it. */
4408
+ openTasks = async () => {
4409
+ let jobs;
4373
4410
  try {
4374
- const jobs = await this.runner.listBackgroundJobs();
4375
- this.showSelection(
4376
- "Background shells and agents",
4377
- jobs.map((job) => ({
4378
- value: job.jobId,
4379
- label: `${job.kind}: ${job.title}`,
4380
- description: job.status
4381
- })),
4382
- "background-tasks",
4383
- (jobId) => {
4384
- void this.runner.backgroundOutput(jobId).then(
4385
- (result) => this.showText(
4386
- `${result.job.kind}: ${result.job.title}`,
4387
- `jobId: ${jobId}
4388
- status: ${result.job.status}
4411
+ jobs = await this.runner.listBackgroundJobs();
4412
+ } catch (error) {
4413
+ this.showText("Background tasks", errorMessage(error, "Failed to list background tasks."));
4414
+ return;
4415
+ }
4416
+ if (jobs.length === 0) {
4417
+ this.showText("Background tasks", "No background shells or agents in this session.");
4418
+ return;
4419
+ }
4420
+ this.showSelection(
4421
+ "Background tasks",
4422
+ jobs.map((job) => ({ value: job.jobId, label: taskLabel(job), description: job.status })),
4423
+ "background-tasks",
4424
+ (jobId) => {
4425
+ const job = jobs.find((candidate) => candidate.jobId === jobId);
4426
+ if (job.status !== "running") {
4427
+ void this.showTaskOutput(jobId);
4428
+ return;
4429
+ }
4430
+ this.showSelection(
4431
+ taskLabel(job),
4432
+ [
4433
+ { value: "output", label: "View output", description: "What it printed so far" },
4434
+ { value: "stop", label: "Stop", description: "Kill this task" }
4435
+ ],
4436
+ "background-tasks",
4437
+ (action) => void (action === "stop" ? this.stopTask(job) : this.showTaskOutput(jobId))
4438
+ );
4439
+ }
4440
+ );
4441
+ };
4442
+ async showTaskOutput(jobId) {
4443
+ try {
4444
+ const result = await this.runner.backgroundOutput(jobId);
4445
+ this.showText(
4446
+ taskLabel(result.job),
4447
+ `status: ${result.job.status}
4389
4448
  outputPath: ${result.outputPath ?? result.job.outputPath ?? ""}
4390
4449
 
4391
4450
  ${result.output}${result.truncated ? "\n\n[output truncated]" : ""}`
4392
- )
4393
- ).catch(
4394
- (error) => this.showText("Background job", errorMessage(error, "Failed to read output."))
4395
- );
4396
- }
4397
4451
  );
4398
4452
  } catch (error) {
4399
- this.showText("Background jobs", errorMessage(error, "Failed to list background jobs."));
4453
+ this.showText("Background task", errorMessage(error, "Failed to read output."));
4400
4454
  }
4401
- };
4455
+ }
4456
+ /** Stops one task, then shows the list again. */
4457
+ async stopTask(job) {
4458
+ try {
4459
+ await this.runner.killBackgroundJob(job.jobId);
4460
+ this.store.setState({ statusLine: `Stopped ${taskLabel(job)}.` });
4461
+ } catch (error) {
4462
+ this.store.setState({ statusLine: errorMessage(error, `Failed to stop ${taskLabel(job)}.`) });
4463
+ }
4464
+ await this.openTasks();
4465
+ }
4402
4466
  openToolInspector = () => {
4403
4467
  if (this.toolCalls().length === 0) {
4404
4468
  this.showText("Tool calls", "No tool calls yet.");
@@ -4654,6 +4718,7 @@ ${skill.path}`
4654
4718
  return "Workspace trusted: its allow rules apply now; MCP servers it declares start with the next session (/clear).";
4655
4719
  },
4656
4720
  openToolInspector: this.openToolInspector,
4721
+ openTasks: this.openTasks,
4657
4722
  openMcpInspector: this.openMcpInspector,
4658
4723
  openSkillInspector: this.openSkillInspector,
4659
4724
  switchSkill: this.switchSkill,
@@ -4861,6 +4926,9 @@ function mcpCounts(runner) {
4861
4926
  function errorMessage(error, fallback) {
4862
4927
  return error instanceof Error ? error.message : fallback;
4863
4928
  }
4929
+ function taskLabel(job) {
4930
+ return `${job.kind === "terminal" ? "shell" : "agent"}: ${job.title}`;
4931
+ }
4864
4932
 
4865
4933
  // src/tui/ink/dialogs/views.tsx
4866
4934
  import { Box as Box5, Text as Text5, useInput as useInput2, usePaste as usePaste2 } from "ink";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalabrotterdam/nova-ai-cli",
3
- "version": "1.2.0-alpha.15",
3
+ "version": "1.2.0-alpha.17",
4
4
  "description": "Agentic coding with DataLab Rotterdam's Nova AI in your terminal: interactive TUI, headless mode, and an ACP agent for editors",
5
5
  "license": "Apache-2.0",
6
6
  "author": "DataLab Rotterdam",