@noir-ai/cli 1.8.0 → 1.9.0-beta.1

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/dist/tui/index.js CHANGED
@@ -1181,6 +1181,264 @@ function shouldShowBanner(opts) {
1181
1181
  return true;
1182
1182
  }
1183
1183
 
1184
+ // src/tui/commands/registry.ts
1185
+ var DESTRUCTIVE_PREFIXES = [
1186
+ ["context", "index"],
1187
+ ["context", "forget"],
1188
+ ["memory", "save"],
1189
+ ["memory", "forget"],
1190
+ ["memory", "consolidate"],
1191
+ ["task", "new"],
1192
+ ["task", "advance"],
1193
+ ["skills", "sync"],
1194
+ ["daemon", "start"],
1195
+ ["daemon", "stop"],
1196
+ ["daemon", "restart"],
1197
+ ["init"],
1198
+ ["create"],
1199
+ ["sync"],
1200
+ ["install"],
1201
+ ["update"]
1202
+ ];
1203
+ function isDestructive(argv) {
1204
+ return DESTRUCTIVE_PREFIXES.some((prefix) => {
1205
+ if (argv.length < prefix.length) return false;
1206
+ return prefix.every((tok, i) => argv[i] === tok);
1207
+ });
1208
+ }
1209
+ function capitalizeFirst(s) {
1210
+ if (s.length === 0) return s;
1211
+ return `${s[0]?.toUpperCase() ?? ""}${s.slice(1)}`;
1212
+ }
1213
+ function hasAction(cmd) {
1214
+ return cmd._actionHandler != null;
1215
+ }
1216
+ function collectLeaves(cmd, path, out) {
1217
+ const children = cmd.commands;
1218
+ const isLeaf = hasAction(cmd) && children.length === 0;
1219
+ if (isLeaf) {
1220
+ const argv = path;
1221
+ const id = argv.join(" ");
1222
+ const description = cmd.description() || argv.join(" ");
1223
+ out.push({
1224
+ id,
1225
+ label: argv.map(capitalizeFirst).join(": "),
1226
+ argv,
1227
+ description,
1228
+ category: argv[0] ?? "",
1229
+ keywords: argv,
1230
+ destructive: isDestructive(argv)
1231
+ });
1232
+ return;
1233
+ }
1234
+ for (const child of children) {
1235
+ collectLeaves(child, [...path, child.name()], out);
1236
+ }
1237
+ }
1238
+ function buildPaletteCommands(program2) {
1239
+ const out = [];
1240
+ collectLeaves(program2, [], out);
1241
+ return out;
1242
+ }
1243
+
1244
+ // src/tui/commands/sections.ts
1245
+ var HOME_SECTIONS = [
1246
+ {
1247
+ id: "status",
1248
+ label: "Status & context",
1249
+ hint: "snapshot \xB7 index \xB7 search \xB7 doctor",
1250
+ key: "1",
1251
+ items: [
1252
+ {
1253
+ id: "status",
1254
+ label: "Status",
1255
+ hint: "project + daemon + workflow + store snapshot (noir status)"
1256
+ },
1257
+ {
1258
+ id: "context index",
1259
+ label: "Index project",
1260
+ hint: "(re)index files into context \xB7 \u26A0 destructive",
1261
+ destructive: true
1262
+ },
1263
+ {
1264
+ id: "context search",
1265
+ label: "Search context",
1266
+ hint: "hybrid search over indexed files",
1267
+ needsArg: { prompt: "Context search query:", placeholder: "e.g. auth flow, deploy steps" }
1268
+ },
1269
+ {
1270
+ id: "doctor",
1271
+ label: "Doctor",
1272
+ hint: "environment + project health checks"
1273
+ },
1274
+ {
1275
+ id: "context status",
1276
+ label: "Context status",
1277
+ hint: "index freshness + counts"
1278
+ }
1279
+ ]
1280
+ },
1281
+ {
1282
+ id: "memory",
1283
+ label: "Memory",
1284
+ hint: "recall \xB7 save \xB7 sessions \xB7 forget \xB7 consolidate",
1285
+ key: "2",
1286
+ items: [
1287
+ {
1288
+ id: "memory recall",
1289
+ label: "Recall memory",
1290
+ hint: "search cross-session memory",
1291
+ needsArg: { prompt: "Recall query:", placeholder: "e.g. auth flow, deploy steps" }
1292
+ },
1293
+ {
1294
+ id: "memory save",
1295
+ label: "Save memory",
1296
+ hint: "save an observation \xB7 \u26A0 destructive",
1297
+ destructive: true,
1298
+ needsArg: { prompt: "Memory content:", placeholder: "what you want to remember" }
1299
+ },
1300
+ {
1301
+ id: "memory sessions",
1302
+ label: "Sessions",
1303
+ hint: "list recent memory sessions"
1304
+ },
1305
+ {
1306
+ id: "memory forget",
1307
+ label: "Forget memory",
1308
+ hint: "forget observations by id \xB7 \u26A0 destructive",
1309
+ destructive: true
1310
+ },
1311
+ {
1312
+ id: "memory consolidate",
1313
+ label: "Consolidate",
1314
+ hint: "fold observations into lessons \xB7 \u26A0 destructive",
1315
+ destructive: true
1316
+ }
1317
+ ]
1318
+ },
1319
+ {
1320
+ id: "workflow",
1321
+ label: "Workflow",
1322
+ hint: "next \xB7 status \xB7 advance \xB7 handoff \xB7 wrap",
1323
+ key: "3",
1324
+ items: [
1325
+ {
1326
+ id: "task next",
1327
+ label: "Next task",
1328
+ hint: "suggest next phase + applicable skill"
1329
+ },
1330
+ {
1331
+ id: "task status",
1332
+ label: "Task status",
1333
+ hint: "active task status"
1334
+ },
1335
+ {
1336
+ id: "task advance",
1337
+ label: "Advance task",
1338
+ hint: "advance to next phase \xB7 \u26A0 destructive",
1339
+ destructive: true
1340
+ },
1341
+ {
1342
+ id: "handoff",
1343
+ label: "Handoff",
1344
+ hint: "ready-to-paste host prompt"
1345
+ },
1346
+ {
1347
+ id: "wrap",
1348
+ label: "Wrap (end session)",
1349
+ hint: "session-end handoff",
1350
+ dispatch: ["wrap", "--write"]
1351
+ }
1352
+ ]
1353
+ },
1354
+ {
1355
+ id: "setup",
1356
+ label: "Setup & maintenance",
1357
+ hint: "init \xB7 create \xB7 sync \xB7 skills \xB7 install \xB7 update",
1358
+ key: "4",
1359
+ items: [
1360
+ {
1361
+ id: "init",
1362
+ label: "Init",
1363
+ hint: "scaffold Noir in this project \xB7 \u26A0 destructive",
1364
+ destructive: true
1365
+ },
1366
+ {
1367
+ id: "create",
1368
+ label: "Create",
1369
+ hint: "bootstrap in a new/empty dir \xB7 \u26A0 destructive",
1370
+ destructive: true
1371
+ },
1372
+ {
1373
+ id: "sync",
1374
+ label: "Sync",
1375
+ hint: "re-emit managed files + skills \xB7 \u26A0 destructive",
1376
+ destructive: true
1377
+ },
1378
+ {
1379
+ id: "skills list",
1380
+ label: "Skills list",
1381
+ hint: "list installed skills"
1382
+ },
1383
+ {
1384
+ id: "skills sync",
1385
+ label: "Sync skills",
1386
+ hint: "re-emit builtin skills \xB7 \u26A0 destructive",
1387
+ destructive: true
1388
+ },
1389
+ {
1390
+ id: "update",
1391
+ label: "Update",
1392
+ hint: "check for a new version \xB7 \u26A0 destructive",
1393
+ destructive: true
1394
+ },
1395
+ {
1396
+ id: "install",
1397
+ label: "Install / migrate",
1398
+ hint: "native install / migrate \xB7 \u26A0 destructive",
1399
+ destructive: true
1400
+ }
1401
+ ]
1402
+ },
1403
+ {
1404
+ id: "dashboard",
1405
+ label: "Dashboard (full-screen)",
1406
+ hint: "noir tui \xB7 noir palette \xB7 home",
1407
+ key: "5",
1408
+ items: [
1409
+ {
1410
+ id: "tui",
1411
+ label: "Dashboard (full-screen)",
1412
+ hint: "live status \xB7 /command \xB7 Ctrl+K palette (noir tui)"
1413
+ },
1414
+ {
1415
+ id: "palette",
1416
+ label: "All commands (fuzzy palette)",
1417
+ hint: "fuzzy search over every command (noir palette)"
1418
+ }
1419
+ ]
1420
+ }
1421
+ ];
1422
+ async function resolveSections(commands) {
1423
+ const byId = new Map(commands.map((c2) => [c2.id, c2]));
1424
+ const out = [];
1425
+ for (const section of HOME_SECTIONS) {
1426
+ const items = [];
1427
+ for (const action of section.items) {
1428
+ const entry = byId.get(action.id);
1429
+ if (!entry) continue;
1430
+ items.push({
1431
+ ...action,
1432
+ // Resolve the dispatch argv from the registry (never hand-written).
1433
+ dispatch: action.dispatch ?? [...entry.argv]
1434
+ });
1435
+ }
1436
+ if (items.length === 0) continue;
1437
+ out.push({ ...section, items });
1438
+ }
1439
+ return out;
1440
+ }
1441
+
1184
1442
  // src/commands/home.ts
