@namzu/cli 2.6.0 → 3.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 (56) 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 +69 -13
  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/agent.d.ts +12 -0
  45. package/dist/tui/agent.d.ts.map +1 -1
  46. package/dist/tui/agent.js +70 -10
  47. package/dist/tui/agent.js.map +1 -1
  48. package/dist/tui/bottom-spacer.d.ts +72 -0
  49. package/dist/tui/bottom-spacer.d.ts.map +1 -0
  50. package/dist/tui/bottom-spacer.js +83 -0
  51. package/dist/tui/bottom-spacer.js.map +1 -0
  52. package/dist/tui/permission-timing.d.ts +35 -0
  53. package/dist/tui/permission-timing.d.ts.map +1 -0
  54. package/dist/tui/permission-timing.js +39 -0
  55. package/dist/tui/permission-timing.js.map +1 -0
  56. package/package.json +4 -4
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACvF,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEzC,OAAO,EAAE,iBAAiB,EAAmB,MAAM,eAAe,CAAA;AAElE,MAAM,SAAS,GAAG,KAAK,CAAA;AACvB,MAAM,QAAQ,GAAG,KAAK,CAAA;AACtB,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAU,CAAA;AAyClD;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC9C,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,aAAa,KAAK,EAAE,CAAA;AAC/D,CAAC;AAOD,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAC1C,YAAY,OAAe;QAC1B,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAA;IAC/B,CAAC;CACD;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,KAAkB;IACjD,2EAA2E;IAC3E,4EAA4E;IAC5E,0EAA0E;IAC1E,6CAA6C;IAC7C,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAA;IAClC,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,sEAAsE;QACtE,sEAAsE;QACtE,mEAAmE;QACnE,yBAAyB;QACzB,MAAM,IAAI,gBAAgB,CAAC,qDAAqD,CAAC,CAAA;IAClF,CAAC;IACD,OAAO,KAAK,CAAA;AACb,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAe,OAAO,EAAE;IACvD,OAAO,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,kBAAkB,CAAC,CAAA;AAChD,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAe,OAAO,EAAE;IACvD,MAAM,IAAI,GAAG,eAAe,CAAC,IAAI,CAAC,CAAA;IAClC,IAAI,GAAW,CAAA;IACf,IAAI,CAAC;QACJ,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACjC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;QAClF,MAAM,IAAI,gBAAgB,CACzB,kBAAkB,IAAI,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAC7E,CAAA;IACF,CAAC;IACD,IAAI,MAAe,CAAA;IACnB,IAAI,CAAC;QACJ,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACzB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,IAAI,gBAAgB,CACzB,GAAG,IAAI,uBAAuB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAChF,CAAA;IACF,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACnD,MAAM,IAAI,gBAAgB,CAAC,GAAG,IAAI,8BAA8B,CAAC,CAAA;IAClE,CAAC;IACD,MAAM,CAAC,GAAG,MAA+B,CAAA;IACzC,IAAI,CAAC,CAAC,OAAO,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO;YACN,MAAM,EAAE,cAAc;YACtB,MAAM,EACL,+GAA+G;SAChH,CAAA;IACF,CAAC;IACD,IAAI,CAAC,CAAC,OAAO,KAAK,CAAC,EAAE,CAAC;QACrB,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,CAAC,CAAA;QAClC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACf,MAAM,IAAI,gBAAgB,CAAC,GAAG,IAAI,wCAAwC,CAAC,CAAA;QAC5E,CAAC;QACD,MAAM,OAAO,GAAG,oBAAoB,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAA;QACxD,IAAI,OAAO;YAAE,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,OAAO,oBAAoB,EAAE,CAAA;QACtF,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAA;IACzC,CAAC;IACD,IAAI,CAAC,CAAC,OAAO,KAAK,wBAAwB,EAAE,CAAC;QAC5C,OAAO;YACN,MAAM,EAAE,cAAc;YACtB,MAAM,EAAE,2CAA2C,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,oBAAoB;SACxF,CAAA;IACF,CAAC;IACD,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,gBAAgB,CAAC,GAAG,IAAI,0BAA0B,CAAC,CAAA;IAC9D,CAAC;IACD,4EAA4E;IAC5E,2EAA2E;IAC3E,sEAAsE;IACtE,qEAAqE;IACrE,2EAA2E;IAC3E,gCAAgC;IAChC,MAAM,OAAO,GAAG,oBAAoB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IACtD,IAAI,OAAO;QAAE,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,OAAO,oBAAoB,EAAE,CAAA;IACtF,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,CAAA;AACvC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAkB,EAAE,OAAe,OAAO,EAAE;IAC5E,IAAI,KAAK,CAAC,OAAO,KAAK,wBAAwB,EAAE,CAAC;QAChD,MAAM,IAAI,gBAAgB,CACzB,oCAAoC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,cAAc,wBAAwB,GAAG,CAClG,CAAA;IACF,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,gBAAgB,CAAC,uDAAuD,CAAC,CAAA;IACpF,CAAC;IACD,MAAM,OAAO,GAAG,oBAAoB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;IACrD,IAAI,OAAO;QAAE,MAAM,IAAI,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAEhD,MAAM,IAAI,GAAG,eAAe,CAAC,IAAI,CAAC,CAAA;IAClC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAA;IAC7D,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAA;IAClD,MAAM,GAAG,GAAG,GAAG,IAAI,QAAQ,OAAO,CAAC,GAAG,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAA;IAC1E,aAAa,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAA;IAC7C,SAAS,CAAC,GAAG,EAAE,SAAS,CAAC,CAAA;IACzB,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;AACtB,CAAC;AAED;;;;;GAKG;AACH,SAAS,SAAS,CAAC,KAAc;IAChC,MAAM,CAAC,GAAG,KAAgC,CAAA;IAC1C,IAAI,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAC1E,IAAI,CAAC,CAAC,KAAK,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IACrE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAA;IAC1C,MAAM,MAAM,GAAmB;QAC9B,EAAE,EAAE,CAAC,CAAC,QAAsB;QAC5B,GAAG,CAAC,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1D,CAAA;IACD,OAAO;QACN,OAAO,EAAE,wBAAwB;QACjC,SAAS,EAAE,CAAC,MAAM,CAAC;QACnB,GAAG,CAAC,CAAC,CAAC,SAAS,KAAK,SAAS;YAC5B,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,SAAmD,EAAE;YACtE,CAAC,CAAC,EAAE,CAAC;KACN,CAAA;AACF,CAAC;AAED;;;;;;GAMG;AACH,SAAS,oBAAoB,CAAC,OAAkC;IAC/D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,gCAAgC,CAAA;IACxC,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,KAAK,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;QACjD,MAAM,QAAQ,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAA;QACzC,IAAI,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,iBAAiB,CAAC,EAAE,CAAC;YACvC,OAAO,GAAG,QAAQ,KAAK,MAAM,CAAC,EAAE,2CAA2C,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAA;QACxH,CAAC;QACD,oEAAoE;QACpE,sEAAsE;QACtE,qEAAqE;QACrE,wEAAwE;QACxE,yEAAyE;QACzE,iCAAiC;QACjC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAA;QAC7D,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACnB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,EAAE,KAAK,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAA;YACzE,OAAO,GAAG,QAAQ,aAAa,KAAK,kFAAkF,CAAA;QACvH,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACd,CAAC;IACD,OAAO,IAAI,CAAA;AACZ,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IAClC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IACpC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IAC7D,MAAM,EAAE,GAAG,KAAgC,CAAA;IAC3C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAA;IAC3C,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC;QAC9B,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAA;IAC3C,CAAC;IACD,OAAO,IAAI,CAAA;AACZ,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAc;IACvC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IAC7D,MAAM,CAAC,GAAG,KAAgC,CAAA;IAC1C,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ,IAAI,CAAC,CAAC,EAAE,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAC/D,IAAI,CAAC,CAAC,KAAK,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IACtE,OAAO,IAAI,CAAA;AACZ,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACpC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IAC7D,MAAM,CAAC,GAAG,KAAgC,CAAA;IAC1C,IAAI,CAAC,CAAC,OAAO,KAAK,wBAAwB;QAAE,OAAO,KAAK,CAAA;IACxD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;QAAE,OAAO,KAAK,CAAA;IAC7C,KAAK,MAAM,MAAM,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;QAClC,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC;YAAE,OAAO,KAAK,CAAA;IAC5C,CAAC;IACD,IAAI,CAAC,CAAC,uBAAuB,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,uBAAuB,KAAK,SAAS,EAAE,CAAC;QAC/F,OAAO,KAAK,CAAA;IACb,CAAC;IACD,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC;QAAE,OAAO,KAAK,CAAA;IAC3C,OAAO,IAAI,CAAA;AACZ,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/tui/App.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAuDH,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,2CAk1BpC"}
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,2CA+4BpC"}
package/dist/tui/App.js CHANGED
@@ -2,17 +2,19 @@ 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';
9
9
  import { checkUpdates } from '../integrations/updates.js';
