@north-light/crouter 0.3.262 → 0.3.269
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/api/client.d.ts +5 -1
- package/dist/api/client.js +6 -0
- package/dist/api/dto/canvas.d.ts +53 -2
- package/dist/api/dto/health.d.ts +2 -0
- package/dist/api/dto/nodes.d.ts +6 -2
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +2 -2
- package/dist/builtin-memory/internal/memory-loading.md +3 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +50 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +46 -6
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +10 -5
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +12 -5
- package/dist/clients/attach/input/controller.d.ts +4 -0
- package/dist/clients/attach/input/controller.js +8 -0
- package/dist/clients/attach/session/bindings.d.ts +3 -0
- package/dist/clients/attach/session/bindings.js +7 -0
- package/dist/clients/attach/session/keys.d.ts +2 -0
- package/dist/clients/attach/session/keys.js +3 -3
- package/dist/clients/attach/session/whip-achievements.d.ts +4 -0
- package/dist/clients/attach/session/whip-achievements.js +145 -0
- package/dist/clients/attach/slash/dispatch.js +5 -3
- package/dist/clients/attach/viewer.js +668 -667
- package/dist/commands/canvas-history/grep.js +3 -22
- package/dist/commands/canvas-history/read.js +5 -3
- package/dist/commands/canvas-history/search.js +3 -22
- package/dist/commands/canvas-history/shared.d.ts +8 -0
- package/dist/commands/canvas-history/shared.js +64 -1
- package/dist/commands/canvas-history/stats.d.ts +1 -0
- package/dist/commands/canvas-history/stats.js +68 -0
- package/dist/commands/canvas-history.js +4 -3
- package/dist/commands/memory/read.js +15 -38
- package/dist/commands/memory/shared.d.ts +1 -1
- package/dist/commands/memory/shared.js +1 -1
- package/dist/commands/node/create.js +2 -2
- package/dist/commands/sys/config.js +19 -2
- package/dist/commands/sys/context/admin/docs-panel.d.ts +3 -3
- package/dist/commands/sys/context/admin/model.d.ts +7 -2
- package/dist/commands/sys/context/admin/model.js +7 -1
- package/dist/commands/sys/context/admin/read-view.d.ts +5 -5
- package/dist/commands/sys/context/admin/read-view.js +9 -4
- package/dist/commands/sys/context/resolve.d.ts +32 -3
- package/dist/commands/sys/context/resolve.js +74 -25
- package/dist/commands/sys/panels/models-panel.d.ts +28 -5
- package/dist/commands/sys/panels/models-panel.js +560 -75
- package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +1 -1
- package/dist/core/__tests__/integration/revive.test.js +4 -2
- package/dist/core/__tests__/model-pin-durability.test.js +25 -1
- package/dist/core/__tests__/model-routes-config.test.d.ts +1 -0
- package/dist/core/__tests__/model-routes-config.test.js +142 -0
- package/dist/core/__tests__/on-read-nested-store.test.js +8 -0
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +264 -1
- package/dist/core/__tests__/relaunch-root.test.js +17 -0
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +21 -6
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +19 -2
- package/dist/core/broker-client/__tests__/transport-relay.test.js +3 -1
- package/dist/core/broker-client/transport-relay.js +5 -1
- package/dist/core/canvas/__tests__/history-transcript.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/history-transcript.test.js +102 -0
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +13 -10
- package/dist/core/canvas/browse/model.js +16 -1
- package/dist/core/canvas/browse/render.d.ts +23 -3
- package/dist/core/canvas/browse/render.js +429 -64
- package/dist/core/canvas/canvas.js +12 -8
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/canvas/history.d.ts +36 -1
- package/dist/core/canvas/history.js +328 -7
- package/dist/core/canvas/labels.d.ts +2 -2
- package/dist/core/canvas/migrations.js +32 -0
- package/dist/core/canvas/node-recap.d.ts +23 -0
- package/dist/core/canvas/node-recap.js +67 -0
- package/dist/core/canvas/render-source.d.ts +34 -2
- package/dist/core/canvas/render-source.js +83 -24
- package/dist/core/canvas/types.d.ts +9 -0
- package/dist/core/config.d.ts +19 -2
- package/dist/core/config.js +174 -51
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +11 -8
- package/dist/core/model-routes.js +11 -4
- package/dist/core/runtime/broker/auth-reload.js +4 -1
- package/dist/core/runtime/broker/fault-retry.js +7 -2
- package/dist/core/runtime/broker/frame-client.js +4 -0
- package/dist/core/runtime/broker/frame-dispatch.js +7 -0
- package/dist/core/runtime/broker/inbox.d.ts +12 -2
- package/dist/core/runtime/broker/inbox.js +68 -38
- package/dist/core/runtime/broker-protocol.d.ts +6 -0
- package/dist/core/runtime/broker.js +14 -0
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/interactive-deliver.d.ts +8 -2
- package/dist/core/runtime/interactive-deliver.js +12 -3
- package/dist/core/runtime/launch-target.js +1 -1
- package/dist/core/runtime/launch.d.ts +12 -6
- package/dist/core/runtime/launch.js +19 -17
- package/dist/core/runtime/model-swap.d.ts +5 -4
- package/dist/core/runtime/model-swap.js +7 -5
- package/dist/core/runtime/promote.js +2 -1
- package/dist/core/runtime/reset.js +4 -2
- package/dist/core/runtime/revive.js +22 -0
- package/dist/core/runtime/spawn.d.ts +3 -3
- package/dist/core/runtime/spawn.js +20 -3
- package/dist/core/runtime/stamp/channel.d.ts +18 -0
- package/dist/core/runtime/stamp/channel.js +74 -0
- package/dist/core/runtime/stamp/protocol.d.ts +86 -0
- package/dist/core/runtime/stamp/protocol.js +113 -0
- package/dist/core/runtime/tmux-driver.d.ts +11 -1
- package/dist/core/runtime/tmux-driver.js +27 -11
- package/dist/core/self-update.js +2 -5
- package/dist/core/substrate/listings.d.ts +1 -1
- package/dist/core/substrate/listings.js +6 -2
- package/dist/core/substrate/on-read.d.ts +61 -5
- package/dist/core/substrate/on-read.js +129 -22
- package/dist/core/substrate/render.d.ts +3 -2
- package/dist/core/substrate/render.js +133 -38
- package/dist/core/tui/draw.d.ts +1 -0
- package/dist/core/tui/draw.js +5 -1
- package/dist/core/tui/markdown.d.ts +17 -0
- package/dist/core/tui/markdown.js +407 -0
- package/dist/core/user-settings.d.ts +4 -0
- package/dist/core/user-settings.js +1 -0
- package/dist/daemon/api/__tests__/node-model-validation.test.js +21 -0
- package/dist/daemon/api/__tests__/seam/api-server.test.js +34 -0
- package/dist/daemon/api/bridge.d.ts +3 -2
- package/dist/daemon/api/bridge.js +34 -9
- package/dist/daemon/api/handlers/canvas.js +80 -18
- package/dist/daemon/api/handlers/health.js +17 -12
- package/dist/daemon/api/handlers/messages.js +12 -1
- package/dist/daemon/api/handlers/nodes.js +10 -3
- package/dist/daemon/api/map.js +2 -0
- package/dist/daemon/messaging/node-message.js +15 -1
- package/dist/daemon/reconcilers/broker-supervision.js +26 -9
- package/dist/daemon/reconcilers/live-obligation.d.ts +8 -1
- package/dist/daemon/reconcilers/live-obligation.js +17 -0
- package/dist/pi-extensions/__tests__/pre-command-gate.test.js +2 -2
- package/dist/pi-extensions/canvas-inbox-watcher.js +27 -2
- package/dist/pi-extensions/canvas-recap.js +4 -0
- package/dist/pi-extensions/canvas-stamp.d.ts +14 -0
- package/dist/pi-extensions/canvas-stamp.js +77 -0
- package/dist/pi-extensions/canvas-stophook.js +3 -0
- package/dist/shared/__tests__/env-boundary.test.js +4 -2
- package/dist/shared/crtr-version.d.ts +2 -0
- package/dist/shared/crtr-version.js +18 -0
- package/dist/shared/env.d.ts +10 -0
- package/dist/shared/env.js +19 -4
- package/dist/types.d.ts +14 -2
- package/dist/types.js +4 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
package/dist/core/config.js
CHANGED
|
@@ -114,10 +114,11 @@ export function ensureScopeInitialized(scope, root) {
|
|
|
114
114
|
// a missing `modelLadders` from whatever build is CURRENTLY running, and an explicit
|
|
115
115
|
// user override (`sys config set modelLadders...`) still persists and still wins.
|
|
116
116
|
// Keybindings are sparse user overrides, so an untouched scope omits that member too.
|
|
117
|
-
const { modelLadders: _omittedModelLadders, keybindings: _omittedKeybindings, tmux_passthrough: _omittedTmuxPassthrough, brokerThresholds, ...rest } = defaultScopeConfig();
|
|
118
|
-
// Broker thresholds are host-wide daemon policy, so
|
|
119
|
-
// persists them. Project config must never acquire a
|
|
120
|
-
|
|
117
|
+
const { modelLadders: _omittedModelLadders, keybindings: _omittedKeybindings, tmux_passthrough: _omittedTmuxPassthrough, brokerThresholds, lifecycle, ...rest } = defaultScopeConfig();
|
|
118
|
+
// Broker thresholds and lifecycle timing are host-wide daemon policy, so
|
|
119
|
+
// only user scope persists them. Project config must never acquire a
|
|
120
|
+
// machine setting.
|
|
121
|
+
atomicWriteJson(cfgPath, scope === 'user' ? { ...rest, brokerThresholds, lifecycle } : rest);
|
|
121
122
|
});
|
|
122
123
|
}
|
|
123
124
|
function normalizeKeybindings(raw) {
|
|
@@ -167,66 +168,140 @@ function mergeModelLadders(raw, base = defaultModelLaddersConfig()) {
|
|
|
167
168
|
function validStrength(value) {
|
|
168
169
|
return value === 'ultra' || value === 'strong' || value === 'medium' || value === 'light';
|
|
169
170
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
const MODEL_STRENGTHS = ['ultra', 'strong', 'medium', 'light'];
|
|
172
|
+
function routeRecord() {
|
|
173
|
+
return Object.create(null);
|
|
174
|
+
}
|
|
175
|
+
function setRouteRecord(record, routeId, value) {
|
|
176
|
+
Object.defineProperty(record, routeId, { value, writable: true, enumerable: true, configurable: true });
|
|
177
|
+
}
|
|
178
|
+
function copyRouteRecord(record) {
|
|
179
|
+
const copy = routeRecord();
|
|
180
|
+
for (const [routeId, value] of Object.entries(record ?? {}))
|
|
181
|
+
setRouteRecord(copy, routeId, value);
|
|
182
|
+
return copy;
|
|
183
|
+
}
|
|
184
|
+
function routeInvalid(routeId, rule) {
|
|
185
|
+
emitEvent({ level: 'warn', event: 'config.model_routes.invalid', fields: { route_id: routeId, rule } });
|
|
186
|
+
}
|
|
187
|
+
/** Overlay raw route patches first so a higher scope may replace one cell of a
|
|
188
|
+
* complete lower route. Completeness belongs to the resulting effective route,
|
|
189
|
+
* never an individual raw scope patch. */
|
|
190
|
+
function mergeModelRoutePatches(raw, base = {}) {
|
|
191
|
+
const out = copyRouteRecord(base);
|
|
174
192
|
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
175
193
|
return out;
|
|
176
194
|
for (const [routeId, value] of Object.entries(raw)) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
180
|
-
invalid('route must be an object');
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
const route = { ...out[routeId], ...patch, models: { ...out[routeId]?.models, ...patch.models } };
|
|
184
|
-
if (typeof route.family !== 'string' || route.family.trim() === '') {
|
|
185
|
-
invalid('family must be a non-empty string');
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
if (typeof route.credentialSource !== 'string' || route.credentialSource.trim() === '') {
|
|
189
|
-
invalid('credentialSource must be a non-empty string');
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (typeof route.providerId !== 'string' || route.providerId.trim() === '') {
|
|
193
|
-
invalid('providerId must be a non-empty string');
|
|
195
|
+
if (routeId.trim() === '') {
|
|
196
|
+
routeInvalid(routeId, 'route id must be a non-empty string');
|
|
194
197
|
continue;
|
|
195
198
|
}
|
|
196
|
-
if (
|
|
197
|
-
|
|
199
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
200
|
+
routeInvalid(routeId, 'route must be an object');
|
|
198
201
|
continue;
|
|
199
202
|
}
|
|
200
|
-
const
|
|
203
|
+
const patch = value;
|
|
204
|
+
const current = out[routeId];
|
|
205
|
+
const route = { ...current };
|
|
201
206
|
let rule;
|
|
202
|
-
for (const [
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
rule = `models.${strength} must be a non-empty provider/id ref`;
|
|
207
|
+
for (const field of ['family', 'credentialSource', 'providerId']) {
|
|
208
|
+
const candidate = patch[field];
|
|
209
|
+
if (candidate === undefined)
|
|
210
|
+
continue;
|
|
211
|
+
if (typeof candidate !== 'string' || candidate.trim() === '') {
|
|
212
|
+
rule = `${field} must be a non-empty string`;
|
|
209
213
|
break;
|
|
210
214
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
+
route[field] = candidate;
|
|
216
|
+
}
|
|
217
|
+
if (rule === undefined && patch.models !== undefined) {
|
|
218
|
+
if (patch.models === null || typeof patch.models !== 'object' || Array.isArray(patch.models))
|
|
219
|
+
rule = 'models must be an object';
|
|
220
|
+
else {
|
|
221
|
+
const models = { ...current?.models };
|
|
222
|
+
for (const [strength, spec] of Object.entries(patch.models)) {
|
|
223
|
+
if (!validStrength(strength)) {
|
|
224
|
+
rule = `models.${strength} is not a model strength`;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
if (typeof spec !== 'string' || spec.trim() === '') {
|
|
228
|
+
rule = `models.${strength} must be a non-empty provider/id ref`;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
const slash = spec.indexOf('/');
|
|
232
|
+
if (slash <= 0 || slash === spec.length - 1) {
|
|
233
|
+
rule = `models.${strength} must be provider/id`;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
models[strength] = spec;
|
|
237
|
+
}
|
|
238
|
+
route.models = models;
|
|
215
239
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
240
|
+
}
|
|
241
|
+
if (rule === undefined && route.models !== undefined && route.providerId !== undefined) {
|
|
242
|
+
for (const [strength, spec] of Object.entries(route.models)) {
|
|
243
|
+
if (spec.slice(0, spec.indexOf('/')) !== route.providerId) {
|
|
244
|
+
rule = `models.${strength} provider prefix must equal providerId`;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
219
247
|
}
|
|
220
|
-
models[strength] = spec;
|
|
221
248
|
}
|
|
222
249
|
if (rule !== undefined) {
|
|
223
|
-
|
|
250
|
+
routeInvalid(routeId, rule);
|
|
224
251
|
continue;
|
|
225
252
|
}
|
|
226
|
-
out
|
|
253
|
+
setRouteRecord(out, routeId, route);
|
|
227
254
|
}
|
|
228
255
|
return out;
|
|
229
256
|
}
|
|
257
|
+
/** Resolve complete routes after every scope has overlaid its sparse raw patch.
|
|
258
|
+
* Sparse standalone routes are read for one compatibility release only: they
|
|
259
|
+
* retain their historical candidate behavior, emit a repair warning, and never
|
|
260
|
+
* gain invented cells. New writes cannot create them. */
|
|
261
|
+
function resolveModelRoutes(routes) {
|
|
262
|
+
const complete = routeRecord();
|
|
263
|
+
const legacy = routeRecord();
|
|
264
|
+
for (const [routeId, route] of Object.entries(routes)) {
|
|
265
|
+
if (route.family === undefined || route.credentialSource === undefined || route.providerId === undefined || route.models === undefined) {
|
|
266
|
+
routeInvalid(routeId, 'route must include family, credentialSource, providerId, and models');
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const missing = MODEL_STRENGTHS.filter((strength) => route.models[strength] === undefined);
|
|
270
|
+
if (missing.length > 0) {
|
|
271
|
+
routeInvalid(routeId, `incomplete route; missing strengths: ${missing.join(', ')}`);
|
|
272
|
+
setRouteRecord(legacy, routeId, { family: route.family, credentialSource: route.credentialSource, providerId: route.providerId, models: route.models });
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
setRouteRecord(complete, routeId, {
|
|
276
|
+
family: route.family,
|
|
277
|
+
credentialSource: route.credentialSource,
|
|
278
|
+
providerId: route.providerId,
|
|
279
|
+
models: route.models,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return { complete, legacy };
|
|
283
|
+
}
|
|
284
|
+
function assertCompleteModelRoute(routeId, route) {
|
|
285
|
+
if (routeId.trim() === '')
|
|
286
|
+
throw new Error('model route id must be a non-empty string');
|
|
287
|
+
for (const field of ['family', 'credentialSource', 'providerId']) {
|
|
288
|
+
if (typeof route[field] !== 'string' || route[field].trim() === '')
|
|
289
|
+
throw new Error(`model route "${routeId}" ${field} must be a non-empty string`);
|
|
290
|
+
}
|
|
291
|
+
const missing = MODEL_STRENGTHS.filter((strength) => route.models?.[strength] === undefined);
|
|
292
|
+
if (missing.length > 0)
|
|
293
|
+
throw new Error(`model route "${routeId}" is incomplete; missing strengths: ${missing.join(', ')}`);
|
|
294
|
+
for (const strength of MODEL_STRENGTHS) {
|
|
295
|
+
const spec = route.models[strength];
|
|
296
|
+
if (typeof spec !== 'string' || spec.trim() === '')
|
|
297
|
+
throw new Error(`model route "${routeId}" models.${strength} must be a non-empty provider/id ref`);
|
|
298
|
+
const slash = spec.indexOf('/');
|
|
299
|
+
if (slash <= 0 || slash === spec.length - 1)
|
|
300
|
+
throw new Error(`model route "${routeId}" models.${strength} must be provider/id`);
|
|
301
|
+
if (spec.slice(0, slash) !== route.providerId)
|
|
302
|
+
throw new Error(`model route "${routeId}" models.${strength} provider prefix must equal providerId`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
230
305
|
function mergeModelRouting(raw, base) {
|
|
231
306
|
if (raw === undefined)
|
|
232
307
|
return base;
|
|
@@ -627,6 +702,12 @@ function mergeBrokerThresholds(raw, defaults) {
|
|
|
627
702
|
? { warning, automaticReviveCap }
|
|
628
703
|
: { ...defaults };
|
|
629
704
|
}
|
|
705
|
+
function mergeLifecycle(raw, defaults) {
|
|
706
|
+
const partial = raw !== null && typeof raw === 'object'
|
|
707
|
+
? raw
|
|
708
|
+
: {};
|
|
709
|
+
return { unattendedParkMs: normalizePositiveInteger(partial.unattendedParkMs, defaults.unattendedParkMs) };
|
|
710
|
+
}
|
|
630
711
|
function normalizeWhipMessages(raw) {
|
|
631
712
|
if (!Array.isArray(raw))
|
|
632
713
|
return [...DEFAULT_WHIP_MESSAGES];
|
|
@@ -654,6 +735,7 @@ function mergeConfig(partial) {
|
|
|
654
735
|
? partial.auto_open_child_viewers
|
|
655
736
|
: defaults.auto_open_child_viewers;
|
|
656
737
|
const brokerThresholds = mergeBrokerThresholds(partial.brokerThresholds, defaults.brokerThresholds);
|
|
738
|
+
const lifecycle = mergeLifecycle(partial.lifecycle, defaults.lifecycle);
|
|
657
739
|
const completion_bell = typeof partial.completion_bell === 'boolean' ? partial.completion_bell : defaults.completion_bell;
|
|
658
740
|
const working_gerunds = partial.working_gerunds === undefined
|
|
659
741
|
? [...defaults.working_gerunds]
|
|
@@ -679,14 +761,15 @@ function mergeConfig(partial) {
|
|
|
679
761
|
const keybindings = normalizeKeybindings(partial.keybindings);
|
|
680
762
|
const tmux_passthrough = normalizeTmuxPassthrough(partial.tmux_passthrough);
|
|
681
763
|
const modelLadders = mergeModelLadders(partial.modelLadders);
|
|
682
|
-
const
|
|
764
|
+
const routes = resolveModelRoutes(mergeModelRoutePatches(partial.modelRoutes));
|
|
765
|
+
const modelRoutes = routes.complete;
|
|
683
766
|
const modelRouting = mergeModelRouting(partial.modelRouting, undefined);
|
|
684
767
|
const kinds = mergeKinds(partial.kinds);
|
|
685
768
|
const remoteCanvas = mergeRemoteCanvas(partial.remoteCanvas);
|
|
686
769
|
const spawnEnv = mergeSpawnEnv(partial.spawnEnv, defaults.spawnEnv?.allow);
|
|
687
770
|
const bin = normalizeBin(partial.bin);
|
|
688
771
|
const humanActions = normalizeHumanActions(partial.humanActions);
|
|
689
|
-
return { bin, humanActions, schema_version, marketplaces, plugins, auto_update, max_panes_per_window, auto_open_child_viewers, brokerThresholds, completion_bell, working_gerunds, whip_mode, page_components, page_surface, whip_messages, whip_message_mode, mouse_mode_default, live_cycles, condensed_history, bash_tool_purpose, fold_finished_tools, summarize_tool_calls, detailed_tool_recaps, keybindings, tmux_passthrough, modelLadders, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
|
|
772
|
+
return { bin, humanActions, schema_version, marketplaces, plugins, auto_update, max_panes_per_window, auto_open_child_viewers, brokerThresholds, lifecycle, completion_bell, working_gerunds, whip_mode, page_components, page_surface, whip_messages, whip_message_mode, mouse_mode_default, live_cycles, condensed_history, bash_tool_purpose, fold_finished_tools, summarize_tool_calls, detailed_tool_recaps, keybindings, tmux_passthrough, modelLadders, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(Object.keys(routes.legacy).length > 0 ? { legacyModelRoutes: routes.legacy } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
|
|
690
773
|
}
|
|
691
774
|
/** Raw (un-defaulted) partial config for one scope, or null if the scope has
|
|
692
775
|
* no root or no config.json. Used by `readMergedLaunchConfig` to layer
|
|
@@ -775,13 +858,13 @@ export function readMergedLaunchConfig(targetCwd = process.cwd(), targetProfileI
|
|
|
775
858
|
// (ladders/routes/spawnEnv) have no plugin channel.
|
|
776
859
|
let kinds = mergeKinds(userRaw?.kinds, layerPluginKinds('user', scopeRoot('user'), defaults.kinds));
|
|
777
860
|
let modelLadders = mergeModelLadders(userRaw?.modelLadders, defaults.modelLadders);
|
|
778
|
-
let
|
|
861
|
+
let modelRoutePatches = mergeModelRoutePatches(userRaw?.modelRoutes);
|
|
779
862
|
let modelRouting = mergeModelRouting(userRaw?.modelRouting, undefined);
|
|
780
863
|
let spawnEnv = mergeSpawnEnv(userRaw?.spawnEnv, defaults.spawnEnv?.allow);
|
|
781
864
|
if (profileRaw !== null) {
|
|
782
865
|
kinds = mergeKinds(profileRaw.kinds, kinds);
|
|
783
866
|
modelLadders = mergeModelLadders(profileRaw.modelLadders, modelLadders);
|
|
784
|
-
|
|
867
|
+
modelRoutePatches = mergeModelRoutePatches(profileRaw.modelRoutes, modelRoutePatches);
|
|
785
868
|
modelRouting = mergeModelRouting(profileRaw.modelRouting, modelRouting);
|
|
786
869
|
spawnEnv = mergeSpawnEnv(profileRaw.spawnEnv, spawnEnv.allow);
|
|
787
870
|
}
|
|
@@ -790,11 +873,19 @@ export function readMergedLaunchConfig(targetCwd = process.cwd(), targetProfileI
|
|
|
790
873
|
if (raw === null)
|
|
791
874
|
continue;
|
|
792
875
|
modelLadders = mergeModelLadders(raw.modelLadders, modelLadders);
|
|
793
|
-
|
|
876
|
+
modelRoutePatches = mergeModelRoutePatches(raw.modelRoutes, modelRoutePatches);
|
|
794
877
|
modelRouting = mergeModelRouting(raw.modelRouting, modelRouting);
|
|
795
878
|
spawnEnv = mergeSpawnEnv(raw.spawnEnv, spawnEnv.allow);
|
|
796
879
|
}
|
|
797
|
-
|
|
880
|
+
const routes = resolveModelRoutes(modelRoutePatches);
|
|
881
|
+
return {
|
|
882
|
+
kinds,
|
|
883
|
+
modelLadders,
|
|
884
|
+
...(Object.keys(routes.complete).length > 0 ? { modelRoutes: routes.complete } : {}),
|
|
885
|
+
...(Object.keys(routes.legacy).length > 0 ? { legacyModelRoutes: routes.legacy } : {}),
|
|
886
|
+
...(modelRouting !== undefined ? { modelRouting } : {}),
|
|
887
|
+
spawnEnv,
|
|
888
|
+
};
|
|
798
889
|
}
|
|
799
890
|
/** The effective `KindConfig` for one full kind string (top-level, e.g.
|
|
800
891
|
* `developer`, or sub-kind, e.g. `audit/security`), across
|
|
@@ -930,6 +1021,38 @@ export function persistModelLadders(root, patch) {
|
|
|
930
1021
|
atomicWriteJson(lockedPath, { ...raw, modelLadders: next });
|
|
931
1022
|
});
|
|
932
1023
|
}
|
|
1024
|
+
/** Persist one complete route at one exact config root. The route is the only
|
|
1025
|
+
* raw route entry touched; unrelated routes and local ladder defaults remain
|
|
1026
|
+
* absent. Pass `undefined` to remove this scope's entry, which resumes lower
|
|
1027
|
+
* scope inheritance rather than introducing a tombstone. */
|
|
1028
|
+
export function persistModelRoute(root, routeId, route) {
|
|
1029
|
+
if (route !== undefined)
|
|
1030
|
+
assertCompleteModelRoute(routeId, route);
|
|
1031
|
+
else if (routeId.trim() === '')
|
|
1032
|
+
throw new Error('model route id must be a non-empty string');
|
|
1033
|
+
const path = configPathFor(root);
|
|
1034
|
+
ensureDir(root);
|
|
1035
|
+
withConfigPathLock(path, (lockedPath) => {
|
|
1036
|
+
const raw = readJsonIfExists(lockedPath) ?? {};
|
|
1037
|
+
const routes = copyRouteRecord((raw.modelRoutes ?? {}));
|
|
1038
|
+
if (route === undefined)
|
|
1039
|
+
delete routes[routeId];
|
|
1040
|
+
else {
|
|
1041
|
+
const current = routes[routeId];
|
|
1042
|
+
setRouteRecord(routes, routeId, {
|
|
1043
|
+
...(current !== null && typeof current === 'object' && !Array.isArray(current) ? current : {}),
|
|
1044
|
+
...route,
|
|
1045
|
+
models: { ...route.models },
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
const next = { ...raw };
|
|
1049
|
+
if (Object.keys(routes).length === 0)
|
|
1050
|
+
delete next.modelRoutes;
|
|
1051
|
+
else
|
|
1052
|
+
next.modelRoutes = routes;
|
|
1053
|
+
atomicWriteJson(lockedPath, next);
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
933
1056
|
/** Mutate one exact config root. Setup uses this for profile and explicit
|
|
934
1057
|
* project targets that cannot be represented by the user/project Scope enum. */
|
|
935
1058
|
export function updateConfigAtRoot(root, mutate) {
|
|
@@ -3,13 +3,13 @@ import type { AttachBindingId, AttachMenuBindingId } from './attach-control.js';
|
|
|
3
3
|
type TmuxMenuAction = 'open' | 'attach' | 'show' | 'view-settings' | 'fold-tools' | 'metadata' | 'promote' | 'demote-mode' | 'resume' | 'fork' | 'demote' | 'make-resident' | 'detach' | 'close-subtree' | 'background-bash' | 'file-review' | 'pin' | 'reload-viewer' | 'recap' | 'context' | 'context-admin' | 'providers' | 'plugins' | 'graph' | 'focus-manager' | 'copy-id' | 'issues';
|
|
4
4
|
type TmuxMenuEntryAction = Exclude<TmuxMenuAction, 'open' | 'attach'> | `issue.${'idea' | 'bug'}`;
|
|
5
5
|
type GraphAction = 'down' | 'up' | 'first' | 'last' | 'collapse-or-manager' | 'expand-or-child' | 'focus' | 'revive' | 'focus-manager' | 'close-subtree' | 'dismiss';
|
|
6
|
-
type BrowseAction = 'quit' | 'down' | 'up' | 'first' | 'last' | 'expand-or-child' | 'collapse-or-parent' | 'toggle-fold' | 'tab-next' | 'tab-previous' | `tab.${1 | 2 | 3 | 4 | 5}` | 'close-subtree' | 'confirm-close' | 'sort' | 'scope' | 'profile' | 'residents' | 'preview' | 'search' | 'revive-now' | 'focus' | 'focus-background' | 'yank-id';
|
|
6
|
+
type BrowseAction = 'quit' | 'down' | 'up' | 'first' | 'last' | 'expand-or-child' | 'collapse-or-parent' | 'toggle-fold' | 'tab-next' | 'tab-previous' | `tab.${1 | 2 | 3 | 4 | 5}` | 'close-subtree' | 'confirm-close' | 'sort' | 'scope' | 'profile' | 'residents' | 'preview' | 'pin' | 'search' | 'revive-now' | 'focus' | 'focus-background' | 'yank-id';
|
|
7
7
|
type ProfileMenuAction = 'up' | 'down' | 'select' | 'accept-default' | 'abort' | `slot.${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}` | 'create' | 'search-existing' | 'use-root' | 'set-default' | 'add-directory' | 'unpin' | 'manage';
|
|
8
8
|
type SetupAction = 'cancel' | 'tab-next' | 'tab-previous' | 'open' | 'list-down' | 'list-up' | 'toggle' | 'details' | 'advance' | 'cell-previous' | 'cell-next';
|
|
9
9
|
type SetupKeybindingAction = 'edit' | 'commit-edit' | 'cancel-edit' | 'unbind' | 'reset-row' | 'reset-all' | 'save' | 'discard' | 'page-up' | 'page-down' | 'filter' | 'filter-delete' | 'filter-clear';
|
|
10
10
|
type SetupModelsAction = 'open' | 'commit' | 'cancel' | 'scope-next';
|
|
11
11
|
export type BindingId = `crtr.tmux.menu.${TmuxMenuAction}` | `crtr.tmux.menu.issue.${'idea' | 'bug'}` | AttachMenuBindingId | `crtr.tmux.menu-global.${TmuxMenuEntryAction}` | `crtr.tmux.node.${'next' | 'previous'}` | 'crtr.tmux.inbox.toggle' | `crtr.attach.scroll.${'up' | 'down' | 'page-up' | 'page-down' | 'live'}` | 'crtr.attach.mouse.toggle' | 'crtr.attach.copy.message' | 'crtr.attach.help.toggle' | `crtr.attach.search.${'next' | 'previous' | 'edit' | 'close'}` | AttachBindingId | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.inspector.${'force-quit' | 'down' | 'up' | 'section-next' | 'section-prev' | 'page-down' | 'page-up' | 'toggle-detail' | 'cancel-item' | 'open-log' | 'confirm' | 'dismiss' | 'retarget' | 'refresh' | 'quit'}` | `crtr.browse.${BrowseAction}` | `crtr.browse.search.${'cancel' | 'commit' | 'delete' | 'quit'}` | `crtr.profile.menu.${ProfileMenuAction}` | `crtr.profile.search.${'up' | 'down' | 'pick' | 'back' | 'delete' | 'abort'}` | `crtr.setup.${SetupAction}` | `crtr.setup.confirm.${'accept' | 'back'}` | `crtr.setup.keybindings.${SetupKeybindingAction}` | `crtr.setup.models.${SetupModelsAction}`;
|
|
12
|
-
export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.attach">, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.tmux.node.next"> | BindingDefinition<"crtr.tmux.node.previous"> | BindingDefinition<"crtr.tmux.inbox.toggle"> | BindingDefinition<"crtr.attach.scroll.up"> | BindingDefinition<"crtr.attach.scroll.down"> | BindingDefinition<"crtr.attach.scroll.page-up"> | BindingDefinition<"crtr.attach.scroll.page-down"> | BindingDefinition<"crtr.attach.scroll.live"> | BindingDefinition<"crtr.attach.mouse.toggle"> | BindingDefinition<"crtr.attach.copy.message"> | BindingDefinition<"crtr.attach.search.next"> | BindingDefinition<"crtr.attach.search.previous"> | BindingDefinition<"crtr.attach.search.edit"> | BindingDefinition<"crtr.attach.search.close"> | BindingDefinition<"crtr.graph.down"> | BindingDefinition<"crtr.graph.up"> | BindingDefinition<"crtr.graph.first"> | BindingDefinition<"crtr.graph.last"> | BindingDefinition<"crtr.graph.collapse-or-manager"> | BindingDefinition<"crtr.graph.expand-or-child"> | BindingDefinition<"crtr.graph.focus"> | BindingDefinition<"crtr.graph.revive"> | BindingDefinition<"crtr.graph.focus-manager"> | BindingDefinition<"crtr.graph.close-subtree"> | BindingDefinition<"crtr.graph.dismiss"> | BindingDefinition<"crtr.graph.confirm.accept"> | BindingDefinition<"crtr.graph.confirm.cancel"> | BindingDefinition<"crtr.inspector.force-quit"> | BindingDefinition<"crtr.inspector.down"> | BindingDefinition<"crtr.inspector.up"> | BindingDefinition<"crtr.inspector.section-next"> | BindingDefinition<"crtr.inspector.section-prev"> | BindingDefinition<"crtr.inspector.page-down"> | BindingDefinition<"crtr.inspector.page-up"> | BindingDefinition<"crtr.inspector.toggle-detail"> | BindingDefinition<"crtr.inspector.cancel-item"> | BindingDefinition<"crtr.inspector.open-log"> | BindingDefinition<"crtr.inspector.confirm"> | BindingDefinition<"crtr.inspector.dismiss"> | BindingDefinition<"crtr.inspector.retarget"> | BindingDefinition<"crtr.inspector.refresh"> | BindingDefinition<"crtr.inspector.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.yank-id"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.menu.manage">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">, BindingDefinition<"crtr.setup.cell-previous">, BindingDefinition<"crtr.setup.cell-next">, BindingDefinition<"crtr.setup.models.open">, BindingDefinition<"crtr.setup.models.scope-next">, BindingDefinition<"crtr.setup.models.commit">, BindingDefinition<"crtr.setup.models.cancel">];
|
|
12
|
+
export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.attach">, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.tmux.node.next"> | BindingDefinition<"crtr.tmux.node.previous"> | BindingDefinition<"crtr.tmux.inbox.toggle"> | BindingDefinition<"crtr.attach.scroll.up"> | BindingDefinition<"crtr.attach.scroll.down"> | BindingDefinition<"crtr.attach.scroll.page-up"> | BindingDefinition<"crtr.attach.scroll.page-down"> | BindingDefinition<"crtr.attach.scroll.live"> | BindingDefinition<"crtr.attach.mouse.toggle"> | BindingDefinition<"crtr.attach.copy.message"> | BindingDefinition<"crtr.attach.search.next"> | BindingDefinition<"crtr.attach.search.previous"> | BindingDefinition<"crtr.attach.search.edit"> | BindingDefinition<"crtr.attach.search.close"> | BindingDefinition<"crtr.graph.down"> | BindingDefinition<"crtr.graph.up"> | BindingDefinition<"crtr.graph.first"> | BindingDefinition<"crtr.graph.last"> | BindingDefinition<"crtr.graph.collapse-or-manager"> | BindingDefinition<"crtr.graph.expand-or-child"> | BindingDefinition<"crtr.graph.focus"> | BindingDefinition<"crtr.graph.revive"> | BindingDefinition<"crtr.graph.focus-manager"> | BindingDefinition<"crtr.graph.close-subtree"> | BindingDefinition<"crtr.graph.dismiss"> | BindingDefinition<"crtr.graph.confirm.accept"> | BindingDefinition<"crtr.graph.confirm.cancel"> | BindingDefinition<"crtr.inspector.force-quit"> | BindingDefinition<"crtr.inspector.down"> | BindingDefinition<"crtr.inspector.up"> | BindingDefinition<"crtr.inspector.section-next"> | BindingDefinition<"crtr.inspector.section-prev"> | BindingDefinition<"crtr.inspector.page-down"> | BindingDefinition<"crtr.inspector.page-up"> | BindingDefinition<"crtr.inspector.toggle-detail"> | BindingDefinition<"crtr.inspector.cancel-item"> | BindingDefinition<"crtr.inspector.open-log"> | BindingDefinition<"crtr.inspector.confirm"> | BindingDefinition<"crtr.inspector.dismiss"> | BindingDefinition<"crtr.inspector.retarget"> | BindingDefinition<"crtr.inspector.refresh"> | BindingDefinition<"crtr.inspector.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.pin"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.yank-id"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.menu.manage">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">, BindingDefinition<"crtr.setup.cell-previous">, BindingDefinition<"crtr.setup.cell-next">, BindingDefinition<"crtr.setup.models.open">, BindingDefinition<"crtr.setup.models.scope-next">, BindingDefinition<"crtr.setup.models.commit">, BindingDefinition<"crtr.setup.models.cancel">];
|
|
13
13
|
export declare const BINDING_IDS: ReadonlySet<string>;
|
|
14
14
|
/** True for an action whose direct gesture is handled inside an attach viewer.
|
|
15
15
|
* Tmux menu selectors and root shortcuts are chrome, not pane input. */
|
|
@@ -72,7 +72,7 @@ export const BINDING_CATALOG = Object.freeze([
|
|
|
72
72
|
b('crtr.tmux.node.previous', 'Global tmux', 'Previous node', 'Focus the previous canvas node.', ['alt+['], 'host-global', ['tmux.root']),
|
|
73
73
|
b('crtr.tmux.inbox.toggle', 'Global tmux', 'Toggle inbox', 'Open or close the human inbox in the focused viewer.', ['alt+i'], 'host-global', ['tmux.root']),
|
|
74
74
|
...attachEntry('crtr.tmux.menu.attach.detach', 'crtr.attach.detach', 'Detach', 'Detach from the node.', ['d'], ['ctrl+d']),
|
|
75
|
-
...attachEntry('crtr.tmux.menu.attach.clear-or-detach', 'crtr.attach.clear-or-detach', 'Clear input (
|
|
75
|
+
...attachEntry('crtr.tmux.menu.attach.clear-or-detach', 'crtr.attach.clear-or-detach', 'Clear input (cancel agent on repeat)', 'Clear the input; press again within a second to cancel the running agent turn. Detach lives on its own binding.', ['c'], ['ctrl+c']),
|
|
76
76
|
...attachEntry('crtr.tmux.menu.attach.graph.toggle', 'crtr.attach.graph.toggle', 'Toggle graph', 'Open or close the graph overlay.', ['g'], ['alt+g']),
|
|
77
77
|
...attachEntry('crtr.tmux.menu.attach.help.toggle', 'crtr.attach.help.toggle', 'Help', 'Open the attach help overlay.', ['?'], ['alt+shift+k']),
|
|
78
78
|
...attachEntry('crtr.tmux.menu.attach.inbox.toggle', 'crtr.attach.inbox.toggle', 'Toggle inbox', 'Open or close the human inbox surface in this viewer.', ['i'], []),
|
|
@@ -147,7 +147,8 @@ function browseBindings() {
|
|
|
147
147
|
b('crtr.browse.scope', 'Browse and pickers', 'Change scope', 'Toggle the browse cwd scope.', ['c'], 'terminal', [nav]),
|
|
148
148
|
b('crtr.browse.profile', 'Browse and pickers', 'Toggle profile', 'Toggle the browse profile scope.', ['shift+p'], 'terminal', [nav]),
|
|
149
149
|
b('crtr.browse.residents', 'Browse and pickers', 'Toggle residents', 'Toggle resident display.', ['r'], 'terminal', [nav]),
|
|
150
|
-
b('crtr.browse.preview', 'Browse and pickers', 'Toggle preview', 'Toggle the preview panel.', ['
|
|
150
|
+
b('crtr.browse.preview', 'Browse and pickers', 'Toggle preview', 'Toggle the preview panel.', ['v'], 'terminal', [nav]),
|
|
151
|
+
b('crtr.browse.pin', 'Browse and pickers', 'Pin node', 'Star or unstar the selected node.', ['p'], 'terminal', [nav]),
|
|
151
152
|
b('crtr.browse.search', 'Browse and pickers', 'Search', 'Open browse search.', ['/'], 'terminal', [nav]),
|
|
152
153
|
b('crtr.browse.revive-now', 'Browse and pickers', 'Revive now', 'Revive the selected node now.', ['shift+k'], 'terminal', [nav]),
|
|
153
154
|
b('crtr.browse.focus', 'Browse and pickers', 'Focus node', 'Focus the selected node.', ['enter', 'o'], 'terminal', [nav]),
|
|
@@ -190,13 +191,13 @@ function setupBindings() {
|
|
|
190
191
|
b('crtr.setup.tab-next', 'Setup', 'Next tab', 'Select the next setup tab.', ['tab', 'right'], 'terminal', SETUP_TAB_CONTEXTS),
|
|
191
192
|
b('crtr.setup.tab-previous', 'Setup', 'Previous tab', 'Select the previous setup tab.', ['shift+tab', 'left'], 'terminal', SETUP_TAB_CONTEXTS),
|
|
192
193
|
b('crtr.setup.open', 'Setup', 'Open', 'Open the selected setup item.', ['enter'], 'terminal', ['setup.login']),
|
|
193
|
-
b('crtr.setup.list-down', 'Setup', 'Move down', 'Move to the next setup item.', ['j', 'down'], 'terminal', ['setup.list', 'setup.models', 'setup.models.dimension-picker']),
|
|
194
|
-
b('crtr.setup.list-up', 'Setup', 'Move up', 'Move to the previous setup item.', ['k', 'up'], 'terminal', ['setup.list', 'setup.models', 'setup.models.dimension-picker']),
|
|
194
|
+
b('crtr.setup.list-down', 'Setup', 'Move down', 'Move to the next setup item.', ['j', 'down'], 'terminal', ['setup.list', 'setup.models', 'setup.models.dimension-picker', 'setup.models.route-form']),
|
|
195
|
+
b('crtr.setup.list-up', 'Setup', 'Move up', 'Move to the previous setup item.', ['k', 'up'], 'terminal', ['setup.list', 'setup.models', 'setup.models.dimension-picker', 'setup.models.route-form']),
|
|
195
196
|
b('crtr.setup.toggle', 'Setup', 'Toggle', 'Toggle the selected setup item.', ['space'], 'terminal', ['setup.list', 'setup.keybindings']),
|
|
196
197
|
b('crtr.setup.details', 'Setup', 'Show details', 'Show details for the selected item.', ['?'], 'terminal', ['setup.list']),
|
|
197
198
|
b('crtr.setup.advance', 'Setup', 'Advance', 'Advance the setup flow.', ['enter'], 'terminal', ['setup.list']),
|
|
198
|
-
b('crtr.setup.confirm.accept', 'Setup', 'Confirm', 'Accept the setup confirmation.', ['y', 'enter'], 'terminal', ['setup.confirm']),
|
|
199
|
-
b('crtr.setup.confirm.back', 'Setup', 'Go back', 'Cancel the setup confirmation.', ['n', 'escape', 'q'], 'terminal', ['setup.confirm'], 'dismiss-modal'),
|
|
199
|
+
b('crtr.setup.confirm.accept', 'Setup', 'Confirm', 'Accept the setup confirmation.', ['y', 'enter'], 'terminal', ['setup.confirm', 'setup.models.confirm']),
|
|
200
|
+
b('crtr.setup.confirm.back', 'Setup', 'Go back', 'Cancel the setup confirmation.', ['n', 'escape', 'q'], 'terminal', ['setup.confirm', 'setup.models.confirm'], 'dismiss-modal'),
|
|
200
201
|
b('crtr.setup.keybindings.edit', 'Setup', 'Edit binding', 'Edit the selected binding.', ['enter'], 'terminal', ['setup.keybindings']),
|
|
201
202
|
b('crtr.setup.keybindings.commit-edit', 'Setup', 'Commit edit', 'Commit the active binding edit.', ['enter'], 'terminal', ['setup.keybindings.edit']),
|
|
202
203
|
b('crtr.setup.keybindings.cancel-edit', 'Setup', 'Cancel edit', 'Cancel the active binding edit.', ['escape'], 'terminal', ['setup.keybindings.edit'], 'dismiss-modal'),
|
|
@@ -212,10 +213,10 @@ function setupBindings() {
|
|
|
212
213
|
b('crtr.setup.keybindings.filter-clear', 'Setup', 'Clear filter', 'Clear the keybinding filter.', ['escape'], 'terminal', ['setup.keybindings.filter'], 'dismiss-modal'),
|
|
213
214
|
b('crtr.setup.cell-previous', 'Setup', 'Previous field', 'Select the previous field or setup table cell.', ['h'], 'terminal', ['setup.models', 'setup.keybindings']),
|
|
214
215
|
b('crtr.setup.cell-next', 'Setup', 'Next field', 'Select the next field or setup table cell.', ['l'], 'terminal', ['setup.models', 'setup.keybindings']),
|
|
215
|
-
b('crtr.setup.models.open', 'Setup', 'Edit model', 'Edit the selected model cell.', ['enter'], 'terminal', ['setup.models']),
|
|
216
|
+
b('crtr.setup.models.open', 'Setup', 'Edit model', 'Edit the selected model cell or route field.', ['enter'], 'terminal', ['setup.models', 'setup.models.route-form']),
|
|
216
217
|
b('crtr.setup.models.scope-next', 'Setup', 'Next model config scope', 'Edit the next applicable user, profile, or project config.', ['s'], 'terminal', ['setup.models']),
|
|
217
218
|
b('crtr.setup.models.commit', 'Setup', 'Commit pick', 'Commit the provider, strength, model, or thinking-level pick.', ['enter'], 'terminal', ['setup.models.picker', 'setup.models.dimension-picker']),
|
|
218
|
-
b('crtr.setup.models.cancel', 'Setup', 'Cancel pick', 'Cancel the
|
|
219
|
+
b('crtr.setup.models.cancel', 'Setup', 'Cancel pick', 'Cancel the model picker or route form.', ['escape'], 'terminal', ['setup.models.picker', 'setup.models.dimension-picker', 'setup.models.route-form'], 'dismiss-modal'),
|
|
219
220
|
];
|
|
220
221
|
}
|
|
221
222
|
export const BINDING_IDS = new Set(BINDING_CATALOG.map((definition) => definition.id));
|
|
@@ -246,4 +247,6 @@ export const SAFETY_REQUIREMENTS = Object.freeze([
|
|
|
246
247
|
{ context: 'setup.models', capability: 'leave-screen' },
|
|
247
248
|
{ context: 'setup.models.picker', capability: 'dismiss-modal' },
|
|
248
249
|
{ context: 'setup.models.dimension-picker', capability: 'dismiss-modal' },
|
|
250
|
+
{ context: 'setup.models.route-form', capability: 'dismiss-modal' },
|
|
251
|
+
{ context: 'setup.models.confirm', capability: 'dismiss-modal' },
|
|
249
252
|
]);
|
|
@@ -19,6 +19,8 @@ function parseSpec(spec) {
|
|
|
19
19
|
function synthesizeRoutes(ladders) {
|
|
20
20
|
const route = (family) => {
|
|
21
21
|
const providerId = ladders[family].strong.slice(0, ladders[family].strong.indexOf('/'));
|
|
22
|
+
// `credentialSource` orders routes. Auth and subscription-pool lookup are
|
|
23
|
+
// provider-wide, keyed by providerId, so this does not select credentials.
|
|
22
24
|
return { family, credentialSource: 'default', providerId, models: ladders[family] };
|
|
23
25
|
};
|
|
24
26
|
return { anthropic: route('anthropic'), openai: route('openai') };
|
|
@@ -32,7 +34,10 @@ function strengths(requested, configured) {
|
|
|
32
34
|
/** Expand config-derived route order only. Availability never affects rank. */
|
|
33
35
|
export function expandModelCandidates(request, cwd, profileId) {
|
|
34
36
|
const cfg = readMergedLaunchConfig(cwd, profileId ?? null);
|
|
35
|
-
|
|
37
|
+
// Complete routes are the effective contract. Sparse entries written by an
|
|
38
|
+
// older crouter release remain readable for one cutover release only.
|
|
39
|
+
const declaredRoutes = { ...(cfg.modelRoutes ?? {}), ...(cfg.legacyModelRoutes ?? {}) };
|
|
40
|
+
const routes = Object.keys(declaredRoutes).length > 0 ? declaredRoutes : synthesizeRoutes(cfg.modelLadders);
|
|
36
41
|
const routeEntries = Object.entries(routes);
|
|
37
42
|
const fallback = cfg.modelRouting?.strengthFallback ?? [readRotationConfig().defaultFallbackStrength ?? 'strong'];
|
|
38
43
|
const declarationFamilies = routeEntries.map(([, route]) => route.family).filter((value, index, all) => all.indexOf(value) === index);
|
|
@@ -79,7 +84,9 @@ export function expandModelCandidates(request, cwd, profileId) {
|
|
|
79
84
|
return;
|
|
80
85
|
if (concrete && parsed.modelId !== concrete.modelId)
|
|
81
86
|
return;
|
|
82
|
-
|
|
87
|
+
// Route identity is meaningful: two panel-managed routes can intentionally
|
|
88
|
+
// share a provider/model while carrying distinct credential-source order.
|
|
89
|
+
const key = JSON.stringify([routeId, parsed.providerId, parsed.modelId, parsed.thinkingLevel]);
|
|
83
90
|
if (seen.has(key))
|
|
84
91
|
return;
|
|
85
92
|
seen.add(key);
|
|
@@ -91,7 +98,7 @@ export function expandModelCandidates(request, cwd, profileId) {
|
|
|
91
98
|
if (concrete) {
|
|
92
99
|
const [routeId, route] = concreteRoute ?? [
|
|
93
100
|
'concrete',
|
|
94
|
-
{ family: requestedFamily, credentialSource: 'default'
|
|
101
|
+
{ family: requestedFamily, credentialSource: 'default' },
|
|
95
102
|
];
|
|
96
103
|
append(routeId, route, requestedStrength, request.spec);
|
|
97
104
|
}
|
|
@@ -182,7 +189,7 @@ export function modelRequestFromConfig(model, modelIntent, exact) {
|
|
|
182
189
|
if (exact)
|
|
183
190
|
return { kind: 'exact', spec: model };
|
|
184
191
|
if (modelIntent) {
|
|
185
|
-
const slash = modelIntent.
|
|
192
|
+
const slash = modelIntent.lastIndexOf('/');
|
|
186
193
|
const strength = slash > 0 ? modelIntent.slice(slash + 1) : '';
|
|
187
194
|
if (STRENGTHS.includes(strength))
|
|
188
195
|
return { kind: 'logical', family: modelIntent.slice(0, slash), strength: strength, spec: model };
|
|
@@ -3,6 +3,7 @@ import { envModelIntent } from '../../../shared/env.js';
|
|
|
3
3
|
import { emitEvent } from '../../events/emit.js';
|
|
4
4
|
import { clearFault, readFault } from '../fault.js';
|
|
5
5
|
import { isUnknownModel } from '../model-selection.js';
|
|
6
|
+
import { announceMessageOrigin } from '../stamp/channel.js';
|
|
6
7
|
export function createAuthReloadGate(deps) {
|
|
7
8
|
const { nodeId, cfg, replies, rebind, session, services, broadcastModelChanged, formatModelSpec, resolveLaunchModel, driveKickoff, turnAdmission, } = deps;
|
|
8
9
|
let reloadInFlight = null;
|
|
@@ -118,7 +119,9 @@ export function createAuthReloadGate(deps) {
|
|
|
118
119
|
return Promise.resolve();
|
|
119
120
|
}
|
|
120
121
|
clearFault(nodeId, { link: 'pi→provider' });
|
|
121
|
-
|
|
122
|
+
const card = formatCard('recovery', {}, AUTH_FAULT_RECOVERY_BODY);
|
|
123
|
+
announceMessageOrigin(card, { type: 'runtime', kind: 'recovery' });
|
|
124
|
+
return faultSession.prompt(card, { preflightResult: release });
|
|
122
125
|
});
|
|
123
126
|
}
|
|
124
127
|
catch (err) {
|
|
@@ -12,6 +12,7 @@ import { markFatalOnExhaust, nextFaultRetry, policyFor } from '../fault-recovery
|
|
|
12
12
|
import { parseModelSpec } from '../model-selection.js';
|
|
13
13
|
import { endedByAbort } from '../session-cycles.js';
|
|
14
14
|
import { resolveProviderCandidates } from '../../subscription-state.js';
|
|
15
|
+
import { announceMessageOrigin } from '../stamp/channel.js';
|
|
15
16
|
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
16
17
|
/** Owns provider fallback and daemon-driven fault retry state for one broker session. */
|
|
17
18
|
export class FaultRetry {
|
|
@@ -266,7 +267,9 @@ export class FaultRetry {
|
|
|
266
267
|
return;
|
|
267
268
|
if (unconfigured)
|
|
268
269
|
clearFault(this.deps.nodeId, { link: 'pi→provider' });
|
|
269
|
-
|
|
270
|
+
const fallbackCard = formatCard('recovery', {}, formatModelFallbackRecovery(currentSpec, resolved, unconfigured ? 'credential' : 'not-found'));
|
|
271
|
+
announceMessageOrigin(fallbackCard, { type: 'runtime', kind: 'recovery' });
|
|
272
|
+
await this.deps.turnAdmission.admit((release) => operationIdContext.fresh(() => candidateSession.prompt(fallbackCard, { preflightResult: release })));
|
|
270
273
|
})().catch((err) => {
|
|
271
274
|
emitEvent({ level: 'debug', event: 'broker.runtime.diagnostic', fields: { message: `${unconfigured ? 'unconfigured-provider' : 'not_found'} fallback failed: ${err instanceof Error ? err.message : String(err)}` } });
|
|
272
275
|
}).finally(() => {
|
|
@@ -405,7 +408,9 @@ export class FaultRetry {
|
|
|
405
408
|
return Promise.resolve();
|
|
406
409
|
}
|
|
407
410
|
recordFault(this.deps.nodeId, plan.fault);
|
|
408
|
-
|
|
411
|
+
const retryCard = formatCard('recovery', {}, fault.kind === 'connection' ? CONNECTION_FAULT_RECOVERY_BODY : PROVIDER_FAULT_RECOVERY_BODY);
|
|
412
|
+
announceMessageOrigin(retryCard, { type: 'runtime', kind: 'recovery' });
|
|
413
|
+
return operationIdContext.fresh(() => candidateSession.prompt(retryCard, { preflightResult: release }));
|
|
409
414
|
});
|
|
410
415
|
}
|
|
411
416
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SITUATIONAL_CONTEXT_CUSTOM_TYPE } from '../../../shared/generated-context.js';
|
|
2
2
|
import { emitEvent } from '../../events/emit.js';
|
|
3
3
|
import { normalizeSnapshotTail } from '../broker-protocol.js';
|
|
4
|
+
import { announceMessageOrigin } from '../stamp/channel.js';
|
|
4
5
|
export function createClientHandlers(deps) {
|
|
5
6
|
const { registry, replies, session, driveEngine, turnAcceptance, pendingDialogs, sendWelcome, replayExtraPendingDialogsTo, disposeAndExit, } = deps;
|
|
6
7
|
const { notWritable, ackTo } = replies;
|
|
@@ -82,6 +83,9 @@ export function createClientHandlers(deps) {
|
|
|
82
83
|
};
|
|
83
84
|
if (via === 'prompt')
|
|
84
85
|
turnAcceptance.waitFor(ackAccepted);
|
|
86
|
+
// The daemon knows who authored this body; the stamp writer lives here.
|
|
87
|
+
if (frame.origin !== undefined)
|
|
88
|
+
announceMessageOrigin(frame.text, frame.origin);
|
|
85
89
|
const outcome = driveEngine(client, {
|
|
86
90
|
type: 'prompt',
|
|
87
91
|
text: frame.text,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createFrameReplies } from './frame-replies.js';
|
|
2
2
|
import { createEngineDriver } from './engine-drive.js';
|
|
3
|
+
import { announceMessageOrigin } from '../stamp/channel.js';
|
|
3
4
|
import { createClientHandlers } from './frame-client.js';
|
|
4
5
|
import { createMemoryRefGuidance } from './frame-memory-refs.js';
|
|
5
6
|
import { createAuthReloadGate } from './auth-reload.js';
|
|
@@ -51,6 +52,12 @@ export function createFrameDispatchContext(deps) {
|
|
|
51
52
|
const handleDriveFrame = (client, frame) => {
|
|
52
53
|
if (notWritable(client, 'drive the engine'))
|
|
53
54
|
return;
|
|
55
|
+
// These frames are the viewer's input vocabulary — a person typed them.
|
|
56
|
+
// (crtrd's `deliver` never lands here: frame-client synthesizes its own
|
|
57
|
+
// prompt frame and calls driveEngine directly, announcing its own origin.)
|
|
58
|
+
if (frame.type === 'prompt' || frame.type === 'steer' || frame.type === 'follow_up') {
|
|
59
|
+
announceMessageOrigin(frame.text, { type: 'human', channel: { type: 'viewer' } });
|
|
60
|
+
}
|
|
54
61
|
driveEngine(client, frame);
|
|
55
62
|
};
|
|
56
63
|
const clientHandlers = createClientHandlers({
|
|
@@ -27,7 +27,9 @@ export declare function readBrokerCanceledEntryIds(nodeId: string): Set<BrokerIn
|
|
|
27
27
|
export declare function reportRefNodeId(ref: string | undefined): string | undefined;
|
|
28
28
|
/** One-shot runtime cards an entry carries (rendered envelopes written by the
|
|
29
29
|
* message API). They ride the delivery their own entry belongs to, so a note
|
|
30
|
-
* can never land ahead of an unrelated person's message
|
|
30
|
+
* can never land ahead of an unrelated person's message — except in the one
|
|
31
|
+
* deliberate fold below, where deferred entries join a person's delivery as
|
|
32
|
+
* the context their turn reads. */
|
|
31
33
|
export declare function entryCards(entry: BrokerInboxEntry): readonly string[];
|
|
32
34
|
/** One deliverable user message: either a person's verbatim words or the whole
|
|
33
35
|
* `<runtime kind="inbox">` envelope. Never both — a reader parses an envelope
|
|
@@ -40,5 +42,13 @@ export interface InboxDelivery {
|
|
|
40
42
|
entries: readonly BrokerInboxEntry[];
|
|
41
43
|
}
|
|
42
44
|
/** Split unread entries into ordered deliveries: each human entry verbatim, and
|
|
43
|
-
*
|
|
45
|
+
* each CONTIGUOUS run of node/system entries as one card in the physical slot
|
|
46
|
+
* that run occupies. A deferred-only card is folded into the person's delivery
|
|
47
|
+
* waiting immediately behind it rather than being deliverable on its own.
|
|
48
|
+
*
|
|
49
|
+
* The runs are what keep the cursor honest. Exactly one delivery goes out per
|
|
50
|
+
* cycle and the rest requeue, while a sent delivery commits the cursor across
|
|
51
|
+
* its own entries — so a delivery that spanned a person's message would commit
|
|
52
|
+
* past words this cycle never sent. A run can never span one, and node mail
|
|
53
|
+
* that arrived after a person's message now waits behind it. */
|
|
44
54
|
export declare function coalesceBrokerInbox(entries: readonly BrokerInboxEntry[], reportNodes: ReadonlyMap<string, BrokerReportNodeDTO>): readonly InboxDelivery[];
|