@namzu/cli 2.6.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/commands/doctor.d.ts.map +1 -1
  2. package/dist/commands/doctor.js +15 -4
  3. package/dist/commands/doctor.js.map +1 -1
  4. package/dist/commands/run-flags.d.ts +23 -0
  5. package/dist/commands/run-flags.d.ts.map +1 -1
  6. package/dist/commands/run-flags.js +39 -0
  7. package/dist/commands/run-flags.js.map +1 -1
  8. package/dist/commands/run-stream.d.ts.map +1 -1
  9. package/dist/commands/run-stream.js +12 -6
  10. package/dist/commands/run-stream.js.map +1 -1
  11. package/dist/commands/run.d.ts.map +1 -1
  12. package/dist/commands/run.js +12 -6
  13. package/dist/commands/run.js.map +1 -1
  14. package/dist/doctor/checks/chain.d.ts +32 -0
  15. package/dist/doctor/checks/chain.d.ts.map +1 -0
  16. package/dist/doctor/checks/chain.js +118 -0
  17. package/dist/doctor/checks/chain.js.map +1 -0
  18. package/dist/doctor/checks/index.d.ts +2 -1
  19. package/dist/doctor/checks/index.d.ts.map +1 -1
  20. package/dist/doctor/checks/index.js +6 -1
  21. package/dist/doctor/checks/index.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/integrations/providers/chain-capabilities.d.ts +80 -0
  27. package/dist/integrations/providers/chain-capabilities.d.ts.map +1 -0
  28. package/dist/integrations/providers/chain-capabilities.js +207 -0
  29. package/dist/integrations/providers/chain-capabilities.js.map +1 -0
  30. package/dist/integrations/providers/index.d.ts +2 -1
  31. package/dist/integrations/providers/index.d.ts.map +1 -1
  32. package/dist/integrations/providers/index.js +2 -1
  33. package/dist/integrations/providers/index.js.map +1 -1
  34. package/dist/integrations/providers/preferences.d.ts +84 -15
  35. package/dist/integrations/providers/preferences.d.ts.map +1 -1
  36. package/dist/integrations/providers/preferences.js +180 -25
  37. package/dist/integrations/providers/preferences.js.map +1 -1
  38. package/dist/tui/App.d.ts.map +1 -1
  39. package/dist/tui/App.js +92 -16
  40. package/dist/tui/App.js.map +1 -1
  41. package/dist/tui/PermissionOverlay.d.ts.map +1 -1
  42. package/dist/tui/PermissionOverlay.js +1 -1
  43. package/dist/tui/PermissionOverlay.js.map +1 -1
  44. package/dist/tui/TrustPrompt.d.ts.map +1 -1
  45. package/dist/tui/TrustPrompt.js +1 -1
  46. package/dist/tui/TrustPrompt.js.map +1 -1
  47. package/dist/tui/agent.d.ts +12 -0
  48. package/dist/tui/agent.d.ts.map +1 -1
  49. package/dist/tui/agent.js +70 -10
  50. package/dist/tui/agent.js.map +1 -1
  51. package/dist/tui/consent-timing.d.ts +44 -0
  52. package/dist/tui/consent-timing.d.ts.map +1 -0
  53. package/dist/tui/consent-timing.js +48 -0
  54. package/dist/tui/consent-timing.js.map +1 -0
  55. package/package.json +3 -3