10
10
  import { LiveActivity, formatElapsed } from './LiveActivity.js';
11
+ import { bottomSpacerRows } from './bottom-spacer.js';
11
12
  import { expandFileMentions } from './mentions.js';
12
13
  import { Composer } from './Composer.js';
13
14
  import { TrustPrompt } from './TrustPrompt.js';
14
15
  import { NAMZU_MARK, NAMZU_MARK_COLOR, NAMZU_WORDMARK, NAMZU_WORDMARK_GRADIENT, NAMZU_WORDMARK_MIN_WIDTH, } from './logo.js';
15
16
  import { PermissionOverlay } from './PermissionOverlay.js';
17
+ import { approvalIsDeliberate } from './permission-timing.js';
16
18
  import { Picker } from './Picker.js';
17
19
  import { StatusBar } from './StatusBar.js';
18
20
  import { Transcript } from './Transcript.js';
@@ -70,6 +72,14 @@ export function App({ ctx }) {
70
72
  setActiveTools([]);
71
73
  }, []);
72
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);
73
83
  // SDK-backed conversation persistence (DiskSessionStore). `scopeRef` carries
74
84
  // the active session id used by query() — mutated in place on /resume so new
75
85
  // turns attribute to the resumed conversation.
@@ -143,10 +153,16 @@ export function App({ ctx }) {
143
153
  // the file is shadowed instead of leaving it silently unused.
144
154
  reserved: SLASH_COMMANDS.map((c) => c.name),
145
155
  }));
