thoth-agents 0.12.0 → 0.13.1

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 (43) hide show
  1. package/README.md +11 -2
  2. package/dist/{chunk-7F33WUSD.js → chunk-422L76R5.js} +2 -2
  3. package/dist/chunk-6O4GKDS7.js +207 -0
  4. package/dist/{chunk-SEF6PFM6.js → chunk-A7IGKE4R.js} +132 -157
  5. package/dist/{chunk-NY2IPJ7V.js → chunk-HFBCW4MD.js} +554 -236
  6. package/dist/{chunk-QOA2AETX.js → chunk-KUA3F4D6.js} +3 -3
  7. package/dist/cli/index.js +6 -6
  8. package/dist/cli/opencode-profiles.d.ts +10 -0
  9. package/dist/cli/tui/index.js +3 -3
  10. package/dist/config/loader.d.ts +1 -4
  11. package/dist/config/schema.d.ts +0 -22
  12. package/dist/index.js +824 -30
  13. package/dist/opencode/context.d.ts +16 -0
  14. package/dist/opencode/profile-rpc-handlers.d.ts +3 -0
  15. package/dist/opencode/profile-rpc.d.ts +688 -0
  16. package/dist/opencode/profile-runtime.d.ts +3 -0
  17. package/dist/opencode/profile-sessions.d.ts +18 -0
  18. package/dist/opencode/profiles/editor.d.ts +44 -0
  19. package/dist/opencode/profiles/index.d.ts +18 -0
  20. package/dist/opencode/profiles/resolver.d.ts +12 -0
  21. package/dist/opencode/profiles/store.d.ts +63 -0
  22. package/dist/opencode/profiles/types.d.ts +26 -0
  23. package/dist/opencode/tool-overrides/evaluator.d.ts +13 -0
  24. package/dist/opencode/tool-overrides/index.d.ts +5 -0
  25. package/dist/opencode/tool-overrides/store.d.ts +38 -0
  26. package/dist/opencode/tool-overrides/types.d.ts +32 -0
  27. package/dist/opencode/tool-overrides/validation.d.ts +5 -0
  28. package/dist/opencode/tool-overrides/wildcard.d.ts +3 -0
  29. package/dist/opencode/tool-rpc-handlers.d.ts +3 -0
  30. package/dist/opencode/tool-rpc.d.ts +428 -0
  31. package/dist/pi.js +2 -2
  32. package/dist/tui/fixtures.d.ts +2 -0
  33. package/dist/tui/flow.d.ts +16 -0
  34. package/dist/tui/index.d.ts +7 -0
  35. package/dist/tui/index.js +1386 -0
  36. package/dist/tui/model.d.ts +84 -0
  37. package/dist/tui/tools/fixtures.d.ts +3 -0
  38. package/dist/tui/tools/flow.d.ts +11 -0
  39. package/dist/tui/tools/model.d.ts +97 -0
  40. package/package.json +5 -1
  41. package/plugin/.claude-plugin/plugin.json +1 -1
  42. package/plugin/.codex-plugin/plugin.json +1 -1
  43. package/thoth-agents.schema.json +0 -70