@@ -1,33 +1,88 @@
1
1
  /**
2
- * `~/.namzu/preferences.json` — the user's primary-chat picker selection.
2
+ * `~/.namzu/preferences.json` — the operator's provider chain.
3
3
  *
4
- * Schema (current = v2):
4
+ * Schema (current = v3):
5
5
  * {
6
- * "version": 2,
7
- * "provider": "anthropic", // ProviderId
8
- * "model": "claude-opus-4-7", // optional model override
6
+ * "version": 3,
7
+ * "providers": [ // ordered; index 0 is the primary
8
+ * { "id": "anthropic", "model": "claude-opus-4-7" },
9
+ * { "id": "openai" } // model omitted → the registry default
10
+ * ],
9
11
  * "subagents": { "active": [...] } // instances reserved for subagent dispatch
10
12
  * }
11
13
  *
12
- * The previous schema (v1) stored a peer instance as the
13
- * default — a different primitive (subagent dispatch, not primary
14
- * chat). On-disk v1 files trigger a forced re-pick rather than an
15
- * auto-migration: mapping between the two semantics would surprise
16
- * more than help.
14
+ * The chain is an ORDER the operator declares, replacing the single provider v2
15
+ * held. It is deliberately a plain readable array in a file the operator owns:
16
+ * the order has to be legible and editable without launching the TUI, which a
17
+ * value only reachable through an interactive picker is not.
18
+ *
19
+ * **Only `providers[0]` runs today.** The rest are validated and reported (see
20
+ * `readPreferences` and the `providers.chain` doctor check) but nothing falls
21
+ * over to them yet. That is the point of shipping this first: a chain you can
22
+ * declare, read back, and have checked is useful before failover exists, and it
23
+ * is what failover will consume.
24
+ *
25
+ * ## Versions
26
+ *
27
+ * v1 stored a peer instance as the default — a different primitive (subagent
28
+ * dispatch, not primary chat). It is still REFUSED rather than migrated, because
29
+ * mapping between the two semantics would surprise more than help.
30
+ *
31
+ * v2 stored a single `provider` + optional `model`. It IS migrated, on read, in
32
+ * memory: one provider is a one-element chain, which is unambiguous, and the
33
+ * distinction from v1 is exactly that. Migration does not rewrite the file — a
34
+ * reader with a write side effect would rewrite `$HOME` on a `--help`. The next
35
+ * `writePreferences` lands v3.
36
+ *
37
+ * Note the consequence: a v3 file read by an older namzu reports `needs-repick`
38
+ * rather than silently losing the tail, which is the safe direction.
17
39
  */
18
40
  import { randomBytes } from 'node:crypto';
19
41
  import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
20
42
  import { homedir } from 'node:os';
21
43
  import { dirname, join } from 'node:path';
44
+ import { PROVIDER_REGISTRY } from './registry.js';
22
45
  const FILE_MODE = 0o600;
23
46
  const DIR_MODE = 0o700;
24
- export const PREFERENCES_FILE_VERSION = 2;
47
+ export const PREFERENCES_FILE_VERSION = 3;
48
+ /**
49
+ * How a member is named to the operator, by position.
50
+ *
51
+ * One function because three surfaces say it — validation, the doctor listing
52
+ * and the capability refusal — and a chain member called `fallback #1` in one
53
+ * message and `fallback 1` in the next reads as two different things.
54
+ */
55
+ export function chainPositionName(index) {
56
+ return index === 0 ? 'primary provider' : `fallback #${index}`;
57
+ }
25
58
  export class PreferencesError extends Error {
26
59
  constructor(message) {
27
60
  super(message);
28
61
  this.name = 'PreferencesError';
29
62
  }
30
63
  }
64
+ /**
65
+ * The member that actually runs.
66
+ *
67
+ * A helper rather than `prefs.providers[0]` at each call site: the chain is
68
+ * guaranteed non-empty by validation, and every reader repeating an index
69
+ * lookup plus a non-null assertion would be four places to get that wrong.
70
+ */
71
+ export function primaryProvider(prefs) {
72
+ // Optional-chained rather than indexed: a caller handing over an object of
73
+ // the old shape is a real case (a stale test fixture did exactly that), and
74
+ // a `TypeError: cannot read '0' of undefined` names neither the field nor
75
+ // the fix, while the error below names both.
76
+ const first = prefs.providers?.[0];
77
+ if (!first) {
78
+ // Unreachable through readPreferences/writePreferences, both of which
79
+ // refuse an empty chain. Reachable only from a hand-built object, and
80
+ // silently returning a placeholder there would put the caller on a
81
+ // provider nobody chose.
82
+ throw new PreferencesError('preferences.providers is empty — no provider to run');
83
+ }
84
+ return first;
85
+ }
31
86
  export function preferencesPath(home = homedir()) {
32
87
  return join(home, '.namzu', 'preferences.json');
33
88
  }
