@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.
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +15 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/run-flags.d.ts +23 -0
- package/dist/commands/run-flags.d.ts.map +1 -1
- package/dist/commands/run-flags.js +39 -0
- package/dist/commands/run-flags.js.map +1 -1
- package/dist/commands/run-stream.d.ts.map +1 -1
- package/dist/commands/run-stream.js +12 -6
- package/dist/commands/run-stream.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +12 -6
- package/dist/commands/run.js.map +1 -1
- package/dist/doctor/checks/chain.d.ts +32 -0
- package/dist/doctor/checks/chain.d.ts.map +1 -0
- package/dist/doctor/checks/chain.js +118 -0
- package/dist/doctor/checks/chain.js.map +1 -0
- package/dist/doctor/checks/index.d.ts +2 -1
- package/dist/doctor/checks/index.d.ts.map +1 -1
- package/dist/doctor/checks/index.js +6 -1
- package/dist/doctor/checks/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/providers/chain-capabilities.d.ts +80 -0
- package/dist/integrations/providers/chain-capabilities.d.ts.map +1 -0
- package/dist/integrations/providers/chain-capabilities.js +207 -0
- package/dist/integrations/providers/chain-capabilities.js.map +1 -0
- package/dist/integrations/providers/index.d.ts +2 -1
- package/dist/integrations/providers/index.d.ts.map +1 -1
- package/dist/integrations/providers/index.js +2 -1
- package/dist/integrations/providers/index.js.map +1 -1
- package/dist/integrations/providers/preferences.d.ts +84 -15
- package/dist/integrations/providers/preferences.d.ts.map +1 -1
- package/dist/integrations/providers/preferences.js +180 -25
- package/dist/integrations/providers/preferences.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +92 -16
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/PermissionOverlay.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.js +1 -1
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/TrustPrompt.d.ts.map +1 -1
- package/dist/tui/TrustPrompt.js +1 -1
- package/dist/tui/TrustPrompt.js.map +1 -1
- package/dist/tui/agent.d.ts +12 -0
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +70 -10
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/consent-timing.d.ts +44 -0
- package/dist/tui/consent-timing.d.ts.map +1 -0
- package/dist/tui/consent-timing.js +48 -0
- package/dist/tui/consent-timing.js.map +1 -0
- package/package.json +3 -3
|
@@ -1,33 +1,88 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `~/.namzu/preferences.json` — the
|
|
2
|
+
* `~/.namzu/preferences.json` — the operator's provider chain.
|
|
3
3
|
*
|
|
4
|
-
* Schema (current =
|
|
4
|
+
* Schema (current = v3):
|
|
5
5
|
* {
|
|
6
|
-
* "version":
|
|
7
|
-
* "
|
|
8
|
-
*
|
|
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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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 =
|
|
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 (
|
|
78
|
-
throw new PreferencesError('preferences.
|
|
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 (
|
|
253
|
+
if (!Array.isArray(v.providers))
|
|
95
254
|
return false;
|
|
96
|
-
|
|
97
|
-
|
|
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":"AAAA
|
|
1
|
+
{"version":3,"file":"preferences.js","sourceRoot":"","sources":["../../../src/integrations/providers/preferences.ts"],"names":[],"mappings":"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"}
|
package/dist/tui/App.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/tui/App.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;
|
|
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.
|
|
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({
|
|
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:
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
|
|
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 === '
|
|
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 &&
|
|
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';
|