1185
1443
  function tryProject() {
1186
1444
  try {
@@ -1195,7 +1453,7 @@ function shouldShowMigrationBanner(rec, currentVersion) {
1195
1453
  if (rec.dismissedVersions?.includes(currentVersion)) return false;
1196
1454
  return true;
1197
1455
  }
1198
- var COMMANDS_HINT = "Commands: init \xB7 create \xB7 sync \xB7 status \xB7 context \xB7 memory \xB7 skills \xB7 task \xB7 daemon \xB7 doctor";
1456
+ var COMMANDS_HINT = "Commands: status \xB7 context \xB7 memory \xB7 task \xB7 skills \xB7 daemon \xB7 doctor \xB7 install \xB7 update \xB7 tui \xB7 palette";
1199
1457
  async function home(opts, deps) {
1200
1458
  if (isInteractive(opts) && opts.tui !== false) {
1201
1459
  await runMenu(opts, deps);
@@ -1207,6 +1465,138 @@ async function home(opts, deps) {
1207
1465
  }
1208
1466
  await deps.dispatch(["status"]);
1209
1467
  }
1468
+ async function pickSection(clack, sections, opts) {
1469
+ const result = await clack.selectKey({
1470
+ message: "What would you like to do?",
1471
+ options: [
1472
+ ...sections.map((s) => ({
1473
+ value: s.id,
1474
+ label: s.label,
1475
+ hint: s.hint
1476
+ })),
1477
+ { value: "exit", label: "Exit", hint: "leave the home menu" }
1478
+ ]
1479
+ });
1480
+ if (clack.isCancel(result)) {
1481
+ clack.cancel("Cancelled.");
1482
+ fail(EXIT.CANCELLED, "cancelled", opts);
1483
+ }
1484
+ return result;
1485
+ }
1486
+ async function pickAction(clack, section, sections, opts) {
1487
+ const idx = sections.findIndex((s) => s.id === section.id);
1488
+ const hasPrev = idx > 0;
1489
+ const hasNext = idx !== -1 && idx < sections.length - 1;
1490
+ const result = await clack.select({
1491
+ message: section.label,
1492
+ initialValue: section.items[0]?.id,
1493
+ options: [
1494
+ ...section.items.map((a) => ({
1495
+ value: a.id,
1496
+ label: a.label,
1497
+ hint: a.hint
1498
+ })),
1499
+ // Navigation options so the user can move across sections without
1500
+ // re-entering the section picker (the "lancar" navigation requirement).
1501
+ ...hasPrev ? [{ value: "prev", label: "\u2190 Prev section", hint: sections[idx - 1]?.label }] : [],
1502
+ ...hasNext ? [{ value: "next", label: "\u2192 Next section", hint: sections[idx + 1]?.label }] : [],
1503
+ { value: "back", label: "\u2191 Back to sections", hint: "return to the section picker" }
1504
+ ]
1505
+ });
1506
+ if (clack.isCancel(result)) {
1507
+ clack.cancel("Back to the home menu.");
1508
+ return "back";
1509
+ }
1510
+ if (opts.verbose) {
1511
+ process.stderr.write(`noir: home: section '${section.id}' selected
1512
+ `);
1513
+ }
1514
+ return result;
1515
+ }
1516
+ async function collectArg(clack, action, opts) {
1517
+ if (!action.needsArg) return null;
1518
+ const value = await clack.text({
1519
+ message: action.needsArg.prompt,
1520
+ placeholder: action.needsArg.placeholder
1521
+ });
1522
+ if (clack.isCancel(value)) {
1523
+ clack.cancel("Cancelled.");
1524
+ fail(EXIT.CANCELLED, "cancelled", opts);
1525
+ }
1526
+ return String(value);
1527
+ }
1528
+ async function argvForAction(clack, action, opts) {
1529
+ const base = action.dispatch ?? [action.id];
1530
+ let argv = [...base];
1531
+ if (action.needsArg) {
1532
+ const arg = await collectArg(clack, action, opts);
1533
+ if (arg === null) return null;
1534
+ argv = [...base, arg];
1535
+ }
1536
+ if (action.destructive || isDestructive(argv)) {
1537
+ const confirmed = await clack.confirm({
1538
+ message: `Run \`noir ${argv.join(" ")}\`? This may modify project files / the store.`
1539
+ });
1540
+ if (clack.isCancel(confirmed) || !confirmed) return null;
1541
+ }
1542
+ return argv;
1543
+ }
1544
+ async function runGroupedMenu(clack, opts, deps, sections) {
1545
+ let currentSection = null;
1546
+ while (true) {
1547
+ if (currentSection === null) {
1548
+ const picked = await pickSection(clack, sections, opts);
1549
+ if (clack.isCancel(picked) || picked === "exit") {
1550
+ clack.outro("bye");
1551
+ return;
1552
+ }
1553
+ const idx = sections.findIndex((s) => s.id === picked);
1554
+ if (idx === -1) {
1555
+ continue;
1556
+ }
1557
+ currentSection = idx;
1558
+ continue;
1559
+ }
1560
+ const section = sections[currentSection];
1561
+ if (!section) {
1562
+ currentSection = null;
1563
+ continue;
1564
+ }
1565
+ const chosen = await pickAction(clack, section, sections, opts);
1566
+ if (clack.isCancel(chosen)) {
1567
+ currentSection = null;
1568
+ continue;
1569
+ }
1570
+ switch (chosen) {
1571
+ case "back": {
1572
+ currentSection = null;
1573
+ continue;
1574
+ }
1575
+ case "next": {
1576
+ currentSection = (currentSection + 1) % sections.length;
1577
+ continue;
1578
+ }
1579
+ case "prev": {
1580
+ currentSection = (currentSection - 1 + sections.length) % sections.length;
1581
+ continue;
1582
+ }
1583
+ default: {
1584
+ const action = section.items.find((a) => a.id === chosen);
1585
+ if (!action) {
1586
+ currentSection = null;
1587
+ continue;
1588
+ }
1589
+ const argv = await argvForAction(clack, action, opts);
1590
+ if (argv === null) {
1591
+ continue;
1592
+ }
1593
+ await deps.dispatch(argv);
1594
+ clack.outro("done");
1595
+ return;
1596
+ }
1597
+ }
1598
+ }
1599
+ }
1210
1600
  async function runMenu(opts, deps) {
1211
1601
  const clack = await import("@clack/prompts");
1212
1602
  const project = tryProject();
@@ -1245,62 +1635,8 @@ ${COMMANDS_HINT}
1245
1635
  );
1246
1636
  }
1247
1637
  clack.intro(project ? `noir \u2014 ${project.name}` : "noir");
1248
- const choice = await clack.select({
1249
- message: "What would you like to do?",
1250
- initialValue: "status",
1251
- options: [
1252
- { value: "status", label: "Status", hint: "project + daemon + store snapshot" },
1253
- { value: "index", label: "Index project", hint: "(re)index files into context" },
1254
- { value: "recall", label: "Recall memory", hint: "search cross-session memory" },
1255
- { value: "next", label: "Next task", hint: "suggest next phase + skill" },
1256
- { value: "handoff", label: "Handoff", hint: "ready-to-paste host prompt" },
1257
- { value: "daemon", label: "Start daemon", hint: "foreground daemon" },
1258
- { value: "sync", label: "Sync skills", hint: "re-emit builtin skills" },
1259
- { value: "exit", label: "Exit" }
1260
- ]
1261
- });
1262
- if (clack.isCancel(choice)) {
1263
- clack.cancel("Cancelled.");
1264
- fail(EXIT.CANCELLED, "cancelled", opts);
1265
- }
1266
- const argv = await argvForChoice(choice, clack, opts);
1267
- if (argv === null) {
1268
- clack.outro("bye");
1269
- return;
1270
- }
1271
- await deps.dispatch(argv);
1272
- clack.outro("done");
1273
- }
1274
- async function argvForChoice(choice, clack, opts) {
1275
- switch (choice) {
1276
- case "status":
1277
- return ["status"];
1278
- case "index":
1279
- return ["context", "index"];
1280
- case "next":
1281
- return ["task", "next"];
1282
- case "handoff":
1283
- return ["handoff"];
1284
- case "daemon":
1285
- return ["daemon", "start"];
1286
- case "sync":
1287
- return ["sync"];
1288
- case "exit":
1289
- return null;
1290
- case "recall": {
1291
- const query = await clack.text({
1292
- message: "Recall query:",
1293
- placeholder: "e.g. auth flow, ContextEngine, deploy steps"
1294
- });
1295
- if (clack.isCancel(query)) {
1296
- clack.cancel("Cancelled.");
1297
- fail(EXIT.CANCELLED, "cancelled", opts);
1298
- }
1299
- return ["memory", "recall", String(query)];
1300
- }
1301
- default:
1302
- return null;
1303
- }
1638
+ const sections = await resolveSections(deps.commands ?? []);
1639
+ await runGroupedMenu(clack, opts, deps, sections);
1304
1640
  }
1305
1641
 
1306
1642
  // src/commands/memory.ts
@@ -1729,66 +2065,6 @@ async function serve(opts) {
1729
2065
  }
1730
2066
  }