@@ -59,6 +114,16 @@ export function readPreferences(home = homedir()) {
59
114
  reason: 'preferences file uses an older schema (v1) — namzu now picks an LLM provider client directly. Please re-pick.',
60
115
  };
61
116
  }
117
+ if (v.version === 2) {
118
+ const migrated = migrateV2(parsed);
119
+ if (!migrated) {
120
+ throw new PreferencesError(`${path} has an unexpected shape for a v2 file`);
121
+ }
122
+ const invalid = describeInvalidChain(migrated.providers);
123
+ if (invalid)
124
+ return { status: 'needs-repick', reason: `${invalid} — please re-pick.` };
125
+ return { status: 'ok', prefs: migrated };
126
+ }
62
127
  if (v.version !== PREFERENCES_FILE_VERSION) {
63
128
  return {
64
129
  status: 'needs-repick',
@@ -68,15 +133,27 @@ export function readPreferences(home = homedir()) {
68
133
  if (!isPreferences(parsed)) {
69
134
  throw new PreferencesError(`${path} has an unexpected shape`);
70
135
  }
136
+ // Shape is right; the CONTENT may still name a provider that does not exist
137
+ // or repeat a member. Reported as `needs-repick` with the offending member
138
+ // named, not thrown: a bad chain is something the operator can fix by
139
+ // picking again, and it is the tail this is really for — an unusable
140
+ // fallback that only surfaces the day the primary goes down is the failure
141
+ // this check exists to prevent.
142
+ const invalid = describeInvalidChain(parsed.providers);
143
+ if (invalid)
144
+ return { status: 'needs-repick', reason: `${invalid} — please re-pick.` };
71
145
  return { status: 'ok', prefs: parsed };
72
146
  }
73
147
  export function writePreferences(prefs, home = homedir()) {
74
148
  if (prefs.version !== PREFERENCES_FILE_VERSION) {
75
149
  throw new PreferencesError(`unsupported preferences version: ${String(prefs.version)} (expected ${PREFERENCES_FILE_VERSION})`);
76
150
  }
77
- if (typeof prefs.provider !== 'string' || prefs.provider.length === 0) {
78
- throw new PreferencesError('preferences.provider is required');
151
+ if (!Array.isArray(prefs.providers) || prefs.providers.length === 0) {
152
+ throw new PreferencesError('preferences.providers must list at least one provider');
79
153
  }
154
+ const invalid = describeInvalidChain(prefs.providers);
155
+ if (invalid)
156
+ throw new PreferencesError(invalid);
80
157
  const path = preferencesPath(home);
81
158
  mkdirSync(dirname(path), { recursive: true, mode: DIR_MODE });
82
159
  const body = `${JSON.stringify(prefs, null, 2)}\n`;
@@ -85,27 +162,105 @@ export function writePreferences(prefs, home = homedir()) {
85
162
  chmodSync(tmp, FILE_MODE);
86
163
  renameSync(tmp, path);
87
164
  }
165
+ /**
166
+ * A v2 file as a one-element chain, or null if it was not a well-formed v2.
167
+ *
168
+ * `model` is carried only when present, so a v2 file that never pinned a model
169
+ * does not gain one.
170
+ */
171
+ function migrateV2(value) {
172
+ const v = value;
173
+ if (typeof v.provider !== 'string' || v.provider.length === 0)
174
+ return null;
175
+ if (v.model !== undefined && typeof v.model !== 'string')
176
+ return null;
177
+ if (!isSubagents(v.subagents))
178
+ return null;
179
+ const member = {
180
+ id: v.provider,
181
+ ...(typeof v.model === 'string' ? { model: v.model } : {}),
182
+ };
183
+ return {
184
+ version: PREFERENCES_FILE_VERSION,
185
+ providers: [member],
186
+ ...(v.subagents !== undefined
187
+ ? { subagents: v.subagents }
188
+ : {}),
189
+ };
190
+ }
191
+ /**
192
+ * Why this chain is unusable, or null if it is fine.
193
+ *
194
+ * Every member is checked, not only the head. A chain exists to be fallen back
195
+ * to, so a tail member naming a provider that does not exist is precisely as
196
+ * broken as a bad head — it is just broken later, on the worst day.
197
+ */
198
+ function describeInvalidChain(members) {
199
+ if (members.length === 0) {
200
+ return 'preferences lists no providers';
201
+ }
202
+ const seen = new Set();
203
+ for (const [index, member] of members.entries()) {
204
+ const position = chainPositionName(index);
205
+ if (!(member.id in PROVIDER_REGISTRY)) {
206
+ return `${position} "${member.id}" is not a provider namzu knows (known: ${Object.keys(PROVIDER_REGISTRY).join(', ')})`;
207
+ }
208
+ // Compared as the pair the operator WROTE, not against the resolved
209
+ // default model. Resolving would make a file that was valid yesterday
210
+ // invalid the day a registry default changes, which is a config that
211
+ // breaks on upgrade. Two entries for one provider with different models
212
+ // stay legitimate: falling back from a large model to a small one on the
213
+ // same provider is a real chain.
214
+ const key = JSON.stringify([member.id, member.model ?? null]);
215
+ if (seen.has(key)) {
216
+ const shown = member.model ? `${member.id} (${member.model})` : member.id;
217
+ return `${position} repeats "${shown}", which is already earlier in the chain — a provider cannot be its own fallback`;
218
+ }
219
+ seen.add(key);
220
+ }
221
+ return null;
222
+ }
223
+ function isSubagents(value) {
224
+ if (value === undefined)
225
+ return true;
226
+ if (typeof value !== 'object' || value === null)
227
+ return false;
228
+ const sa = value;
229
+ if (!Array.isArray(sa.active))
230
+ return false;
231
+ for (const item of sa.active) {
232
+ if (typeof item !== 'string')
233
+ return false;
234
+ }
235
+ return true;
236
+ }
237
+ function isProviderChoice(value) {
238
+ if (typeof value !== 'object' || value === null)
239
+ return false;
240
+ const v = value;
241
+ if (typeof v.id !== 'string' || v.id.length === 0)
242
+ return false;
243
+ if (v.model !== undefined && typeof v.model !== 'string')
244
+ return false;
245
+ return true;
246
+ }
88
247
  function isPreferences(value) {
89
248
  if (typeof value !== 'object' || value === null)
90
249
  return false;
91
250
  const v = value;
92
251
  if (v.version !== PREFERENCES_FILE_VERSION)
93
252
  return false;
94
- if (typeof v.provider !== 'string' || v.provider.length === 0)
253
+ if (!Array.isArray(v.providers))
95
254
  return false;
96
- if (v.model !== undefined && typeof v.model !== 'string')
97
- return false;
98
- if (v.subagents !== undefined) {
99
- if (typeof v.subagents !== 'object' || v.subagents === null)
100
- return false;
101
- const sa = v.subagents;
102
- if (!Array.isArray(sa.active))
255
+ for (const member of v.providers) {
256
+ if (!isProviderChoice(member))
103
257
  return false;
104
- for (const item of sa.active) {
105
- if (typeof item !== 'string')
106
- return false;
107
- }
108
258
  }
259
+ if (v.allowCapabilityMismatch !== undefined && typeof v.allowCapabilityMismatch !== 'boolean') {
260
+ return false;
261
+ }
262
+ if (!isSubagents(v.subagents))
263
+ return false;
109
264
  return true;
110
265
  }
111
266
  //# sourceMappingURL=preferences.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"preferences.js","sourceRoot":"","sources":["../../../src/integrations/providers/preferences.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"preferences.js","sourceRoot":"","sources":["../../../src/integrations/providers/preferences.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/tui/App.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAwDH,OAAO,KAAK,EAAqB,UAAU,EAAE,MAAM,YAAY,CAAA;AAE/D,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAA;CACxB;AAWD,wBAAgB,GAAG,CAAC,EAAE,GAAG,EAAE,EAAE,QAAQ,2CAw2BpC"}
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/tui/App.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA0DH,OAAO,KAAK,EAAqB,UAAU,EAAE,MAAM,YAAY,CAAA;AAE/D,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAA;CACxB;AAWD,wBAAgB,GAAG,CAAC,EAAE,GAAG,EAAE,EAAE,QAAQ,2CAg7BpC"}
package/dist/tui/App.js CHANGED
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { Box, Text, useApp, useInput } from 'ink';
3
3
  import { relative } from 'node:path';
4
4
  import { useCallback, useEffect, useRef, useState } from 'react';
5
- import { writePreferences, } from '../integrations/providers/index.js';
5
+ import { primaryProvider, writePreferences, } from '../integrations/providers/index.js';
6
6
  import { isTrusted, trustDir } from '../integrations/trust/store.js';
7
7
  import { appendMemory, composeMemoryPrompt, readMemory } from '../memory/store.js';
8
8
  import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
@@ -14,6 +14,7 @@ import { Composer } from './Composer.js';
14
14
  import { TrustPrompt } from './TrustPrompt.js';
15
15
  import { NAMZU_MARK, NAMZU_MARK_COLOR, NAMZU_WORDMARK, NAMZU_WORDMARK_GRADIENT, NAMZU_WORDMARK_MIN_WIDTH, } from './logo.js';
16
16
  import { PermissionOverlay } from './PermissionOverlay.js';
17
+ import { approvalIsDeliberate } from './consent-timing.js';
17
18
  import { Picker } from './Picker.js';
18
19
  import { StatusBar } from './StatusBar.js';
19
20
  import { Transcript } from './Transcript.js';
@@ -71,6 +72,23 @@ export function App({ ctx }) {
71
72
  setActiveTools([]);
72
73
  }, []);
73
74
  const permissionResolveRef = useRef(null);
75
+ /**
76
+ * When the pending prompt took the screen, or `null` with none open.
77
+ *
78
+ * A ref rather than state because the keypress handler reads it in the same
79
+ * tick the prompt opens, before any re-render could carry a new value — see
80
+ * `permission-timing.ts` for why an approving key has to wait on this.
81
+ */
82
+ const permissionOpenedAtRef = useRef(null);
83
+ /**
84
+ * When the trust gate was painted, or `null` while it is not up.
85
+ *
86
+ * The same quantity as `permissionOpenedAtRef`, for the other consent
87
+ * screen. Set from an effect rather than at the `setPhase('trust')` call
88
+ * because effects run after the commit, so the window starts when the gate
89
+ * could first have been SEEN rather than when it was decided upon.
90
+ */
91
+ const trustShownAtRef = useRef(null);
74
92
  // SDK-backed conversation persistence (DiskSessionStore). `scopeRef` carries
75
93
  // the active session id used by query() — mutated in place on /resume so new
76
94
  // turns attribute to the resumed conversation.
@@ -144,10 +162,16 @@ export function App({ ctx }) {
144
162
  // the file is shadowed instead of leaving it silently unused.
145
163
  reserved: SLASH_COMMANDS.map((c) => c.name),
146
164
  }));
147
- setCurrentProvider(prefs.provider);
165
+ setCurrentProvider(primaryProvider(prefs).id);
148
166
  if (s.hasProvider) {
149
167
  setPhase('ready');
150
168
  pushMessage('system', `Connected to ${s.providerSummary}${s.modelSummary ? ` · ${s.modelSummary}` : ''} · ${s.toolNames.length} tools`);
169
+ // Before the rest: a limitation the operator accepted once and has
170
+ // been living with since is the thing they are least likely to
171
+ // remember and most likely to be surprised by.
172
+ for (const notice of s.configNotices) {
173
+ pushMessage('system', notice);
174
+ }
151
175
  // Named, not counted. "2 files" tells a user their conventions were
152
176
  // found but not WHICH ones, and the interesting case is the one
153
177
  // they did not expect — an instructions file in a parent directory
@@ -204,6 +228,11 @@ export function App({ ctx }) {
204
228
  setPhase('trust');
205
229
  }
206
230
  }, [ctx.cwd, runProbe]);
231
+ // Starts the settle window when the gate is on screen, and clears it when it
232
+ // leaves so a later stray key can never be measured against a stale one.
233
+ useEffect(() => {
234
+ trustShownAtRef.current = phase === 'trust' ? Date.now() : null;
235
+ }, [phase]);
207
236
  const acceptTrust = useCallback(() => {
208
237
  try {
209
238
  trustDir(ctx.cwd);
@@ -295,6 +324,7 @@ export function App({ ctx }) {
295
324
  const resolvePermission = useCallback((decision) => {
296
325
  const resolve = permissionResolveRef.current;
297
326
  permissionResolveRef.current = null;
327
+ permissionOpenedAtRef.current = null;
298
328
  setPermission(null);
299
329
  if (resolve)
300
330
  resolve(decision);
@@ -303,6 +333,7 @@ export function App({ ctx }) {
303
333
  // non-read-only tool batch; it parks until the user presses y/n/a.
304
334
  const onPermission = useCallback((req) => new Promise((resolve) => {
305
335
  permissionResolveRef.current = resolve;
336
+ permissionOpenedAtRef.current = Date.now();
306
337
  setPermission(req);
307
338
  setState('awaiting-permission');
308
339
  }), []);
@@ -451,6 +482,7 @@ export function App({ ctx }) {
451
482
  finally {
452
483
  abortRef.current = null;
453
484
  permissionResolveRef.current = null;
485
+ permissionOpenedAtRef.current = null;
454
486
  setPermission(null);
455
487
  clearActiveTools();
456
488
  setState('idle');
@@ -601,13 +633,23 @@ export function App({ ctx }) {
601
633
  setDetected(next);
602
634
  // The disposition, not the key. `credential` never reaches a message.
603
635
  pushMessage('system', disposition);
604
- void hydrateSession({ version: 2, provider: credential.entry.id, subagents: { active: [] } }, next);
636
+ void hydrateSession({
637
+ version: 3,
638
+ providers: [{ id: credential.entry.id }],
639
+ subagents: { active: [] },
640
+ }, next);
605
641
  }, [detected, hydrateSession, pushMessage]);
606
642
  const handlePickerSubmit = useCallback((selection) => {
643
+ // One member for now. The picker builds a longer chain in a later
644
+ // change; the shape it writes into is already the chain.
607
645
  const prefs = {
608
- version: 2,
609
- provider: selection.provider,
610
- model: selection.model,
646
+ version: 3,
647
+ providers: [
648
+ {
649
+ id: selection.provider,
650
+ ...(selection.model !== undefined ? { model: selection.model } : {}),
651
+ },
652
+ ],
611
653
  subagents: { active: [] },
612
654
  };
613
655
  try {
@@ -642,26 +684,56 @@ export function App({ ctx }) {
642
684
  // Trust gate owns the keyboard until the folder is trusted or we exit.
643
685
  if (phase === 'trust') {
644
686
  const ch = input.toLowerCase();
645
- if (ch === 'y' || key.return)
646
- acceptTrust();
647
- else if (ch === 'n' || key.escape || (key.ctrl && input === 'c'))
687
+ // Refusal answers on the first press and is never deferred. It only
688
+ // exits: nothing is written, nothing has run, and a relaunch costs a
689
+ // second — so an accidental refusal is the recoverable direction,
690
+ // and making the escape hatch hesitate on the program's first screen
691
+ // would read as a hang.
692
+ if (ch === 'n' || key.escape || (key.ctrl && input === 'c')) {
648
693
  exit();
694
+ return;
695
+ }
696
+ // Enter is deliberately NOT trust, for the reason it is not approval
697
+ // at the tool prompt — and more so here. The operator reached this
698
+ // screen by typing `namzu` and pressing Enter, so a key repeat or an
699
+ // impatient second press arrives while the gate is still painting;
700
+ // this is the one moment in the program where an in-flight Enter is
701
+ // close to expected. And the decision is durable: `acceptTrust`
702
+ // writes the folder into `~/.namzu/trust.json`, which covers every
703
+ // subfolder, so a stray keystroke grants standing permission to a
704
+ // whole tree rather than to one call.
705
+ if (!approvalIsDeliberate(trustShownAtRef.current, Date.now()))
706
+ return;
707
+ if (ch === 'y')
708
+ acceptTrust();
649
709
  return;
650
710
  }
651
711
  // A pending permission prompt owns the keyboard: y/n/a decide it.
652
712
  if (permissionResolveRef.current) {
653
713
  const ch = input.toLowerCase();
654
- if (key.ctrl && (input === 'c' || input === '\x03')) {
714
+ // Refusing is never deferred or gated — it is the direction a
715
+ // mistake is recoverable in, so it answers on the first press.
716
+ if (key.ctrl && input === 'c') {
655
717
  resolvePermission({ kind: 'reject', feedback: 'User interrupted.' });
656
718
  abortRef.current?.abort();
657
719
  return;
658
720
  }
659
- if (ch === 'y' || key.return)
721
+ if (ch === 'n' || key.escape) {
722
+ resolvePermission({ kind: 'reject' });
723
+ return;
724
+ }
725
+ // Enter is deliberately NOT an approval. It is the key people press
726
+ // to dismiss a thing that appeared, it is the key already in flight
727
+ // when a turn's follow-up was being typed, and it is named nowhere
728
+ // on this prompt — so reading it as consent approves a tool call the
729
+ // operator never looked at. `y` and `a` are the only approvals, and
730
+ // they must arrive after the prompt has been up long enough to read.
731
+ if (!approvalIsDeliberate(permissionOpenedAtRef.current, Date.now()))
732
+ return;
733
+ if (ch === 'y')
660
734
  resolvePermission({ kind: 'approve' });
661
735
  else if (ch === 'a')
662
736
  resolvePermission({ kind: 'approve-all' });
663
- else if (ch === 'n' || key.escape)
664
- resolvePermission({ kind: 'reject' });
665
737
  return;
666
738
  }
667
739
  // Esc interrupts a running turn (Ctrl+C stays reserved for exit). Mirrors
@@ -678,7 +750,7 @@ export function App({ ctx }) {
678
750
  setExpanded((e) => !e);
679
751
  return;
680
752
  }
681
- if (key.ctrl && (input === 'c' || input === '\x03')) {
753
+ if (key.ctrl && input === 'c') {
682
754
  // A turn is running → first Ctrl+C interrupts it, not exits.
683
755
  if (abortRef.current) {
684
756
  abortRef.current.abort();
@@ -732,8 +804,10 @@ function formatToolCall(toolName, summary) {
732
804
  return summary.length > 0 ? `${display}(${summary})` : display;
733
805
  }
734
806
  function hintForPhase(phase, state) {
807
+ // Enter is absent because Enter grants nothing here, deliberately — see the
808
+ // trust branch in the key handler above.
735
809
  if (phase === 'trust')
736
- return 'y trust this folder · n exit';
810
+ return 'y trust this folder · n / esc exit';
737
811
  if (phase === 'resume')
738
812
  return '↑↓ navigate · enter resume · esc cancel';
739
813
  if (phase === 'probing')
@@ -742,8 +816,10 @@ function hintForPhase(phase, state) {
742
816
  return '↑↓ navigate · enter accept · esc cancel';
743
817
  if (phase === 'unhealthy')
744
818
  return 'Ctrl+C ×2 to exit';
819
+ // Enter is absent because Enter decides nothing here, deliberately — see the
820
+ // permission gate in the key handler above.
745
821
  if (state === 'awaiting-permission')
746
- return 'y approve · n reject · a approve all';
822
+ return 'y approve · n / esc reject · a approve all';
747
823
  if (state !== 'idle')
748
824
  return 'agent is working — esc to interrupt';
749
825
  return '/help · @file / Ctrl+V to attach · Ctrl+C ×2 to exit';