@asathinkeroops/nova-code 0.1.0-beta.5 → 0.1.0-beta.8

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.
Files changed (3) hide show
  1. package/README.md +10 -4
  2. package/dist/index.js +485 -235
  3. package/package.json +9 -8
package/dist/index.js CHANGED
@@ -1220,8 +1220,10 @@ var DEFAULT_SANDBOX_ALLOW_WRITE = [
1220
1220
  // pnpm store/bin (Linux)
1221
1221
  "~/Library/pnpm",
1222
1222
  // pnpm store/bin (macOS)
1223
- "~/.yarn"
1223
+ "~/.yarn",
1224
1224
  // yarn global
1225
+ "~/.config/gh"
1226
+ // gh CLI auth/token refresh (git/PR workflow)
1225
1227
  ];
1226
1228
  var modelPricingSchema = z2.object({
1227
1229
  input: z2.number().nonnegative().describe("Price per 1M uncached (cache-miss) input tokens."),
@@ -1393,6 +1395,20 @@ var settingsSchema = z2.object({
1393
1395
  additionalDirectories: [],
1394
1396
  autoMode: { llmClassifier: true, classifierTimeoutMs: 8e3 }
1395
1397
  }),
1398
+ // Workspace trust. On startup nova checks whether the workspace it was
1399
+ // launched in has been granted file access; an untrusted workspace prompts
1400
+ // the user to confirm, and declining exits. Trust is recorded HERE (in the
1401
+ // user-global config, which a project cannot write) rather than in any
1402
+ // project-checked-in file — mirroring Claude Code's `~/.claude.json` model,
1403
+ // so a cloned repo can never mark itself trusted. `trustedRoots` are absolute
1404
+ // canonical paths; a workspace equal to or nested under any of them is
1405
+ // trusted (so trusting a repo root covers its subdirectories). Users may
1406
+ // pre-seed roots by hand. Set `enabled: false` to restore the legacy
1407
+ // "workspace is always trusted on launch" behavior.
1408
+ trust: z2.object({
1409
+ enabled: z2.boolean().default(true),
1410
+ trustedRoots: z2.array(z2.string().min(1)).default([])
1411
+ }).default({ enabled: true, trustedRoots: [] }),
1396
1412
  transcript: z2.object({
1397
1413
  enabled: z2.boolean().default(true)
1398
1414
  }).default({ enabled: true }),
@@ -2415,7 +2431,7 @@ function buildSystemPrompt(workspace, memory, sessionId, skillsBlock = "", langu
2415
2431
  - Run long-lived commands (dev servers, watchers, builds) with runInBackground; it returns immediately and its output is delivered to you automatically when the command finishes \u2014 no need to poll.
2416
2432
  - Load specialized knowledge with loadSkill.
2417
2433
  - Delegate focused subtasks to parallel sub-agents with createSubAgent (type: explore = read-only retrieval, plan = read-only planning, general-purpose = full tools).
2418
- - Don't guess file paths. When unsure whether a file exists or where it lives, locate it with glob/grep before read \u2014 a read on a wrong path just wastes a turn.
2434
+ - Don't guess file paths. When unsure whether a file or directory exists, locate it with glob/grep/ls before acting \u2014 a read, write, edit, or mkdir on a wrong path just wastes a turn or causes damage.
2419
2435
  - Respond in ${language} by default, even when a tool or sub-agent returns content in another language \u2014 relay and summarize it in ${language}. Preserve that exact script and regional variant; do not switch it.
2420
2436
 
2421
2437
  When I ask you to commit, follow the repo's own version-control conventions:
@@ -2424,6 +2440,11 @@ When I ask you to commit, follow the repo's own version-control conventions:
2424
2440
  - Default to Conventional Commits (\`type(scope): summary\` \u2014 imperative subject \u226472 chars, a short body explaining the *why* when the change is non-trivial), but defer to the repo's own convention where it differs.
2425
2441
  - Never push, and never add co-author or tool-attribution trailers, unless I ask.
2426
2442
 
2443
+ When I ask you to open a pull request, use the \`gh\` CLI (it is already authenticated \u2014 don't build a GitHub API client):
2444
+ - Gather context first: \`git status\` / \`git diff\`, \`git log <base>..HEAD\` for the commits, and the default branch (\`gh repo view --json defaultBranchRef\`) for the PR base.
2445
+ - Push the branch if it has no upstream (never straight to main/master), then run \`gh pr create\`. Pass the body via a quoted HEREDOC so its markdown survives: \`gh pr create --title "\u2026" --body "$(cat <<'EOF' \u2026 EOF)"\`.
2446
+ - Structure the body as \`## Summary\` (1\u20133 bullets on what changed and why) and \`## Test plan\` (a checklist of how to verify) \u2014 keep it to what the diff actually supports.
2447
+
2427
2448
  Act, don't explain.
2428
2449
 
2429
2450
  <identity name="Nova"></identity>
@@ -5291,13 +5312,15 @@ ${xmlEscape(payload)}
5291
5312
 
5292
5313
  // ../../packages/tools/src/builtin/read.ts
5293
5314
  import { readFile as readFile10 } from "fs/promises";
5294
- import { extname as extname2, resolve as resolve9 } from "path";
5315
+ import { basename as basename3, extname as extname2, resolve as resolve9 } from "path";
5295
5316
  import { z as z13 } from "zod";
5296
5317
  import * as XLSX from "xlsx";
5318
+ import { extractText as extractText2 } from "unpdf";
5297
5319
  var MAX_CHARS = 2e5;
5298
5320
  var MAX_LINE_CHARS = 16e3;
5299
5321
  var LINE_NO_WIDTH = 6;
5300
5322
  var EXCEL_EXTENSIONS = /* @__PURE__ */ new Set([".xlsx", ".xls", ".xlsm", ".xlsb", ".ods"]);
5323
+ var MAX_PDF_BYTES = 3e7;
5301
5324
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
5302
5325
  var MAX_IMAGE_BYTES = 2e7;
5303
5326
  var IMAGE_MAGIC = {
@@ -5345,6 +5368,28 @@ function renderLines(lines, total, startIdx, endIdx, path3) {
5345
5368
  }
5346
5369
  return body;
5347
5370
  }
5371
+ function paginateLines(raw, input, path3) {
5372
+ const lines = raw.match(/[^\n]*\n|[^\n]+$/g) ?? [];
5373
+ const total = lines.length;
5374
+ const startLine = input.offset ?? 1;
5375
+ const startIdx = startLine - 1;
5376
+ if (startIdx >= total && total > 0) {
5377
+ return {
5378
+ output: `read: offset ${startLine} is past end of file (it has ${total} lines)`,
5379
+ isError: true
5380
+ };
5381
+ }
5382
+ let endIdx = startIdx;
5383
+ let chars = 0;
5384
+ while (endIdx < total) {
5385
+ if (input.limit !== void 0 && endIdx - startIdx >= input.limit) break;
5386
+ const cost = Math.min(lines[endIdx].length, MAX_LINE_CHARS);
5387
+ if (chars > 0 && chars + cost > MAX_CHARS) break;
5388
+ chars += cost;
5389
+ endIdx += 1;
5390
+ }
5391
+ return { output: renderLines(lines, total, startIdx, endIdx, path3) };
5392
+ }
5348
5393
  async function readText(abs, input, path3) {
5349
5394
  let raw;
5350
5395
  try {
@@ -5372,26 +5417,60 @@ async function readText(abs, input, path3) {
5372
5417
  isError: true
5373
5418
  };
5374
5419
  }
5375
- const lines = raw.match(/[^\n]*\n|[^\n]+$/g) ?? [];
5376
- const total = lines.length;
5377
- const startLine = input.offset ?? 1;
5378
- const startIdx = startLine - 1;
5379
- if (startIdx >= total && total > 0) {
5420
+ return paginateLines(raw, input, path3);
5421
+ }
5422
+ async function readPdf(abs, input, path3) {
5423
+ let buf;
5424
+ try {
5425
+ buf = await readFile10(abs);
5426
+ } catch (err) {
5427
+ const code = err.code;
5428
+ if (code === "ENOENT") {
5429
+ return {
5430
+ output: `read failed: no such file: ${path3}. It may not exist at this path (or anywhere) \u2014 use glob/grep to locate it rather than guessing another path.`,
5431
+ isError: true
5432
+ };
5433
+ }
5434
+ if (code === "EISDIR") {
5435
+ return {
5436
+ output: `read failed: ${path3} is a directory, not a file. Use glob to list its contents or grep to search inside it.`,
5437
+ isError: true
5438
+ };
5439
+ }
5440
+ const msg = err instanceof Error ? err.message : String(err);
5441
+ return { output: `read failed: ${msg}`, isError: true };
5442
+ }
5443
+ if (buf.length > MAX_PDF_BYTES) {
5444
+ const mb = (buf.length / 1e6).toFixed(1);
5445
+ const cap = (MAX_PDF_BYTES / 1e6).toFixed(0);
5380
5446
  return {
5381
- output: `read: offset ${startLine} is past end of file (it has ${total} lines)`,
5447
+ output: `read failed: ${path3} is ${mb} MB (PDF cap is ${cap} MB). Use bash with a command-line tool (e.g. \`pdftotext\`, \`qpdf\`) to split or extract it.`,
5382
5448
  isError: true
5383
5449
  };
5384
5450
  }
5385
- let endIdx = startIdx;
5386
- let chars = 0;
5387
- while (endIdx < total) {
5388
- if (input.limit !== void 0 && endIdx - startIdx >= input.limit) break;
5389
- const cost = Math.min(lines[endIdx].length, MAX_LINE_CHARS);
5390
- if (chars > 0 && chars + cost > MAX_CHARS) break;
5391
- chars += cost;
5392
- endIdx += 1;
5451
+ let totalPages;
5452
+ let pages;
5453
+ try {
5454
+ const extracted = await extractText2(new Uint8Array(buf), { mergePages: false });
5455
+ totalPages = extracted.totalPages;
5456
+ pages = extracted.text;
5457
+ } catch (err) {
5458
+ const msg = err instanceof Error ? err.message : String(err);
5459
+ return { output: `read failed: cannot parse ${path3} as a PDF: ${msg}`, isError: true };
5393
5460
  }
5394
- return { output: renderLines(lines, total, startIdx, endIdx, path3) };
5461
+ const meta = `PDF "${basename3(path3)}" \u2014 ${totalPages} page${totalPages === 1 ? "" : "s"}`;
5462
+ if (pages.every((p) => p.trim() === "")) {
5463
+ return {
5464
+ output: `${meta}
5465
+ read: no extractable text \u2014 this PDF is likely scanned or image-only. Use bash with an OCR tool (e.g. \`ocrmypdf\`, \`tesseract\`) to extract its text.`
5466
+ };
5467
+ }
5468
+ const combined = pages.map((p, i) => `[Page ${i + 1}]
5469
+ ${p.trim()}`).join("\n\n");
5470
+ const result = paginateLines(combined, input, path3);
5471
+ if (result.isError) return result;
5472
+ return { output: `${meta}
5473
+ ${result.output}` };
5395
5474
  }
5396
5475
  function resolveSheet(sheetNames, spec) {
5397
5476
  if (spec === void 0) return sheetNames[0] ?? null;
@@ -5557,7 +5636,7 @@ async function readImage(abs, ext, path3) {
5557
5636
  var readTool = {
5558
5637
  definition: {
5559
5638
  name: "read",
5560
- description: "Read a text file, spreadsheet, or image from disk. For text files, output is line-numbered (`<line>\\t<text>`, `cat -n` style, 1-based); returns up to `limit` lines (and at most ~200K characters) per call. If more remains, the result tells you the exact read(path, offset) call to continue from. The line-number prefix is display only \u2014 strip it before passing text to `edit`. For Excel files (.xlsx/.xls/.xlsm/.xlsb/.ods), each row is rendered as a TSV line with a metadata header; use the optional `sheet` parameter to select a sheet. For image files (.png/.jpg/.jpeg/.gif/.webp), returns the image as a base64 block alongside metadata \u2014 only when the active model supports image input; capped at 20 MB.",
5639
+ description: "Read a text file, spreadsheet, PDF, or image from disk. For text files, output is line-numbered (`<line>\\t<text>`, `cat -n` style, 1-based); returns up to `limit` lines (and at most ~200K characters) per call. If more remains, the result tells you the exact read(path, offset) call to continue from. The line-number prefix is display only \u2014 strip it before passing text to `edit`. For Excel files (.xlsx/.xls/.xlsm/.xlsb/.ods), each row is rendered as a TSV line with a metadata header; use the optional `sheet` parameter to select a sheet. For PDF files (.pdf), extracted text is returned line-numbered with a `[Page N]` marker before each page and the same offset/limit paging; scanned/image-only PDFs have no extractable text (capped at 30 MB). For image files (.png/.jpg/.jpeg/.gif/.webp), returns the image as a base64 block alongside metadata \u2014 only when the active model supports image input; capped at 20 MB.",
5561
5640
  inputSchema: inputSchema8
5562
5641
  },
5563
5642
  async run(rawInput, ctx) {
@@ -5576,6 +5655,9 @@ var readTool = {
5576
5655
  if (EXCEL_EXTENSIONS.has(ext)) {
5577
5656
  return readExcel(abs, input, input.path);
5578
5657
  }
5658
+ if (ext === ".pdf") {
5659
+ return readPdf(abs, input, input.path);
5660
+ }
5579
5661
  return readText(abs, input, input.path);
5580
5662
  }
5581
5663
  };
@@ -5642,11 +5724,14 @@ function parseSkillFile(text) {
5642
5724
  if (typeof descRaw !== "string" || descRaw.trim().length === 0) {
5643
5725
  return { error: "missing description" };
5644
5726
  }
5645
- const description = descRaw.length > DESCRIPTION_MAX ? descRaw.slice(0, DESCRIPTION_MAX) : descRaw;
5646
- const triggersRaw = meta["triggers"];
5647
- const triggers = Array.isArray(triggersRaw) ? triggersRaw.filter((x) => typeof x === "string") : [];
5727
+ const whenRaw = meta["when_to_use"];
5728
+ const whenToUse = typeof whenRaw === "string" ? whenRaw.trim() : "";
5729
+ const combined = whenToUse ? `${descRaw} ${whenToUse}` : descRaw;
5730
+ const description = combined.length > DESCRIPTION_MAX ? combined.slice(0, DESCRIPTION_MAX) : combined;
5731
+ const disableModelInvocation = parseBool(meta["disable-model-invocation"], false);
5732
+ const userInvocable = parseBool(meta["user-invocable"], true);
5648
5733
  const body = normalized.slice(fm[0].length).trimStart();
5649
- return { ok: { name, description, triggers, body } };
5734
+ return { ok: { name, description, disableModelInvocation, userInvocable, body } };
5650
5735
  }
5651
5736
  function parseFrontMatter2(src) {
5652
5737
  const out = {};
@@ -5691,6 +5776,13 @@ function parseScalar2(v) {
5691
5776
  }
5692
5777
  return v;
5693
5778
  }
5779
+ function parseBool(v, dflt) {
5780
+ if (typeof v !== "string") return dflt;
5781
+ const s = v.trim().toLowerCase();
5782
+ if (s === "true") return true;
5783
+ if (s === "false") return false;
5784
+ return dflt;
5785
+ }
5694
5786
  function scan(r, logger) {
5695
5787
  const targets = [];
5696
5788
  for (const d of r.projectDirs) targets.push({ kind: "project", root: resolve10(r.cwd, d) });
@@ -5727,7 +5819,8 @@ function scan(r, logger) {
5727
5819
  list.push({
5728
5820
  name: parsed.ok.name,
5729
5821
  description: parsed.ok.description,
5730
- triggers: parsed.ok.triggers,
5822
+ disableModelInvocation: parsed.ok.disableModelInvocation,
5823
+ userInvocable: parsed.ok.userInvocable,
5731
5824
  location: dir
5732
5825
  });
5733
5826
  seen.add(parsed.ok.name);
@@ -8219,7 +8312,7 @@ Working directory: ${workspace}
8219
8312
  - Use tools to complete the task yourself. Work autonomously; do not ask the parent for clarification unless you are truly blocked.
8220
8313
  - You run in isolation. The parent sees ONLY your final message \u2014 it cannot see your intermediate steps or tool output. End by reporting results concisely: what you did, key findings, and concrete file paths / follow-ups.
8221
8314
  - Stay within the assigned task. Do not spawn further sub-agents.
8222
- - Don't guess file paths. When unsure whether a file exists or where it lives, locate it with glob/grep before read.
8315
+ - Don't guess file paths. When unsure whether a file or directory exists, locate it with glob/grep/ls before acting \u2014 read, write, edit, or mkdir.
8223
8316
  - Write your final report in the language of the task you were given, matching its exact script and regional variant (e.g. Simplified vs Traditional Chinese \u2014 don't switch a Simplified-Chinese task to Traditional), not the language of these instructions.
8224
8317
  ${extra}
8225
8318
  Act, don't explain.
@@ -9238,6 +9331,9 @@ var DENY_RULES = [
9238
9331
  { re: /\bgit\s+clean\s+-[a-z]*f/i, reason: "git clean -f (deletes untracked files)" },
9239
9332
  { re: /\bgit\s+stash\s+(?:drop|clear)\b/i, reason: "git stash drop/clear (loses stashed work)" },
9240
9333
  { re: /\bgit\s+commit\b[^|&;]*--amend\b/i, reason: "git commit --amend (rewrites history)" },
9334
+ // Irreversible GitHub (gh) operations — merging or deleting.
9335
+ { re: /\bgh\s+pr\s+merge\b/i, reason: "gh pr merge (merges a pull request)" },
9336
+ { re: /\bgh\s+repo\s+delete\b/i, reason: "gh repo delete" },
9241
9337
  // Infrastructure-as-code teardown.
9242
9338
  { re: /\b(?:terraform|terragrunt|pulumi|cdk|cdktf)\s+destroy\b/i, reason: "infrastructure destroy" },
9243
9339
  // Mass cloud deletion.
@@ -9298,6 +9394,18 @@ var READ_ONLY_GIT_SUBCOMMANDS = /* @__PURE__ */ new Set([
9298
9394
  "tag",
9299
9395
  "config"
9300
9396
  ]);
9397
+ var READ_ONLY_GH_SIGNATURES = /* @__PURE__ */ new Set([
9398
+ "pr view",
9399
+ "pr diff",
9400
+ "pr list",
9401
+ "pr checks",
9402
+ "pr status",
9403
+ "issue view",
9404
+ "issue list",
9405
+ "repo view",
9406
+ "run view",
9407
+ "run list"
9408
+ ]);
9301
9409
  var COMPOUND_RE = /[|&;<>`]|\$\(|\bxargs\b|\beval\b|\bexec\b/;
9302
9410
  function effectiveTokens(command) {
9303
9411
  const tokens = command.trim().split(/\s+/);
@@ -9316,9 +9424,10 @@ function classifyCommandStatic(command) {
9316
9424
  if (re.test(cmd)) return "deny";
9317
9425
  }
9318
9426
  if (!COMPOUND_RE.test(cmd)) {
9319
- const [verb, sub] = effectiveTokens(cmd);
9427
+ const [verb, sub, action] = effectiveTokens(cmd);
9320
9428
  if (verb && READ_ONLY_COMMANDS.has(verb)) return "allow";
9321
9429
  if (verb === "git" && sub && READ_ONLY_GIT_SUBCOMMANDS.has(sub)) return "allow";
9430
+ if (verb === "gh" && sub && action && READ_ONLY_GH_SIGNATURES.has(`${sub} ${action}`)) return "allow";
9322
9431
  }
9323
9432
  return "unknown";
9324
9433
  }
@@ -9810,7 +9919,16 @@ async function writeStamp(dir) {
9810
9919
  async function git(cwd, args, logger) {
9811
9920
  logger?.debug({ args }, "nova-code-guide: git");
9812
9921
  try {
9813
- await execFileP2("git", [...args], { cwd });
9922
+ const p = execFileP2("git", [...args], { cwd });
9923
+ const child = p.child;
9924
+ if (child) {
9925
+ const unref = (s) => s?.unref?.();
9926
+ child.unref();
9927
+ unref(child.stdin);
9928
+ unref(child.stdout);
9929
+ unref(child.stderr);
9930
+ }
9931
+ await p;
9814
9932
  } catch (err) {
9815
9933
  if (isEnoent(err)) {
9816
9934
  throw new GuideProvisionError(
@@ -10709,6 +10827,7 @@ function loadSkillCommandsInto(registry, opts) {
10709
10827
  const taken = new Set(registry.list().map((c) => c.name));
10710
10828
  let added = 0;
10711
10829
  for (const item of items) {
10830
+ if (!item.userInvocable) continue;
10712
10831
  const cmd = {
10713
10832
  name: item.name,
10714
10833
  description: item.description,
@@ -10942,7 +11061,7 @@ import { render } from "ink";
10942
11061
 
10943
11062
  // src/ui/app.tsx
10944
11063
  import React12 from "react";
10945
- import { Box as Box14, Text as Text14 } from "ink";
11064
+ import { Box as Box15, Text as Text15 } from "ink";
10946
11065
  import { useShallow as useShallow3 } from "zustand/react/shallow";
10947
11066
 
10948
11067
  // src/ui/input-box.tsx
@@ -11811,20 +11930,24 @@ import { Box as Box4, Text as Text4 } from "ink";
11811
11930
 
11812
11931
  // src/ui/logo.ts
11813
11932
  var LOGO = [
11814
- "\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588",
11815
- "\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 ",
11816
- "\u2588\u2588 \u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588 ",
11817
- "\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 ",
11818
- "\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588"
11933
+ " \xB7 \u22C6 \u2726 \u22C6 \xB7 ",
11934
+ " \u22C6 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u22C6 ",
11935
+ " \u2727 \xB7 \u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \xB7 \u2727 ",
11936
+ "\u2726 \u207A \u2588\u2588 \u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u207A \u2726",
11937
+ " \xB7 \u2727 \u2588\u2588 \u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2727 \xB7 ",
11938
+ " \u22C6 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u22C6 ",
11939
+ " \u207A \xB7 \u22C6 \xB7 \u2726 \u207A "
11819
11940
  ];
11820
11941
  var LOGO_GRADIENT = [
11942
+ [140, 246, 255],
11821
11943
  [0, 238, 255],
11822
11944
  [90, 160, 255],
11823
11945
  [170, 110, 245],
11824
11946
  [230, 80, 210],
11825
- [255, 60, 170]
11947
+ [255, 60, 170],
11948
+ [255, 140, 215]
11826
11949
  ];
11827
- var LOGO_FALLBACK = [accent, blue, magenta, magenta, magenta];
11950
+ var LOGO_FALLBACK = [accent, accent, blue, magenta, magenta, magenta, magenta];
11828
11951
  function bannerLine(line, row) {
11829
11952
  if (useTruecolor) {
11830
11953
  const stop = LOGO_GRADIENT[row];
@@ -11928,10 +12051,28 @@ function SetupView({ state }) {
11928
12051
  ] });
11929
12052
  }
11930
12053
 
12054
+ // src/ui/trust-view.tsx
12055
+ import { Box as Box5, Text as Text5 } from "ink";
12056
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
12057
+ function TrustView({ state }) {
12058
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: [
12059
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text5, { children: [
12060
+ /* @__PURE__ */ jsx5(Text5, { color: ACCENT_HEX, children: ">_" }),
12061
+ " Nova Code",
12062
+ " ",
12063
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `(v${state.version})` })
12064
+ ] }) }),
12065
+ /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginTop: 1, children: LOGO.map((line, i) => /* @__PURE__ */ jsx5(Text5, { color: LOGO_ROW_HEX[i] ?? ACCENT_HEX, children: line }, i)) }),
12066
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { bold: true, color: ACCENT_HEX, children: "Do you trust the files in this folder?" }) }),
12067
+ /* @__PURE__ */ jsx5(Text5, { children: state.workspace }),
12068
+ /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginTop: 1, children: state.lines.map((line, i) => /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: line }, i)) })
12069
+ ] });
12070
+ }
12071
+
11931
12072
  // src/ui/status-line.tsx
11932
12073
  import React3 from "react";
11933
- import { Box as Box5, Text as Text5 } from "ink";
11934
- import { basename as basename3 } from "path";
12074
+ import { Box as Box6, Text as Text6 } from "ink";
12075
+ import { basename as basename4 } from "path";
11935
12076
  import { useShallow } from "zustand/react/shallow";
11936
12077
 
11937
12078
  // src/ui/status-format.ts
@@ -12009,17 +12150,17 @@ function fitSegments(segments, maxWidth, sep3 = " | ") {
12009
12150
  }
12010
12151
 
12011
12152
  // src/ui/status-line.tsx
12012
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
12153
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
12013
12154
  function SegmentRow({
12014
12155
  segments,
12015
12156
  termCols
12016
12157
  }) {
12017
12158
  const shown = fitSegments(segments, Math.max(0, termCols - 2));
12018
- return /* @__PURE__ */ jsxs4(Box5, { children: [
12019
- /* @__PURE__ */ jsx5(Text5, { children: " " }),
12020
- shown.map((seg, i) => /* @__PURE__ */ jsxs4(React3.Fragment, { children: [
12021
- i > 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " | " }) : null,
12022
- /* @__PURE__ */ jsxs4(Text5, { color: seg.color, children: [
12159
+ return /* @__PURE__ */ jsxs5(Box6, { children: [
12160
+ /* @__PURE__ */ jsx6(Text6, { children: " " }),
12161
+ shown.map((seg, i) => /* @__PURE__ */ jsxs5(React3.Fragment, { children: [
12162
+ i > 0 ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " | " }) : null,
12163
+ /* @__PURE__ */ jsxs5(Text6, { color: seg.color, children: [
12023
12164
  seg.icon,
12024
12165
  " ",
12025
12166
  seg.text
@@ -12060,15 +12201,15 @@ function StatusLine({ store, shellMode = false }) {
12060
12201
  }))
12061
12202
  );
12062
12203
  if (shellMode) {
12063
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
12064
- /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { color: SHELL_MODE_INDICATOR.color, children: ` ${SHELL_MODE_INDICATOR.label}` }) }),
12065
- /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { children: " " }) })
12204
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
12205
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { color: SHELL_MODE_INDICATOR.color, children: ` ${SHELL_MODE_INDICATOR.label}` }) }),
12206
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { children: " " }) })
12066
12207
  ] });
12067
12208
  }
12068
12209
  if (copyNotice) {
12069
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
12070
- /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { color: copyNoticeTone === "warn" ? "red" : "green", children: ` ${copyNotice}` }) }),
12071
- /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { children: " " }) })
12210
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
12211
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { color: copyNoticeTone === "warn" ? "red" : "green", children: ` ${copyNotice}` }) }),
12212
+ /* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { children: " " }) })
12072
12213
  ] });
12073
12214
  }
12074
12215
  const main = [];
@@ -12083,7 +12224,7 @@ function StatusLine({ store, shellMode = false }) {
12083
12224
  main.push({ icon: "\u25CB", text: `${contextBar(pct)} ${pct}%`, color: "yellow" });
12084
12225
  }
12085
12226
  if (banner?.cwd) {
12086
- main.push({ icon: "\u25C8", text: basename3(banner.cwd) || banner.cwd, color: "green" });
12227
+ main.push({ icon: "\u25C8", text: basename4(banner.cwd) || banner.cwd, color: "green" });
12087
12228
  }
12088
12229
  if (gitBranch) {
12089
12230
  main.push({ icon: "\u2387", text: gitBranch, color: "blue" });
@@ -12129,15 +12270,15 @@ function StatusLine({ store, shellMode = false }) {
12129
12270
  color: "#d97706"
12130
12271
  });
12131
12272
  }
12132
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
12133
- /* @__PURE__ */ jsx5(SegmentRow, { segments: main, termCols }),
12134
- /* @__PURE__ */ jsx5(SegmentRow, { segments: usage, termCols })
12273
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
12274
+ /* @__PURE__ */ jsx6(SegmentRow, { segments: main, termCols }),
12275
+ /* @__PURE__ */ jsx6(SegmentRow, { segments: usage, termCols })
12135
12276
  ] });
12136
12277
  }
12137
12278
 
12138
12279
  // src/ui/picker.tsx
12139
12280
  import { useState as useState2 } from "react";
12140
- import { Box as Box6, Text as Text6, useInput as useInput2, useStdout as useStdout2 } from "ink";
12281
+ import { Box as Box7, Text as Text7, useInput as useInput2, useStdout as useStdout2 } from "ink";
12141
12282
 
12142
12283
  // src/ui/selection.ts
12143
12284
  function sliceVisualCols(line, startCol, endCol) {
@@ -12233,7 +12374,7 @@ function highlightLines(lines, rect) {
12233
12374
  }
12234
12375
 
12235
12376
  // src/ui/picker.tsx
12236
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
12377
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
12237
12378
  function highlightRow(text, width) {
12238
12379
  const tw = visibleWidth(text);
12239
12380
  const padded = tw < width ? text + " ".repeat(width - tw) : text;
@@ -12336,12 +12477,12 @@ function PickList({ opts, onResolve, panelWidth }) {
12336
12477
  const text = rendered[i - windowStart];
12337
12478
  const body = i === selected && useColor ? highlightRow(text, barWidth) : text;
12338
12479
  rows.push(
12339
- /* @__PURE__ */ jsx6(Text6, { ...fullWidth ? { wrap: "truncate-end" } : {}, children: body }, i)
12480
+ /* @__PURE__ */ jsx7(Text7, { ...fullWidth ? { wrap: "truncate-end" } : {}, children: body }, i)
12340
12481
  );
12341
12482
  }
12342
12483
  const borderProps = overlayBorderProps(bordered, opts.topRuleColor);
12343
- return /* @__PURE__ */ jsxs5(
12344
- Box6,
12484
+ return /* @__PURE__ */ jsxs6(
12485
+ Box7,
12345
12486
  {
12346
12487
  flexDirection: "column",
12347
12488
  marginTop: 1,
@@ -12349,10 +12490,10 @@ function PickList({ opts, onResolve, panelWidth }) {
12349
12490
  ...fullWidth ? { width: cols } : {},
12350
12491
  ...borderProps,
12351
12492
  children: [
12352
- opts.header ? /* @__PURE__ */ jsx6(Text6, { children: opts.header }) : null,
12493
+ opts.header ? /* @__PURE__ */ jsx7(Text7, { children: opts.header }) : null,
12353
12494
  rows,
12354
- indicator ? /* @__PURE__ */ jsx6(Text6, { children: indicator }) : null,
12355
- opts.footer ? /* @__PURE__ */ jsx6(Text6, { children: opts.footer }) : null
12495
+ indicator ? /* @__PURE__ */ jsx7(Text7, { children: indicator }) : null,
12496
+ opts.footer ? /* @__PURE__ */ jsx7(Text7, { children: opts.footer }) : null
12356
12497
  ]
12357
12498
  }
12358
12499
  );
@@ -12408,13 +12549,13 @@ function ScrollViewer({ opts, onResolve, panelWidth }) {
12408
12549
  for (let i = 0; i < visible.length; i++) {
12409
12550
  const line = visible[i];
12410
12551
  const body = line.bg && useColor ? tintRow(line.text, barWidth, line.bg) : line.text;
12411
- rows.push(/* @__PURE__ */ jsx6(Text6, { children: (line.gutter ?? "") + body }, start + i));
12552
+ rows.push(/* @__PURE__ */ jsx7(Text7, { children: (line.gutter ?? "") + body }, start + i));
12412
12553
  }
12413
12554
  const indicator = lines.length > pageSize ? useColor ? dim(` (${start + 1}\u2013${end}/${lines.length})`) : ` (${start + 1}\u2013${end}/${lines.length})` : null;
12414
12555
  const fullWidth = !bordered && !!opts.topRuleColor;
12415
12556
  const borderProps = overlayBorderProps(bordered, opts.topRuleColor);
12416
- return /* @__PURE__ */ jsxs5(
12417
- Box6,
12557
+ return /* @__PURE__ */ jsxs6(
12558
+ Box7,
12418
12559
  {
12419
12560
  flexDirection: "column",
12420
12561
  marginTop: 1,
@@ -12422,10 +12563,10 @@ function ScrollViewer({ opts, onResolve, panelWidth }) {
12422
12563
  ...fullWidth ? { width: cols } : {},
12423
12564
  ...borderProps,
12424
12565
  children: [
12425
- opts.header ? /* @__PURE__ */ jsx6(Text6, { children: opts.header }) : null,
12566
+ opts.header ? /* @__PURE__ */ jsx7(Text7, { children: opts.header }) : null,
12426
12567
  rows,
12427
- indicator ? /* @__PURE__ */ jsx6(Text6, { children: indicator }) : null,
12428
- opts.footer ? /* @__PURE__ */ jsx6(Text6, { children: opts.footer }) : null
12568
+ indicator ? /* @__PURE__ */ jsx7(Text7, { children: indicator }) : null,
12569
+ opts.footer ? /* @__PURE__ */ jsx7(Text7, { children: opts.footer }) : null
12429
12570
  ]
12430
12571
  }
12431
12572
  );
@@ -12473,11 +12614,11 @@ function PickHorizontal({ opts, onResolve, panelWidth }) {
12473
12614
  items.forEach((item, i) => {
12474
12615
  const badge = opts.badge?.(item) ?? null;
12475
12616
  const label = ` ${opts.label(item)} `;
12476
- if (i > 0) cells.push(/* @__PURE__ */ jsx6(Text6, { children: separator }, `sep-${i}`));
12617
+ if (i > 0) cells.push(/* @__PURE__ */ jsx7(Text7, { children: separator }, `sep-${i}`));
12477
12618
  cells.push(
12478
- /* @__PURE__ */ jsxs5(Text6, { children: [
12479
- /* @__PURE__ */ jsx6(Text6, { inverse: i === selected, dimColor: i !== selected, children: label }),
12480
- badge ? /* @__PURE__ */ jsx6(Text6, { color: ACCENT_HEX, bold: true, children: ` ${badge} ` }) : null
12619
+ /* @__PURE__ */ jsxs6(Text7, { children: [
12620
+ /* @__PURE__ */ jsx7(Text7, { inverse: i === selected, dimColor: i !== selected, children: label }),
12621
+ badge ? /* @__PURE__ */ jsx7(Text7, { color: ACCENT_HEX, bold: true, children: ` ${badge} ` }) : null
12481
12622
  ] }, i)
12482
12623
  );
12483
12624
  });
@@ -12485,8 +12626,8 @@ function PickHorizontal({ opts, onResolve, panelWidth }) {
12485
12626
  const fullWidth = !bordered && !!opts.topRuleColor;
12486
12627
  const cols = panelWidth ?? stdout?.columns ?? 80;
12487
12628
  const borderProps = overlayBorderProps(bordered, opts.topRuleColor);
12488
- return /* @__PURE__ */ jsxs5(
12489
- Box6,
12629
+ return /* @__PURE__ */ jsxs6(
12630
+ Box7,
12490
12631
  {
12491
12632
  flexDirection: "column",
12492
12633
  marginTop: 1,
@@ -12495,11 +12636,11 @@ function PickHorizontal({ opts, onResolve, panelWidth }) {
12495
12636
  ...fullWidth ? { width: cols } : {},
12496
12637
  ...borderProps,
12497
12638
  children: [
12498
- opts.header ? /* @__PURE__ */ jsx6(Text6, { children: opts.header }) : null,
12499
- /* @__PURE__ */ jsx6(Text6, { children: " " }),
12500
- /* @__PURE__ */ jsx6(Box6, { children: cells }),
12501
- /* @__PURE__ */ jsx6(Text6, { children: " " }),
12502
- opts.footer ? /* @__PURE__ */ jsx6(Text6, { children: opts.footer }) : null
12639
+ opts.header ? /* @__PURE__ */ jsx7(Text7, { children: opts.header }) : null,
12640
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
12641
+ /* @__PURE__ */ jsx7(Box7, { children: cells }),
12642
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
12643
+ opts.footer ? /* @__PURE__ */ jsx7(Text7, { children: opts.footer }) : null
12503
12644
  ]
12504
12645
  }
12505
12646
  );
@@ -12507,12 +12648,12 @@ function PickHorizontal({ opts, onResolve, panelWidth }) {
12507
12648
 
12508
12649
  // src/ui/viewport.tsx
12509
12650
  import React11 from "react";
12510
- import { Box as Box13, Text as Text13 } from "ink";
12651
+ import { Box as Box14, Text as Text14 } from "ink";
12511
12652
  import { useShallow as useShallow2 } from "zustand/react/shallow";
12512
12653
 
12513
12654
  // src/ui/approval.tsx
12514
12655
  import { useState as useState3 } from "react";
12515
- import { Box as Box7, Text as Text7, useInput as useInput3 } from "ink";
12656
+ import { Box as Box8, Text as Text8, useInput as useInput3 } from "ink";
12516
12657
 
12517
12658
  // src/ui/measure.ts
12518
12659
  import wrapAnsi2 from "wrap-ansi";
@@ -13069,7 +13210,7 @@ function wrapThinkingBody(text, width) {
13069
13210
  return wrapAnsi(trimmed, bodyWidth, { hard: true, wordWrap: false, trim: false }).split("\n");
13070
13211
  }
13071
13212
  function renderThinking(text, label, width, collapsed = false, expanded = false) {
13072
- const head = `${magenta("\u273B")} ${dim(`thinking${label ? ` \xB7 ${label}` : ""}`)}`;
13213
+ const head = `${magenta("\u2726")} ${dim(`thinking${label ? ` \xB7 ${label}` : ""}`)}`;
13073
13214
  const lines = wrapThinkingBody(text, width);
13074
13215
  if (lines.length === 0) return head;
13075
13216
  const overflow = collapsed ? Math.max(0, lines.length - THINKING_COLLAPSED_MAX_LINES) : 0;
@@ -13092,7 +13233,7 @@ function thinkingToggleLineIndex(item, width) {
13092
13233
  return 1 + shown;
13093
13234
  }
13094
13235
  function renderRedactedThinking(label) {
13095
- return `${magenta("\u273B")} ${dim(`thinking${label ? ` \xB7 ${label}` : ""} (redacted)`)}`;
13236
+ return `${magenta("\u2726")} ${dim(`thinking${label ? ` \xB7 ${label}` : ""} (redacted)`)}`;
13096
13237
  }
13097
13238
  function renderCard(card) {
13098
13239
  const color = cardColor[card.kind];
@@ -13364,7 +13505,7 @@ function genericUseHeader(use) {
13364
13505
  return header(use.name, dim(trim(compact)));
13365
13506
  }
13366
13507
  var DETAIL_MARK = {
13367
- thinking: "\u273B",
13508
+ thinking: "\u2726",
13368
13509
  tool_use: "\u2692",
13369
13510
  final: "\u2192"
13370
13511
  };
@@ -13563,7 +13704,7 @@ function sliceLines(items, width, offset, viewportRows) {
13563
13704
  }
13564
13705
 
13565
13706
  // src/ui/approval.tsx
13566
- import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
13707
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
13567
13708
  var TOOL_PROMPTS = {
13568
13709
  read: "Allow reading this file?",
13569
13710
  write: "Allow writing this file?",
@@ -13680,8 +13821,8 @@ function ApprovalPrompt({
13680
13821
  });
13681
13822
  const inner = approvalInnerWidth(width);
13682
13823
  const { text: detail, truncated } = clampDetail(describeToolInput(input.input));
13683
- return /* @__PURE__ */ jsxs6(
13684
- Box7,
13824
+ return /* @__PURE__ */ jsxs7(
13825
+ Box8,
13685
13826
  {
13686
13827
  flexDirection: "column",
13687
13828
  marginTop: 1,
@@ -13690,31 +13831,31 @@ function ApprovalPrompt({
13690
13831
  borderStyle: "round",
13691
13832
  borderColor: "gray",
13692
13833
  children: [
13693
- /* @__PURE__ */ jsx7(Text7, { children: promptFor(input.tool) }),
13694
- /* @__PURE__ */ jsx7(Text7, { children: " " }),
13834
+ /* @__PURE__ */ jsx8(Text8, { children: promptFor(input.tool) }),
13835
+ /* @__PURE__ */ jsx8(Text8, { children: " " }),
13695
13836
  input.tool === "bash" ? (
13696
13837
  // Mirror the message-stream bash rendering: `● bash` header with the
13697
13838
  // command under a `⎿` gutter, so heredocs/long one-liners preview the
13698
13839
  // way they'll print in the transcript instead of as a flat line.
13699
- /* @__PURE__ */ jsxs6(Fragment2, { children: [
13700
- /* @__PURE__ */ jsx7(Text7, { children: BASH_HEADER }),
13701
- /* @__PURE__ */ jsx7(Text7, { children: renderCommandBody(detail, inner) }),
13702
- truncated ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: `${BASH_BODY_INDENT}\u2026 (truncated)` }) : null
13840
+ /* @__PURE__ */ jsxs7(Fragment2, { children: [
13841
+ /* @__PURE__ */ jsx8(Text8, { children: BASH_HEADER }),
13842
+ /* @__PURE__ */ jsx8(Text8, { children: renderCommandBody(detail, inner) }),
13843
+ truncated ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: `${BASH_BODY_INDENT}\u2026 (truncated)` }) : null
13703
13844
  ] })
13704
- ) : /* @__PURE__ */ jsxs6(Text7, { children: [
13705
- /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
13845
+ ) : /* @__PURE__ */ jsxs7(Text8, { children: [
13846
+ /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
13706
13847
  input.tool,
13707
13848
  " "
13708
13849
  ] }),
13709
- /* @__PURE__ */ jsx7(Text7, { color: ACCENT_HEX, children: detail }),
13710
- truncated ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " \u2026 (truncated)" }) : null
13850
+ /* @__PURE__ */ jsx8(Text8, { color: ACCENT_HEX, children: detail }),
13851
+ truncated ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " \u2026 (truncated)" }) : null
13711
13852
  ] }),
13712
- /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", marginTop: 1, children: OPTIONS.map((opt, i) => {
13853
+ /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", marginTop: 1, children: OPTIONS.map((opt, i) => {
13713
13854
  const active = i === cursor;
13714
- return /* @__PURE__ */ jsxs6(Text7, { color: active ? opt.color : void 0, children: [
13855
+ return /* @__PURE__ */ jsxs7(Text8, { color: active ? opt.color : void 0, children: [
13715
13856
  active ? "\u276F " : " ",
13716
- /* @__PURE__ */ jsx7(Text7, { children: opt.label }),
13717
- /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
13857
+ /* @__PURE__ */ jsx8(Text8, { children: opt.label }),
13858
+ /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
13718
13859
  " (",
13719
13860
  opt.hint,
13720
13861
  ")"
@@ -13728,8 +13869,8 @@ function ApprovalPrompt({
13728
13869
 
13729
13870
  // src/ui/ask-user.tsx
13730
13871
  import { useState as useState4 } from "react";
13731
- import { Box as Box8, Text as Text8, useInput as useInput4 } from "ink";
13732
- import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
13872
+ import { Box as Box9, Text as Text9, useInput as useInput4 } from "ink";
13873
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
13733
13874
  var OTHER_LABEL = "Other";
13734
13875
  var CONFIRM_HEADER = "Confirm";
13735
13876
  var CONFIRM_SUBMIT = 0;
@@ -14000,25 +14141,25 @@ function AskPanel({ req, onResolve }) {
14000
14141
  });
14001
14142
  const labelWidth = q ? Math.max(...q.options.map((o) => visibleWidth(o.label))) : 0;
14002
14143
  const everyAnswered = allAnswered(states);
14003
- return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", padding: 1, marginTop: 1, marginBottom: 1, borderStyle: "round", children: [
14004
- /* @__PURE__ */ jsxs7(Text8, { children: [
14005
- /* @__PURE__ */ jsx8(Text8, { bold: true, color: ACCENT_HEX, children: "?" }),
14144
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", padding: 1, marginTop: 1, marginBottom: 1, borderStyle: "round", children: [
14145
+ /* @__PURE__ */ jsxs8(Text9, { children: [
14146
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: ACCENT_HEX, children: "?" }),
14006
14147
  " ",
14007
14148
  isConfirm ? "Review your answers and submit." : q?.spec.question
14008
14149
  ] }),
14009
- /* @__PURE__ */ jsxs7(Box8, { children: [
14150
+ /* @__PURE__ */ jsxs8(Box9, { children: [
14010
14151
  states.map((s, i) => {
14011
14152
  const status = isAnswered(s) ? "\u2713" : i === tab ? "\u25CF" : "\u25CB";
14012
14153
  const text = ` ${status} ${s.spec.header} `;
14013
- return /* @__PURE__ */ jsxs7(Text8, { color: i === tab ? ACCENT_HEX : void 0, dimColor: i !== tab, children: [
14154
+ return /* @__PURE__ */ jsxs8(Text9, { color: i === tab ? ACCENT_HEX : void 0, dimColor: i !== tab, children: [
14014
14155
  i > 0 ? " " : "",
14015
14156
  "[",
14016
14157
  text,
14017
14158
  "]"
14018
14159
  ] }, i);
14019
14160
  }),
14020
- /* @__PURE__ */ jsxs7(
14021
- Text8,
14161
+ /* @__PURE__ */ jsxs8(
14162
+ Text9,
14022
14163
  {
14023
14164
  color: isConfirm ? ACCENT_HEX : everyAnswered ? "green" : void 0,
14024
14165
  dimColor: !isConfirm,
@@ -14033,34 +14174,34 @@ function AskPanel({ req, onResolve }) {
14033
14174
  }
14034
14175
  )
14035
14176
  ] }),
14036
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
14037
- isConfirm ? /* @__PURE__ */ jsxs7(Fragment3, { children: [
14177
+ /* @__PURE__ */ jsx9(Text9, { children: " " }),
14178
+ isConfirm ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
14038
14179
  states.map((s, i) => {
14039
14180
  const labels = answerLabels(s);
14040
14181
  const val = labels.length > 0 ? labels.join(", ") : "(no answer)";
14041
- return /* @__PURE__ */ jsxs7(Text8, { children: [
14182
+ return /* @__PURE__ */ jsxs8(Text9, { children: [
14042
14183
  " ",
14043
- /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
14184
+ /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
14044
14185
  s.spec.header,
14045
14186
  ":"
14046
14187
  ] }),
14047
14188
  " ",
14048
- /* @__PURE__ */ jsx8(Text8, { color: labels.length > 0 ? void 0 : "yellow", children: val })
14189
+ /* @__PURE__ */ jsx9(Text9, { color: labels.length > 0 ? void 0 : "yellow", children: val })
14049
14190
  ] }, i);
14050
14191
  }),
14051
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
14192
+ /* @__PURE__ */ jsx9(Text9, { children: " " }),
14052
14193
  [
14053
14194
  { idx: CONFIRM_SUBMIT, label: "Submit", color: "green" },
14054
14195
  { idx: CONFIRM_CANCEL, label: "Cancel", color: "red" }
14055
14196
  ].map((b) => {
14056
14197
  const isCur = confirmIndex === b.idx;
14057
14198
  const disabled = b.idx === CONFIRM_SUBMIT && !everyAnswered;
14058
- return /* @__PURE__ */ jsxs7(Text8, { children: [
14199
+ return /* @__PURE__ */ jsxs8(Text9, { children: [
14059
14200
  " ",
14060
- /* @__PURE__ */ jsx8(Text8, { color: isCur ? ACCENT_HEX : void 0, children: isCur ? "\u276F" : " " }),
14201
+ /* @__PURE__ */ jsx9(Text9, { color: isCur ? ACCENT_HEX : void 0, children: isCur ? "\u276F" : " " }),
14061
14202
  " ",
14062
- /* @__PURE__ */ jsx8(Text8, { color: isCur ? b.color : void 0, dimColor: disabled && !isCur, children: b.label }),
14063
- disabled ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " (answer all questions first)" }) : null
14203
+ /* @__PURE__ */ jsx9(Text9, { color: isCur ? b.color : void 0, dimColor: disabled && !isCur, children: b.label }),
14204
+ disabled ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " (answer all questions first)" }) : null
14064
14205
  ] }, b.idx);
14065
14206
  })
14066
14207
  ] }) : q?.options.map((o, i) => {
@@ -14069,34 +14210,34 @@ function AskPanel({ req, onResolve }) {
14069
14210
  const marker2 = q.spec.multiSelect ? isSelected ? "[x]" : "[ ]" : isSelected ? "\u25CF" : "\u25CB";
14070
14211
  const cur = isCur ? "\u276F" : " ";
14071
14212
  const pad = " ".repeat(Math.max(0, labelWidth - visibleWidth(o.label)));
14072
- return /* @__PURE__ */ jsxs7(Text8, { children: [
14213
+ return /* @__PURE__ */ jsxs8(Text9, { children: [
14073
14214
  " ",
14074
- /* @__PURE__ */ jsx8(Text8, { color: isCur ? ACCENT_HEX : void 0, children: cur }),
14215
+ /* @__PURE__ */ jsx9(Text9, { color: isCur ? ACCENT_HEX : void 0, children: cur }),
14075
14216
  " ",
14076
14217
  marker2,
14077
14218
  " ",
14078
- /* @__PURE__ */ jsx8(Text8, { color: isCur ? ACCENT_HEX : void 0, children: o.label }),
14219
+ /* @__PURE__ */ jsx9(Text9, { color: isCur ? ACCENT_HEX : void 0, children: o.label }),
14079
14220
  pad,
14080
- o.description ? /* @__PURE__ */ jsxs7(Fragment3, { children: [
14221
+ o.description ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
14081
14222
  " ",
14082
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: o.description })
14223
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: o.description })
14083
14224
  ] }) : null
14084
14225
  ] }, i);
14085
14226
  }),
14086
- phase === "freeform" ? /* @__PURE__ */ jsxs7(Fragment3, { children: [
14087
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
14088
- /* @__PURE__ */ jsxs7(Box8, { children: [
14089
- /* @__PURE__ */ jsx8(Text8, { color: ACCENT_HEX, children: " \u203A " }),
14090
- /* @__PURE__ */ jsx8(Text8, { children: freeformBuffer }),
14091
- /* @__PURE__ */ jsx8(Text8, { inverse: true, children: " " }),
14092
- freeformBuffer.length === 0 ? /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
14227
+ phase === "freeform" ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
14228
+ /* @__PURE__ */ jsx9(Text9, { children: " " }),
14229
+ /* @__PURE__ */ jsxs8(Box9, { children: [
14230
+ /* @__PURE__ */ jsx9(Text9, { color: ACCENT_HEX, children: " \u203A " }),
14231
+ /* @__PURE__ */ jsx9(Text9, { children: freeformBuffer }),
14232
+ /* @__PURE__ */ jsx9(Text9, { inverse: true, children: " " }),
14233
+ freeformBuffer.length === 0 ? /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
14093
14234
  " ",
14094
14235
  "type your custom answer, Enter to confirm, Esc to cancel"
14095
14236
  ] }) : null
14096
14237
  ] })
14097
14238
  ] }) : null,
14098
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
14099
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: [
14239
+ /* @__PURE__ */ jsx9(Text9, { children: " " }),
14240
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: [
14100
14241
  "\u2190/\u2192 tab",
14101
14242
  isConfirm ? "\u2191/\u2193 button" : "\u2191/\u2193 option",
14102
14243
  !isConfirm && q?.spec.multiSelect ? "space toggle" : "",
@@ -14108,8 +14249,8 @@ function AskPanel({ req, onResolve }) {
14108
14249
 
14109
14250
  // src/ui/slider.tsx
14110
14251
  import { useEffect as useEffect2, useState as useState5 } from "react";
14111
- import { Box as Box9, Text as Text9, useInput as useInput5, useStdout as useStdout3 } from "ink";
14112
- import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
14252
+ import { Box as Box10, Text as Text10, useInput as useInput5, useStdout as useStdout3 } from "ink";
14253
+ import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
14113
14254
  var SEP = 3;
14114
14255
  function hslToRgb(h, s, l) {
14115
14256
  const c = (1 - Math.abs(2 * l - 1)) * s;
@@ -14211,8 +14352,8 @@ function SliderPicker({ opts, onResolve, panelWidth }) {
14211
14352
  const fullWidth = !!opts.topRuleColor;
14212
14353
  const cols = panelWidth ?? stdout?.columns ?? 80;
14213
14354
  const borderProps = overlayBorderProps(false, opts.topRuleColor);
14214
- return /* @__PURE__ */ jsxs8(
14215
- Box9,
14355
+ return /* @__PURE__ */ jsxs9(
14356
+ Box10,
14216
14357
  {
14217
14358
  flexDirection: "column",
14218
14359
  marginTop: 1,
@@ -14220,15 +14361,15 @@ function SliderPicker({ opts, onResolve, panelWidth }) {
14220
14361
  ...fullWidth ? { width: cols } : {},
14221
14362
  ...borderProps,
14222
14363
  children: [
14223
- /* @__PURE__ */ jsx9(Text9, { children: scaleLine }),
14224
- /* @__PURE__ */ jsx9(Text9, { children: itemLine }),
14225
- description ? /* @__PURE__ */ jsxs8(Fragment4, { children: [
14226
- /* @__PURE__ */ jsx9(Text9, { children: " " }),
14227
- /* @__PURE__ */ jsx9(Text9, { children: dim(description) })
14364
+ /* @__PURE__ */ jsx10(Text10, { children: scaleLine }),
14365
+ /* @__PURE__ */ jsx10(Text10, { children: itemLine }),
14366
+ description ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
14367
+ /* @__PURE__ */ jsx10(Text10, { children: " " }),
14368
+ /* @__PURE__ */ jsx10(Text10, { children: dim(description) })
14228
14369
  ] }) : null,
14229
- opts.footer ? /* @__PURE__ */ jsxs8(Fragment4, { children: [
14230
- /* @__PURE__ */ jsx9(Text9, { children: " " }),
14231
- /* @__PURE__ */ jsx9(Text9, { children: opts.footer })
14370
+ opts.footer ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
14371
+ /* @__PURE__ */ jsx10(Text10, { children: " " }),
14372
+ /* @__PURE__ */ jsx10(Text10, { children: opts.footer })
14232
14373
  ] }) : null
14233
14374
  ]
14234
14375
  }
@@ -14455,9 +14596,10 @@ function appendAssistantItems(items, msg, resultIndex, thinkingLabel, nextKey, t
14455
14596
 
14456
14597
  // src/ui/spinner.tsx
14457
14598
  import { useEffect as useEffect3, useState as useState6 } from "react";
14458
- import { Box as Box10, Text as Text10 } from "ink";
14459
- import { jsx as jsx10 } from "react/jsx-runtime";
14460
- var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
14599
+ import { Box as Box11, Text as Text11 } from "ink";
14600
+ import { jsx as jsx11 } from "react/jsx-runtime";
14601
+ var FRAMES = ["\xB7", "\u2726", "\u2727", "\u2736", "\u2738", "\u273A", "\u2738", "\u2736", "\u2727", "\u2726"];
14602
+ var ANIM_SLOWDOWN = 2;
14461
14603
  function shimmer(text, frame, [r, g, b]) {
14462
14604
  let out = "\x1B[1m";
14463
14605
  for (let i = 0; i < text.length; i++) {
@@ -14488,28 +14630,29 @@ function Spinner({ spec }) {
14488
14630
  return () => clearInterval(id);
14489
14631
  }, [tickMs]);
14490
14632
  const elapsed = formatElapsed(Date.now() - spec.startedAt);
14491
- const frameChar = FRAMES[frame % FRAMES.length] ?? "";
14633
+ const phase = Math.floor(frame / ANIM_SLOWDOWN);
14634
+ const frameChar = FRAMES[phase % FRAMES.length] ?? "";
14492
14635
  const upStr = spec.inputTokens != null ? ` \xB7 \u2191 ${formatTokenCount(spec.inputTokens)} tok` : "";
14493
14636
  const downStr = spec.tokens != null ? ` \xB7 \u2193 ~${formatTokenCount(spec.tokens)} tok` : "";
14494
14637
  const tokenStr = `${upStr}${downStr}`;
14495
14638
  const hintStr = spec.hint ? ` \xB7 ${spec.hint}` : "";
14496
14639
  let line;
14497
14640
  if (canShimmer && tint) {
14498
- const head = shimmer(frameChar, frame + 1, tint);
14499
- const word = shimmer(spec.activeWord, frame, tint);
14641
+ const head = shimmer(frameChar, phase + 1, tint);
14642
+ const word = shimmer(spec.activeWord, phase, tint);
14500
14643
  line = `${head} ${word} \xB7 ${elapsed}${tokenStr}${hintStr}`;
14501
14644
  } else {
14502
14645
  const renderedFrame = isStatic ? frameChar : bold(colorize(frameChar));
14503
14646
  const word = isStatic ? spec.activeWord : bold(colorize(spec.activeWord));
14504
14647
  line = `${renderedFrame} ${word} \xB7 ${elapsed}${tokenStr}${hintStr}`;
14505
14648
  }
14506
- return /* @__PURE__ */ jsx10(Box10, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: /* @__PURE__ */ jsx10(Text10, { children: line }) });
14649
+ return /* @__PURE__ */ jsx11(Box11, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: /* @__PURE__ */ jsx11(Text11, { children: line }) });
14507
14650
  }
14508
14651
 
14509
14652
  // src/ui/task-footer.tsx
14510
14653
  import { useEffect as useEffect4, useState as useState7 } from "react";
14511
- import { Box as Box11, Text as Text11 } from "ink";
14512
- import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
14654
+ import { Box as Box12, Text as Text12 } from "ink";
14655
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
14513
14656
  var MAX_VISIBLE = 5;
14514
14657
  var MIN_VISIBLE_TASKS = 2;
14515
14658
  function taskFooterVisible(tasks) {
@@ -14521,28 +14664,28 @@ var STATUS_RANK = {
14521
14664
  completed: 2
14522
14665
  };
14523
14666
  function TaskRow({ task, isFirst }) {
14524
- const prefix = isFirst ? /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: " \u23BF " }) : /* @__PURE__ */ jsx11(Text11, { children: " " });
14525
- const suffix = task.blockedBy.length > 0 ? /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: ` \u27F5 ${task.blockedBy.length}` }) : null;
14667
+ const prefix = isFirst ? /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: " \u23BF " }) : /* @__PURE__ */ jsx12(Text12, { children: " " });
14668
+ const suffix = task.blockedBy.length > 0 ? /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: ` \u27F5 ${task.blockedBy.length}` }) : null;
14526
14669
  switch (task.status) {
14527
14670
  case "completed":
14528
- return /* @__PURE__ */ jsxs9(Text11, { children: [
14671
+ return /* @__PURE__ */ jsxs10(Text12, { children: [
14529
14672
  prefix,
14530
- /* @__PURE__ */ jsx11(Text11, { color: "green", children: "\u2713" }),
14673
+ /* @__PURE__ */ jsx12(Text12, { color: "green", children: "\u2713" }),
14531
14674
  " ",
14532
- /* @__PURE__ */ jsx11(Text11, { color: "gray", strikethrough: true, children: task.description }),
14675
+ /* @__PURE__ */ jsx12(Text12, { color: "gray", strikethrough: true, children: task.description }),
14533
14676
  suffix
14534
14677
  ] });
14535
14678
  case "in_progress":
14536
- return /* @__PURE__ */ jsxs9(Text11, { children: [
14679
+ return /* @__PURE__ */ jsxs10(Text12, { children: [
14537
14680
  prefix,
14538
- /* @__PURE__ */ jsx11(Text11, { color: ACCENT_HEX, children: "\u25A0" }),
14681
+ /* @__PURE__ */ jsx12(Text12, { color: ACCENT_HEX, children: "\u25A0" }),
14539
14682
  " ",
14540
- /* @__PURE__ */ jsx11(Text11, { color: ACCENT_HEX, bold: true, children: task.description }),
14683
+ /* @__PURE__ */ jsx12(Text12, { color: ACCENT_HEX, bold: true, children: task.description }),
14541
14684
  suffix
14542
14685
  ] });
14543
14686
  case "pending":
14544
14687
  default:
14545
- return /* @__PURE__ */ jsxs9(Text11, { children: [
14688
+ return /* @__PURE__ */ jsxs10(Text12, { children: [
14546
14689
  prefix,
14547
14690
  "\u25A1 ",
14548
14691
  task.description,
@@ -14554,9 +14697,9 @@ function SummaryRow({
14554
14697
  hidden,
14555
14698
  counts
14556
14699
  }) {
14557
- return /* @__PURE__ */ jsxs9(Text11, { children: [
14700
+ return /* @__PURE__ */ jsxs10(Text12, { children: [
14558
14701
  " ",
14559
- /* @__PURE__ */ jsxs9(Text11, { dimColor: true, children: [
14702
+ /* @__PURE__ */ jsxs10(Text12, { dimColor: true, children: [
14560
14703
  "... ",
14561
14704
  hidden,
14562
14705
  " More, ",
@@ -14605,17 +14748,17 @@ function TaskFooter({ tasks }) {
14605
14748
  startedAt,
14606
14749
  activeWord: `TASK: ${spinnerTask.description}...`
14607
14750
  } : null;
14608
- return /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", marginTop: 1, children: [
14609
- spinnerSpec ? /* @__PURE__ */ jsx11(Spinner, { spec: spinnerSpec }) : null,
14610
- visible.map((t, i) => /* @__PURE__ */ jsx11(TaskRow, { task: t, isFirst: i === 0 }, t.id)),
14611
- hidden > 0 ? /* @__PURE__ */ jsx11(SummaryRow, { hidden, counts }) : null
14751
+ return /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", marginTop: 1, children: [
14752
+ spinnerSpec ? /* @__PURE__ */ jsx12(Spinner, { spec: spinnerSpec }) : null,
14753
+ visible.map((t, i) => /* @__PURE__ */ jsx12(TaskRow, { task: t, isFirst: i === 0 }, t.id)),
14754
+ hidden > 0 ? /* @__PURE__ */ jsx12(SummaryRow, { hidden, counts }) : null
14612
14755
  ] });
14613
14756
  }
14614
14757
 
14615
14758
  // src/ui/todo-footer.tsx
14616
14759
  import { useEffect as useEffect5, useState as useState8 } from "react";
14617
- import { Box as Box12, Text as Text12 } from "ink";
14618
- import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
14760
+ import { Box as Box13, Text as Text13 } from "ink";
14761
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
14619
14762
  var MAX_VISIBLE2 = 5;
14620
14763
  var MIN_VISIBLE_TODOS = 2;
14621
14764
  function todoFooterVisible(todos) {
@@ -14627,25 +14770,25 @@ var STATUS_RANK2 = {
14627
14770
  completed: 2
14628
14771
  };
14629
14772
  function TodoRow({ todo, isFirst }) {
14630
- const prefix = isFirst ? /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: " \u23BF " }) : /* @__PURE__ */ jsx12(Text12, { children: " " });
14773
+ const prefix = isFirst ? /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: " \u23BF " }) : /* @__PURE__ */ jsx13(Text13, { children: " " });
14631
14774
  switch (todo.status) {
14632
14775
  case "completed":
14633
- return /* @__PURE__ */ jsxs10(Text12, { children: [
14776
+ return /* @__PURE__ */ jsxs11(Text13, { children: [
14634
14777
  prefix,
14635
- /* @__PURE__ */ jsx12(Text12, { color: "green", children: "\u2713" }),
14778
+ /* @__PURE__ */ jsx13(Text13, { color: "green", children: "\u2713" }),
14636
14779
  " ",
14637
- /* @__PURE__ */ jsx12(Text12, { color: "gray", strikethrough: true, children: todo.description })
14780
+ /* @__PURE__ */ jsx13(Text13, { color: "gray", strikethrough: true, children: todo.description })
14638
14781
  ] });
14639
14782
  case "in_progress":
14640
- return /* @__PURE__ */ jsxs10(Text12, { children: [
14783
+ return /* @__PURE__ */ jsxs11(Text13, { children: [
14641
14784
  prefix,
14642
- /* @__PURE__ */ jsx12(Text12, { color: "blue", children: "\u25A0" }),
14785
+ /* @__PURE__ */ jsx13(Text13, { color: "blue", children: "\u25A0" }),
14643
14786
  " ",
14644
- /* @__PURE__ */ jsx12(Text12, { color: "blue", bold: true, children: todo.description })
14787
+ /* @__PURE__ */ jsx13(Text13, { color: "blue", bold: true, children: todo.description })
14645
14788
  ] });
14646
14789
  case "pending":
14647
14790
  default:
14648
- return /* @__PURE__ */ jsxs10(Text12, { children: [
14791
+ return /* @__PURE__ */ jsxs11(Text13, { children: [
14649
14792
  prefix,
14650
14793
  "\u25A1 ",
14651
14794
  todo.description
@@ -14656,9 +14799,9 @@ function SummaryRow2({
14656
14799
  hidden,
14657
14800
  counts
14658
14801
  }) {
14659
- return /* @__PURE__ */ jsxs10(Text12, { children: [
14802
+ return /* @__PURE__ */ jsxs11(Text13, { children: [
14660
14803
  " ",
14661
- /* @__PURE__ */ jsxs10(Text12, { dimColor: true, children: [
14804
+ /* @__PURE__ */ jsxs11(Text13, { dimColor: true, children: [
14662
14805
  "... ",
14663
14806
  hidden,
14664
14807
  " More, ",
@@ -14707,15 +14850,15 @@ function TodoFooter({ todos }) {
14707
14850
  startedAt,
14708
14851
  activeWord: `TODO: ${spinnerTodo.description}...`
14709
14852
  } : null;
14710
- return /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", marginTop: 1, children: [
14711
- spinnerSpec ? /* @__PURE__ */ jsx12(Spinner, { spec: spinnerSpec }) : null,
14712
- visible.map((t, i) => /* @__PURE__ */ jsx12(TodoRow, { todo: t, isFirst: i === 0 }, t.id)),
14713
- hidden > 0 ? /* @__PURE__ */ jsx12(SummaryRow2, { hidden, counts }) : null
14853
+ return /* @__PURE__ */ jsxs11(Box13, { flexDirection: "column", marginTop: 1, children: [
14854
+ spinnerSpec ? /* @__PURE__ */ jsx13(Spinner, { spec: spinnerSpec }) : null,
14855
+ visible.map((t, i) => /* @__PURE__ */ jsx13(TodoRow, { todo: t, isFirst: i === 0 }, t.id)),
14856
+ hidden > 0 ? /* @__PURE__ */ jsx13(SummaryRow2, { hidden, counts }) : null
14714
14857
  ] });
14715
14858
  }
14716
14859
 
14717
14860
  // src/ui/viewport.tsx
14718
- import { Fragment as Fragment5, jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
14861
+ import { Fragment as Fragment5, jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
14719
14862
  var MIN_ROWS = 3;
14720
14863
  var H_PAD = 2;
14721
14864
  function Viewport({ store, rows, resolveModal }) {
@@ -14829,7 +14972,7 @@ function Viewport({ store, rows, resolveModal }) {
14829
14972
  }, [hasPending]);
14830
14973
  const displayLines = hasPending && !blinkOn ? highlighted.map((l) => blinkPendingOff(l)) : highlighted;
14831
14974
  if (inStreamModal) {
14832
- const modalEl = /* @__PURE__ */ jsx13(
14975
+ const modalEl = /* @__PURE__ */ jsx14(
14833
14976
  InStreamModal,
14834
14977
  {
14835
14978
  modal: inStreamModal,
@@ -14839,26 +14982,26 @@ function Viewport({ store, rows, resolveModal }) {
14839
14982
  }
14840
14983
  );
14841
14984
  if (inStreamModal.kind === "approval") {
14842
- return /* @__PURE__ */ jsxs11(Box13, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflowY: "hidden", paddingX: H_PAD, children: [
14843
- displayLines.length > 0 ? /* @__PURE__ */ jsx13(Text13, { children: displayLines.join("\n") }) : null,
14985
+ return /* @__PURE__ */ jsxs12(Box14, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflowY: "hidden", paddingX: H_PAD, children: [
14986
+ displayLines.length > 0 ? /* @__PURE__ */ jsx14(Text14, { children: displayLines.join("\n") }) : null,
14844
14987
  modalEl,
14845
- /* @__PURE__ */ jsx13(Box13, { flexGrow: 1 })
14988
+ /* @__PURE__ */ jsx14(Box14, { flexGrow: 1 })
14846
14989
  ] });
14847
14990
  }
14848
- return /* @__PURE__ */ jsxs11(Box13, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflowY: "hidden", paddingX: H_PAD, children: [
14849
- displayLines.length > 0 ? /* @__PURE__ */ jsx13(Text13, { children: displayLines.join("\n") }) : null,
14850
- /* @__PURE__ */ jsx13(Box13, { flexGrow: 1 }),
14991
+ return /* @__PURE__ */ jsxs12(Box14, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflowY: "hidden", paddingX: H_PAD, children: [
14992
+ displayLines.length > 0 ? /* @__PURE__ */ jsx14(Text14, { children: displayLines.join("\n") }) : null,
14993
+ /* @__PURE__ */ jsx14(Box14, { flexGrow: 1 }),
14851
14994
  modalEl
14852
14995
  ] });
14853
14996
  }
14854
- return /* @__PURE__ */ jsxs11(Box13, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflowY: "hidden", paddingX: H_PAD, children: [
14855
- displayLines.length > 0 ? /* @__PURE__ */ jsx13(Text13, { children: displayLines.join("\n") }) : null,
14856
- hasSpinner && !anyFooterVisible ? /* @__PURE__ */ jsx13(SpinnerWrapper, { store }) : null,
14857
- hasSpinner && anyFooterVisible ? /* @__PURE__ */ jsxs11(Fragment5, { children: [
14858
- /* @__PURE__ */ jsx13(TaskFooter, { tasks }),
14859
- /* @__PURE__ */ jsx13(TodoFooter, { todos })
14997
+ return /* @__PURE__ */ jsxs12(Box14, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflowY: "hidden", paddingX: H_PAD, children: [
14998
+ displayLines.length > 0 ? /* @__PURE__ */ jsx14(Text14, { children: displayLines.join("\n") }) : null,
14999
+ hasSpinner && !anyFooterVisible ? /* @__PURE__ */ jsx14(SpinnerWrapper, { store }) : null,
15000
+ hasSpinner && anyFooterVisible ? /* @__PURE__ */ jsxs12(Fragment5, { children: [
15001
+ /* @__PURE__ */ jsx14(TaskFooter, { tasks }),
15002
+ /* @__PURE__ */ jsx14(TodoFooter, { todos })
14860
15003
  ] }) : null,
14861
- /* @__PURE__ */ jsx13(Box13, { flexGrow: 1 })
15004
+ /* @__PURE__ */ jsx14(Box14, { flexGrow: 1 })
14862
15005
  ] });
14863
15006
  }
14864
15007
  function SpinnerWrapper({
@@ -14866,7 +15009,7 @@ function SpinnerWrapper({
14866
15009
  }) {
14867
15010
  const spec = store((s) => s.spinner);
14868
15011
  if (!spec) return null;
14869
- return /* @__PURE__ */ jsx13(Spinner, { spec });
15012
+ return /* @__PURE__ */ jsx14(Spinner, { spec });
14870
15013
  }
14871
15014
  function chromeRowsFor(hasSpinner, modal, todos, tasks, cols) {
14872
15015
  if (modal) {
@@ -14941,7 +15084,7 @@ function InStreamModal({
14941
15084
  }) {
14942
15085
  switch (modal.kind) {
14943
15086
  case "approval":
14944
- return /* @__PURE__ */ jsx13(
15087
+ return /* @__PURE__ */ jsx14(
14945
15088
  ApprovalPrompt,
14946
15089
  {
14947
15090
  decision: modal.decision,
@@ -14953,9 +15096,9 @@ function InStreamModal({
14953
15096
  }
14954
15097
  );
14955
15098
  case "ask":
14956
- return /* @__PURE__ */ jsx13(AskPanel, { req: modal.req, onResolve: (value) => resolveModal(value) });
15099
+ return /* @__PURE__ */ jsx14(AskPanel, { req: modal.req, onResolve: (value) => resolveModal(value) });
14957
15100
  case "pick":
14958
- return /* @__PURE__ */ jsx13(
15101
+ return /* @__PURE__ */ jsx14(
14959
15102
  PickList,
14960
15103
  {
14961
15104
  opts: modal.opts,
@@ -14964,7 +15107,7 @@ function InStreamModal({
14964
15107
  }
14965
15108
  );
14966
15109
  case "pickH":
14967
- return /* @__PURE__ */ jsx13(
15110
+ return /* @__PURE__ */ jsx14(
14968
15111
  PickHorizontal,
14969
15112
  {
14970
15113
  opts: modal.opts,
@@ -14973,7 +15116,7 @@ function InStreamModal({
14973
15116
  }
14974
15117
  );
14975
15118
  case "slider":
14976
- return /* @__PURE__ */ jsx13(
15119
+ return /* @__PURE__ */ jsx14(
14977
15120
  SliderPicker,
14978
15121
  {
14979
15122
  opts: modal.opts,
@@ -14982,7 +15125,7 @@ function InStreamModal({
14982
15125
  }
14983
15126
  );
14984
15127
  case "viewer":
14985
- return /* @__PURE__ */ jsx13(
15128
+ return /* @__PURE__ */ jsx14(
14986
15129
  ScrollViewer,
14987
15130
  {
14988
15131
  opts: modal.opts,
@@ -15005,7 +15148,7 @@ function hitTestJumpButton(row, col) {
15005
15148
  }
15006
15149
 
15007
15150
  // src/ui/app.tsx
15008
- import { Fragment as Fragment6, jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
15151
+ import { Fragment as Fragment6, jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
15009
15152
  function pinnedBottomRows(inputRows, extraRows) {
15010
15153
  return STATUS_LINE_ROWS + INPUT_TOP_SPACER_ROWS + inputRows + extraRows;
15011
15154
  }
@@ -15036,12 +15179,13 @@ function JumpToBottomHint({
15036
15179
  store.getState().setJumpButtonHovered(false);
15037
15180
  };
15038
15181
  }, [show, row, colStart, colEnd, store]);
15039
- return /* @__PURE__ */ jsx14(Box14, { flexShrink: 0, height: INPUT_TOP_SPACER_ROWS, justifyContent: "center", children: show ? /* @__PURE__ */ jsx14(Text14, { color: "black", backgroundColor: hovered ? "cyan" : "gray", children: JUMP_TO_BOTTOM_LABEL }) : null });
15182
+ return /* @__PURE__ */ jsx15(Box15, { flexShrink: 0, height: INPUT_TOP_SPACER_ROWS, justifyContent: "center", children: show ? /* @__PURE__ */ jsx15(Text15, { color: "black", backgroundColor: hovered ? "cyan" : "gray", children: JUMP_TO_BOTTOM_LABEL }) : null });
15040
15183
  }
15041
15184
  var MIN_FRAME_ROWS = 4;
15042
15185
  function App({ store }) {
15043
15186
  const {
15044
15187
  setup,
15188
+ trust,
15045
15189
  modal,
15046
15190
  slashCommands,
15047
15191
  mentionFiles,
@@ -15056,6 +15200,7 @@ function App({ store }) {
15056
15200
  } = store(
15057
15201
  useShallow3((s) => ({
15058
15202
  setup: s.setup,
15203
+ trust: s.trust,
15059
15204
  modal: s.modal,
15060
15205
  slashCommands: s.slashCommands,
15061
15206
  mentionFiles: s.mentionFiles,
@@ -15092,12 +15237,13 @@ function App({ store }) {
15092
15237
  [store]
15093
15238
  );
15094
15239
  React12.useEffect(() => {
15095
- if (setup || overlayModal) setCursorTarget(null);
15096
- }, [setup, overlayModal]);
15097
- if (setup) {
15098
- return /* @__PURE__ */ jsxs12(Box14, { flexDirection: "column", children: [
15099
- /* @__PURE__ */ jsx14(SetupView, { state: setup }),
15100
- modal?.kind === "input" ? /* @__PURE__ */ jsx14(
15240
+ if (setup || trust || overlayModal) setCursorTarget(null);
15241
+ }, [setup, trust, overlayModal]);
15242
+ const fullscreenView = setup ? /* @__PURE__ */ jsx15(SetupView, { state: setup }) : trust ? /* @__PURE__ */ jsx15(TrustView, { state: trust }) : null;
15243
+ if (fullscreenView) {
15244
+ return /* @__PURE__ */ jsxs13(Box15, { flexDirection: "column", children: [
15245
+ fullscreenView,
15246
+ modal?.kind === "input" ? /* @__PURE__ */ jsx15(
15101
15247
  InputBox,
15102
15248
  {
15103
15249
  options: modal.opts,
@@ -15106,14 +15252,14 @@ function App({ store }) {
15106
15252
  onMeasure: onMeasureInput
15107
15253
  }
15108
15254
  ) : null,
15109
- modal?.kind === "pickH" ? /* @__PURE__ */ jsx14(
15255
+ modal?.kind === "pickH" ? /* @__PURE__ */ jsx15(
15110
15256
  PickHorizontal,
15111
15257
  {
15112
15258
  opts: modal.opts,
15113
15259
  onResolve: (value) => resolveModal(value)
15114
15260
  }
15115
15261
  ) : null,
15116
- modal?.kind === "pick" ? /* @__PURE__ */ jsx14(
15262
+ modal?.kind === "pick" ? /* @__PURE__ */ jsx15(
15117
15263
  PickList,
15118
15264
  {
15119
15265
  opts: modal.opts,
@@ -15126,10 +15272,10 @@ function App({ store }) {
15126
15272
  const indicatorRows = modeIndicator ? 1 : 0;
15127
15273
  const bottomChromeRows = overlayModal ? 0 : pinnedBottomRows(inputRows, indicatorRows);
15128
15274
  const viewportRows = Math.max(3, termRows - bottomChromeRows - 1);
15129
- return /* @__PURE__ */ jsxs12(Box14, { flexDirection: "column", height: Math.max(MIN_FRAME_ROWS, termRows - 1), overflowY: "hidden", children: [
15130
- /* @__PURE__ */ jsx14(Viewport, { store, rows: viewportRows, resolveModal }),
15131
- overlayModal ? null : /* @__PURE__ */ jsxs12(Fragment6, { children: [
15132
- /* @__PURE__ */ jsx14(
15275
+ return /* @__PURE__ */ jsxs13(Box15, { flexDirection: "column", height: Math.max(MIN_FRAME_ROWS, termRows - 1), overflowY: "hidden", children: [
15276
+ /* @__PURE__ */ jsx15(Viewport, { store, rows: viewportRows, resolveModal }),
15277
+ overlayModal ? null : /* @__PURE__ */ jsxs13(Fragment6, { children: [
15278
+ /* @__PURE__ */ jsx15(
15133
15279
  JumpToBottomHint,
15134
15280
  {
15135
15281
  store,
@@ -15140,7 +15286,7 @@ function App({ store }) {
15140
15286
  indicatorRows
15141
15287
  }
15142
15288
  ),
15143
- /* @__PURE__ */ jsx14(Box14, { flexShrink: 0, flexDirection: "column", children: /* @__PURE__ */ jsx14(
15289
+ /* @__PURE__ */ jsx15(Box15, { flexShrink: 0, flexDirection: "column", children: /* @__PURE__ */ jsx15(
15144
15290
  InputBox,
15145
15291
  {
15146
15292
  options: {
@@ -15166,11 +15312,11 @@ function App({ store }) {
15166
15312
  onImageAttached: imagePaste?.attached
15167
15313
  }
15168
15314
  ) }),
15169
- /* @__PURE__ */ jsxs12(Box14, { flexShrink: 0, flexDirection: "column", children: [
15170
- /* @__PURE__ */ jsx14(StatusLine, { store, shellMode }),
15171
- modeIndicator ? /* @__PURE__ */ jsxs12(Box14, { children: [
15172
- /* @__PURE__ */ jsx14(Text14, { color: modeIndicator.color, children: ` ${modeIndicator.label}` }),
15173
- /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: ` ${PERMISSION_MODE_HINT}` })
15315
+ /* @__PURE__ */ jsxs13(Box15, { flexShrink: 0, flexDirection: "column", children: [
15316
+ /* @__PURE__ */ jsx15(StatusLine, { store, shellMode }),
15317
+ modeIndicator ? /* @__PURE__ */ jsxs13(Box15, { children: [
15318
+ /* @__PURE__ */ jsx15(Text15, { color: modeIndicator.color, children: ` ${modeIndicator.label}` }),
15319
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: ` ${PERMISSION_MODE_HINT}` })
15174
15320
  ] }) : null
15175
15321
  ] })
15176
15322
  ] })
@@ -15544,6 +15690,7 @@ function createAppStore(opts = {}) {
15544
15690
  escHandler: null,
15545
15691
  thinkingLabel: void 0,
15546
15692
  setup: null,
15693
+ trust: null,
15547
15694
  termCols: process.stdout.columns ?? 80,
15548
15695
  termRows: process.stdout.rows ?? 24,
15549
15696
  scrollOffset: 0,
@@ -15685,6 +15832,12 @@ function createAppStore(opts = {}) {
15685
15832
  beginSetup(state) {
15686
15833
  set({ setup: state });
15687
15834
  },
15835
+ beginTrust(state) {
15836
+ set({ trust: state });
15837
+ },
15838
+ endTrust() {
15839
+ set({ trust: null });
15840
+ },
15688
15841
  setSetupPrompt(prompt) {
15689
15842
  const cur = get().setup;
15690
15843
  if (!cur) return;
@@ -16423,6 +16576,12 @@ var Screen = class {
16423
16576
  endSetup() {
16424
16577
  this.store.getState().endSetup();
16425
16578
  }
16579
+ beginTrust(state) {
16580
+ this.store.getState().beginTrust(state);
16581
+ }
16582
+ endTrust() {
16583
+ this.store.getState().endTrust();
16584
+ }
16426
16585
  async promptInput(opts) {
16427
16586
  return this.store.getState().openInputModal(opts);
16428
16587
  }
@@ -17173,7 +17332,7 @@ async function handleDiff(ctx, args) {
17173
17332
 
17174
17333
  // src/doctor.ts
17175
17334
  import { readFile as readFile21 } from "fs/promises";
17176
- import { basename as basename4 } from "path";
17335
+ import { basename as basename5 } from "path";
17177
17336
  import { Command } from "commander";
17178
17337
  function zodIssues(err) {
17179
17338
  if (err && typeof err === "object" && Array.isArray(err.issues)) {
@@ -17291,7 +17450,7 @@ async function diagnoseConfig(opts = {}) {
17291
17450
  for (const e of hooks.errors) {
17292
17451
  issues.push({
17293
17452
  level: "warn",
17294
- title: `invalid hook file: ${basename4(e.source)}`,
17453
+ title: `invalid hook file: ${basename5(e.source)}`,
17295
17454
  detail: e.message,
17296
17455
  hint: "fix or remove it \u2014 nova skips it and continues"
17297
17456
  });
@@ -18323,8 +18482,17 @@ async function handleResume(ctx, arg) {
18323
18482
  }
18324
18483
 
18325
18484
  // src/commands/review.ts
18485
+ var DEFAULT_FOCUS = "correctness and regressions in adjacent code";
18486
+ var PR_REF_RE = /^(?:#?(\d+)|https?:\/\/github\.com\/[^/\s]+\/[^/\s]+\/pull\/(\d+))\b\s*/;
18326
18487
  function handleReview(args) {
18327
- const focus = args.trim() || "correctness and regressions in adjacent code";
18488
+ const trimmed = args.trim();
18489
+ const pr = trimmed.match(PR_REF_RE);
18490
+ if (pr) {
18491
+ const number = pr[1] ?? pr[2] ?? "";
18492
+ const focus2 = trimmed.slice(pr[0].length).trim() || DEFAULT_FOCUS;
18493
+ return { kind: "prompt", text: prReviewText(number, focus2) };
18494
+ }
18495
+ const focus = trimmed || DEFAULT_FOCUS;
18328
18496
  const text = `Review the repository's uncommitted changes. Do NOT edit anything \u2014 this is a review only.
18329
18497
 
18330
18498
  1. Run \`git status\` and \`git diff\` (include \`git diff --staged\`) to see every pending change.
@@ -18334,6 +18502,15 @@ function handleReview(args) {
18334
18502
  Write your review in the same language and script as this request.`;
18335
18503
  return { kind: "prompt", text };
18336
18504
  }
18505
+ function prReviewText(pr, focus) {
18506
+ return `Review GitHub pull request #${pr}. Do NOT edit anything and do NOT post any comment to GitHub \u2014 this is a read-only review.
18507
+
18508
+ 1. Fetch it with \`gh pr view ${pr}\` (title, description, checks) and \`gh pr diff ${pr}\` (the full diff). If \`gh\` is missing or not authenticated, say so and stop.
18509
+ 2. Review the diff with a focus on ${focus}. Flag anything risky, surprising, broken, or inconsistent with the surrounding code and conventions. Note missing tests or edge cases where they matter.
18510
+ 3. Be concrete \u2014 reference file paths and line numbers, group findings by severity, and skip praise and restating the obvious. If the diff looks clean, say so plainly.
18511
+
18512
+ Write your review in the same language and script as this request.`;
18513
+ }
18337
18514
 
18338
18515
  // src/commands/rewind.ts
18339
18516
  import { relative as relative4 } from "path";
@@ -18570,8 +18747,7 @@ async function handleSkills(ctx) {
18570
18747
  topRuleColor: PURPLE_HEX,
18571
18748
  render: (s, selected) => {
18572
18749
  const name = s.name.padEnd(nameWidth, " ");
18573
- const trig = s.triggers.length > 0 ? ` ${dim(`triggers: ${s.triggers.join(", ")}`)}` : "";
18574
- return `${pickerArrow(selected)} ${name} ${s.description}${trig}`;
18750
+ return `${pickerArrow(selected)} ${name} ${s.description}`;
18575
18751
  }
18576
18752
  });
18577
18753
  }
@@ -18936,8 +19112,8 @@ function registerBuiltinSlashCommands(ctx) {
18936
19112
  });
18937
19113
  ctx.registry.register({
18938
19114
  name: "review",
18939
- description: "review the current uncommitted diff (read-only)",
18940
- argHint: "[focus\u2026]",
19115
+ description: "review the uncommitted diff, or a GitHub PR by number (read-only)",
19116
+ argHint: "[PR# | focus\u2026]",
18941
19117
  source: { kind: "builtin" },
18942
19118
  run: (_c, args) => handleReview(args)
18943
19119
  });
@@ -19170,7 +19346,8 @@ async function createContext(settings, screen, cliOpts) {
19170
19346
  logger
19171
19347
  } : void 0;
19172
19348
  const skillItems = skillsOpts ? getSkillList(skillsOpts) : [];
19173
- const skillsBlock = skillsOpts ? renderSkillsBlock(skillItems, settings.skills.maxIndexBytes) : "";
19349
+ const modelSkillItems = skillItems.filter((s) => !s.disableModelInvocation);
19350
+ const skillsBlock = skillsOpts ? renderSkillsBlock(modelSkillItems, settings.skills.maxIndexBytes) : "";
19174
19351
  if (skillsOpts) {
19175
19352
  await transcript.append({
19176
19353
  kind: "skills_loaded",
@@ -21537,6 +21714,68 @@ function buildUpgradeCommand() {
21537
21714
  });
21538
21715
  }
21539
21716
 
21717
+ // src/workspace-trust.ts
21718
+ import { homedir as homedir17 } from "os";
21719
+ async function isWorkspaceTrusted(settings, workspace) {
21720
+ if (!settings.trust.enabled) return true;
21721
+ const wsCanon = await canonicalizePath(workspace, ".");
21722
+ for (const root of settings.trust.trustedRoots) {
21723
+ const rootCanon = await canonicalizePath(workspace, root);
21724
+ if (isWithin(rootCanon, wsCanon)) return true;
21725
+ }
21726
+ return false;
21727
+ }
21728
+ async function trustWorkspace(settings, workspace, configPath) {
21729
+ const wsCanon = await canonicalizePath(workspace, ".");
21730
+ const roots = settings.trust.trustedRoots.includes(wsCanon) ? settings.trust.trustedRoots : [...settings.trust.trustedRoots, wsCanon];
21731
+ settings.trust = { ...settings.trust, trustedRoots: roots };
21732
+ const home = await canonicalizePath(homedir17(), ".");
21733
+ if (wsCanon === home) return;
21734
+ await saveSettings({ trust: settings.trust }, configPath);
21735
+ }
21736
+ async function ensureWorkspaceTrust(settings, screen, workspace, configPath) {
21737
+ if (await isWorkspaceTrusted(settings, workspace)) return;
21738
+ const wsCanon = await canonicalizePath(workspace, ".");
21739
+ screen.beginTrust({
21740
+ version: await readCliVersion(),
21741
+ workspace: wsCanon,
21742
+ lines: [
21743
+ "nova has not been granted access to this folder yet. Granting access",
21744
+ "lets it read and edit files here (and in subdirectories) without",
21745
+ "confirming each time, and runs any project hooks the folder defines.",
21746
+ "Only trust folders you recognize \u2014 declining exits without touching",
21747
+ "anything."
21748
+ ]
21749
+ });
21750
+ try {
21751
+ const choice = await screen.pickOne({
21752
+ items: [{ trust: true }, { trust: false }],
21753
+ footer: dim("\u2191/\u2193 choose \xB7 Enter confirm \xB7 Ctrl+C to exit"),
21754
+ border: false,
21755
+ topRuleColor: PURPLE_HEX,
21756
+ render: (it, selected) => {
21757
+ const label = it.trust ? "Yes, trust this folder" : "No, exit";
21758
+ return `${pickerArrow(selected)} ${label}`;
21759
+ }
21760
+ });
21761
+ if (choice === null || !choice.trust) {
21762
+ await fatalExit(screen, `workspace not trusted \u2014 exiting.
21763
+ ${wsCanon}`, 1);
21764
+ }
21765
+ try {
21766
+ await trustWorkspace(settings, workspace, configPath);
21767
+ } catch (err) {
21768
+ const msg = err instanceof Error ? err.message : String(err);
21769
+ screen.card(`could not persist workspace trust: ${msg}`, {
21770
+ kind: "warn",
21771
+ title: "workspace trust"
21772
+ });
21773
+ }
21774
+ } finally {
21775
+ screen.endTrust();
21776
+ }
21777
+ }
21778
+
21540
21779
  // src/index.ts
21541
21780
  function applyCliOverrides(settings, opts) {
21542
21781
  if (opts.model) {
@@ -21610,6 +21849,14 @@ async function runHeadlessMode(settings, positional, opts) {
21610
21849
  dieHeadless(err instanceof Error ? err.message : String(err), 2);
21611
21850
  }
21612
21851
  const approvalPolicy = opts.dangerouslySkipPermissions ? "allow" : "deny";
21852
+ const headlessCwd = opts.cwd ?? process.cwd();
21853
+ if (!opts.dangerouslySkipPermissions && !await isWorkspaceTrusted(settings, headlessCwd)) {
21854
+ dieHeadless(
21855
+ `workspace not trusted: ${headlessCwd}
21856
+ Trust it by running nova interactively here once, by adding it to trust.trustedRoots in nova.config.json, or by passing --dangerously-skip-permissions.`,
21857
+ 1
21858
+ );
21859
+ }
21613
21860
  let code;
21614
21861
  try {
21615
21862
  code = await runHeadless(settings, {
@@ -21680,6 +21927,9 @@ ${formatIssues(report)}
21680
21927
  "apiKey is not set in nova.config.json (or equivalent settings file)."
21681
21928
  );
21682
21929
  }
21930
+ if (!opts.dangerouslySkipPermissions) {
21931
+ await ensureWorkspaceTrust(settings, screen, opts.cwd ?? process.cwd());
21932
+ }
21683
21933
  const ctx = await createContext(settings, screen, {
21684
21934
  ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
21685
21935
  ...opts.resume !== void 0 ? { resume: opts.resume } : {},