1731
2067
 
1732
- // src/tui/commands/registry.ts
1733
- var DESTRUCTIVE_PREFIXES = [
1734
- ["context", "index"],
1735
- ["context", "forget"],
1736
- ["memory", "save"],
1737
- ["memory", "forget"],
1738
- ["memory", "consolidate"],
1739
- ["task", "new"],
1740
- ["task", "advance"],
1741
- ["skills", "sync"],
1742
- ["daemon", "start"],
1743
- ["daemon", "stop"],
1744
- ["daemon", "restart"],
1745
- ["init"],
1746
- ["create"],
1747
- ["sync"],
1748
- ["install"],
1749
- ["update"]
1750
- ];
1751
- function isDestructive(argv) {
1752
- return DESTRUCTIVE_PREFIXES.some((prefix) => {
1753
- if (argv.length < prefix.length) return false;
1754
- return prefix.every((tok, i) => argv[i] === tok);
1755
- });
1756
- }
1757
- function capitalizeFirst(s) {
1758
- if (s.length === 0) return s;
1759
- return `${s[0]?.toUpperCase() ?? ""}${s.slice(1)}`;
1760
- }
1761
- function hasAction(cmd) {
1762
- return cmd._actionHandler != null;
1763
- }
1764
- function collectLeaves(cmd, path, out) {
1765
- const children = cmd.commands;
1766
- const isLeaf = hasAction(cmd) && children.length === 0;
1767
- if (isLeaf) {
1768
- const argv = path;
1769
- const id = argv.join(" ");
1770
- const description = cmd.description() || argv.join(" ");
1771
- out.push({
1772
- id,
1773
- label: argv.map(capitalizeFirst).join(": "),
1774
- argv,
1775
- description,
1776
- category: argv[0] ?? "",
1777
- keywords: argv,
1778
- destructive: isDestructive(argv)
1779
- });
1780
- return;
1781
- }
1782
- for (const child of children) {
1783
- collectLeaves(child, [...path, child.name()], out);
1784
- }
1785
- }
1786
- function buildPaletteCommands(program2) {
1787
- const out = [];
1788
- collectLeaves(program2, [], out);
1789
- return out;
1790
- }
1791
-
1792
2068
  // src/bin.ts
1793
2069
  function buildPaletteCommandsForTui() {
1794
2070
  return buildPaletteCommands(createProgram());
@@ -2228,16 +2504,25 @@ function createProgram() {
2228
2504
  };
2229
2505
  await runTui2(opts, dispatch);
2230
2506
  });
