@aiwayds/dsh-tui-pi 2.15.0 → 2.17.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/lib/profile.js DELETED
@@ -1,665 +0,0 @@
1
- /**
2
- * `/profile-switch` + `/profile-cfg` — user model profiles: named snapshots
3
- * of the whole model configuration (default model + think level, every
4
- * subagent's model/thinking) that switch in one step between contexts like
5
- * work and personal.
6
- *
7
- * Layers, all on the select-panel framework (src/panels.ts):
8
- *
9
- * 1. `/profile-switch` switcher (TablePanel) — one row per profile (name |
10
- * default model | think | agents). Enter applies the profile to the
11
- * live selection AND the agent markdown files, and BINDS this directory
12
- * tree (.dsh-profile) so new sessions here start on it — switching is
13
- * workspace-scoped: other trees keep their own binding, or the global
14
- * default. Esc backs out; p pins/unpins manually.
15
- * 2. `/profile-cfg` manager (TablePanel) — the roster: n new, d delete
16
- * (double-press confirm), Enter opens the profile's fields.
17
- * 3. profile fields (FieldPanel) — model / think / agents rows plus the
18
- * s save-current, r rename, v review shortcuts.
19
- * 4. agents sub-table (TablePanel) — every discovered agent's recorded
20
- * model/thinking; Enter picks through the SAME favorites/hidden model
21
- * table /model uses (pickModel with the shared PanelHost), i sets
22
- * explicit inherit, t edits think alone.
23
- *
24
- * Every edit persists to `$DSH_HOME/model-profiles.json` immediately
25
- * (src/model-profiles.ts). Applying a profile sets the live default model,
26
- * binds the workspace tree via `.dsh-profile` (src/model-profiles.ts) and
27
- * records the last-applied pointer — it writes NO agent files: each agent's
28
- * runtime model/thinking is composed at spawn from the frontmatter baseline
29
- * ⊕ this pinned profile's per-agent overrides (src/agent-runtime.ts, the
30
- * registry's profile-aware composer), so a switch is fully workspace-scoped.
31
- * The live default model rides the bridge selection. The GLOBAL
32
- * agent-default-model is written only by /model, never by a profile switch.
33
- * The ● "current" marker is the tree's own binding (boundProfileName), not a
34
- * machine-wide pointer.
35
- */
36
- import { agentsDir, listAgentFiles, } from "./agent-manager.js";
37
- import { bindWorkspaceProfile, boundProfileName, captureAgentsSnapshot, createProfile, deleteProfile, findProfile, formatProfileRoute, loadModelProfiles, modelProfilesPath, profileReviewLines, readNearestProfilePin, removeProfilePin, renameProfile, saveModelProfiles, writeProfilePin, PROFILE_PIN_FILE, } from "./model-profiles.js";
38
- import { join } from 'node:path';
39
- import { openEffortPicker, pickModel } from "./selectors.js";
40
- import { EditField } from "./settings.js";
41
- import { autoColumns, FieldPanel, PanelHost, TablePanel, ViewerPanel, } from "./panels.js";
42
- import { ansiFg, BOLD, RESET } from "./theme/index.js";
43
- import { clipToWidth } from "./text.js";
44
- /** How long a `d` delete arm stays confirmable (double-press window). */
45
- const DELETE_CONFIRM_MS = 2500;
46
- /** The profile table columns: NAME/MODEL/THINK fit (capped), AGENTS right-aligned tail. */
47
- function profileColumns(profiles, current) {
48
- return autoColumns([
49
- { key: 'name', title: 'Profile', cap: 18 },
50
- { key: 'model', title: 'Default model', cap: 36 },
51
- { key: 'think', title: 'Think', cap: 10 },
52
- { key: 'agents', title: 'Agents', align: 'right' },
53
- ], profiles, (profile, key) => profileCell(profile, key, current));
54
- }
55
- /** Cell text for one profile table row; the current profile gets the ● mark. */
56
- function profileCell(profile, key, current) {
57
- switch (key) {
58
- case 'name': return current === profile.name ? `● ${profile.name}` : profile.name;
59
- case 'model': return profile.defaultModel === undefined
60
- ? '(not set)'
61
- : `${profile.defaultModel.provider}/${profile.defaultModel.model}`;
62
- case 'think': return profile.defaultModel?.reasoningEffort ?? 'default';
63
- default: return String(Object.keys(profile.agents).length);
64
- }
65
- }
66
- /** A profile's default route as a ModelSelection (the /model handler shape). */
67
- function routeToSelection(route) {
68
- return {
69
- provider: route.provider,
70
- model: route.model,
71
- ...(route.reasoningEffort !== undefined
72
- ? { reasoningEffort: route.reasoningEffort }
73
- : {}),
74
- };
75
- }
76
- /** Record what a live selection says into a plain storable route. */
77
- function selectionToRoute(selection) {
78
- return selection.reasoningEffort === undefined
79
- ? { provider: selection.provider, model: selection.model }
80
- : {
81
- provider: selection.provider,
82
- model: selection.model,
83
- reasoningEffort: selection.reasoningEffort,
84
- };
85
- }
86
- /**
87
- * Apply `profile` end to end, scoped to the current workspace tree:
88
- * default model + think level through the live selection ref (a profile
89
- * without a default model leaves the selection alone), the tree binding
90
- * (`.dsh-profile` in `cwd` — new sessions here assemble from this profile)
91
- * and the store's informational last-applied pointer. Agent model/thinking
92
- * values are NOT written anywhere here: each agent's runtime values compose
93
- * at spawn from the frontmatter baseline ⊕ this pinned profile's overrides
94
- * (src/agent-runtime.ts), which is what makes a switch workspace-scoped.
95
- * Resolves the command summary text — failures ride along as ⚠ parts
96
- * instead of aborting the rest. Deliberately does NOT touch the global
97
- * agent-default-model: that is /model's write, and crossing tree boundaries
98
- * is exactly what a profile switch must not do.
99
- */
100
- export async function applyProfile(path, doc, profile, deps, cwd) {
101
- const parts = [];
102
- if (profile.defaultModel !== undefined) {
103
- deps.setSelection(routeToSelection(profile.defaultModel));
104
- }
105
- // Bind the tree BEFORE reporting: an ancestor pin would otherwise keep
106
- // winning at assembly while the summary claims the switch took. A
107
- // hand-decorated pin file is refused by bindWorkspaceProfile — surfaced,
108
- // never clobbered.
109
- const ancestor = readNearestProfilePin(cwd);
110
- const pinError = bindWorkspaceProfile(cwd, profile.name);
111
- if (pinError !== undefined) {
112
- parts.push(`⚠ ${pinError}`);
113
- }
114
- else if (ancestor !== undefined
115
- && ancestor.path !== join(cwd, PROFILE_PIN_FILE)
116
- && ancestor.name.toLowerCase() !== profile.name.toLowerCase()) {
117
- parts.push(`overrides ancestor pin "${ancestor.name}"`);
118
- }
119
- doc.current = profile.name;
120
- const saveError = saveModelProfiles(path, doc);
121
- if (saveError !== undefined)
122
- parts.push(`⚠ profile not saved: ${saveError}`);
123
- const modelText = profile.defaultModel !== undefined
124
- ? formatProfileRoute(profile.defaultModel)
125
- : 'model unchanged';
126
- const pinText = pinError === undefined ? `pinned this tree (${PROFILE_PIN_FILE})` : '';
127
- return [
128
- `Profile → ${profile.name} · ${modelText} · agent values compose per-workspace at spawn`,
129
- pinText,
130
- ...parts,
131
- ]
132
- .filter(part => part !== '')
133
- .join(' · ');
134
- }
135
- /**
136
- * Open the `/profile-switch` switcher. Resolves the apply summary when a
137
- * profile was switched, or `undefined` when cancelled / nothing applied.
138
- *
139
- * Enter is a WORKSPACE-SCOPED switch: apply to the live session, and bind
140
- * this directory tree via `.dsh-profile` (see src/model-profiles.ts) so
141
- * every NEW session here auto-loads it — agent model/think values are not
142
- * written anywhere; each agent composes its runtime values at spawn from
143
- * the frontmatter baseline ⊕ this pinned profile (src/agent-runtime.ts).
144
- * Other trees are untouched. A
145
- * hand-decorated pin file refuses the binding and says so in the summary.
146
- * `p` binds/unbinds the cwd manually — pressing it on the already-bound
147
- * profile unpins — unless the pin lives in an ancestor or was
148
- * hand-decorated, which is reported in the status line instead.
149
- */
150
- export async function openProfileSwitcher(tui, theme, deps, restoreFocus) {
151
- const path = modelProfilesPath();
152
- const doc = loadModelProfiles(path);
153
- const cwd = process.cwd();
154
- // The ● current marker is THIS tree's binding, not a machine-wide pointer.
155
- const currentName = boundProfileName(doc, cwd);
156
- let settle;
157
- const host = new PanelHost(tui, theme, message => {
158
- restoreFocus();
159
- settle?.(`✘ failed to open the profile switcher: ${message}`);
160
- });
161
- return new Promise(resolve => {
162
- settle = resolve;
163
- // Enter can fire again while the async apply runs — resolve-once guard,
164
- // the same settle discipline the pickers use.
165
- let applying = false;
166
- // Session-local status flash for the p-toggle outcome.
167
- let pinStatus;
168
- /** The nearest `.dsh-profile` binding for the cwd, resolved per render. */
169
- const boundPin = () => readNearestProfilePin(cwd);
170
- /**
171
- * Show the table once through the host (a p-toggle swaps the whole panel:
172
- * simplest way to refresh the `(dir)` markers, and PanelHost's
173
- * show-new-then-hide-old keeps focus stable).
174
- */
175
- const showTable = () => {
176
- const pinnedName = boundPin()?.name;
177
- const table = new TablePanel(theme, {
178
- title: `● Model profiles · ${PROFILE_PIN_FILE} here: ${pinnedName ?? 'none'}`,
179
- columns: profileColumns(doc.profiles, currentName),
180
- rows: doc.profiles,
181
- renderCell: (profile, column) => profileCell(profile, column.key, currentName),
182
- preselect: Math.max(0, doc.profiles.findIndex(profile => profile.name === currentName)),
183
- status: () => pinStatus,
184
- onSelect: profile => { void switchTo(profile); },
185
- onCancel: () => { host.close(); restoreFocus(); resolve(undefined); },
186
- footer: '↑↓ navigate · Enter switch (binds this dir) · p pin/unpin · Esc back',
187
- shortcuts: { p: () => togglePin(table) },
188
- });
189
- host.open(table);
190
- };
191
- /** Bind/unbind the cwd to the cursor profile; re-show to refresh markers. */
192
- const togglePin = (table) => {
193
- const profile = table.selectedRow();
194
- if (profile === undefined)
195
- return;
196
- const existing = boundPin();
197
- if (existing !== undefined
198
- && existing.name.toLowerCase() === profile.name.toLowerCase()
199
- && existing.path === join(cwd, PROFILE_PIN_FILE)) {
200
- const error = removeProfilePin(cwd, profile.name);
201
- pinStatus = error === undefined ? `unpinned ${cwd}` : `✘ ${error}`;
202
- }
203
- else {
204
- const error = writeProfilePin(cwd, profile.name);
205
- if (error !== undefined)
206
- pinStatus = `✘ ${error}`;
207
- else {
208
- const inheritedNote = existing !== undefined && existing.path !== join(cwd, PROFILE_PIN_FILE)
209
- ? ` (overrides ancestor pin "${existing.name}")`
210
- : '';
211
- pinStatus = `pinned "${profile.name}" → .dsh-profile${inheritedNote}`;
212
- }
213
- }
214
- showTable();
215
- };
216
- // First mount; a mount failure has already settled through the host's
217
- // error path (this promise resolves there, everything below is no-op).
218
- showTable();
219
- const switchTo = async (profile) => {
220
- if (applying)
221
- return;
222
- applying = true;
223
- // The summary carries the whole outcome — binding result, ancestor
224
- // overrides, refusals — as its tail parts.
225
- const summary = await applyProfile(path, doc, profile, deps, cwd);
226
- host.close();
227
- restoreFocus();
228
- resolve(summary);
229
- };
230
- });
231
- }
232
- /**
233
- * Open the `/profile-cfg` manager. Resolves a summary when anything changed,
234
- * or `undefined` when the manager closed untouched. `preselect` jumps
235
- * straight into one profile's fields window.
236
- */
237
- export async function openProfileManager(ctx, tui, theme, deps, restoreFocus, preselect) {
238
- const path = modelProfilesPath();
239
- const doc = loadModelProfiles(path);
240
- // The ● current marker is THIS tree's binding, not a machine-wide pointer.
241
- const currentName = boundProfileName(doc, process.cwd());
242
- const changed = [];
243
- let tableStatus;
244
- let fieldsStatus;
245
- let agentsStatus;
246
- /** Armed double-press delete: profile name + when it was armed. */
247
- let pendingDelete;
248
- let pendingDeleteAt = 0;
249
- let pendingPreselect = preselect;
250
- let fieldsHandle;
251
- let agentsHandle;
252
- let agentsCursor;
253
- /** Settles the manager promise (assigned inside the executor below). */
254
- let settle;
255
- const host = new PanelHost(tui, theme, message => {
256
- restoreFocus();
257
- settle?.(`✘ failed to open the profiles manager: ${message}`);
258
- });
259
- /** Persist the working doc; resolves a ✘ status line on failure. */
260
- const persistDoc = () => {
261
- const error = saveModelProfiles(path, doc);
262
- return error === undefined ? undefined : `✘ could not save profiles: ${error}`;
263
- };
264
- /** Mark one profile as touched for the closing summary (first touch wins). */
265
- const markChanged = (name) => {
266
- if (!changed.includes(name))
267
- changed.push(name);
268
- };
269
- return new Promise(resolve => {
270
- settle = resolve;
271
- const showTable = () => {
272
- const preselectIndex = pendingPreselect === undefined
273
- ? undefined
274
- : doc.profiles.findIndex(profile => profile.name === pendingPreselect);
275
- pendingPreselect = undefined;
276
- const table = new TablePanel(theme, {
277
- title: '● Model profiles — configure',
278
- columns: profileColumns(doc.profiles, currentName),
279
- rows: doc.profiles,
280
- renderCell: (profile, column) => profileCell(profile, column.key, currentName),
281
- preselect: preselectIndex !== undefined && preselectIndex >= 0 ? preselectIndex : undefined,
282
- status: () => tableStatus,
283
- footer: '↑↓ navigate · Enter open · n new · d delete · Esc back',
284
- onSelect: profile => showFields(profile),
285
- onCancel: closeManager,
286
- shortcuts: {
287
- n: showNew,
288
- d: () => {
289
- const profile = table.selectedRow();
290
- if (profile === undefined)
291
- return;
292
- // Double-press confirm, time-boxed: the armed state must NOT
293
- // rebuild the table (a rebuild resets the cursor to row 0, so
294
- // the confirming press would target a different profile), and
295
- // the window expires so a stale arm can never fire one press
296
- // after the user navigated away and back.
297
- const armed = pendingDelete === profile.name
298
- && Date.now() - pendingDeleteAt < DELETE_CONFIRM_MS;
299
- if (armed) {
300
- pendingDelete = undefined;
301
- const error = deleteProfile(doc, profile.name);
302
- if (error !== undefined)
303
- tableStatus = `✘ ${error}`;
304
- else {
305
- tableStatus = persistDoc() ?? `deleted profile "${profile.name}"`;
306
- markChanged(`-${profile.name}`);
307
- }
308
- showTable();
309
- }
310
- else {
311
- pendingDelete = profile.name;
312
- pendingDeleteAt = Date.now();
313
- tableStatus = `press d again to delete "${profile.name}"`;
314
- tui.requestRender();
315
- }
316
- },
317
- },
318
- });
319
- host.open(table);
320
- };
321
- const closeManager = () => {
322
- host.close();
323
- restoreFocus();
324
- resolve(changed.length > 0 ? `Profiles updated: ${changed.join(', ')}` : undefined);
325
- };
326
- const showFields = (profile) => {
327
- const content = [
328
- `named model snapshot — /profile-switch applies it live and binds this directory tree · agent overrides compose per-workspace at spawn (frontmatter baseline ⊕ this profile — no global write)`,
329
- ];
330
- const agentsSet = Object.keys(profile.agents).length;
331
- const fields = [
332
- {
333
- key: 'model',
334
- value: profile.defaultModel === undefined
335
- ? '(not set — switching keeps the current model)'
336
- : formatProfileRoute(profile.defaultModel),
337
- editable: true,
338
- },
339
- {
340
- key: 'think',
341
- value: profile.defaultModel?.reasoningEffort ?? '(provider default)',
342
- editable: true,
343
- },
344
- { key: 'agents', value: `${String(agentsSet)} agent override(s) recorded`, editable: true },
345
- ];
346
- const view = new FieldPanel(theme, {
347
- title: profileTitle(theme, profile, currentName),
348
- content,
349
- fields,
350
- status: () => fieldsStatus,
351
- footer: '↑↓ field · Enter edit (m model · t think · a agents) · s save current · r rename · v review · Esc back',
352
- shortcuts: {
353
- m: () => void changeModel(profile),
354
- t: () => void changeThink(profile),
355
- a: () => showAgentsTable(profile),
356
- s: () => saveFromCurrent(profile),
357
- r: () => showRename(profile),
358
- v: () => showReview(profile),
359
- },
360
- onEdit: index => {
361
- if (index === 0)
362
- void changeModel(profile);
363
- else if (index === 1)
364
- void changeThink(profile);
365
- else
366
- showAgentsTable(profile);
367
- },
368
- onCancel: () => showTable(),
369
- });
370
- fieldsHandle = host.open(view);
371
- };
372
- /** Commit path shared by model/think/agent edits: flash and re-show. */
373
- const writeFields = (profile, status, message) => {
374
- if (status !== undefined)
375
- fieldsStatus = status;
376
- else {
377
- markChanged(profile.name);
378
- fieldsStatus = message;
379
- }
380
- showFields(profile);
381
- };
382
- const changeModel = async (profile) => {
383
- const current = profile.defaultModel === undefined ? undefined : routeToSelection(profile.defaultModel);
384
- const picked = await pickModel(ctx, tui, theme, current, () => { }, host);
385
- if (picked === undefined) {
386
- showFields(profile);
387
- return;
388
- }
389
- profile.defaultModel = selectionToRoute(picked);
390
- const status = persistDoc();
391
- writeFields(profile, status, `saved ${profile.name} default model → ${formatProfileRoute(profile.defaultModel)}`);
392
- };
393
- const changeThink = async (profile) => {
394
- if (profile.defaultModel === undefined) {
395
- fieldsStatus = '✘ no default model set — pick a model first (m)';
396
- showFields(profile);
397
- return;
398
- }
399
- const llm = ctx.get('llm');
400
- if (llm === undefined) {
401
- fieldsStatus = '✘ llm service unavailable';
402
- showFields(profile);
403
- return;
404
- }
405
- const { provider, model } = profile.defaultModel;
406
- let efforts;
407
- try {
408
- efforts = (await llm.resolveModelInfo(provider, model)).reasoning?.efforts;
409
- }
410
- catch {
411
- efforts = undefined;
412
- }
413
- if (efforts === undefined || efforts.length === 0) {
414
- fieldsStatus = `✘ ${provider}/${model} exposes no think levels`;
415
- showFields(profile);
416
- return;
417
- }
418
- const currentEffort = profile.defaultModel.reasoningEffort;
419
- const chosen = await openEffortPicker(tui, theme, efforts, currentEffort, () => { }, () => fieldsHandle?.hide());
420
- if (chosen === undefined) {
421
- showFields(profile);
422
- return;
423
- }
424
- profile.defaultModel.reasoningEffort = chosen.effort === 'default' ? undefined : chosen.effort;
425
- const status = persistDoc();
426
- const thinkText = profile.defaultModel.reasoningEffort === undefined
427
- ? 'provider default'
428
- : `think ${String(profile.defaultModel.reasoningEffort)}`;
429
- writeFields(profile, status, `saved ${profile.name} think → ${thinkText}`);
430
- };
431
- const saveFromCurrent = (profile) => {
432
- const selection = deps.getSelection();
433
- if (selection !== undefined)
434
- profile.defaultModel = selectionToRoute(selection);
435
- const { agents } = listAgentFiles(agentsDir());
436
- profile.agents = captureAgentsSnapshot(agents);
437
- const status = persistDoc();
438
- const message = selection === undefined
439
- ? `saved ${String(agents.length)} agent(s) into "${profile.name}" · no live model selection — default model kept`
440
- : `saved current configuration into "${profile.name}" (model + ${String(agents.length)} agent(s))`;
441
- writeFields(profile, status, message);
442
- };
443
- const showAgentsTable = (profile) => {
444
- const { agents } = listAgentFiles(agentsDir());
445
- const rows = agents.map(agent => ({ agent, entry: profile.agents[agent.meta.name] }));
446
- const cell = (row, key) => {
447
- switch (key) {
448
- case 'name': return row.agent.meta.displayName ?? row.agent.meta.name;
449
- case 'model': return row.entry?.model ?? (row.entry === undefined ? '(not saved)' : '(inherit)');
450
- default: return row.entry?.thinking ?? 'inherit';
451
- }
452
- };
453
- const table = new TablePanel(theme, {
454
- title: `● Agents — ${profile.name}`,
455
- columns: autoColumns([
456
- { key: 'name', title: 'Agent', cap: 16 },
457
- { key: 'model', title: 'Model', cap: 36 },
458
- { key: 'thinking', title: 'Think', cap: 10 },
459
- ], rows, cell),
460
- rows,
461
- renderCell: (row, column) => cell(row, column.key),
462
- preselect: Math.max(0, rows.findIndex(row => row.agent.meta.name === agentsCursor)),
463
- status: () => agentsStatus,
464
- footer: '↑↓ navigate · Enter pick model (i inherit · t think) · Esc back',
465
- onSelect: row => {
466
- agentsCursor = row.agent.meta.name;
467
- void pickAgentModel(profile, row);
468
- },
469
- onCancel: () => showFields(profile),
470
- shortcuts: {
471
- i: () => {
472
- const row = table.selectedRow();
473
- if (row === undefined)
474
- return;
475
- profile.agents[row.agent.meta.name] = {};
476
- const status = persistDoc();
477
- agentsStatus = status ?? `saved ${row.agent.meta.name} → inherit`;
478
- if (status === undefined)
479
- markChanged(profile.name);
480
- showAgentsTable(profile);
481
- },
482
- t: () => {
483
- const row = table.selectedRow();
484
- if (row === undefined)
485
- return;
486
- void changeAgentThinking(profile, row);
487
- },
488
- },
489
- });
490
- agentsHandle = host.open(table);
491
- };
492
- /** Pick one agent's model + think through the full /model picker. */
493
- const pickAgentModel = async (profile, row) => {
494
- const current = row.entry?.model === undefined
495
- ? undefined
496
- : selectionFromRoute(row.entry.model, row.entry.thinking);
497
- const picked = await pickModel(ctx, tui, theme, current, () => { }, host);
498
- if (picked === undefined) {
499
- showAgentsTable(profile);
500
- return;
501
- }
502
- profile.agents[row.agent.meta.name] = picked.reasoningEffort === undefined
503
- ? { model: `${picked.provider}/${picked.model}` }
504
- : { model: `${picked.provider}/${picked.model}`, thinking: picked.reasoningEffort };
505
- const status = persistDoc();
506
- const entry = profile.agents[row.agent.meta.name];
507
- const thinkText = entry.thinking === undefined ? '' : ` · think ${entry.thinking}`;
508
- agentsStatus = status ?? `saved ${row.agent.meta.name} → ${entry.model ?? ''}${thinkText}`;
509
- if (status === undefined)
510
- markChanged(profile.name);
511
- showAgentsTable(profile);
512
- };
513
- /** Edit one agent's think level alone (its recorded model must be set). */
514
- const changeAgentThinking = async (profile, row) => {
515
- const route = row.entry?.model;
516
- if (route === undefined) {
517
- agentsStatus = '✘ no model recorded — pick a model first (Enter)';
518
- showAgentsTable(profile);
519
- return;
520
- }
521
- const llm = ctx.get('llm');
522
- if (llm === undefined) {
523
- agentsStatus = '✘ llm service unavailable';
524
- showAgentsTable(profile);
525
- return;
526
- }
527
- const slash = route.indexOf('/');
528
- if (slash <= 0) {
529
- agentsStatus = `✘ cannot resolve think levels for ${route}`;
530
- showAgentsTable(profile);
531
- return;
532
- }
533
- let efforts;
534
- try {
535
- efforts = (await llm.resolveModelInfo(route.slice(0, slash), route.slice(slash + 1))).reasoning?.efforts;
536
- }
537
- catch {
538
- efforts = undefined;
539
- }
540
- if (efforts === undefined || efforts.length === 0) {
541
- agentsStatus = `✘ ${route} exposes no think levels`;
542
- showAgentsTable(profile);
543
- return;
544
- }
545
- const entry = profile.agents[row.agent.meta.name] ?? {};
546
- const currentEffort = entry.thinking;
547
- const chosen = await openEffortPicker(tui, theme, efforts, currentEffort, () => { }, () => agentsHandle?.hide());
548
- if (chosen === undefined) {
549
- showAgentsTable(profile);
550
- return;
551
- }
552
- const next = chosen.effort === 'default' ? undefined : chosen.effort;
553
- if (next === undefined)
554
- delete entry.thinking;
555
- else
556
- entry.thinking = next;
557
- profile.agents[row.agent.meta.name] = entry;
558
- const status = persistDoc();
559
- agentsStatus = status ?? `saved ${row.agent.meta.name} → think ${next ?? 'inherit'}`;
560
- if (status === undefined)
561
- markChanged(profile.name);
562
- showAgentsTable(profile);
563
- };
564
- const showRename = (profile) => {
565
- let committed = false;
566
- const field = new EditField(tui, {
567
- title: `Rename profile — ${profile.name}`,
568
- subtitle: 'letters, digits, dash, space · must stay unique',
569
- initial: profile.name,
570
- parse: text => parseProfileName(doc, profile.name, text),
571
- onCommit: async (parsed) => {
572
- if (parsed.kind !== 'value')
573
- return undefined;
574
- const name = parsed.value;
575
- const error = renameProfile(doc, profile.name, name);
576
- if (error !== undefined)
577
- return { error };
578
- committed = true;
579
- pendingPreselect = name;
580
- return undefined;
581
- },
582
- onDone: () => {
583
- if (committed) {
584
- const status = persistDoc();
585
- tableStatus = status ?? `renamed → ${profile.name}`;
586
- if (status === undefined)
587
- markChanged(profile.name);
588
- }
589
- showTable();
590
- },
591
- onError: message => { tableStatus = `✘ ${message}`; },
592
- }, theme);
593
- host.open(field);
594
- };
595
- const showNew = () => {
596
- let committed = false;
597
- const field = new EditField(tui, {
598
- title: 'New model profile',
599
- subtitle: 'a name like "work" or "personal" · configured next',
600
- initial: '',
601
- parse: text => parseProfileName(doc, '', text),
602
- onCommit: async (parsed) => {
603
- if (parsed.kind !== 'value')
604
- return undefined;
605
- const { profile, error } = createProfile(doc, parsed.value);
606
- if (error !== undefined || profile === undefined)
607
- return { error: error ?? 'create failed' };
608
- committed = true;
609
- pendingPreselect = profile.name;
610
- return undefined;
611
- },
612
- onDone: () => {
613
- if (committed) {
614
- const status = persistDoc();
615
- tableStatus = status ?? `created profile "${pendingPreselect}"`;
616
- if (status === undefined && pendingPreselect !== undefined)
617
- markChanged(`+${pendingPreselect}`);
618
- }
619
- showTable();
620
- },
621
- onError: message => { tableStatus = `✘ ${message}`; },
622
- }, theme);
623
- host.open(field);
624
- };
625
- const showReview = (profile) => {
626
- const { agents } = listAgentFiles(agentsDir());
627
- host.open(new ViewerPanel(theme, {
628
- title: `ⓘ Model profile — ${profile.name}`,
629
- lines: profileReviewLines(profile, agents, currentName === profile.name),
630
- footer: ' Esc to close',
631
- onClose: () => showFields(profile),
632
- }));
633
- };
634
- showTable();
635
- });
636
- }
637
- /** The accent title line of the profile fields window (current profiles get the ● mark). */
638
- function profileTitle(theme, profile, currentName) {
639
- const mark = currentName === profile.name ? '● ' : '';
640
- return ansiFg(theme.palette.accent) + BOLD + `${mark}${clipToWidth(profile.name, 100)}` + RESET;
641
- }
642
- /** Name-editor parse: trimmed non-empty, unique against the doc (case-insensitive). */
643
- function parseProfileName(doc, currentName, text) {
644
- const name = text.trim();
645
- if (name === '')
646
- return { kind: 'error', error: 'profile name must not be empty' };
647
- if (name.length > 32)
648
- return { kind: 'error', error: 'profile name must stay under 33 characters' };
649
- if (name.toLowerCase() !== currentName.toLowerCase() && findProfile(doc, name) !== undefined) {
650
- return { kind: 'error', error: `a profile named "${name}" already exists` };
651
- }
652
- return { kind: 'value', value: name };
653
- }
654
- /** An agent entry's `provider/model` route back into a picker preselection. */
655
- function selectionFromRoute(route, thinking) {
656
- const slash = route.indexOf('/');
657
- if (slash <= 0)
658
- return { provider: route, model: route };
659
- return {
660
- provider: route.slice(0, slash),
661
- model: route.slice(slash + 1),
662
- ...(thinking !== undefined ? { reasoningEffort: thinking } : {}),
663
- };
664
- }
665
- //# sourceMappingURL=profile.js.map