@crouton-kit/crouter 0.3.67 → 0.3.70
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/build-root.js +1 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +31 -31
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +28 -8
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +11 -11
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -0
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +8 -4
- package/dist/clients/attach/attach-cmd.js +668 -668
- package/dist/clients/attach/auth-pickers.d.ts +12 -0
- package/dist/clients/attach/auth-pickers.js +11 -0
- package/dist/clients/attach/pickers.js +2 -0
- package/dist/clients/attach/slash-commands.js +1 -0
- package/dist/commands/capture.d.ts +2 -0
- package/dist/commands/capture.js +28 -0
- package/dist/commands/node.js +3 -3
- package/dist/commands/profile/new.js +30 -5
- package/dist/commands/sys/setup-core.js +2 -0
- package/dist/commands/sys/setup.js +132 -16
- package/dist/core/__tests__/full/broker-pane-resolution.test.js +9 -6
- package/dist/core/__tests__/full/cascade-close.test.js +5 -2
- package/dist/core/__tests__/full/dead-pane-regression.test.js +6 -3
- package/dist/core/__tests__/full/detach-focus.test.js +10 -5
- package/dist/core/__tests__/full/human-new-window-regression.test.js +6 -3
- package/dist/core/__tests__/full/review-render-pane-regression.test.js +2 -2
- package/dist/core/__tests__/helpers/harness.d.ts +1 -0
- package/dist/core/__tests__/helpers/harness.js +28 -4
- package/dist/core/__tests__/live-mutation-verbs.test.js +5 -4
- package/dist/core/__tests__/review-model-floor.test.js +1 -1
- package/dist/core/__tests__/revive.test.js +70 -1
- package/dist/core/canvas/__tests__/remote-event-stream.test.js +11 -9
- package/dist/core/canvas/__tests__/render-remote.test.js +7 -4
- package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.js +30 -13
- package/dist/core/canvas/pid.d.ts +14 -10
- package/dist/core/canvas/pid.js +14 -10
- package/dist/core/command.d.ts +15 -0
- package/dist/core/command.js +72 -0
- package/dist/core/profiles/select.d.ts +4 -1
- package/dist/core/profiles/select.js +356 -81
- package/dist/core/runtime/broker.js +2 -0
- package/dist/core/runtime/launch.js +1 -1
- package/dist/core/runtime/revive.js +187 -140
- package/dist/core/runtime/tmux.js +4 -1
- package/dist/core/view/__tests__/transport-remote.test.js +9 -6
- package/dist/core/view/transport-local.js +12 -3
- package/dist/daemon/crtrd-cli.d.ts +1 -1
- package/dist/daemon/crtrd-cli.js +1 -0
- package/dist/suppress-experimental-warnings.d.ts +1 -0
- package/dist/suppress-experimental-warnings.js +15 -0
- package/dist/types.js +10 -10
- package/dist/web-client/assets/index-BUdm9s9s.css +2 -0
- package/dist/web-client/assets/{index-BvzxXXGU.js → index-DiFuLcp6.js} +19 -18
- package/dist/web-client/index.html +3 -3
- package/dist/web-client/sw.js +1 -1
- package/package.json +10 -7
- package/scripts/postinstall.mjs +1 -1
- package/dist/web-client/assets/index-BnmSLNLa.css +0 -2
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
// PINNED default (set from the menu with `d`, wins over MRU while it still
|
|
4
4
|
// covers cwd) > a profile whose project dir IS cwd exactly (auto) > an
|
|
5
5
|
// interactive menu of profiles covering cwd from a parent/workspace dir +
|
|
6
|
-
// create (MRU auto when headless) > synchronous create-
|
|
7
|
-
// nothing covers. This is the ONLY place that decision
|
|
6
|
+
// create (MRU auto when headless) > synchronous create / select-existing /
|
|
7
|
+
// root decision when nothing covers. This is the ONLY place that decision
|
|
8
|
+
// runs; front-door.ts/
|
|
8
9
|
// spawn.ts call it, never re-derive it.
|
|
9
10
|
import { existsSync, realpathSync } from 'node:fs';
|
|
10
11
|
import { homedir } from 'node:os';
|
|
11
12
|
import { basename, resolve as resolvePath, sep } from 'node:path';
|
|
12
13
|
import { createInterface } from 'node:readline/promises';
|
|
13
|
-
import {
|
|
14
|
+
import { emitKeypressEvents } from 'node:readline';
|
|
15
|
+
import { listProfiles, loadProfileManifest, updateProfileLastUsed, createProfile, addProfileProject, ensureRootProfile, ROOT_PROFILE_ID, } from './manifest.js';
|
|
14
16
|
import { getDefaultProfileId, setDefaultProfileId, clearDefaultProfile, } from './default-binding.js';
|
|
15
17
|
import { stdoutColor } from '../output.js';
|
|
16
18
|
/** Resolve cwd to an absolute, realpath'd form so it compares against the
|
|
@@ -120,13 +122,134 @@ function writeHeader(title, subtitle) {
|
|
|
120
122
|
const sub = subtitle !== undefined ? ` ${dim(subtitle)}\n` : '';
|
|
121
123
|
process.stdout.write(`\n ${bold(title)}\n${sub}\n`);
|
|
122
124
|
}
|
|
123
|
-
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
function
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
125
|
+
/** Compact one-line summary of a profile's whole purview: its first project
|
|
126
|
+
* dir plus a `· N dirs total` tail when it owns more than one. Used where
|
|
127
|
+
* there is no cwd-match to point at (the searchable select list). */
|
|
128
|
+
function purviewSummary(entry) {
|
|
129
|
+
const n = entry.manifest.projects.length;
|
|
130
|
+
const first = entry.manifest.projects[0];
|
|
131
|
+
if (first === undefined)
|
|
132
|
+
return relativeUsed(entry.manifest.last_used_at);
|
|
133
|
+
return n > 1 ? `${tildify(first)} \u00b7 ${n} dirs total` : tildify(first);
|
|
134
|
+
}
|
|
135
|
+
/** ▸ marks the highlighted row. */
|
|
136
|
+
const HILITE = accent('\u25b8');
|
|
137
|
+
const cursorUp = (n) => (n > 0 ? `\u001b[${n}A\r` : '');
|
|
138
|
+
const CLEAR_DOWN = '\u001b[0J';
|
|
139
|
+
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
140
|
+
/** Terminal rows a just-written block occupies — i.e. how far the cursor
|
|
141
|
+
* advanced, so a redraw can move exactly that far back up. Counts each '\n'
|
|
142
|
+
* as one row AND adds the extra rows a long line wraps into at the current
|
|
143
|
+
* terminal width (ANSI color codes stripped so they don't count toward width).
|
|
144
|
+
* A logical-line count alone undershoots in a narrow pane, stacking redraws. */
|
|
145
|
+
function blockRows(block) {
|
|
146
|
+
const cols = process.stdout.columns ?? 80;
|
|
147
|
+
const segs = block.split('\n');
|
|
148
|
+
const newlines = segs.length - 1; // block ends with '\n' → last seg is ''
|
|
149
|
+
let extra = 0;
|
|
150
|
+
for (let i = 0; i < newlines; i++) {
|
|
151
|
+
const w = segs[i].replace(ANSI_RE, '').length;
|
|
152
|
+
if (cols > 0 && w > cols)
|
|
153
|
+
extra += Math.ceil(w / cols) - 1;
|
|
154
|
+
}
|
|
155
|
+
return newlines + extra;
|
|
156
|
+
}
|
|
157
|
+
function renderMenu(title, subtitle, rows, sel, hint) {
|
|
158
|
+
let out = `\n ${bold(title)}\n`;
|
|
159
|
+
if (subtitle !== undefined)
|
|
160
|
+
out += ` ${dim(subtitle)}\n`;
|
|
161
|
+
out += '\n';
|
|
162
|
+
let num = 0;
|
|
163
|
+
for (const [i, r] of rows.entries()) {
|
|
164
|
+
const tag = r.hotkey ?? String(++num);
|
|
165
|
+
const mark = i === sel ? HILITE : ' ';
|
|
166
|
+
const det = r.detail !== '' ? ' ' + dim(r.detail) : '';
|
|
167
|
+
const def = r.isDefault ? ' ' + accent('\u00b7 ' + (r.defaultText ?? 'default')) : '';
|
|
168
|
+
out += ` ${mark} ${key(tag)} ${r.label}${det}${def}\n`;
|
|
169
|
+
}
|
|
170
|
+
if (hint !== undefined)
|
|
171
|
+
out += `\n ${dim(hint)}\n`;
|
|
172
|
+
out += `\n ${dim('\u2191\u2193/jk move \u21b5 select esc default ^C quit')}\n`;
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
/** Drive a highlighted keypress menu to a resolution. Returns the selected row
|
|
176
|
+
* index, plus the action letter when one of `actionKeys` was pressed on the
|
|
177
|
+
* highlighted row. */
|
|
178
|
+
function runMenu(title, subtitle, rows, opts = {}) {
|
|
179
|
+
const numbered = [];
|
|
180
|
+
rows.forEach((r, i) => {
|
|
181
|
+
if (r.hotkey === undefined)
|
|
182
|
+
numbered.push(i);
|
|
183
|
+
});
|
|
184
|
+
let sel = opts.initialIndex ?? 0;
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
let prevLines = 0;
|
|
187
|
+
const draw = () => {
|
|
188
|
+
const block = renderMenu(title, subtitle, rows, sel, opts.hint);
|
|
189
|
+
process.stdout.write((prevLines > 0 ? cursorUp(prevLines) + CLEAR_DOWN : '') + block);
|
|
190
|
+
prevLines = blockRows(block);
|
|
191
|
+
};
|
|
192
|
+
const cleanup = () => {
|
|
193
|
+
process.stdin.off('keypress', onKey);
|
|
194
|
+
if (process.stdin.isTTY)
|
|
195
|
+
process.stdin.setRawMode(false);
|
|
196
|
+
process.stdin.pause();
|
|
197
|
+
};
|
|
198
|
+
const onKey = (str, k) => {
|
|
199
|
+
if (k.ctrl === true && k.name === 'c') {
|
|
200
|
+
cleanup();
|
|
201
|
+
process.stdout.write('\n');
|
|
202
|
+
process.exit(130);
|
|
203
|
+
}
|
|
204
|
+
if (k.name === 'up' || k.name === 'k' || str === 'k') {
|
|
205
|
+
sel = (sel - 1 + rows.length) % rows.length;
|
|
206
|
+
draw();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (k.name === 'down' || k.name === 'j' || str === 'j') {
|
|
210
|
+
sel = (sel + 1) % rows.length;
|
|
211
|
+
draw();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (k.name === 'return' || k.name === 'enter') {
|
|
215
|
+
cleanup();
|
|
216
|
+
resolve({ index: sel });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// Esc cancels navigation and accepts the default row (the MRU/pinned
|
|
220
|
+
// profile, or the root row on the no-coverage menu) — a safe exit, never
|
|
221
|
+
// a dead key. Ctrl-C still aborts crtr entirely.
|
|
222
|
+
if (k.name === 'escape') {
|
|
223
|
+
cleanup();
|
|
224
|
+
resolve({ index: opts.initialIndex ?? 0 });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (str !== undefined && /^[0-9]$/.test(str)) {
|
|
228
|
+
const d = Number.parseInt(str, 10);
|
|
229
|
+
if (d >= 1 && d <= numbered.length) {
|
|
230
|
+
sel = numbered[d - 1];
|
|
231
|
+
draw();
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const hk = rows.findIndex((r) => r.hotkey !== undefined && r.hotkey === str);
|
|
236
|
+
if (hk >= 0) {
|
|
237
|
+
cleanup();
|
|
238
|
+
resolve({ index: hk });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (str !== undefined && opts.actionKeys?.includes(str) === true) {
|
|
242
|
+
cleanup();
|
|
243
|
+
resolve({ index: sel, action: str });
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
emitKeypressEvents(process.stdin);
|
|
247
|
+
if (process.stdin.isTTY)
|
|
248
|
+
process.stdin.setRawMode(true);
|
|
249
|
+
process.stdin.resume();
|
|
250
|
+
process.stdin.on('keypress', onKey);
|
|
251
|
+
draw();
|
|
252
|
+
});
|
|
130
253
|
}
|
|
131
254
|
/** The trailing readline prompt: ` › <dim hint> `. */
|
|
132
255
|
function caret(hint) {
|
|
@@ -156,17 +279,131 @@ async function promptAddDirToProfile(entry, cwd) {
|
|
|
156
279
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
157
280
|
try {
|
|
158
281
|
writeHeader(`Profile "${entry.manifest.name}" doesn't cover this directory`);
|
|
159
|
-
|
|
160
|
-
.trim()
|
|
161
|
-
.toLowerCase();
|
|
162
|
-
if (choice === 'n' || choice === 'no')
|
|
163
|
-
return;
|
|
164
|
-
addProfileProject(entry.profileId, cwd);
|
|
282
|
+
await offerAddDirToProfile(rl, entry, cwd);
|
|
165
283
|
}
|
|
166
284
|
finally {
|
|
167
285
|
rl.close();
|
|
168
286
|
}
|
|
169
287
|
}
|
|
288
|
+
/** Ask (default yes) whether to widen `entry`'s purview to include `cwd`, on an
|
|
289
|
+
* ALREADY-OPEN readline. Shared by the explicit-`--profile` path
|
|
290
|
+
* (`promptAddDirToProfile`) and the select-existing path in
|
|
291
|
+
* `promptCreateOrRoot`, so a profile chosen for a directory it doesn't cover
|
|
292
|
+
* can adopt that directory without a separate `crtr profile project add`. */
|
|
293
|
+
async function offerAddDirToProfile(rl, entry, cwd) {
|
|
294
|
+
const choice = (await rl.question(caret(`add this directory to "${entry.manifest.name}"? [Y/n]`)))
|
|
295
|
+
.trim()
|
|
296
|
+
.toLowerCase();
|
|
297
|
+
if (choice === 'n' || choice === 'no')
|
|
298
|
+
return;
|
|
299
|
+
addProfileProject(entry.profileId, cwd);
|
|
300
|
+
process.stdout.write(` ${accent('\u2713')} ${dim(`added this directory to "${entry.manifest.name}"`)}\n`);
|
|
301
|
+
}
|
|
302
|
+
/** Search-and-pick among EVERY existing profile (root excluded — it is the
|
|
303
|
+
* separate `[r]` option). Reached from the no-coverage prompt so the user can
|
|
304
|
+
* run under a profile they can't fully name when they've started crtr in a
|
|
305
|
+
* directory none of their profiles cover. A live search box: type to filter
|
|
306
|
+
* (matched against name AND project paths — you may recall the directory, not
|
|
307
|
+
* the name), ↑/↓ (or Ctrl-N/P) move the highlight, Enter picks it, Esc backs
|
|
308
|
+
* out (returns null → caller falls back to root). Because typing filters,
|
|
309
|
+
* navigation here is arrows-only — j/k are literal filter characters (fzf
|
|
310
|
+
* convention), unlike the non-search menus where j/k move. */
|
|
311
|
+
async function promptSelectExistingProfile() {
|
|
312
|
+
const all = [...listProfiles()]
|
|
313
|
+
.filter((e) => e.profileId !== ROOT_PROFILE_ID)
|
|
314
|
+
.sort((a, b) => (isMoreRecent(a.manifest.last_used_at, b.manifest.last_used_at) ? -1 : 1));
|
|
315
|
+
if (all.length === 0) {
|
|
316
|
+
process.stdout.write(` ${dim('no existing profiles to select')}\n`);
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
let filter = '';
|
|
320
|
+
let sel = 0;
|
|
321
|
+
const matchesOf = () => filter === ''
|
|
322
|
+
? all
|
|
323
|
+
: all.filter((e) => e.manifest.name.toLowerCase().includes(filter) ||
|
|
324
|
+
e.manifest.projects.some((p) => tildify(p).toLowerCase().includes(filter)));
|
|
325
|
+
return new Promise((resolve) => {
|
|
326
|
+
let prevLines = 0;
|
|
327
|
+
const draw = () => {
|
|
328
|
+
const matches = matchesOf();
|
|
329
|
+
if (sel >= matches.length)
|
|
330
|
+
sel = Math.max(0, matches.length - 1);
|
|
331
|
+
let block = `\n ${bold('Select an existing profile')}\n`;
|
|
332
|
+
block += ` ${dim('across every profile, not just this directory')}\n\n`;
|
|
333
|
+
if (matches.length === 0) {
|
|
334
|
+
block += ` ${dim(`no match for "${filter}"`)}\n`;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
const nameWidth = Math.max(...matches.map((e) => e.manifest.name.length));
|
|
338
|
+
matches.forEach((e, i) => {
|
|
339
|
+
const mark = i === sel ? HILITE : ' ';
|
|
340
|
+
block += ` ${mark} ${bold(e.manifest.name.padEnd(nameWidth))} ${dim(purviewSummary(e))}\n`;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
block += `\n ${accent('\u203a')} ${filter}${dim('\u2588')}\n`;
|
|
344
|
+
block += ` ${dim('type to filter \u2191\u2193 move \u21b5 pick esc back')}\n`;
|
|
345
|
+
process.stdout.write((prevLines > 0 ? cursorUp(prevLines) + CLEAR_DOWN : '') + block);
|
|
346
|
+
prevLines = blockRows(block);
|
|
347
|
+
};
|
|
348
|
+
const cleanup = () => {
|
|
349
|
+
process.stdin.off('keypress', onKey);
|
|
350
|
+
if (process.stdin.isTTY)
|
|
351
|
+
process.stdin.setRawMode(false);
|
|
352
|
+
process.stdin.pause();
|
|
353
|
+
};
|
|
354
|
+
const onKey = (str, k) => {
|
|
355
|
+
if (k.ctrl === true && k.name === 'c') {
|
|
356
|
+
cleanup();
|
|
357
|
+
process.stdout.write('\n');
|
|
358
|
+
process.exit(130);
|
|
359
|
+
}
|
|
360
|
+
if (k.name === 'escape') {
|
|
361
|
+
cleanup();
|
|
362
|
+
resolve(null);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const matches = matchesOf();
|
|
366
|
+
if (k.name === 'return' || k.name === 'enter') {
|
|
367
|
+
if (matches.length > 0) {
|
|
368
|
+
cleanup();
|
|
369
|
+
resolve(matches[sel].profileId);
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (k.name === 'up' || (k.ctrl === true && k.name === 'p')) {
|
|
374
|
+
if (matches.length > 0) {
|
|
375
|
+
sel = (sel - 1 + matches.length) % matches.length;
|
|
376
|
+
draw();
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (k.name === 'down' || (k.ctrl === true && k.name === 'n')) {
|
|
381
|
+
if (matches.length > 0) {
|
|
382
|
+
sel = (sel + 1) % matches.length;
|
|
383
|
+
draw();
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (k.name === 'backspace') {
|
|
388
|
+
filter = filter.slice(0, -1);
|
|
389
|
+
sel = 0;
|
|
390
|
+
draw();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (str !== undefined && str.length === 1 && str >= ' ' && k.ctrl !== true) {
|
|
394
|
+
filter += str.toLowerCase();
|
|
395
|
+
sel = 0;
|
|
396
|
+
draw();
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
emitKeypressEvents(process.stdin);
|
|
400
|
+
if (process.stdin.isTTY)
|
|
401
|
+
process.stdin.setRawMode(true);
|
|
402
|
+
process.stdin.resume();
|
|
403
|
+
process.stdin.on('keypress', onKey);
|
|
404
|
+
draw();
|
|
405
|
+
});
|
|
406
|
+
}
|
|
170
407
|
/** Ask for a name (defaulting to cwd's basename) and create a profile pointing
|
|
171
408
|
* at cwd on the SAME open readline — shared by both interactive create paths
|
|
172
409
|
* so the name prompt reads identically whether reached via the covering menu
|
|
@@ -180,19 +417,47 @@ async function createProfileHere(rl, cwd) {
|
|
|
180
417
|
return profileId;
|
|
181
418
|
}
|
|
182
419
|
async function promptCreateOrRoot(cwd) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
420
|
+
// Root is the highlighted default so a bare Enter (or Esc) still lands on the
|
|
421
|
+
// safe no-op, exactly as the old `[c/s/r]` prompt defaulted. Looped so Esc
|
|
422
|
+
// out of the search sub-list returns HERE rather than silently dropping to
|
|
423
|
+
// root — the create/select/root choice is always the fallback, never skipped.
|
|
424
|
+
const rows = [
|
|
425
|
+
{ label: 'create a profile here', detail: '', hotkey: 'c' },
|
|
426
|
+
{ label: 'select an existing profile', detail: '', hotkey: 's' },
|
|
427
|
+
{ label: dim('use the root profile'), detail: '', hotkey: 'r', isDefault: true },
|
|
428
|
+
];
|
|
429
|
+
for (;;) {
|
|
430
|
+
const res = await runMenu('No profile covers this directory', undefined, rows, {
|
|
431
|
+
initialIndex: 2,
|
|
432
|
+
});
|
|
433
|
+
if (res.index === 0) {
|
|
434
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
435
|
+
try {
|
|
436
|
+
return await createProfileHere(rl, cwd);
|
|
437
|
+
}
|
|
438
|
+
finally {
|
|
439
|
+
rl.close();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (res.index === 1) {
|
|
443
|
+
const selected = await promptSelectExistingProfile();
|
|
444
|
+
// Esc/back out of the search list (or no profiles to pick) → re-show this
|
|
445
|
+
// create/select/root menu, not a silent fall-through to root.
|
|
446
|
+
if (selected === null)
|
|
447
|
+
continue;
|
|
448
|
+
const entry = loadProfileManifest(selected);
|
|
449
|
+
// The picked profile does not cover cwd (nothing does here) — offer to
|
|
450
|
+
// widen its purview so the next node started here is covered too.
|
|
451
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
452
|
+
try {
|
|
453
|
+
await offerAddDirToProfile(rl, entry, cwd);
|
|
454
|
+
}
|
|
455
|
+
finally {
|
|
456
|
+
rl.close();
|
|
457
|
+
}
|
|
458
|
+
return entry.profileId;
|
|
459
|
+
}
|
|
460
|
+
return ensureRootProfile().profileId;
|
|
196
461
|
}
|
|
197
462
|
}
|
|
198
463
|
/** Choose among a set of candidate profiles for `cwd`, or create a new one.
|
|
@@ -215,63 +480,70 @@ async function promptPickProfileOrCreate(candidates, cwd, exact, pinnedId) {
|
|
|
215
480
|
const ordered = pinned !== null ? [pinned, ...mru.filter((e) => e !== pinned)] : mru;
|
|
216
481
|
// Pad names to a shared column so the dim detail lines up down the menu.
|
|
217
482
|
const nameWidth = Math.max(...ordered.map((e) => e.manifest.name.length));
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
483
|
+
const rows = ordered.map((entry, i) => {
|
|
484
|
+
const n = entry.manifest.projects.length;
|
|
485
|
+
// For exact ties the matched path is cwd itself, so surface last-used to
|
|
486
|
+
// tell them apart; for parent/workspace coverage, WHICH of the profile's
|
|
487
|
+
// dirs matches cwd is the differentiator. Either way append the total dir
|
|
488
|
+
// count so "covers X" is never mistaken for the profile's full purview.
|
|
489
|
+
const detail = exact
|
|
490
|
+
? `${relativeUsed(entry.manifest.last_used_at)}${n > 1 ? ` \u00b7 ${n} dirs total` : ''}`
|
|
491
|
+
: (() => {
|
|
492
|
+
const md = entry.manifest.projects.find((p) => projectCovers(cwd, p)) ??
|
|
493
|
+
entry.manifest.projects[0];
|
|
494
|
+
return n > 1 ? `matches ${tildify(md)} \u00b7 ${n} dirs total` : `matches ${tildify(md)}`;
|
|
495
|
+
})();
|
|
496
|
+
const defaultText = pinned !== null && ordered[0] === pinned ? 'default (pinned)' : 'default';
|
|
497
|
+
return {
|
|
498
|
+
label: bold(entry.manifest.name.padEnd(nameWidth)),
|
|
499
|
+
detail,
|
|
500
|
+
isDefault: i === 0,
|
|
501
|
+
defaultText,
|
|
502
|
+
};
|
|
503
|
+
});
|
|
504
|
+
rows.push({ label: dim('create a new profile here'), detail: '', hotkey: 'c' });
|
|
505
|
+
// Action letters resolve on the highlighted row: `d` pins it as this
|
|
506
|
+
// directory's default; `a` widens its purview to include cwd (meaningless
|
|
507
|
+
// when the candidates already claim cwd exactly, so hidden there); `u` clears
|
|
508
|
+
// an existing pin.
|
|
509
|
+
const actionKeys = ['d'];
|
|
510
|
+
if (!exact)
|
|
511
|
+
actionKeys.push('a');
|
|
512
|
+
if (pinned !== null)
|
|
513
|
+
actionKeys.push('u');
|
|
514
|
+
const hintParts = ['d=set default'];
|
|
515
|
+
if (!exact)
|
|
516
|
+
hintParts.push('a=add dir here');
|
|
517
|
+
if (pinned !== null)
|
|
518
|
+
hintParts.push('u=unpin');
|
|
519
|
+
const res = await runMenu(exact ? 'Choose a profile' : 'Select a profile', exact
|
|
520
|
+
? 'Several profiles claim this directory as their project'
|
|
521
|
+
: 'This directory falls under existing profile(s)', rows, { actionKeys, hint: hintParts.join(' ') });
|
|
522
|
+
// The create row is last; selecting it (Enter on it, or `c`) creates here.
|
|
523
|
+
if (res.index >= ordered.length) {
|
|
524
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
525
|
+
try {
|
|
248
526
|
return await createProfileHere(rl, cwd);
|
|
249
|
-
if (answer === 'u' || answer === 'unpin') {
|
|
250
|
-
clearDefaultProfile(cwd);
|
|
251
|
-
process.stdout.write(` ${accent('\u2713')} ${dim('cleared the default for this directory')}\n`);
|
|
252
|
-
return ordered[0].profileId;
|
|
253
|
-
}
|
|
254
|
-
const m = /^(\d+)?([da])?$/.exec(answer);
|
|
255
|
-
// Unrecognized input → the safe default (choice 1); never loop, since this
|
|
256
|
-
// prompt is gating pi's boot on the shared TTY.
|
|
257
|
-
if (m === null)
|
|
258
|
-
return ordered[0].profileId;
|
|
259
|
-
const [, numStr, action] = m;
|
|
260
|
-
const idx = numStr !== undefined ? Number.parseInt(numStr, 10) - 1 : 0;
|
|
261
|
-
const picked = idx >= 0 && idx < ordered.length ? ordered[idx] : ordered[0];
|
|
262
|
-
if (action === 'd') {
|
|
263
|
-
setDefaultProfileId(cwd, picked.profileId);
|
|
264
|
-
process.stdout.write(` ${accent('\u2713')} ${dim(`"${picked.manifest.name}" is now the default here`)}\n`);
|
|
265
527
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
process.stdout.write(` ${accent('\u2713')} ${dim(`added this directory to "${picked.manifest.name}"`)}\n`);
|
|
528
|
+
finally {
|
|
529
|
+
rl.close();
|
|
269
530
|
}
|
|
531
|
+
}
|
|
532
|
+
const picked = ordered[res.index];
|
|
533
|
+
if (res.action === 'u') {
|
|
534
|
+
clearDefaultProfile(cwd);
|
|
535
|
+
process.stdout.write(` ${accent('\u2713')} ${dim('cleared the default for this directory')}\n`);
|
|
270
536
|
return picked.profileId;
|
|
271
537
|
}
|
|
272
|
-
|
|
273
|
-
|
|
538
|
+
if (res.action === 'd') {
|
|
539
|
+
setDefaultProfileId(cwd, picked.profileId);
|
|
540
|
+
process.stdout.write(` ${accent('\u2713')} ${dim(`"${picked.manifest.name}" is now the default here`)}\n`);
|
|
541
|
+
}
|
|
542
|
+
else if (res.action === 'a') {
|
|
543
|
+
addProfileProject(picked.profileId, cwd);
|
|
544
|
+
process.stdout.write(` ${accent('\u2713')} ${dim(`added this directory to "${picked.manifest.name}"`)}\n`);
|
|
274
545
|
}
|
|
546
|
+
return picked.profileId;
|
|
275
547
|
}
|
|
276
548
|
/** Select the profile a node about to boot at `cwd` should run under.
|
|
277
549
|
*
|
|
@@ -296,7 +568,10 @@ async function promptPickProfileOrCreate(candidates, cwd, exact, pinnedId) {
|
|
|
296
568
|
* is covered). Default is the MRU covering profile.
|
|
297
569
|
* - Non-interactive: auto-pick the MRU covering profile, no prompt.
|
|
298
570
|
* 4. Else — nothing covers cwd walking all the way up. Interactive: prompt to
|
|
299
|
-
* create a profile here
|
|
571
|
+
* create a profile here, select an existing profile (a searchable list of
|
|
572
|
+
* every profile, for when you started somewhere none of them cover and
|
|
573
|
+
* can't recall the exact name — the pick then offers to adopt cwd), or use
|
|
574
|
+
* the root profile. Non-interactive (no TTY):
|
|
300
575
|
* default to the root profile and print the recovery instruction to STDERR —
|
|
301
576
|
* never stdout, which the caller may be piping. */
|
|
302
577
|
export async function selectProfileForCwd(cwd, explicitProfile, forcePicker = false) {
|
|
@@ -1086,6 +1086,8 @@ export async function runBroker(nodeId) {
|
|
|
1086
1086
|
showTerminalProgress: sm.getShowTerminalProgress(),
|
|
1087
1087
|
warnings: sm.getWarnings(),
|
|
1088
1088
|
defaultProjectTrust: sm.getDefaultProjectTrust(),
|
|
1089
|
+
showCacheMissNotices: sm.getShowCacheMissNotices(),
|
|
1090
|
+
outputPad: sm.getOutputPad(),
|
|
1089
1091
|
autoRetry: session.autoRetryEnabled,
|
|
1090
1092
|
model: toModelRef(session.model),
|
|
1091
1093
|
};
|
|
@@ -111,7 +111,7 @@ const STRENGTH_ALIASES = {
|
|
|
111
111
|
const MODEL_PROVIDER_KEYS = ['anthropic', 'openai'];
|
|
112
112
|
const MODEL_STRENGTHS = ['ultra', 'strong', 'medium', 'light'];
|
|
113
113
|
const MODEL_STRENGTH_RANK = { light: 0, medium: 1, strong: 2, ultra: 3 };
|
|
114
|
-
const THINKING_SUFFIXES = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh']);
|
|
114
|
+
const THINKING_SUFFIXES = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
|
|
115
115
|
function stripThinkingSuffix(spec) {
|
|
116
116
|
const i = spec.lastIndexOf(':');
|
|
117
117
|
if (i <= 0)
|