2231
- const homeDeps = {
2232
- dispatch: async (argv) => {
2507
+ program2.command("palette").description("fuzzy command palette \u2014 run any noir command (Ink)").action(async (...args) => {
2508
+ const g = actionGlobals(args);
2509
+ const opts = toCliOptions(g);
2510
+ requireInteractive(opts, "`noir palette`");
2511
+ const tuiUrl = new URL("./tui/index.js", import.meta.url).href;
2512
+ const { runPalette: runPalette2 } = await import(
2513
+ /* @vite-ignore */
2514
+ tuiUrl
2515
+ );
2516
+ const dispatch = async (argv) => {
2233
2517
  const sub = createProgram();
2234
2518
  try {
2235
2519
  await sub.parseAsync([...argv], { from: "user" });
2236
2520
  } catch (err) {
2237
2521
  handleError(err);
2238
2522
  }
2239
- }
2240
- };
2523
+ };
2524
+ await runPalette2(opts, dispatch);
2525
+ });
2241
2526
  program2.action(async (...args) => {
2242
2527
  const cmd = trailingCmd(args);
2243
2528
  const leftovers = cmd.args;
@@ -2253,6 +2538,20 @@ function createProgram() {
2253
2538
  return program2;
2254
2539
  }
2255
2540
  var program = createProgram();
2541
+ var homeDeps = {
2542
+ dispatch: async (argv) => {
2543
+ const sub = createProgram();
2544
+ try {
2545
+ await sub.parseAsync([...argv], { from: "user" });
2546
+ } catch (err) {
2547
+ handleError(err);
2548
+ }
2549
+ },
2550
+ // The grouped home menu resolves its sections against the LIVE palette
2551
+ // registry (home-consolidation S1/S2) — the same source the TUI palette
2552
+ // uses — so the menu cannot drift from the commander tree.
2553
+ commands: buildPaletteCommands(createProgram())
2554
+ };
2256
2555
  async function run(argv = []) {
2257
2556
  try {
2258
2557
  await program.parseAsync([...argv], { from: "user" });
@@ -2275,8 +2574,8 @@ if (isMainModule) {
2275
2574
  }
2276
2575
 
2277
2576
  // src/tui/App.tsx
2278
- import { Box as Box2, Text as Text8, useApp, useInput as useInput2 } from "ink";
2279
- import { useEffect, useState as useState3 } from "react";
2577
+ import { Box as Box3, Text as Text9, useApp, useInput as useInput3 } from "ink";
2578
+ import { useEffect, useState as useState4 } from "react";
2280
2579
 
2281
2580
  // src/tui/CommandInput.tsx
2282
2581
  import { Text } from "ink";
@@ -2363,10 +2662,86 @@ function Header({ tagline }) {
2363
2662
  ] });
2364
2663
  }
2365
2664
 
2665
+ // src/tui/HomeMenu.tsx
2666
+ import { Box, Text as Text4, useInput } from "ink";
2667
+ import { useState } from "react";
2668
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2669
+ function flatten(sections) {
2670
+ const rows = [];
2671
+ for (const section of sections) {
2672
+ for (const action of section.items) {
2673
+ rows.push({ section, action });
2674
+ }
2675
+ }
2676
+ return rows;
2677
+ }
2678
+ function HomeMenu({ sections, onSelect, onClose }) {
2679
+ const [active, setActive] = useState(0);
2680
+ const rows = flatten(sections);
2681
+ const activeRow = rows[Math.min(Math.max(active, 0), Math.max(0, rows.length - 1))];
2682
+ useInput((input, key) => {
2683
+ if (key.escape || key.tab) {
2684
+ onClose();
2685
+ return;
2686
+ }
2687
+ if (key.return) {
2688
+ if (activeRow) onSelect(activeRow.action);
2689
+ return;
2690
+ }
2691
+ if (key.upArrow) {
2692
+ setActive((i) => Math.max(0, i - 1));
2693
+ return;
2694
+ }
2695
+ if (key.downArrow) {
2696
+ setActive((i) => Math.min(rows.length - 1, i + 1));
2697
+ return;
2698
+ }
2699
+ if (input === "k") {
2700
+ setActive((i) => Math.max(0, i - 1));
2701
+ return;
2702
+ }
2703
+ if (input === "j") {
2704
+ setActive((i) => Math.min(rows.length - 1, i + 1));
2705
+ return;
2706
+ }
2707
+ });
2708
+ const rowsEl = [];
2709
+ let lastSectionId = null;
2710
+ for (let i = 0; i < rows.length; i++) {
2711
+ const row = rows[i];
2712
+ if (!row) continue;
2713
+ const { section, action } = row;
2714
+ if (section.id !== lastSectionId) {
2715
+ rowsEl.push(/* @__PURE__ */ jsx4(Text4, { children: c.dim(`\u2500\u2500 ${section.label} \u2500\u2500`) }, `sec:${section.id}`));
2716
+ lastSectionId = section.id;
2717
+ }
2718
+ const isActive = i === activeRowIndex(active, rows);
2719
+ const prefix = isActive ? "\u25B8 " : " ";
2720
+ rowsEl.push(
2721
+ /* @__PURE__ */ jsxs3(Text4, { children: [
2722
+ prefix,
2723
+ isActive ? c.accent(action.label) : action.label,
2724
+ /* @__PURE__ */ jsx4(Text4, { children: c.dim(` ${action.hint}`) })
2725
+ ] }, action.id)
2726
+ );
2727
+ }
2728
+ return /* @__PURE__ */ jsxs3(Box, { flexDirection: "column", children: [
2729
+ /* @__PURE__ */ jsxs3(Text4, { children: [
2730
+ c.bold("\u25B8 home "),
2731
+ /* @__PURE__ */ jsx4(Text4, { children: c.dim(`quick actions (${rows.length})`) })
2732
+ ] }),
2733
+ rowsEl,
2734
+ /* @__PURE__ */ jsx4(Text4, { children: c.dim("\u2191/\u2193 navigate \xB7 Enter run \xB7 Esc close") })
2735
+ ] });
2736
+ }
2737
+ function activeRowIndex(active, rows) {
2738
+ return Math.min(Math.max(active, 0), Math.max(0, rows.length - 1));
2739
+ }
2740
+
2366
2741
  // src/tui/hooks/useInputBuffer.ts
2367
- import { useRef, useState } from "react";
2742
+ import { useRef, useState as useState2 } from "react";
2368
2743
  function useInputBuffer() {
2369
- const [buffer, setBuffer] = useState("");
2744
+ const [buffer, setBuffer] = useState2("");
2370
2745
  const historyRef = useRef([]);
2371
2746
  const cursorRef = useRef(-1);
2372
2747
  function pushHistory(text) {
@@ -2395,8 +2770,8 @@ function useInputBuffer() {
2395
2770
  }
2396
2771
 
2397
2772
  // src/tui/OutputPane.tsx
2398
- import { Text as Text4 } from "ink";
2399
- import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2773
+ import { Text as Text5 } from "ink";
2774
+ import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2400
2775
  function toRows(visible, baseIndex) {
2401
2776
  const rows = [];
2402
2777
  const seen = /* @__PURE__ */ new Map();
@@ -2430,13 +2805,13 @@ function OutputPane({
2430
2805
  }) {
2431
2806
  const width = terminalWidth();
2432
2807
  if (lines.length === 0) {
2433
- return /* @__PURE__ */ jsx4(Text4, { children: c.dim("(no output \u2014 type a /command, or ? for help)") });
2808
+ return /* @__PURE__ */ jsx5(Text5, { children: c.dim("(no output \u2014 type a /command, or ? for help)") });
2434
2809
  }
2435
2810
  const maxOffset = Math.max(0, lines.length - height);
2436
2811
  const offset = Math.min(Math.max(0, scrollOffset), maxOffset);
2437
2812
  const rows = toRows(lines.slice(offset, offset + height), offset);
2438
- return /* @__PURE__ */ jsxs3(Fragment, { children: [
2439
- title !== void 0 ? /* @__PURE__ */ jsx4(Text4, { children: c.dim(`\u2500\u2500 ${title} \u2500\u2500`) }) : null,
2813
+ return /* @__PURE__ */ jsxs4(Fragment, { children: [
2814
+ title !== void 0 ? /* @__PURE__ */ jsx5(Text5, { children: c.dim(`\u2500\u2500 ${title} \u2500\u2500`) }) : null,
2440
2815
  rows.map((row) => {
2441
2816
  const truncated = row.text.length > width ? `${row.text.slice(0, Math.max(1, width - 1))}\u2026` : row.text;
2442
2817
  let body = truncated;
@@ -2445,18 +2820,18 @@ function OutputPane({
2445
2820
  } else if (highlightQuery !== void 0 && highlightQuery.length > 0) {
2446
2821
  body = highlightFirst(truncated, highlightQuery);
2447
2822
  }
2448
- return /* @__PURE__ */ jsx4(Text4, { children: body }, row.key);
2823
+ return /* @__PURE__ */ jsx5(Text5, { children: body }, row.key);
2449
2824
  })
2450
2825
  ] });
2451
2826
  }
2452
2827
 
2453
2828
  // src/tui/overlays/ConfirmOverlay.tsx
2454
- import { Text as Text5 } from "ink";
2455
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2829
+ import { Text as Text6 } from "ink";
2830
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2456
2831
  function ConfirmOverlay({ argv }) {
2457
- return /* @__PURE__ */ jsxs4(Text5, { children: [
2832
+ return /* @__PURE__ */ jsxs5(Text6, { children: [
2458
2833
  c.warn(`! run /${argv.join(" ")}? (y/N)`),
2459
- /* @__PURE__ */ jsx5(Text5, { children: c.dim(" [y approve \xB7 n/Esc back to palette]") })
2834
+ /* @__PURE__ */ jsx6(Text6, { children: c.dim(" [y approve \xB7 n/Esc back to palette]") })
2460
2835
  ] });
2461
2836
  }
2462
2837
 
@@ -2564,9 +2939,9 @@ var handRolledMatcher = {
2564
2939
  };
2565
2940
 
2566
2941
  // src/tui/palette/Palette.tsx
2567
- import { Box, Text as Text6, useInput } from "ink";
2568
- import { useState as useState2 } from "react";
2569
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2942
+ import { Box as Box2, Text as Text7, useInput as useInput2 } from "ink";
2943
+ import { useState as useState3 } from "react";
2944
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2570
2945
  var VISIBLE_ROWS = 10;
2571
2946
  var MATCH_LIMIT = 20;
2572
2947
  function rankPalette(query, matcher, commands, recent) {
@@ -2589,9 +2964,9 @@ function highlight(label, matchedIndices) {
2589
2964
  const chars = [];
2590
2965
  for (let i = 0; i < label.length; i++) {
2591
2966
  const ch = label[i] ?? "";
2592
- chars.push(set.has(i) ? /* @__PURE__ */ jsx6(Text6, { children: c.accent(ch) }, i) : /* @__PURE__ */ jsx6(Text6, { children: ch }, i));
2967
+ chars.push(set.has(i) ? /* @__PURE__ */ jsx7(Text7, { children: c.accent(ch) }, i) : /* @__PURE__ */ jsx7(Text7, { children: ch }, i));
2593
2968
  }
2594
- return /* @__PURE__ */ jsx6(Fragment2, { children: chars });
2969
+ return /* @__PURE__ */ jsx7(Fragment2, { children: chars });
2595
2970
  }
2596
2971
  function Palette({
2597
2972
  commands,
@@ -2600,13 +2975,13 @@ function Palette({
2600
2975
  onSelect,
2601
2976
  onClose
2602
2977
  }) {
2603
- const [query, setQuery] = useState2("");
2604
- const [activeIndex, setActiveIndex] = useState2(0);
2978
+ const [query, setQuery] = useState3("");
2979
+ const [activeIndex, setActiveIndex] = useState3(0);
2605
2980
  const ranked = rankPalette(query, matcher, commands, recent);
2606
2981
  const visible = ranked.slice(0, VISIBLE_ROWS);
2607
2982
  const active = Math.min(Math.max(activeIndex, 0), Math.max(0, visible.length - 1));
2608
2983
  const recents = recentDeduped(recent, commands);
2609
- useInput((input, key) => {
2984
+ useInput2((input, key) => {
2610
2985
  if (key.escape) {
2611
2986
  onClose();
2612
2987
  return;
@@ -2636,14 +3011,14 @@ function Palette({
2636
3011
  }
2637
3012
  });
2638
3013
  const rows = [
2639
- /* @__PURE__ */ jsxs5(Text6, { children: [
3014
+ /* @__PURE__ */ jsxs6(Text7, { children: [
2640
3015
  c.bold("\u25B8 palette "),
2641
- /* @__PURE__ */ jsx6(Text6, { children: c.dim(`search (${visible.length} shown)`) })
3016
+ /* @__PURE__ */ jsx7(Text7, { children: c.dim(`search (${visible.length} shown)`) })
2642
3017
  ] }, "header"),
2643
- /* @__PURE__ */ jsxs5(Text6, { children: [
3018
+ /* @__PURE__ */ jsxs6(Text7, { children: [
2644
3019
  c.dim("> "),
2645
3020
  query,
2646
- /* @__PURE__ */ jsx6(Text6, { children: c.dim("\u258C") })
3021
+ /* @__PURE__ */ jsx7(Text7, { children: c.dim("\u258C") })
2647
3022
  ] }, "query")
2648
3023
  ];
2649
3024
  let lastCategory = null;
@@ -2651,25 +3026,25 @@ function Palette({
2651
3026
  const isRecent = recents.some((r) => r.id === cmd.id);
2652
3027
  if (query.length === 0) {
2653
3028
  if (isRecent && lastCategory !== "recent") {
2654
- rows.push(/* @__PURE__ */ jsx6(Text6, { children: c.dim("\u2500\u2500 recent \u2500\u2500") }, "cat:recent"));
3029
+ rows.push(/* @__PURE__ */ jsx7(Text7, { children: c.dim("\u2500\u2500 recent \u2500\u2500") }, "cat:recent"));
2655
3030
  lastCategory = "recent";
2656
3031
  } else if (!isRecent && cmd.category !== lastCategory) {
2657
- rows.push(/* @__PURE__ */ jsx6(Text6, { children: c.dim(`\u2500\u2500 ${cmd.category} \u2500\u2500`) }, `cat:${cmd.category}`));
3032
+ rows.push(/* @__PURE__ */ jsx7(Text7, { children: c.dim(`\u2500\u2500 ${cmd.category} \u2500\u2500`) }, `cat:${cmd.category}`));
2658
3033
  lastCategory = cmd.category;
2659
3034
  }
2660
3035
  }
2661
3036
  const prefix = cmd.id === visible[active]?.cmd.id ? "\u25B8 " : " ";
2662
3037
  rows.push(
2663
- /* @__PURE__ */ jsxs5(Text6, { children: [
2664
- isRecent && query.length === 0 ? /* @__PURE__ */ jsx6(Text6, { children: c.dim("\u21BA ") }) : null,
3038
+ /* @__PURE__ */ jsxs6(Text7, { children: [
3039
+ isRecent && query.length === 0 ? /* @__PURE__ */ jsx7(Text7, { children: c.dim("\u21BA ") }) : null,
2665
3040
  prefix,
2666
3041
  highlight(cmd.label, indices),
2667
- /* @__PURE__ */ jsx6(Text6, { children: c.dim(` ${cmd.description}`) })
3042
+ /* @__PURE__ */ jsx7(Text7, { children: c.dim(` ${cmd.description}`) })
2668
3043
  ] }, cmd.id)
2669
3044
  );
2670
3045
  }
2671
- rows.push(/* @__PURE__ */ jsx6(Text6, { children: c.dim("\u2191/\u2193 navigate \xB7 Enter run \xB7 Esc close") }, "footer"));
2672
- return /* @__PURE__ */ jsx6(Box, { flexDirection: "column", children: rows.map((row) => row) });
3046
+ rows.push(/* @__PURE__ */ jsx7(Text7, { children: c.dim("\u2191/\u2193 navigate \xB7 Enter run \xB7 Esc close") }, "footer"));
3047
+ return /* @__PURE__ */ jsx7(Box2, { flexDirection: "column", children: rows.map((row) => row) });
2673
3048
  }
2674
3049
  function recentDeduped(recent, commands) {
2675
3050
  const known = new Set(commands.map((cmd) => cmd.id));
@@ -2677,17 +3052,17 @@ function recentDeduped(recent, commands) {
2677
3052
  }
2678
3053
 
2679
3054
  // src/tui/StatusBar.tsx
2680
- import { Text as Text7 } from "ink";
2681
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3055
+ import { Text as Text8 } from "ink";
3056
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2682
3057
  function Cell({
2683
3058
  label,
2684
3059
  value,
2685
3060
  accent
2686
3061
  }) {
2687
- return /* @__PURE__ */ jsxs6(Text7, { children: [
2688
- /* @__PURE__ */ jsx7(Text7, { children: c.dim(`${label}:`) }),
3062
+ return /* @__PURE__ */ jsxs7(Text8, { children: [
3063
+ /* @__PURE__ */ jsx8(Text8, { children: c.dim(`${label}:`) }),
2689
3064
  " ",
2690
- /* @__PURE__ */ jsx7(Text7, { children: accent ? c.accent(value) : value })
3065
+ /* @__PURE__ */ jsx8(Text8, { children: accent ? c.accent(value) : value })
2691
3066
  ] });
2692
3067
  }
2693
3068
  function StatusBar({ payload, loading }) {
@@ -2696,14 +3071,14 @@ function StatusBar({ payload, loading }) {
2696
3071
  const phase = payload?.workflow?.phase ?? (loading ? "\u2026" : "idle");
2697
3072
  const daemonRunning = payload?.daemon.running === true;
2698
3073
  const daemon = payload === null && loading ? "\u2026" : daemonRunning ? "up" : "down";
2699
- return /* @__PURE__ */ jsxs6(Text7, { children: [
2700
- /* @__PURE__ */ jsx7(Cell, { label: "host", value: host, accent: true }),
2701
- /* @__PURE__ */ jsx7(Text7, { children: c.dim(" \xB7 ") }),
2702
- /* @__PURE__ */ jsx7(Cell, { label: "mode", value: mode }),
2703
- /* @__PURE__ */ jsx7(Text7, { children: c.dim(" \xB7 ") }),
2704
- /* @__PURE__ */ jsx7(Cell, { label: "phase", value: phase }),
2705
- /* @__PURE__ */ jsx7(Text7, { children: c.dim(" \xB7 ") }),
2706
- /* @__PURE__ */ jsxs6(Text7, { children: [
3074
+ return /* @__PURE__ */ jsxs7(Text8, { children: [
3075
+ /* @__PURE__ */ jsx8(Cell, { label: "host", value: host, accent: true }),
3076
+ /* @__PURE__ */ jsx8(Text8, { children: c.dim(" \xB7 ") }),
3077
+ /* @__PURE__ */ jsx8(Cell, { label: "mode", value: mode }),
3078
+ /* @__PURE__ */ jsx8(Text8, { children: c.dim(" \xB7 ") }),
3079
+ /* @__PURE__ */ jsx8(Cell, { label: "phase", value: phase }),
3080
+ /* @__PURE__ */ jsx8(Text8, { children: c.dim(" \xB7 ") }),
3081
+ /* @__PURE__ */ jsxs7(Text8, { children: [
2707
3082
  c.dim("daemon:"),
2708
3083
  " ",
2709
3084
  daemonRunning ? c.ok(daemon) : c.warn(daemon)
@@ -2712,21 +3087,27 @@ function StatusBar({ payload, loading }) {
2712
3087
  }
2713
3088
 
2714
3089
  // src/tui/App.tsx
2715
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2716
- function App({ deps, initialPayload = null, refreshMs = 5e3 }) {
3090
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
3091
+ function App({
3092
+ deps,
3093
+ initialPayload = null,
3094
+ refreshMs = 5e3,
3095
+ initialMode = { kind: "dashboard" }
3096
+ }) {
2717
3097
  const { exit } = useApp();
2718
3098
  const { buffer, setBuffer, pushHistory, recall, clear } = useInputBuffer();
2719
- const [mode, setMode] = useState3({ kind: "dashboard" });
2720
- const [payload, setPayload] = useState3(initialPayload);
2721
- const [loading, setLoading] = useState3(initialPayload === null);
2722
- const [output, setOutput] = useState3(null);
2723
- const [searchLines, setSearchLines] = useState3([]);
2724
- const [scrollOffset, setScrollOffset] = useState3(0);
2725
- const [help, setHelp] = useState3(false);
2726
- const [running, setRunning] = useState3(false);
2727
- const [notice, setNotice] = useState3(null);
2728
- const [pending, setPending] = useState3(null);
2729
- const [recent, setRecent] = useState3([]);
3099
+ const [mode, setMode] = useState4(initialMode);
3100
+ const [payload, setPayload] = useState4(initialPayload);
3101
+ const [loading, setLoading] = useState4(initialPayload === null);
3102
+ const [output, setOutput] = useState4(null);
3103
+ const [searchLines, setSearchLines] = useState4([]);
3104
+ const [scrollOffset, setScrollOffset] = useState4(0);
3105
+ const [help, setHelp] = useState4(false);
3106
+ const [running, setRunning] = useState4(false);
3107
+ const [notice, setNotice] = useState4(null);
3108
+ const [pending, setPending] = useState4(null);
3109
+ const [recent, setRecent] = useState4([]);
3110
+ const [homeSections, setHomeSections] = useState4([]);
2730
3111
  useEffect(() => {
2731
3112
  if (!deps.loadRecent) return;
2732
3113
  const loader = deps.loadRecent;
@@ -2739,6 +3120,15 @@ function App({ deps, initialPayload = null, refreshMs = 5e3 }) {
2739
3120
  cancelled = true;
2740
3121
  };
2741
3122
  }, [deps.loadRecent]);
3123
+ useEffect(() => {
3124
+ let cancelled = false;
3125
+ void resolveSections(deps.commands ?? []).then((sections) => {
3126
+ if (!cancelled) setHomeSections(sections);
3127
+ });
3128
+ return () => {
3129
+ cancelled = true;
3130
+ };
3131
+ }, [deps.commands]);
2742
3132
  useEffect(() => {
2743
3133
  if (running) return;
2744
3134
  let cancelled = false;
@@ -2856,6 +3246,10 @@ function App({ deps, initialPayload = null, refreshMs = 5e3 }) {
2856
3246
  setHelp(true);
2857
3247
  return;
2858
3248
  }
3249
+ if (input === "h") {
3250
+ setMode({ kind: "home" });
3251
+ return;
3252
+ }
2859
3253
  }
2860
3254
  setBuffer((b) => b + input);
2861
3255
  return;
@@ -2883,6 +3277,16 @@ function App({ deps, initialPayload = null, refreshMs = 5e3 }) {
2883
3277
  void recordRuns([...cmd.argv]);
2884
3278
  setMode({ kind: "dashboard" });
2885
3279
  }
3280
+ function handleHomeSelect(action) {
3281
+ const argv = action.dispatch ?? [action.id];
3282
+ if (action.destructive) {
3283
+ setMode({ kind: "confirm", argv: [...argv] });
3284
+ return;
3285
+ }
3286
+ dispatchCmd([...argv]);
3287
+ void recordRuns([...argv]);
3288
+ setMode({ kind: "dashboard" });
3289
+ }
2886
3290
  function handleConfirmApprove() {
2887
3291
  if (mode.kind !== "confirm") return;
2888
3292
  dispatchCmd([...mode.argv]);
@@ -2945,7 +3349,7 @@ function App({ deps, initialPayload = null, refreshMs = 5e3 }) {
2945
3349
  const next = (mode.active + delta + n) % n;
2946
3350
  setMode({ ...mode, active: next });
2947
3351
  }
2948
- useInput2((input, key) => {
3352
+ useInput3((input, key) => {
2949
3353
  switch (mode.kind) {
2950
3354
  case "dashboard":
2951
3355
  handleDashboardInput(input, key);
@@ -2953,6 +3357,9 @@ function App({ deps, initialPayload = null, refreshMs = 5e3 }) {
2953
3357
  case "palette":
2954
3358
  return;
2955
3359
  // Palette renders its own useInput; no App-level keys here
3360
+ case "home":
3361
+ return;
3362
+ // HomeMenu renders its own useInput (S4)
2956
3363
  case "confirm":
2957
3364
  handleConfirmInput(input, key);
2958
3365
  return;
@@ -2979,12 +3386,12 @@ function App({ deps, initialPayload = null, refreshMs = 5e3 }) {
2979
3386
  void recordRuns(argv);
2980
3387
  }
2981
3388
  if (help) {
2982
- return /* @__PURE__ */ jsx8(Help, {});
3389
+ return /* @__PURE__ */ jsx9(Help, {});
2983
3390
  }
2984
3391
  if (mode.kind === "palette") {
2985
- return /* @__PURE__ */ jsxs7(Box2, { flexDirection: "column", children: [
2986
- /* @__PURE__ */ jsx8(Header, { tagline: "palette" }),
2987
- /* @__PURE__ */ jsx8(
3392
+ return /* @__PURE__ */ jsxs8(Box3, { flexDirection: "column", children: [
3393
+ /* @__PURE__ */ jsx9(Header, { tagline: "palette" }),
3394
+ /* @__PURE__ */ jsx9(
2988
3395
  Palette,
2989
3396
  {
2990
3397
  commands: deps.commands ?? [],
@@ -2994,22 +3401,36 @@ function App({ deps, initialPayload = null, refreshMs = 5e3 }) {
2994
3401
  onClose: () => setMode({ kind: "dashboard" })
2995
3402
  }
2996
3403
  ),
2997
- /* @__PURE__ */ jsx8(Footer, { running: false })
3404
+ /* @__PURE__ */ jsx9(Footer, { running: false })
3405
+ ] });
3406
+ }
3407
+ if (mode.kind === "home") {
3408
+ return /* @__PURE__ */ jsxs8(Box3, { flexDirection: "column", children: [
3409
+ /* @__PURE__ */ jsx9(Header, { tagline: "home" }),
3410
+ /* @__PURE__ */ jsx9(
3411
+ HomeMenu,
3412
+ {
3413
+ sections: homeSections,
3414
+ onSelect: handleHomeSelect,
3415
+ onClose: () => setMode({ kind: "dashboard" })
3416
+ }
3417
+ ),
3418
+ /* @__PURE__ */ jsx9(Footer, { running: false })
2998
3419
  ] });
2999
3420
  }
3000
3421
  if (mode.kind === "confirm") {
3001
- return /* @__PURE__ */ jsxs7(Box2, { flexDirection: "column", children: [
3002
- /* @__PURE__ */ jsx8(Header, { tagline: "confirm" }),
3003
- /* @__PURE__ */ jsx8(ConfirmOverlay, { argv: mode.argv }),
3004
- /* @__PURE__ */ jsx8(Footer, { running: false })
3422
+ return /* @__PURE__ */ jsxs8(Box3, { flexDirection: "column", children: [
3423
+ /* @__PURE__ */ jsx9(Header, { tagline: "confirm" }),
3424
+ /* @__PURE__ */ jsx9(ConfirmOverlay, { argv: mode.argv }),
3425
+ /* @__PURE__ */ jsx9(Footer, { running: false })
3005
3426
  ] });
3006
3427
  }
3007
3428
  if (mode.kind === "search") {
3008
3429
  const activeLine = mode.active >= 0 ? mode.matches[mode.active] : void 0;
3009
3430
  const matchCount = mode.matches.length;
3010
- return /* @__PURE__ */ jsxs7(Box2, { flexDirection: "column", children: [
3011
- /* @__PURE__ */ jsx8(Header, { tagline: "search" }),
3012
- /* @__PURE__ */ jsx8(
3431
+ return /* @__PURE__ */ jsxs8(Box3, { flexDirection: "column", children: [
3432
+ /* @__PURE__ */ jsx9(Header, { tagline: "search" }),
3433
+ /* @__PURE__ */ jsx9(
3013
3434
  OutputPane,
3014
3435
  {
3015
3436
  lines: searchLines,
@@ -3019,55 +3440,60 @@ function App({ deps, initialPayload = null, refreshMs = 5e3 }) {
3019
3440
  activeLine
3020
3441
  }
3021
3442
  ),
3022
- /* @__PURE__ */ jsxs7(Text8, { children: [
3443
+ /* @__PURE__ */ jsxs8(Text9, { children: [
3023
3444
  c.dim("search: "),
3024
3445
  mode.query.length > 0 ? mode.query : c.dim("(type to filter output)"),
3025
- /* @__PURE__ */ jsx8(Text8, { children: c.dim(" \u258C") })
3446
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" \u258C") })
3026
3447
  ] }),
3027
- /* @__PURE__ */ jsx8(Text8, { children: c.dim(
3448
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(
3028
3449
  matchCount === 0 ? "no matches \xB7 Esc exit" : `${mode.active + 1}/${matchCount} matches \xB7 n/Enter next \xB7 N prev \xB7 Esc exit`
3029
3450
  ) }),
3030
- /* @__PURE__ */ jsx8(Footer, { running: false })
3451
+ /* @__PURE__ */ jsx9(Footer, { running: false })
3031
3452
  ] });
3032
3453
  }
3033
3454
  const snapshotLines = formatStatusPayload(payload);
3034
3455
  const paneLines = output?.lines ?? snapshotLines ?? [];
3035
3456
  const paneTitle = output?.title ?? "snapshot";
3036
- return /* @__PURE__ */ jsxs7(Box2, { flexDirection: "column", children: [
3037
- /* @__PURE__ */ jsx8(Header, { tagline: "dashboard" }),
3038
- /* @__PURE__ */ jsx8(StatusBar, { payload, loading }),
3039
- /* @__PURE__ */ jsx8(OutputPane, { lines: paneLines, scrollOffset, title: paneTitle }),
3040
- /* @__PURE__ */ jsx8(CommandInput, { buffer, running }),
3041
- notice !== null ? /* @__PURE__ */ jsx8(Text8, { children: c.dim(notice) }) : null,
3042
- /* @__PURE__ */ jsx8(Footer, { running })
3457
+ return /* @__PURE__ */ jsxs8(Box3, { flexDirection: "column", children: [
3458
+ /* @__PURE__ */ jsx9(Header, { tagline: "dashboard" }),
3459
+ /* @__PURE__ */ jsx9(StatusBar, { payload, loading }),
3460
+ /* @__PURE__ */ jsx9(OutputPane, { lines: paneLines, scrollOffset, title: paneTitle }),
3461
+ /* @__PURE__ */ jsx9(CommandInput, { buffer, running }),
3462
+ notice !== null ? /* @__PURE__ */ jsx9(Text9, { children: c.dim(notice) }) : null,
3463
+ /* @__PURE__ */ jsx9(Footer, { running })
3043
3464
  ] });
3044
3465
  }
3045
3466
  function Help() {
3046
- return /* @__PURE__ */ jsxs7(Box2, { flexDirection: "column", children: [
3047
- /* @__PURE__ */ jsx8(Header, { tagline: "help" }),
3048
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
3049
- /* @__PURE__ */ jsx8(Text8, { children: c.bold("Keybindings") }),
3050
- /* @__PURE__ */ jsx8(Text8, { children: c.dim(" /&lt;command&gt; run a Noir sub-command (e.g. /status, /sync, /task next)") }),
3051
- /* @__PURE__ */ jsx8(Text8, { children: c.dim(" Enter run the typed /command") }),
3052
- /* @__PURE__ */ jsx8(Text8, { children: c.dim(" Esc back: clear input \u2192 dismiss output \u2192 quit") }),
3053
- /* @__PURE__ */ jsx8(Text8, { children: c.dim(" q quit (when the input is empty)") }),
3054
- /* @__PURE__ */ jsx8(Text8, { children: c.dim(" \u2191 / \u2193 scroll the output pane") }),
3055
- /* @__PURE__ */ jsx8(Text8, { children: c.dim(" ? toggle this help") }),
3056
- /* @__PURE__ */ jsx8(Text8, { children: c.dim(" Ctrl+K open the command palette") }),
3057
- /* @__PURE__ */ jsx8(Text8, { children: c.dim(" Ctrl+F find in the dispatched output pane") }),
3058
- /* @__PURE__ */ jsx8(Text8, { children: c.dim(" n / N next / previous match in search (Enter = next)") }),
3059
- /* @__PURE__ */ jsx8(Text8, { children: c.dim(" y / n approve / decline a destructive command prompt") }),
3060
- /* @__PURE__ */ jsx8(Text8, { children: c.dim(" Ctrl+C force exit") }),
3061
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
3062
- /* @__PURE__ */ jsx8(Text8, { children: c.bold("Commands") }),
3063
- /* @__PURE__ */ jsx8(Text8, { children: c.dim(" Dispatched through the same routing as `noir` at the prompt.") }),
3064
- /* @__PURE__ */ jsx8(Text8, { children: c.dim(
3467
+ return /* @__PURE__ */ jsxs8(Box3, { flexDirection: "column", children: [
3468
+ /* @__PURE__ */ jsx9(Header, { tagline: "help" }),
3469
+ /* @__PURE__ */ jsx9(Text9, { children: " " }),
3470
+ /* @__PURE__ */ jsx9(Text9, { children: c.bold("Keybindings") }),
3471
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" /&lt;command&gt; run a Noir sub-command (e.g. /status, /sync, /task next)") }),
3472
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" Enter run the typed /command") }),
3473
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" Esc back: clear input \u2192 dismiss output \u2192 quit") }),
3474
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" q quit (when the input is empty)") }),
3475
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" h open the curated home quick-actions (home menu)") }),
3476
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" \u2191 / \u2193 scroll the output pane") }),
3477
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" ? toggle this help") }),
3478
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" Ctrl+K open the command palette") }),
3479
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" Ctrl+F find in the dispatched output pane") }),
3480
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" n / N next / previous match in search (Enter = next)") }),
3481
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" y / n approve / decline a destructive command prompt") }),
3482
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" Ctrl+C force exit") }),
3483
+ /* @__PURE__ */ jsx9(Text9, { children: " " }),
3484
+ /* @__PURE__ */ jsx9(Text9, { children: c.bold("Home menu (h)") }),
3485
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" The curated quick actions mirror the bare-`noir` home menu:") }),
3486
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" Status &amp; context \xB7 Memory \xB7 Workflow \xB7 Setup &amp; maintenance \xB7 Dashboard") }),
3487
+ /* @__PURE__ */ jsx9(Text9, { children: " " }),
3488
+ /* @__PURE__ */ jsx9(Text9, { children: c.bold("Commands") }),
3489
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" Dispatched through the same routing as `noir` at the prompt.") }),
3490
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(
3065
3491
  " /status /sync /doctor /context search &lt;q&gt; /task next /memory recall &lt;q&gt;"
3066
3492
  ) }),
3067
- /* @__PURE__ */ jsx8(Text8, { children: c.dim(" Commands that need their own interactive prompts (e.g. a /sync with a") }),
3068
- /* @__PURE__ */ jsx8(Text8, { children: c.dim(" conflict) are best run directly \u2014 exit first with q.") }),
3069
- /* @__PURE__ */ jsx8(Text8, { children: " " }),
3070
- /* @__PURE__ */ jsx8(Text8, { children: c.dim("press ? / Esc / q to close this help") })
3493
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" Commands that need their own interactive prompts (e.g. a /sync with a") }),
3494
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim(" conflict) are best run directly \u2014 exit first with q.") }),
3495
+ /* @__PURE__ */ jsx9(Text9, { children: " " }),
3496
+ /* @__PURE__ */ jsx9(Text9, { children: c.dim("press ? / Esc / q to close this help") })
3071
3497
  ] });
3072
3498
  }
3073
3499
 
@@ -3127,7 +3553,7 @@ function recordRecent(projectId, argv) {
3127
3553
  }
3128
3554
 
3129
3555
  // src/tui/index.tsx
3130
- import { jsx as jsx9 } from "react/jsx-runtime";
3556
+ import { jsx as jsx10 } from "react/jsx-runtime";
3131
3557
  function defaultFetchStatus(opts) {
3132
3558
  return async () => {
3133
3559
  try {
@@ -3138,6 +3564,16 @@ function defaultFetchStatus(opts) {
3138
3564
  };
3139
3565
  }
3140
3566
  async function runTui(opts, dispatch) {
3567
+ const deps = await buildTuiDeps(opts, dispatch);
3568
+ const instance = render(/* @__PURE__ */ jsx10(App, { deps }));
3569
+ await instance.waitUntilExit();
3570
+ }
3571
+ async function runPalette(opts, dispatch) {
3572
+ const deps = await buildTuiDeps(opts, dispatch);
3573
+ const instance = render(/* @__PURE__ */ jsx10(App, { deps, initialMode: { kind: "palette" } }));
3574
+ await instance.waitUntilExit();
3575
+ }
3576
+ async function buildTuiDeps(opts, dispatch) {
3141
3577
  let projectId = null;
3142
3578
  try {
3143
3579
  projectId = loadProjectInfo8(process.cwd()).id;
@@ -3161,10 +3597,10 @@ async function runTui(opts, dispatch) {
3161
3597
  return loadRecent(projectId).map((e) => byId.get(e.id)).filter((c2) => c2 !== void 0);
3162
3598
  }
3163
3599
  };
3164
- const instance = render(/* @__PURE__ */ jsx9(App, { deps }));
3165
- await instance.waitUntilExit();
3600
+ return deps;
3166
3601
  }
3167
3602
  export {
3603
+ runPalette,
3168
3604
  runTui
3169
3605
  };
3170
3606
  //# sourceMappingURL=index.js.map