@@ -0,0 +1,1386 @@
1
+ import {
2
+ profileRpc,
3
+ toolRpc
4
+ } from "../chunk-6O4GKDS7.js";
5
+
6
+ // src/tui/model.ts
7
+ var DEFAULT_PROFILE = "default";
8
+ var profileKey = (name, scope) => scope ? `${scope}:${name}` : "default";
9
+ function profileRows(state) {
10
+ return [
11
+ {
12
+ key: "default",
13
+ name: DEFAULT_PROFILE,
14
+ shadowed: false,
15
+ profile: state.defaultProfile
16
+ },
17
+ ...state.profiles.map((entry) => ({
18
+ key: profileKey(entry.name, entry.scope),
19
+ name: entry.name,
20
+ scope: entry.scope,
21
+ shadowed: entry.shadowed,
22
+ profile: entry.profile,
23
+ revision: entry.revision
24
+ }))
25
+ ];
26
+ }
27
+ function markersFor(state, row) {
28
+ const points = (scope) => !row.shadowed && state.selections[scope] === row.name;
29
+ const { effective } = state;
30
+ return {
31
+ effective: effective.name === row.name && (effective.source === void 0 || effective.source === row.scope) && !row.shadowed,
32
+ session: points("session"),
33
+ project: points("project"),
34
+ global: points("global")
35
+ };
36
+ }
37
+ var markerLetters = (markers) => [
38
+ markers.session ? "S" : "",
39
+ markers.project ? "P" : "",
40
+ markers.global ? "G" : ""
41
+ ].join("");
42
+ function profileTitle(row, markers) {
43
+ const flags = `${markers.effective ? "\u25CF" : " "} ${markerLetters(markers).padEnd(3)}`;
44
+ return `${flags} ${row.name}${row.shadowed ? " ~" : ""}`;
45
+ }
46
+ function profileDescription(row) {
47
+ if (!row.scope) return "built-in \xB7 read-only";
48
+ return `${row.scope}${row.shadowed ? " \xB7 shadowed by project profile" : ""}`;
49
+ }
50
+ var PROFILE_LEGEND = "\u25CF effective \xB7 S session \xB7 P project pin \xB7 G global \xB7 ~ shadowed";
51
+ function roleViews(state, profile, isDefault) {
52
+ return state.roles.map((role) => {
53
+ const own = profile.agents[role];
54
+ if (own?.model)
55
+ return { role, inherited: false, model: own.model, variant: own.variant };
56
+ const ownVariant = own?.variant;
57
+ const withVariant = (view) => ownVariant ? { ...view, variant: ownVariant, variantInherited: false } : view;
58
+ const base = state.defaultProfile.agents[role];
59
+ if (!isDefault && base?.model)
60
+ return withVariant({
61
+ role,
62
+ inherited: true,
63
+ model: base.model,
64
+ variant: base.variant,
65
+ inheritedFrom: "default"
66
+ });
67
+ if (role === "orchestrator" && state.orchestrator?.model)
68
+ return withVariant({
69
+ role,
70
+ inherited: true,
71
+ model: state.orchestrator.model,
72
+ variant: state.orchestrator.variant,
73
+ inheritedFrom: "session"
74
+ });
75
+ return { role, inherited: true };
76
+ });
77
+ }
78
+ function roleLabel(view) {
79
+ const model = view.model ?? "unset";
80
+ const variant = view.model ? view.variant ?? "none" : "-";
81
+ const prefix = `${view.role.padEnd(12)} ${model}`;
82
+ if (view.inherited && view.variantInherited === false)
83
+ return `${prefix} (inherited) \xB7 ${variant}`;
84
+ const text = `${prefix} \xB7 ${variant}`;
85
+ return view.inherited ? `${text} (inherited)` : text;
86
+ }
87
+ function roleDescription(view) {
88
+ if (!view.inherited) return "set in this profile";
89
+ if (view.inheritedFrom === "session") return "inherited from session model";
90
+ return view.inheritedFrom === "default" ? "inherited from default" : "unset \xB7 host default";
91
+ }
92
+ function providerModels(state, providerID) {
93
+ const provider = state.providers.find((item) => item.id === providerID);
94
+ return provider && {
95
+ id: provider.id,
96
+ name: provider.name,
97
+ models: provider.models.map((model) => ({
98
+ ...model,
99
+ // Profiles store the full `provider/model` id.
100
+ id: `${provider.id}/${model.id}`
101
+ }))
102
+ };
103
+ }
104
+ function variantsOf(state, modelId) {
105
+ for (const provider of state.providers)
106
+ for (const model of provider.models)
107
+ if (`${provider.id}/${model.id}` === modelId) return model.variants;
108
+ return [];
109
+ }
110
+ function searchOptions(query, options) {
111
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
112
+ return options.filter((option) => {
113
+ const id = typeof option.value === "string" ? option.value : "";
114
+ const text = `${option.title} ${id} ${option.description ?? ""}`.toLowerCase();
115
+ return words.every((word) => text.includes(word));
116
+ });
117
+ }
118
+ function validateProfileName(raw, scope, state) {
119
+ const name = raw.trim();
120
+ if (!name) return "Profile name must not be empty.";
121
+ if (name.toLowerCase() === DEFAULT_PROFILE)
122
+ return '"default" is reserved and read-only.';
123
+ if (state.profiles.some((p) => p.scope === scope && p.name === name))
124
+ return `A ${scope} profile named "${name}" already exists.`;
125
+ return void 0;
126
+ }
127
+ var normalize = (profile) => JSON.stringify(
128
+ Object.entries(profile.agents).filter(([, value]) => value?.model || value?.variant).map(([role, value]) => [role, value?.model, value?.variant]).sort(([a], [b]) => String(a).localeCompare(String(b)))
129
+ );
130
+ var isDirty = (draft) => draft.isNew || normalize(draft.profile) !== normalize(draft.base);
131
+ function withRole(profile, role, value) {
132
+ const agents = { ...profile.agents };
133
+ if (value?.model || value?.variant) agents[role] = value;
134
+ else delete agents[role];
135
+ return { agents };
136
+ }
137
+ function cloneProfile(profile) {
138
+ return { agents: structuredClone(profile.agents) };
139
+ }
140
+ function openDraft(row) {
141
+ if (!row.scope) return void 0;
142
+ return {
143
+ scope: row.scope,
144
+ name: row.name,
145
+ isNew: false,
146
+ base: cloneProfile(row.profile),
147
+ revision: row.revision,
148
+ profile: cloneProfile(row.profile)
149
+ };
150
+ }
151
+ function newDraft(scope, name, from) {
152
+ return {
153
+ scope,
154
+ name,
155
+ isNew: true,
156
+ base: { agents: {} },
157
+ profile: cloneProfile(from)
158
+ };
159
+ }
160
+ var SCOPE_LABEL = {
161
+ session: "this session",
162
+ project: "this project",
163
+ global: "globally"
164
+ };
165
+ function effectiveSummary(state) {
166
+ const { effective } = state;
167
+ return effective.selectedBy === "default" ? `${effective.name} (built-in) is effective` : `${effective.name} is effective (${effective.selectedBy} selection)`;
168
+ }
169
+ function activationMessage(scope, name, state) {
170
+ const { effective } = state;
171
+ const head = `${name} set ${SCOPE_LABEL[scope]}`;
172
+ if (effective.selectedBy === scope && effective.name === name)
173
+ return `${head}; it is now effective.`;
174
+ if (effective.selectedBy === "session" && scope !== "session")
175
+ return `${head}, but this session keeps its session selection: ${effectiveSummary(state)}.`;
176
+ if (effective.selectedBy === "project" && scope === "global")
177
+ return `${head}, but the project pin takes precedence: ${effectiveSummary(state)}.`;
178
+ return `${head}; ${effectiveSummary(state)}.`;
179
+ }
180
+ function clearMessage(scope, state) {
181
+ return `Cleared ${scope} selection; ${effectiveSummary(state)}.`;
182
+ }
183
+ function selectionClearable(state, sessionAvailable) {
184
+ return ["session", "project", "global"].filter(
185
+ (scope) => state.selections[scope] !== void 0 && (scope !== "session" || sessionAvailable)
186
+ );
187
+ }
188
+
189
+ // src/tui/flow.ts
190
+ var TITLE = "Subagent model profiles";
191
+ var NONE = "none";
192
+ async function runModelProfilesFlow(ports) {
193
+ const { ui, rpc } = ports;
194
+ const sessionID = ports.sessionID();
195
+ const session = sessionID ? { sessionID } : {};
196
+ const sessionRequired = "Open a session to use this action.";
197
+ async function call(run) {
198
+ try {
199
+ return await run();
200
+ } catch (error) {
201
+ return {
202
+ ok: false,
203
+ error: {
204
+ code: "OPERATION_FAILED",
205
+ message: error instanceof Error ? error.message : String(error)
206
+ }
207
+ };
208
+ }
209
+ }
210
+ const toast = (message, variant = "info") => ui.toast.show({ title: TITLE, message, variant, duration: 6e3 });
211
+ const fail = (error) => toast(`${error.code}: ${error.message}`, "error");
212
+ let state;
213
+ const loaded = await call(() => rpc.state(session));
214
+ if (!loaded.ok) return fail(loaded.error);
215
+ state = loaded.value;
216
+ const refresh = async () => {
217
+ const next2 = await call(() => rpc.state(session));
218
+ if (!next2.ok) {
219
+ fail(next2.error);
220
+ return false;
221
+ }
222
+ state = next2.value;
223
+ return true;
224
+ };
225
+ const rowOf = (key) => profileRows(state).find((row) => row.key === key);
226
+ const markers = (row) => markersFor(state, row);
227
+ const effectiveText = () => state.effective.selectedBy === "default" ? `${state.effective.name} (built-in)` : `${state.effective.name} (${state.effective.selectedBy})`;
228
+ async function pickProvider(role) {
229
+ if (!state.providers.length) {
230
+ toast("No models available in the catalog.", "warning");
231
+ return void 0;
232
+ }
233
+ const picked = await ui.select({
234
+ title: `Provider for ${role}`,
235
+ placeholder: "Search providers",
236
+ options: [
237
+ {
238
+ title: "Reset to inherited",
239
+ value: "",
240
+ category: "Actions",
241
+ description: "remove this role override"
242
+ },
243
+ ...state.providers.map((provider) => ({
244
+ title: provider.name,
245
+ value: provider.id,
246
+ description: `${provider.models.length} model${provider.models.length === 1 ? "" : "s"}`,
247
+ category: "Providers"
248
+ }))
249
+ ],
250
+ search: searchOptions
251
+ });
252
+ return picked === void 0 ? void 0 : picked || null;
253
+ }
254
+ async function pickModel(role, providerID, current) {
255
+ const provider = providerModels(state, providerID);
256
+ if (!provider) return void 0;
257
+ return ui.select({
258
+ title: `${provider.name} model for ${role}`,
259
+ placeholder: "Search models",
260
+ current,
261
+ options: provider.models.map((model) => ({
262
+ // Names can collide across Codex/API models and dated snapshots. Put
263
+ // the exact local ID first; a non-shrinking footer crowds it out.
264
+ title: model.id.slice(provider.id.length + 1),
265
+ value: model.id,
266
+ description: model.variants.length ? `${model.name} \xB7 variants: ${model.variants.join(", ")}` : model.name
267
+ })),
268
+ search: searchOptions
269
+ });
270
+ }
271
+ async function pickVariant(role, modelId, current) {
272
+ const variants = variantsOf(state, modelId);
273
+ return ui.select({
274
+ title: `Variant for ${role}`,
275
+ placeholder: "Search variants",
276
+ current: current ?? NONE,
277
+ options: [
278
+ {
279
+ title: NONE,
280
+ value: NONE,
281
+ description: `${modelId} default`
282
+ },
283
+ ...variants.map((variant) => ({ title: variant, value: variant }))
284
+ ],
285
+ search: searchOptions
286
+ });
287
+ }
288
+ async function editModel(draft, view, withVariant) {
289
+ const providerID = await pickProvider(view.role);
290
+ if (providerID === void 0) return;
291
+ if (providerID === null) {
292
+ draft.profile = withRole(draft.profile, view.role, void 0);
293
+ return;
294
+ }
295
+ const model = await pickModel(view.role, providerID, view.model);
296
+ if (!model) return;
297
+ let variant;
298
+ if (withVariant && variantsOf(state, model).length) {
299
+ const picked = await pickVariant(view.role, model);
300
+ if (picked === void 0) return;
301
+ variant = picked === NONE ? void 0 : picked;
302
+ }
303
+ draft.profile = withRole(draft.profile, view.role, {
304
+ model,
305
+ ...variant ? { variant } : {}
306
+ });
307
+ }
308
+ async function editVariant(draft, view) {
309
+ if (!view.model) {
310
+ toast("Set a model first; the variant belongs to the model.", "warning");
311
+ return;
312
+ }
313
+ if (!variantsOf(state, view.model).length) {
314
+ toast(`${view.model} has no variants.`, "info");
315
+ return;
316
+ }
317
+ const picked = await pickVariant(view.role, view.model, view.variant);
318
+ if (picked === void 0) return;
319
+ if (!view.inherited) {
320
+ draft.profile = withRole(draft.profile, view.role, {
321
+ model: view.model,
322
+ ...picked === NONE ? {} : { variant: picked }
323
+ });
324
+ return;
325
+ }
326
+ draft.profile = withRole(
327
+ draft.profile,
328
+ view.role,
329
+ picked === NONE ? void 0 : { variant: picked }
330
+ );
331
+ }
332
+ async function editRole(draft, view) {
333
+ const picked = await ui.select({
334
+ title: `Edit ${view.role}`,
335
+ options: [
336
+ {
337
+ title: "Change model and variant",
338
+ value: "both",
339
+ description: view.model ? `now ${roleLabel(view)}` : void 0
340
+ },
341
+ {
342
+ title: "Change variant only",
343
+ value: "variant",
344
+ description: view.model ? `variants of ${view.model}` : "needs a model first"
345
+ },
346
+ {
347
+ title: "Reset to inherited",
348
+ value: "reset",
349
+ description: "remove this role override",
350
+ disabled: view.inherited && view.variantInherited !== false
351
+ }
352
+ ]
353
+ });
354
+ if (picked === "both") await editModel(draft, view, true);
355
+ else if (picked === "variant") await editVariant(draft, view);
356
+ else if (picked === "reset")
357
+ draft.profile = withRole(draft.profile, view.role, void 0);
358
+ }
359
+ async function persist(draft) {
360
+ if (draft.isNew) {
361
+ const created = await call(
362
+ () => rpc.create({
363
+ scope: draft.scope,
364
+ name: draft.name,
365
+ profile: draft.profile
366
+ })
367
+ );
368
+ if (!created.ok) {
369
+ fail(created.error);
370
+ return false;
371
+ }
372
+ draft.revision = created.value.revision;
373
+ } else {
374
+ const baseRevision = draft.revision;
375
+ if (baseRevision === void 0) return false;
376
+ const saved = await call(
377
+ () => rpc.save({
378
+ scope: draft.scope,
379
+ name: draft.name,
380
+ profile: draft.profile,
381
+ baseRevision
382
+ })
383
+ );
384
+ if (!saved.ok) {
385
+ if (saved.error.code === "PROFILE_CONFLICT") {
386
+ await offerReload(draft, saved.error);
387
+ return false;
388
+ }
389
+ fail(saved.error);
390
+ return false;
391
+ }
392
+ draft.revision = saved.value.revision;
393
+ }
394
+ draft.isNew = false;
395
+ draft.base = { agents: structuredClone(draft.profile.agents) };
396
+ await refresh();
397
+ toast(`Saved ${draft.name} (${draft.scope}).`, "success");
398
+ return true;
399
+ }
400
+ async function offerReload(draft, error) {
401
+ const reload = await ui.confirm({
402
+ title: "Profile changed on disk",
403
+ message: `${error.message}
404
+ Reload the saved profile and discard your edits?`,
405
+ label: { confirm: "Reload", cancel: "Keep editing" }
406
+ });
407
+ if (!reload || !await refresh()) return;
408
+ const row = rowOf(profileKey(draft.name, draft.scope));
409
+ const fresh = row && openDraft(row);
410
+ if (fresh) Object.assign(draft, fresh);
411
+ else toast(`${draft.name} no longer exists on disk.`, "warning");
412
+ }
413
+ async function activate(draft, row, scope) {
414
+ if (draft && isDirty(draft)) {
415
+ const choice = await ui.select({
416
+ title: "Unsaved changes",
417
+ options: [
418
+ { title: "Save, then activate", value: "save" },
419
+ { title: "Cancel", value: "cancel" }
420
+ ]
421
+ });
422
+ if (choice !== "save" || !await persist(draft)) return;
423
+ }
424
+ if (row.shadowed) {
425
+ const go = await ui.confirm({
426
+ title: "Shadowed profile",
427
+ message: `A project profile named "${row.name}" shadows this global one; selecting by name activates the project profile.`,
428
+ label: { confirm: "Activate anyway", cancel: "Cancel" }
429
+ });
430
+ if (!go) return;
431
+ }
432
+ const result = await call(
433
+ () => rpc.activate({ ...session, scope, name: row.name })
434
+ );
435
+ if (!result.ok) return fail(result.error);
436
+ state = result.value;
437
+ toast(
438
+ activationMessage(scope, row.name, state),
439
+ state.effective.name === row.name ? "success" : "warning"
440
+ );
441
+ }
442
+ async function clearSelection(scope) {
443
+ const result = await call(() => rpc.clear({ ...session, scope }));
444
+ if (!result.ok) return fail(result.error);
445
+ state = result.value;
446
+ toast(clearMessage(scope, state), "success");
447
+ }
448
+ async function askName(scope, initial) {
449
+ let value = initial;
450
+ for (; ; ) {
451
+ const raw = await ui.prompt({
452
+ title: "Profile name",
453
+ description: `New ${scope} profile (\`default\` is reserved)`,
454
+ placeholder: "e.g. fast-review",
455
+ value
456
+ });
457
+ if (raw === void 0) return void 0;
458
+ const problem = validateProfileName(raw, scope, state);
459
+ if (!problem) return raw.trim();
460
+ toast(problem, "warning");
461
+ value = raw;
462
+ }
463
+ }
464
+ async function startNew(source) {
465
+ const scope = await ui.select({
466
+ title: "Where should the profile live?",
467
+ options: [
468
+ { title: "global", value: "global", description: "all projects" },
469
+ {
470
+ title: "project",
471
+ value: "project",
472
+ description: "this project only (.opencode/thoth)"
473
+ }
474
+ ]
475
+ });
476
+ if (!scope) return void 0;
477
+ const name = await askName(scope);
478
+ if (!name) return void 0;
479
+ const from = (source ?? profileRows(state)[0]).profile;
480
+ return newDraft(scope, name, from);
481
+ }
482
+ async function pickSource() {
483
+ const rows = profileRows(state);
484
+ const key = await ui.select({
485
+ title: "Create from",
486
+ placeholder: "Search profiles",
487
+ options: rows.map((row) => ({
488
+ title: row.key === "default" ? "Start from default" : `Clone ${row.name}`,
489
+ value: row.key,
490
+ description: profileDescription(row),
491
+ category: row.key === "default" ? "Default" : "Clone"
492
+ })),
493
+ search: searchOptions
494
+ });
495
+ return key ? rowOf(key) : void 0;
496
+ }
497
+ const pending = /* @__PURE__ */ new Map();
498
+ async function settlePending() {
499
+ for (const [key, draft] of [...pending]) {
500
+ const choice = await ui.select({
501
+ title: `Unsaved changes in ${draft.name}`,
502
+ options: [
503
+ { title: "Save", value: "save" },
504
+ { title: "Discard", value: "discard" },
505
+ { title: "Keep editing", value: "keep" }
506
+ ]
507
+ });
508
+ if (!choice || choice === "keep") return false;
509
+ if (choice === "save" && !await persist(draft)) return false;
510
+ pending.delete(key);
511
+ }
512
+ return true;
513
+ }
514
+ async function detail(start) {
515
+ const resumed = "key" in start ? pending.get(start.key) : void 0;
516
+ if (resumed && "key" in start) pending.delete(start.key);
517
+ let draft = "key" in start ? resumed ?? openDraft(start) : start;
518
+ const readonlyRow = "key" in start && !draft ? start : void 0;
519
+ for (; ; ) {
520
+ const row = draft ? rowOf(profileKey(draft.name, draft.scope)) ?? (draft.isNew ? {
521
+ key: profileKey(draft.name, draft.scope),
522
+ name: draft.name,
523
+ scope: draft.scope,
524
+ shadowed: false,
525
+ profile: draft.profile
526
+ } : void 0) : readonlyRow && rowOf(readonlyRow.key);
527
+ if (!row) return void 0;
528
+ const profile = draft?.profile ?? row.profile;
529
+ const views = roleViews(state, profile, row.key === "default");
530
+ const dirty = draft ? isDirty(draft) : false;
531
+ const flags = markers(row);
532
+ const options = [
533
+ ...views.map((view) => ({
534
+ title: roleLabel(view),
535
+ value: { t: "role", role: view.role },
536
+ description: roleDescription(view),
537
+ category: "Roles"
538
+ })),
539
+ ...dirty ? [
540
+ {
541
+ title: "Save changes",
542
+ value: { t: "save" },
543
+ category: "Edit"
544
+ },
545
+ {
546
+ title: draft?.isNew ? "Discard draft" : "Discard edits",
547
+ value: { t: "discard" },
548
+ category: "Edit"
549
+ }
550
+ ] : [],
551
+ ...draft ? [
552
+ {
553
+ title: "Capture session model into orchestrator",
554
+ value: { t: "capture" },
555
+ disabled: !sessionID,
556
+ description: !sessionID ? sessionRequired : state.orchestrator?.model ? `now ${state.orchestrator.model}${state.orchestrator.variant ? ` \xB7 ${state.orchestrator.variant}` : ""}` : "no session model yet",
557
+ category: "Edit"
558
+ }
559
+ ] : [],
560
+ {
561
+ title: "Use for this session",
562
+ value: { t: "use", scope: "session" },
563
+ category: "Activate",
564
+ disabled: !sessionID,
565
+ description: !sessionID ? sessionRequired : flags.session ? "already selected" : void 0
566
+ },
567
+ {
568
+ title: "Pin to this project",
569
+ value: { t: "use", scope: "project" },
570
+ category: "Activate",
571
+ description: flags.project ? "already selected" : void 0
572
+ },
573
+ {
574
+ title: "Set as global active",
575
+ value: { t: "use", scope: "global" },
576
+ category: "Activate",
577
+ description: flags.global ? "already selected" : void 0
578
+ },
579
+ {
580
+ title: "Clone this profile",
581
+ value: { t: "clone" },
582
+ category: "Manage"
583
+ },
584
+ ...draft && !draft.isNew ? [
585
+ {
586
+ title: "Rename",
587
+ value: { t: "rename" },
588
+ category: "Manage"
589
+ },
590
+ {
591
+ title: "Delete",
592
+ value: { t: "delete" },
593
+ category: "Manage"
594
+ }
595
+ ] : [],
596
+ {
597
+ title: "Back",
598
+ value: { t: "back" },
599
+ category: "Manage"
600
+ }
601
+ ];
602
+ const title = `${profileTitle(row, flags).trim()}${row.scope ? ` (${row.scope})` : ""}${dirty ? " *" : ""}`;
603
+ const pick = await ui.select({
604
+ title: `${title} \u2014 ${effectiveText()} effective`,
605
+ placeholder: draft ? "Search roles and actions" : "Search (read-only)",
606
+ options,
607
+ search: searchOptions
608
+ });
609
+ if (!pick || pick.t === "back") {
610
+ if (draft && isDirty(draft)) {
611
+ const choice = await ui.select({
612
+ title: `Unsaved changes in ${draft.name}`,
613
+ options: [
614
+ { title: "Save", value: "save" },
615
+ { title: "Discard", value: "discard" },
616
+ { title: "Keep editing", value: "keep" }
617
+ ]
618
+ });
619
+ if (!choice || choice === "keep") continue;
620
+ if (choice === "save" && !await persist(draft)) continue;
621
+ }
622
+ return void 0;
623
+ }
624
+ if (pick.t === "role") {
625
+ const view = views.find((item) => item.role === pick.role);
626
+ if (!view) continue;
627
+ if (!draft) toast("default is read-only; clone it to edit.", "info");
628
+ else await editRole(draft, view);
629
+ } else if (pick.t === "save") await persist(draft);
630
+ else if (pick.t === "discard") {
631
+ const ok = await ui.confirm({
632
+ title: "Discard changes",
633
+ message: `Discard unsaved changes to ${draft?.name}?`,
634
+ label: { confirm: "Discard", cancel: "Keep editing" }
635
+ });
636
+ if (!ok) continue;
637
+ if (draft?.isNew) return void 0;
638
+ const fresh = openDraft(row);
639
+ if (fresh) draft = fresh;
640
+ } else if (pick.t === "capture" && sessionID) {
641
+ const captured = await call(() => rpc.capture({ sessionID }));
642
+ if (!captured.ok) fail(captured.error);
643
+ else if (!captured.value.model)
644
+ toast("No session model to capture yet.", "warning");
645
+ else if (draft) {
646
+ const { model, variant } = captured.value;
647
+ draft.profile = withRole(draft.profile, "orchestrator", {
648
+ model,
649
+ ...variant ? { variant } : {}
650
+ });
651
+ toast(`Orchestrator set to ${model} (unsaved).`, "info");
652
+ }
653
+ } else if (pick.t === "use") await activate(draft, row, pick.scope);
654
+ else if (pick.t === "clone") {
655
+ const clone = await startNew({ ...row, profile });
656
+ if (clone) {
657
+ if (draft && isDirty(draft)) {
658
+ pending.set(profileKey(draft.name, draft.scope), draft);
659
+ toast("Cloned including your unsaved edits.", "info");
660
+ }
661
+ return clone;
662
+ }
663
+ } else if (pick.t === "rename" && draft) {
664
+ if (isDirty(draft)) {
665
+ toast("Save or discard your edits before renaming.", "warning");
666
+ continue;
667
+ }
668
+ const newName = await askName(draft.scope, draft.name);
669
+ if (!newName || newName === draft.name) continue;
670
+ const renamed = await call(
671
+ () => rpc.rename({
672
+ scope: draft?.scope,
673
+ name: draft?.name,
674
+ newName
675
+ })
676
+ );
677
+ if (!renamed.ok) {
678
+ fail(renamed.error);
679
+ continue;
680
+ }
681
+ if (!await refresh()) return void 0;
682
+ const next2 = rowOf(profileKey(newName, draft.scope));
683
+ draft = next2 && openDraft(next2);
684
+ toast(`Renamed to ${newName}.`, "success");
685
+ if (!draft) return void 0;
686
+ } else if (pick.t === "delete" && draft) {
687
+ const ok = await ui.confirm({
688
+ title: `Delete ${draft.name}`,
689
+ message: `Delete the ${draft.scope} profile "${draft.name}"? Selections pointing at it fall back.`,
690
+ label: { confirm: "Delete", cancel: "Cancel" }
691
+ });
692
+ if (!ok) continue;
693
+ const deleted = await call(
694
+ () => rpc.delete({
695
+ scope: draft?.scope,
696
+ name: draft?.name
697
+ })
698
+ );
699
+ if (!deleted.ok) {
700
+ fail(deleted.error);
701
+ continue;
702
+ }
703
+ await refresh();
704
+ toast(`Deleted ${draft.name}.`, "success");
705
+ return void 0;
706
+ }
707
+ }
708
+ }
709
+ let next;
710
+ let last = state.effective.source ? profileKey(state.effective.name, state.effective.source) : "default";
711
+ for (; ; ) {
712
+ if (next) {
713
+ const follow = await detail(next);
714
+ next = follow;
715
+ continue;
716
+ }
717
+ const rows = profileRows(state);
718
+ const clearable = selectionClearable(state, !!sessionID);
719
+ const options = [
720
+ ...rows.map((row) => ({
721
+ title: profileTitle(row, markers(row)),
722
+ value: `profile:${row.key}`,
723
+ description: profileDescription(row),
724
+ category: "Profiles"
725
+ })),
726
+ {
727
+ title: "New profile\u2026",
728
+ value: "new",
729
+ description: "from default or by cloning",
730
+ category: "Actions"
731
+ },
732
+ ...!sessionID ? [
733
+ {
734
+ title: "Clear session selection",
735
+ value: "clear:session",
736
+ disabled: true,
737
+ description: sessionRequired,
738
+ category: "Actions"
739
+ }
740
+ ] : [],
741
+ ...clearable.map((scope) => ({
742
+ title: `Clear ${scope} selection`,
743
+ value: `clear:${scope}`,
744
+ description: `currently ${state.selections[scope]}`,
745
+ category: "Actions"
746
+ })),
747
+ ...state.warnings.length ? [
748
+ {
749
+ title: `Warnings (${state.warnings.length})`,
750
+ value: "warnings",
751
+ description: state.warnings[0],
752
+ category: "Actions"
753
+ }
754
+ ] : []
755
+ ];
756
+ const pick = await ui.select({
757
+ title: `${TITLE} \u2014 ${effectiveText()}`,
758
+ placeholder: `Search profiles \xB7 ${PROFILE_LEGEND}`,
759
+ options,
760
+ current: `profile:${last}`,
761
+ search: searchOptions
762
+ });
763
+ if (!pick) {
764
+ if (await settlePending()) return;
765
+ continue;
766
+ }
767
+ if (pick.startsWith("profile:")) {
768
+ last = pick.slice("profile:".length);
769
+ const row = rowOf(last);
770
+ if (row) next = row;
771
+ } else if (pick === "new") {
772
+ const source = await pickSource();
773
+ if (source) next = await startNew(source);
774
+ } else if (pick.startsWith("clear:"))
775
+ await clearSelection(pick.slice("clear:".length));
776
+ else
777
+ await ui.alert({
778
+ title: "Profile warnings",
779
+ message: state.warnings.join("\n")
780
+ });
781
+ }
782
+ }
783
+
784
+ // src/tui/tools/model.ts
785
+ var LOCKED_ACTION = "subagent";
786
+ var LOCKED_MESSAGE = "subagent is locked: specialists can never delegate, so it cannot be changed.";
787
+ function matchAction(input, pattern, platform = typeof process === "undefined" ? "" : process.platform) {
788
+ const normalized = input.replaceAll("\\", "/");
789
+ let escaped = pattern.replaceAll("\\", "/").replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
790
+ if (escaped.endsWith(" .*")) escaped = `${escaped.slice(0, -3)}( .*)?`;
791
+ return new RegExp(`^${escaped}$`, platform === "win32" ? "si" : "s").test(
792
+ normalized
793
+ );
794
+ }
795
+ var removeOverride = (overrides, action) => overrides.filter((entry) => entry.action !== action).map((e) => ({ ...e }));
796
+ var setOverride = (overrides, value) => [...removeOverride(overrides, value.action), { ...value }];
797
+ var hasWildcard = (pattern) => /[*?]/.test(pattern);
798
+ var roleOf = (state, role) => {
799
+ const found = state.roles.find((item) => item.role === role);
800
+ if (!found) throw new Error(`Unknown role ${role}`);
801
+ return found;
802
+ };
803
+ var stripCounts = (entries) => entries.map(({ action, effect }) => ({ action, effect }));
804
+ function openDraft2(state, role, scope) {
805
+ const view = roleOf(state, role)[scope];
806
+ const overrides = stripCounts(view.overrides);
807
+ return {
808
+ role,
809
+ scope,
810
+ base: structuredClone(overrides),
811
+ overrides: structuredClone(overrides),
812
+ present: view.present,
813
+ basePresent: view.present,
814
+ revision: view.revision
815
+ };
816
+ }
817
+ var isDirty2 = (draft) => draft.present !== draft.basePresent || JSON.stringify(draft.base) !== JSON.stringify(draft.overrides);
818
+ function applyOverrides(draft, next) {
819
+ if (JSON.stringify(next) !== JSON.stringify(draft.overrides)) {
820
+ draft.overrides = next;
821
+ if (draft.scope === "project") draft.present = true;
822
+ }
823
+ }
824
+ function maskGlobal(draft) {
825
+ if (draft.scope === "project") draft.present = true;
826
+ }
827
+ function effectiveLayer(state, draft) {
828
+ const role = roleOf(state, draft.role);
829
+ if (draft.scope === "project") {
830
+ if (draft.present)
831
+ return { scope: "project", overrides: draft.overrides, controls: true };
832
+ return {
833
+ scope: role.global.present ? "global" : "default",
834
+ overrides: stripCounts(role.global.overrides),
835
+ controls: false
836
+ };
837
+ }
838
+ if (role.project.present)
839
+ return {
840
+ scope: "project",
841
+ overrides: stripCounts(role.project.overrides),
842
+ controls: false
843
+ };
844
+ return { scope: "global", overrides: draft.overrides, controls: true };
845
+ }
846
+ var isInactiveScope = (state, draft) => draft.scope === "global" && roleOf(state, draft.role).project.present;
847
+ function scopeStatus(state, draft) {
848
+ if (isInactiveScope(state, draft))
849
+ return "INACTIVE PREVIEW: project controls this role, global edits do not apply here";
850
+ if (draft.scope === "project" && !draft.present)
851
+ return "project entry absent: global applies";
852
+ if (draft.scope === "project" && !draft.overrides.length)
853
+ return "project masks global overrides";
854
+ return "effective after save";
855
+ }
856
+ var defaultScope = (role) => role.effectiveScope === "default" ? "global" : role.effectiveScope;
857
+ var plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
858
+ function roleSummary(role) {
859
+ if (role.effectiveScope === "default") return "defaults";
860
+ const entries = role[role.effectiveScope].overrides;
861
+ if (!entries.length) return "no overrides";
862
+ const parts = [];
863
+ const count = (effect) => entries.filter((entry) => entry.effect === effect).length;
864
+ if (count("deny")) parts.push(`${count("deny")} denied`);
865
+ if (count("ask")) parts.push(`${count("ask")} ask`);
866
+ if (count("allow")) parts.push(`${count("allow")} allowed`);
867
+ return parts.join(", ");
868
+ }
869
+ var scopeMarker = (role) => role.effectiveScope === "global" ? "G" : role.effectiveScope === "project" ? "P" : " ";
870
+ function overviewTitle(role) {
871
+ const count = role.effectiveScope === "default" ? 0 : role[role.effectiveScope].overrides.length;
872
+ return `${scopeMarker(role).padEnd(1)} ${role.role.padEnd(10)} ${plural(count, "override")}`;
873
+ }
874
+ var OVERVIEW_LEGEND = "G global \xB7 P project \xB7 project replaces global";
875
+ var lastMatch = (overrides, action, platform) => {
876
+ let match;
877
+ for (const entry of overrides)
878
+ if (matchAction(action, entry.action, platform)) match = entry;
879
+ return match;
880
+ };
881
+ function previewRows(state, draft) {
882
+ const role = roleOf(state, draft.role);
883
+ const platform = typeof process === "undefined" ? "" : process.platform;
884
+ const layer = effectiveLayer(state, draft);
885
+ const dirty = isDirty2(draft);
886
+ return state.inventory.map((item) => {
887
+ const current = role.actions.find((a) => a.action === item.action);
888
+ const locked = item.locked || current?.locked === true;
889
+ if (locked)
890
+ return {
891
+ action: item.action,
892
+ tools: item.tools,
893
+ locked: true,
894
+ effect: current?.effect ?? "deny",
895
+ source: "default",
896
+ origin: "default",
897
+ editScope: draft.scope,
898
+ inactive: false,
899
+ changed: false
900
+ };
901
+ const match = lastMatch(layer.overrides, item.action, platform);
902
+ const base = current?.baseEffect ?? "ask";
903
+ const effect = match?.effect ?? base;
904
+ const source = !match ? "default" : match.action === item.action ? "entry" : "pattern";
905
+ const own = layer.controls ? void 0 : lastMatch(draft.overrides, item.action, platform)?.effect ?? base;
906
+ return {
907
+ action: item.action,
908
+ tools: item.tools,
909
+ locked: false,
910
+ effect,
911
+ source,
912
+ via: source === "pattern" ? match?.action : void 0,
913
+ origin: match ? layer.scope === "default" ? "default" : layer.scope : "default",
914
+ editScope: draft.scope,
915
+ inactive: draft.scope === "global" && !layer.controls,
916
+ scopeEffect: draft.scope === "global" ? own : void 0,
917
+ changed: dirty && effect !== current?.effect
918
+ };
919
+ });
920
+ }
921
+ function rowTitle(row) {
922
+ const mark = row.locked ? "\u{1F512}" : row.source === "default" ? "\xB7" : row.source === "entry" ? "\u25CF" : "\u25CB";
923
+ return `${mark} ${row.effect.padEnd(5)} ${row.action}${row.changed ? " *" : ""}`;
924
+ }
925
+ function rowDescription(row) {
926
+ const origin = row.locked ? "locked" : row.source === "default" ? "default (inherited)" : row.source === "entry" ? "override" : `via ${row.via}`;
927
+ const tools = row.tools.length ? row.tools.join(", ") : "no tools";
928
+ const from = row.origin !== "default" && row.origin !== row.editScope ? `${row.origin} ` : "";
929
+ const alone = row.inactive ? ` \xB7 global alone would ${row.scopeEffect}` : "";
930
+ return `${from}${origin}${alone} \xB7 ${tools}`;
931
+ }
932
+ var ROLE_LEGEND = "\u25CF override \xB7 \u25CB via pattern \xB7 \xB7 default \xB7 \u{1F512} locked";
933
+ var countMatches = (state, pattern) => {
934
+ const platform = typeof process === "undefined" ? "" : process.platform;
935
+ return state.inventory.filter(
936
+ (item) => matchAction(item.action, pattern, platform)
937
+ ).length;
938
+ };
939
+ function manualEntries(state, draft) {
940
+ const known = new Set(state.inventory.map((item) => item.action));
941
+ const saved = roleOf(state, draft.role)[draft.scope].overrides;
942
+ return draft.overrides.filter((entry) => hasWildcard(entry.action) || !known.has(entry.action)).map((entry) => ({
943
+ ...entry,
944
+ // Saved patterns use host matching semantics; only new patterns need a local estimate.
945
+ matchCount: saved.find((item) => item.action === entry.action)?.matchCount ?? countMatches(state, entry.action)
946
+ }));
947
+ }
948
+ var entryDescription = (entry) => `matches ${plural(entry.matchCount, "action")}${entry.matchCount === 0 ? " (no registered tool)" : ""}`;
949
+ function validatePattern(raw) {
950
+ const pattern = raw.trim();
951
+ if (!pattern) return "Enter a tool action or a wildcard pattern.";
952
+ if (pattern === LOCKED_ACTION) return LOCKED_MESSAGE;
953
+ return void 0;
954
+ }
955
+ function searchOptions2(query, options) {
956
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
957
+ return options.filter((option) => {
958
+ const text = `${option.title} ${option.description ?? ""}`.toLowerCase();
959
+ return words.every((word) => text.includes(word));
960
+ });
961
+ }
962
+
963
+ // src/tui/tools/flow.ts
964
+ var TITLE2 = "Subagent tool permissions";
965
+ var EFFECTS = ["allow", "ask", "deny"];
966
+ async function runToolsFlow(ports) {
967
+ const { ui, rpc } = ports;
968
+ async function call(run) {
969
+ try {
970
+ return await run();
971
+ } catch (error) {
972
+ return {
973
+ ok: false,
974
+ error: {
975
+ code: "OPERATION_FAILED",
976
+ message: error instanceof Error ? error.message : String(error)
977
+ }
978
+ };
979
+ }
980
+ }
981
+ const toast = (message, variant = "info") => ui.toast.show({ title: TITLE2, message, variant, duration: 6e3 });
982
+ const fail = (error) => toast(`${error.code}: ${error.message}`, "error");
983
+ const loaded = await call(() => rpc.state({}));
984
+ if (!loaded.ok) return fail(loaded.error);
985
+ let state = loaded.value;
986
+ const refresh = async () => {
987
+ const next = await call(() => rpc.state({}));
988
+ if (!next.ok) {
989
+ fail(next.error);
990
+ return false;
991
+ }
992
+ state = next.value;
993
+ return true;
994
+ };
995
+ const reopen = (draft) => Object.assign(draft, openDraft2(state, draft.role, draft.scope));
996
+ async function offerReload(draft, error) {
997
+ const reload = await ui.confirm({
998
+ title: "Tool overrides changed on disk",
999
+ message: `${error.message}
1000
+ Reload the saved overrides and discard your edits?`,
1001
+ label: { confirm: "Reload", cancel: "Keep editing" }
1002
+ });
1003
+ if (reload && await refresh()) reopen(draft);
1004
+ }
1005
+ async function persist(draft) {
1006
+ const saved = await call(
1007
+ () => rpc.save({
1008
+ scope: draft.scope,
1009
+ role: draft.role,
1010
+ overrides: draft.overrides,
1011
+ baseRevision: draft.revision
1012
+ })
1013
+ );
1014
+ if (!saved.ok) {
1015
+ if (saved.error.code === "TOOLS_CONFLICT")
1016
+ await offerReload(draft, saved.error);
1017
+ else fail(saved.error);
1018
+ return false;
1019
+ }
1020
+ state = saved.value;
1021
+ reopen(draft);
1022
+ toast(`Saved ${draft.role} (${draft.scope}).`, "success");
1023
+ return true;
1024
+ }
1025
+ async function resetRole(draft) {
1026
+ const ok = await ui.confirm({
1027
+ title: `Reset ${draft.role} to defaults`,
1028
+ message: `Remove all ${draft.scope} overrides for ${draft.role}${isDirty2(draft) ? " and discard unsaved edits" : ""}?${draft.scope === "project" ? "" : " A project entry for this role would still replace global."}`,
1029
+ label: { confirm: "Reset", cancel: "Cancel" }
1030
+ });
1031
+ if (!ok) return;
1032
+ const result = await call(
1033
+ () => rpc.reset({
1034
+ scope: draft.scope,
1035
+ role: draft.role,
1036
+ baseRevision: draft.revision
1037
+ })
1038
+ );
1039
+ if (!result.ok) {
1040
+ if (result.error.code === "TOOLS_CONFLICT")
1041
+ await offerReload(draft, result.error);
1042
+ else fail(result.error);
1043
+ return;
1044
+ }
1045
+ state = result.value;
1046
+ reopen(draft);
1047
+ toast(`Reset ${draft.role} (${draft.scope}) to defaults.`, "success");
1048
+ }
1049
+ async function pickScope(role) {
1050
+ const view = roleOf(state, role);
1051
+ const describe = (scope) => {
1052
+ const entries = view[scope].overrides.length;
1053
+ return `${entries} override${entries === 1 ? "" : "s"}${view.effectiveScope === scope ? " \xB7 in effect" : scope === "global" && view.project.present ? " \xB7 inactive (project replaces global)" : ""}`;
1054
+ };
1055
+ return ui.select({
1056
+ title: `Edit ${role} \u2014 project replaces global for this role`,
1057
+ current: defaultScope(view),
1058
+ options: ["global", "project"].map((scope) => ({
1059
+ title: scope,
1060
+ value: scope,
1061
+ description: describe(scope)
1062
+ }))
1063
+ });
1064
+ }
1065
+ async function pickEffect(title, current, extra) {
1066
+ return ui.select({
1067
+ title,
1068
+ current,
1069
+ options: [
1070
+ {
1071
+ title: extra === "inherit" ? "Inherit" : "Remove entry",
1072
+ value: extra,
1073
+ description: extra === "inherit" ? "drop exact entries for this action; the default applies" : "delete this override"
1074
+ },
1075
+ ...EFFECTS.map((effect) => ({
1076
+ title: effect,
1077
+ value: effect
1078
+ }))
1079
+ ]
1080
+ });
1081
+ }
1082
+ async function addPattern(draft) {
1083
+ let value;
1084
+ for (; ; ) {
1085
+ const raw = await ui.prompt({
1086
+ title: "Add pattern",
1087
+ description: "Tool action or wildcard (* and ?), e.g. github_* \u2014 last match wins",
1088
+ placeholder: "e.g. webfetch or github_*",
1089
+ value
1090
+ });
1091
+ if (raw === void 0) return;
1092
+ const problem = validatePattern(raw);
1093
+ if (problem) {
1094
+ toast(problem, "warning");
1095
+ value = raw;
1096
+ continue;
1097
+ }
1098
+ const pattern = raw.trim();
1099
+ const count = countMatches(state, pattern);
1100
+ const picked = await ui.select({
1101
+ title: `${pattern} matches ${count} action${count === 1 ? "" : "s"}${count === 0 ? " (none registered yet)" : ""} \u2014 effect?`,
1102
+ options: [
1103
+ ...EFFECTS.map((effect) => ({ title: effect, value: effect })),
1104
+ {
1105
+ title: "Edit pattern\u2026",
1106
+ value: "edit",
1107
+ description: "go back to the prompt"
1108
+ }
1109
+ ]
1110
+ });
1111
+ if (picked === void 0) return;
1112
+ if (picked === "edit") {
1113
+ value = raw;
1114
+ continue;
1115
+ }
1116
+ applyOverrides(
1117
+ draft,
1118
+ setOverride(draft.overrides, { action: pattern, effect: picked })
1119
+ );
1120
+ return;
1121
+ }
1122
+ }
1123
+ async function settle(draft) {
1124
+ if (!isDirty2(draft)) return true;
1125
+ const choice = await ui.select({
1126
+ title: `Unsaved changes in ${draft.role} (${draft.scope})`,
1127
+ options: [
1128
+ { title: "Save", value: "save" },
1129
+ { title: "Discard", value: "discard" },
1130
+ { title: "Keep editing", value: "keep" }
1131
+ ]
1132
+ });
1133
+ if (!choice || choice === "keep") return false;
1134
+ return choice === "discard" || await persist(draft);
1135
+ }
1136
+ async function roleView(draft) {
1137
+ for (; ; ) {
1138
+ const rows = previewRows(state, draft);
1139
+ const manual = manualEntries(state, draft);
1140
+ const dirty = isDirty2(draft);
1141
+ const options = [
1142
+ ...rows.map((row) => ({
1143
+ title: rowTitle(row),
1144
+ value: { t: "action", action: row.action },
1145
+ description: rowDescription(row),
1146
+ category: "Actions"
1147
+ })),
1148
+ ...manual.map((entry) => ({
1149
+ title: `${entry.effect.padEnd(5)} ${entry.action}`,
1150
+ value: {
1151
+ t: "entry",
1152
+ index: draft.overrides.findIndex(
1153
+ (o) => o.action === entry.action && o.effect === entry.effect
1154
+ )
1155
+ },
1156
+ description: entryDescription(entry),
1157
+ category: "Manual entries"
1158
+ })),
1159
+ {
1160
+ title: "Add pattern\u2026",
1161
+ value: { t: "add" },
1162
+ description: "allow, ask or deny an action or wildcard",
1163
+ category: "Manual entries"
1164
+ },
1165
+ ...dirty ? [
1166
+ {
1167
+ title: "Save changes",
1168
+ value: { t: "save" },
1169
+ category: "Edit"
1170
+ },
1171
+ {
1172
+ title: "Discard edits",
1173
+ value: { t: "discard" },
1174
+ category: "Edit"
1175
+ }
1176
+ ] : [],
1177
+ ...draft.scope === "project" && !draft.present ? [
1178
+ {
1179
+ title: "Mask global overrides for this project (save empty list)",
1180
+ value: { t: "mask" },
1181
+ description: "a present empty project entry replaces global",
1182
+ category: "Manage"
1183
+ }
1184
+ ] : [],
1185
+ ...draft.scope === "project" && draft.basePresent && !draft.overrides.length ? [
1186
+ {
1187
+ title: "Remove project entry (use global)",
1188
+ value: { t: "reset" },
1189
+ description: "global overrides are masked until removed",
1190
+ category: "Manage"
1191
+ }
1192
+ ] : [],
1193
+ {
1194
+ title: "Reset to defaults",
1195
+ value: { t: "reset" },
1196
+ description: `remove all ${draft.scope} overrides`,
1197
+ category: "Manage"
1198
+ },
1199
+ {
1200
+ title: "Switch scope",
1201
+ value: { t: "scope" },
1202
+ description: `editing ${draft.scope}`,
1203
+ category: "Manage"
1204
+ },
1205
+ { title: "Back", value: { t: "back" }, category: "Manage" }
1206
+ ];
1207
+ const pick = await ui.select({
1208
+ title: `${draft.role} (${draft.scope})${dirty ? " *" : ""} \u2014 ${scopeStatus(state, draft)} \u2014 ${ROLE_LEGEND}`,
1209
+ placeholder: "Search actions and tools",
1210
+ options,
1211
+ search: searchOptions2
1212
+ });
1213
+ if (!pick || pick.t === "back") {
1214
+ if (await settle(draft)) return void 0;
1215
+ continue;
1216
+ }
1217
+ if (pick.t === "action") {
1218
+ const row = rows.find((item) => item.action === pick.action);
1219
+ if (!row) continue;
1220
+ if (row.locked) {
1221
+ toast(LOCKED_MESSAGE, "info");
1222
+ continue;
1223
+ }
1224
+ const exact = draft.overrides.findLast(
1225
+ (entry) => entry.action === row.action
1226
+ );
1227
+ const picked = await pickEffect(
1228
+ `${row.action} \u2014 ${row.effect}`,
1229
+ exact?.effect ?? "inherit",
1230
+ "inherit"
1231
+ );
1232
+ if (!picked) continue;
1233
+ if (picked === "inherit" || picked === "remove") {
1234
+ applyOverrides(draft, removeOverride(draft.overrides, row.action));
1235
+ if (!exact && row.source === "pattern")
1236
+ toast(`${row.via} still applies; edit it under Manual entries.`);
1237
+ } else
1238
+ applyOverrides(
1239
+ draft,
1240
+ setOverride(draft.overrides, {
1241
+ action: row.action,
1242
+ effect: picked
1243
+ })
1244
+ );
1245
+ } else if (pick.t === "entry") {
1246
+ const entry = draft.overrides[pick.index];
1247
+ if (!entry) continue;
1248
+ const picked = await pickEffect(
1249
+ `${entry.action} \u2014 ${entry.effect}`,
1250
+ entry.effect,
1251
+ "remove"
1252
+ );
1253
+ if (!picked) continue;
1254
+ applyOverrides(
1255
+ draft,
1256
+ picked === "remove" || picked === "inherit" ? removeOverride(draft.overrides, entry.action) : setOverride(draft.overrides, {
1257
+ action: entry.action,
1258
+ effect: picked
1259
+ })
1260
+ );
1261
+ } else if (pick.t === "add") await addPattern(draft);
1262
+ else if (pick.t === "mask") maskGlobal(draft);
1263
+ else if (pick.t === "save") await persist(draft);
1264
+ else if (pick.t === "discard") {
1265
+ const ok = await ui.confirm({
1266
+ title: "Discard changes",
1267
+ message: `Discard unsaved changes to ${draft.role} (${draft.scope})?`,
1268
+ label: { confirm: "Discard", cancel: "Keep editing" }
1269
+ });
1270
+ if (ok) reopen(draft);
1271
+ } else if (pick.t === "reset") await resetRole(draft);
1272
+ else if (pick.t === "scope") {
1273
+ if (dirty) {
1274
+ toast(
1275
+ "Save or discard your edits before switching scope.",
1276
+ "warning"
1277
+ );
1278
+ continue;
1279
+ }
1280
+ return "scope";
1281
+ }
1282
+ }
1283
+ }
1284
+ async function editRole(role) {
1285
+ for (; ; ) {
1286
+ const scope = await pickScope(role);
1287
+ if (!scope) return;
1288
+ const draft = openDraft2(state, role, scope);
1289
+ if (await roleView(draft) !== "scope") return;
1290
+ }
1291
+ }
1292
+ let last;
1293
+ for (; ; ) {
1294
+ const roles = state.specialists.map((role) => roleOf(state, role));
1295
+ const pick = await ui.select({
1296
+ title: TITLE2,
1297
+ placeholder: `Search roles \xB7 ${OVERVIEW_LEGEND}`,
1298
+ current: last,
1299
+ options: [
1300
+ ...roles.map((role) => ({
1301
+ title: overviewTitle(role),
1302
+ value: role.role,
1303
+ description: roleSummary(role),
1304
+ category: "Specialists"
1305
+ })),
1306
+ ...state.warnings.length ? [
1307
+ {
1308
+ title: `Warnings (${state.warnings.length})`,
1309
+ value: "warnings",
1310
+ description: state.warnings[0],
1311
+ category: "Actions"
1312
+ }
1313
+ ] : []
1314
+ ],
1315
+ search: searchOptions2
1316
+ });
1317
+ if (!pick) return;
1318
+ if (pick === "warnings")
1319
+ await ui.alert({
1320
+ title: "Tool permission warnings",
1321
+ message: state.warnings.join("\n")
1322
+ });
1323
+ else {
1324
+ last = pick;
1325
+ await editRole(pick);
1326
+ }
1327
+ }
1328
+ }
1329
+
1330
+ // src/tui/index.ts
1331
+ var SLASH = "subagents-model";
1332
+ var TOOLS_SLASH = "subagents-tools";
1333
+ var plugin = {
1334
+ id: "thoth-agents.model-profiles",
1335
+ setup(context) {
1336
+ const rpc = context.client.rpc(profileRpc);
1337
+ const toolsRpc = context.client.rpc(toolRpc);
1338
+ const sessionID = () => {
1339
+ const route = context.ui.router.current();
1340
+ return route.type === "session" ? route.sessionID : void 0;
1341
+ };
1342
+ const { dialog, toast } = context.ui;
1343
+ const ui = {
1344
+ select: (options) => dialog.select(options),
1345
+ prompt: (options) => dialog.prompt(options),
1346
+ confirm: (options) => dialog.confirm(options),
1347
+ alert: (options) => dialog.alert(options),
1348
+ toast: { show: (options) => toast.show(options) }
1349
+ };
1350
+ let running = false;
1351
+ const exclusive = async (run) => {
1352
+ if (running) return;
1353
+ running = true;
1354
+ try {
1355
+ await run();
1356
+ } finally {
1357
+ running = false;
1358
+ }
1359
+ };
1360
+ context.keymap.layer(() => ({
1361
+ mode: "global",
1362
+ commands: [
1363
+ {
1364
+ id: "thoth-agents.subagents-model",
1365
+ title: "Subagent model profiles",
1366
+ description: "View, edit and activate subagent model profiles",
1367
+ palette: true,
1368
+ slash: { name: SLASH },
1369
+ run: () => exclusive(() => runModelProfilesFlow({ ui, rpc, sessionID }))
1370
+ },
1371
+ {
1372
+ id: "thoth-agents.subagents-tools",
1373
+ title: "Subagent tool permissions",
1374
+ description: "View and edit per-role allow/ask/deny tool overrides",
1375
+ palette: true,
1376
+ slash: { name: TOOLS_SLASH },
1377
+ run: () => exclusive(() => runToolsFlow({ ui, rpc: toolsRpc }))
1378
+ }
1379
+ ]
1380
+ }));
1381
+ }
1382
+ };
1383
+ var tui_default = plugin;
1384
+ export {
1385
+ tui_default as default
1386
+ };