146
- setCurrentProvider(prefs.provider);
156
+ setCurrentProvider(primaryProvider(prefs).id);
147
157
  if (s.hasProvider) {
148
158
  setPhase('ready');
149
159
  pushMessage('system', `Connected to ${s.providerSummary}${s.modelSummary ? ` · ${s.modelSummary}` : ''} · ${s.toolNames.length} tools`);
160
+ // Before the rest: a limitation the operator accepted once and has
161
+ // been living with since is the thing they are least likely to
162
+ // remember and most likely to be surprised by.
163
+ for (const notice of s.configNotices) {
164
+ pushMessage('system', notice);
165
+ }
150
166
  // Named, not counted. "2 files" tells a user their conventions were
151
167
  // found but not WHICH ones, and the interesting case is the one
152
168
  // they did not expect — an instructions file in a parent directory
@@ -213,6 +229,19 @@ export function App({ ctx }) {
213
229
  setPhase('probing');
214
230
  void runProbe();
215
231
  }, [ctx.cwd, runProbe]);
232
+ // Blank rows above the composer, while the transcript is short enough that
233
+ // the answer is knowable. `liveRows` is the fixed furniture beneath the
234
+ // transcript — activity line, composer frame, status bar and their padding —
235
+ // counted generously, because over-counting costs a gap and under-counting
236
+ // costs the composer.
237
+ const spacerRows = phase === 'ready'
238
+ ? bottomSpacerRows({
239
+ rows: process.stdout.rows,
240
+ columns: process.stdout.columns,
241
+ transcript: messages.filter((m) => !m.pending).map((m) => m.content),
242
+ liveRows: 10,
243
+ })
244
+ : 0;
216
245
  const slashCtx = {
217
246
  availableTools: session?.toolNames ?? [],
218
247
  providerSummary: session?.providerSummary ?? null,
@@ -281,6 +310,7 @@ export function App({ ctx }) {
281
310
  const resolvePermission = useCallback((decision) => {
282
311
  const resolve = permissionResolveRef.current;
283
312
  permissionResolveRef.current = null;
313
+ permissionOpenedAtRef.current = null;
284
314
  setPermission(null);
285
315
  if (resolve)
286
316
  resolve(decision);
@@ -289,6 +319,7 @@ export function App({ ctx }) {
289
319
  // non-read-only tool batch; it parks until the user presses y/n/a.
290
320
  const onPermission = useCallback((req) => new Promise((resolve) => {
291
321
  permissionResolveRef.current = resolve;
322
+ permissionOpenedAtRef.current = Date.now();
292
323
  setPermission(req);
293
324
  setState('awaiting-permission');
294
325
  }), []);
@@ -437,6 +468,7 @@ export function App({ ctx }) {
437
468
  finally {
438
469
  abortRef.current = null;
439
470
  permissionResolveRef.current = null;
471
+ permissionOpenedAtRef.current = null;
440
472
  setPermission(null);
441
473
  clearActiveTools();
442
474
  setState('idle');
@@ -587,13 +619,23 @@ export function App({ ctx }) {
587
619
  setDetected(next);
588
620
  // The disposition, not the key. `credential` never reaches a message.
589
621
  pushMessage('system', disposition);
590
- void hydrateSession({ version: 2, provider: credential.entry.id, subagents: { active: [] } }, next);
622
+ void hydrateSession({
623
+ version: 3,
624
+ providers: [{ id: credential.entry.id }],
625
+ subagents: { active: [] },
626
+ }, next);
591
627
  }, [detected, hydrateSession, pushMessage]);
592
628
  const handlePickerSubmit = useCallback((selection) => {
629
+ // One member for now. The picker builds a longer chain in a later
630
+ // change; the shape it writes into is already the chain.
593
631
  const prefs = {
594
- version: 2,
595
- provider: selection.provider,
596
- model: selection.model,
632
+ version: 3,
633
+ providers: [
634
+ {
635
+ id: selection.provider,
636
+ ...(selection.model !== undefined ? { model: selection.model } : {}),
637
+ },
638
+ ],
597
639
  subagents: { active: [] },
598
640
  };
599
641
  try {
@@ -637,17 +679,29 @@ export function App({ ctx }) {
637
679
  // A pending permission prompt owns the keyboard: y/n/a decide it.
638
680
  if (permissionResolveRef.current) {
639
681
  const ch = input.toLowerCase();
640
- if (key.ctrl && (input === 'c' || input === '\x03')) {
682
+ // Refusing is never deferred or gated — it is the direction a
683
+ // mistake is recoverable in, so it answers on the first press.
684
+ if (key.ctrl && input === 'c') {
641
685
  resolvePermission({ kind: 'reject', feedback: 'User interrupted.' });
642
686
  abortRef.current?.abort();
643
687
  return;
644
688
  }
645
- if (ch === 'y' || key.return)
689
+ if (ch === 'n' || key.escape) {
690
+ resolvePermission({ kind: 'reject' });
691
+ return;
692
+ }
693
+ // Enter is deliberately NOT an approval. It is the key people press
694
+ // to dismiss a thing that appeared, it is the key already in flight
695
+ // when a turn's follow-up was being typed, and it is named nowhere
696
+ // on this prompt — so reading it as consent approves a tool call the
697
+ // operator never looked at. `y` and `a` are the only approvals, and
698
+ // they must arrive after the prompt has been up long enough to read.
699
+ if (!approvalIsDeliberate(permissionOpenedAtRef.current, Date.now()))
700
+ return;
701
+ if (ch === 'y')
646
702
  resolvePermission({ kind: 'approve' });
647
703
  else if (ch === 'a')
648
704
  resolvePermission({ kind: 'approve-all' });
649
- else if (ch === 'n' || key.escape)
650
- resolvePermission({ kind: 'reject' });
651
705
  return;
652
706
  }
653
707
  // Esc interrupts a running turn (Ctrl+C stays reserved for exit). Mirrors
@@ -664,7 +718,7 @@ export function App({ ctx }) {
664
718
  setExpanded((e) => !e);
665
719
  return;
666
720
  }
667
- if (key.ctrl && (input === 'c' || input === '\x03')) {
721
+ if (key.ctrl && input === 'c') {
668
722
  // A turn is running → first Ctrl+C interrupts it, not exits.
669
723
  if (abortRef.current) {
670
724
  abortRef.current.abort();
@@ -690,7 +744,7 @@ export function App({ ctx }) {
690
744
  // Background is left natural — we inherit the terminal's own background
691
745
  // and only theme the foreground. Forcing
692
746
  // a filled bg left mismatched patches around bordered areas, so we don't.
693
- return (_jsxs(Box, { flexDirection: "column", children: [phase !== 'ready' ? (_jsx(Banner, { version: ctx.version, session: session, bypass: ctx.skipPermissions === true, cwd: ctx.cwd })) : null, _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'picker' ? (_jsx(Picker, { detected: detected, currentProvider: currentProvider, currentModel: session?.modelSummary ?? null, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onCredential: handleTypedCredential })) : (_jsxs(_Fragment, { children: [_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: messages.filter((m) => !m.pending), pending: messages.find((m) => m.pending) ?? null, state: state, expanded: expanded, resetKey: resetKey, header: phase === 'ready' ? (_jsx(Banner, { version: ctx.version, session: session, bypass: ctx.skipPermissions === true, cwd: ctx.cwd })) : undefined }) }), _jsx(LiveActivity, { activeTools: activeTools, thinking: state === 'thinking' && !messages.some((m) => m.pending) }), permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls })) : (_jsxs(ComposerFrame, { focus: state === 'idle' && phase === 'ready', children: [queued.length > 0 ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.text.muted, children: ["\u23CE ", queued.length, " message", queued.length > 1 ? 's' : '', " queued \u2014 sending when ready"] }) })) : null, _jsx(Composer, { disabled: phase !== 'ready' || state === 'awaiting-permission', onSubmit: handleSubmit, userCommands: userCommands, history: history })] }))] })), _jsx(Box, { paddingTop: 1, children: _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, state: state, hint: hintForPhase(phase, state), usage: usage, context: context }) })] })] }));
747
+ return (_jsxs(Box, { flexDirection: "column", children: [phase !== 'ready' ? (_jsx(Banner, { version: ctx.version, session: session, bypass: ctx.skipPermissions === true, cwd: ctx.cwd })) : null, _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'picker' ? (_jsx(Picker, { detected: detected, currentProvider: currentProvider, currentModel: session?.modelSummary ?? null, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onCredential: handleTypedCredential })) : (_jsxs(_Fragment, { children: [_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: messages.filter((m) => !m.pending), pending: messages.find((m) => m.pending) ?? null, state: state, expanded: expanded, resetKey: resetKey, header: phase === 'ready' ? (_jsx(Banner, { version: ctx.version, session: session, bypass: ctx.skipPermissions === true, cwd: ctx.cwd })) : undefined }) }), spacerRows > 0 ? _jsx(Box, { height: spacerRows }) : null, _jsx(LiveActivity, { activeTools: activeTools, thinking: state === 'thinking' && !messages.some((m) => m.pending) }), permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls })) : (_jsxs(ComposerFrame, { focus: state === 'idle' && phase === 'ready', children: [queued.length > 0 ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.text.muted, children: ["\u23CE ", queued.length, " message", queued.length > 1 ? 's' : '', " queued \u2014 sending when ready"] }) })) : null, _jsx(Composer, { disabled: phase !== 'ready' || state === 'awaiting-permission', onSubmit: handleSubmit, userCommands: userCommands, history: history })] }))] })), _jsx(Box, { paddingTop: 1, children: _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, state: state, hint: hintForPhase(phase, state), usage: usage, context: context }) })] })] }));
694
748
  }
695
749
  function Banner({ version, session, bypass, cwd, }) {
696
750
  const cols = process.stdout.columns ?? 80;
@@ -728,8 +782,10 @@ function hintForPhase(phase, state) {
728
782
  return '↑↓ navigate · enter accept · esc cancel';
729
783
  if (phase === 'unhealthy')
730
784
  return 'Ctrl+C ×2 to exit';
785
+ // Enter is absent because Enter decides nothing here, deliberately — see the
786
+ // permission gate in the key handler above.
731
787
  if (state === 'awaiting-permission')
732
- return 'y approve · n reject · a approve all';
788
+ return 'y approve · n / esc reject · a approve all';
733
789
  if (state !== 'idle')
734
790
  return 'agent is working — esc to interrupt';
735
791
  return '/help · @file / Ctrl+V to attach · Ctrl+C ×2 to exit';