@north-light/crouter 0.3.262 → 0.3.263
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/dto/health.d.ts +2 -0
- package/dist/builtin-memory/internal/memory-loading.md +3 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +4 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +5 -3
- 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/viewer.js +616 -616
- 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/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 +558 -75
- package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +1 -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__/seam/broker-provider-retry.test.js +2 -2
- package/dist/core/canvas/render-source.d.ts +1 -1
- package/dist/core/canvas/render-source.js +3 -2
- package/dist/core/config.d.ts +19 -2
- package/dist/core/config.js +162 -47
- package/dist/core/keybindings/catalog.js +9 -7
- package/dist/core/model-routes.js +10 -3
- 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/daemon/api/__tests__/seam/api-server.test.js +34 -0
- package/dist/daemon/api/handlers/canvas.js +18 -2
- package/dist/daemon/api/handlers/health.js +17 -12
- package/dist/pi-extensions/__tests__/pre-command-gate.test.js +2 -2
- package/dist/types.d.ts +4 -2
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
package/dist/core/config.js
CHANGED
|
@@ -167,66 +167,140 @@ function mergeModelLadders(raw, base = defaultModelLaddersConfig()) {
|
|
|
167
167
|
function validStrength(value) {
|
|
168
168
|
return value === 'ultra' || value === 'strong' || value === 'medium' || value === 'light';
|
|
169
169
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
170
|
+
const MODEL_STRENGTHS = ['ultra', 'strong', 'medium', 'light'];
|
|
171
|
+
function routeRecord() {
|
|
172
|
+
return Object.create(null);
|
|
173
|
+
}
|
|
174
|
+
function setRouteRecord(record, routeId, value) {
|
|
175
|
+
Object.defineProperty(record, routeId, { value, writable: true, enumerable: true, configurable: true });
|
|
176
|
+
}
|
|
177
|
+
function copyRouteRecord(record) {
|
|
178
|
+
const copy = routeRecord();
|
|
179
|
+
for (const [routeId, value] of Object.entries(record ?? {}))
|
|
180
|
+
setRouteRecord(copy, routeId, value);
|
|
181
|
+
return copy;
|
|
182
|
+
}
|
|
183
|
+
function routeInvalid(routeId, rule) {
|
|
184
|
+
emitEvent({ level: 'warn', event: 'config.model_routes.invalid', fields: { route_id: routeId, rule } });
|
|
185
|
+
}
|
|
186
|
+
/** Overlay raw route patches first so a higher scope may replace one cell of a
|
|
187
|
+
* complete lower route. Completeness belongs to the resulting effective route,
|
|
188
|
+
* never an individual raw scope patch. */
|
|
189
|
+
function mergeModelRoutePatches(raw, base = {}) {
|
|
190
|
+
const out = copyRouteRecord(base);
|
|
174
191
|
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
175
192
|
return out;
|
|
176
193
|
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');
|
|
194
|
+
if (routeId.trim() === '') {
|
|
195
|
+
routeInvalid(routeId, 'route id must be a non-empty string');
|
|
181
196
|
continue;
|
|
182
197
|
}
|
|
183
|
-
|
|
184
|
-
|
|
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');
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
if (!route.models || typeof route.models !== 'object' || Array.isArray(route.models)) {
|
|
197
|
-
invalid('models must be an object');
|
|
198
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
199
|
+
routeInvalid(routeId, 'route must be an object');
|
|
198
200
|
continue;
|
|
199
201
|
}
|
|
200
|
-
const
|
|
202
|
+
const patch = value;
|
|
203
|
+
const current = out[routeId];
|
|
204
|
+
const route = { ...current };
|
|
201
205
|
let rule;
|
|
202
|
-
for (const [
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
rule = `models.${strength} must be a non-empty provider/id ref`;
|
|
206
|
+
for (const field of ['family', 'credentialSource', 'providerId']) {
|
|
207
|
+
const candidate = patch[field];
|
|
208
|
+
if (candidate === undefined)
|
|
209
|
+
continue;
|
|
210
|
+
if (typeof candidate !== 'string' || candidate.trim() === '') {
|
|
211
|
+
rule = `${field} must be a non-empty string`;
|
|
209
212
|
break;
|
|
210
213
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
214
|
+
route[field] = candidate;
|
|
215
|
+
}
|
|
216
|
+
if (rule === undefined && patch.models !== undefined) {
|
|
217
|
+
if (patch.models === null || typeof patch.models !== 'object' || Array.isArray(patch.models))
|
|
218
|
+
rule = 'models must be an object';
|
|
219
|
+
else {
|
|
220
|
+
const models = { ...current?.models };
|
|
221
|
+
for (const [strength, spec] of Object.entries(patch.models)) {
|
|
222
|
+
if (!validStrength(strength)) {
|
|
223
|
+
rule = `models.${strength} is not a model strength`;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
if (typeof spec !== 'string' || spec.trim() === '') {
|
|
227
|
+
rule = `models.${strength} must be a non-empty provider/id ref`;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
const slash = spec.indexOf('/');
|
|
231
|
+
if (slash <= 0 || slash === spec.length - 1) {
|
|
232
|
+
rule = `models.${strength} must be provider/id`;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
models[strength] = spec;
|
|
236
|
+
}
|
|
237
|
+
route.models = models;
|
|
215
238
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
239
|
+
}
|
|
240
|
+
if (rule === undefined && route.models !== undefined && route.providerId !== undefined) {
|
|
241
|
+
for (const [strength, spec] of Object.entries(route.models)) {
|
|
242
|
+
if (spec.slice(0, spec.indexOf('/')) !== route.providerId) {
|
|
243
|
+
rule = `models.${strength} provider prefix must equal providerId`;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
219
246
|
}
|
|
220
|
-
models[strength] = spec;
|
|
221
247
|
}
|
|
222
248
|
if (rule !== undefined) {
|
|
223
|
-
|
|
249
|
+
routeInvalid(routeId, rule);
|
|
224
250
|
continue;
|
|
225
251
|
}
|
|
226
|
-
out
|
|
252
|
+
setRouteRecord(out, routeId, route);
|
|
227
253
|
}
|
|
228
254
|
return out;
|
|
229
255
|
}
|
|
256
|
+
/** Resolve complete routes after every scope has overlaid its sparse raw patch.
|
|
257
|
+
* Sparse standalone routes are read for one compatibility release only: they
|
|
258
|
+
* retain their historical candidate behavior, emit a repair warning, and never
|
|
259
|
+
* gain invented cells. New writes cannot create them. */
|
|
260
|
+
function resolveModelRoutes(routes) {
|
|
261
|
+
const complete = routeRecord();
|
|
262
|
+
const legacy = routeRecord();
|
|
263
|
+
for (const [routeId, route] of Object.entries(routes)) {
|
|
264
|
+
if (route.family === undefined || route.credentialSource === undefined || route.providerId === undefined || route.models === undefined) {
|
|
265
|
+
routeInvalid(routeId, 'route must include family, credentialSource, providerId, and models');
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const missing = MODEL_STRENGTHS.filter((strength) => route.models[strength] === undefined);
|
|
269
|
+
if (missing.length > 0) {
|
|
270
|
+
routeInvalid(routeId, `incomplete route; missing strengths: ${missing.join(', ')}`);
|
|
271
|
+
setRouteRecord(legacy, routeId, { family: route.family, credentialSource: route.credentialSource, providerId: route.providerId, models: route.models });
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
setRouteRecord(complete, routeId, {
|
|
275
|
+
family: route.family,
|
|
276
|
+
credentialSource: route.credentialSource,
|
|
277
|
+
providerId: route.providerId,
|
|
278
|
+
models: route.models,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return { complete, legacy };
|
|
282
|
+
}
|
|
283
|
+
function assertCompleteModelRoute(routeId, route) {
|
|
284
|
+
if (routeId.trim() === '')
|
|
285
|
+
throw new Error('model route id must be a non-empty string');
|
|
286
|
+
for (const field of ['family', 'credentialSource', 'providerId']) {
|
|
287
|
+
if (typeof route[field] !== 'string' || route[field].trim() === '')
|
|
288
|
+
throw new Error(`model route "${routeId}" ${field} must be a non-empty string`);
|
|
289
|
+
}
|
|
290
|
+
const missing = MODEL_STRENGTHS.filter((strength) => route.models?.[strength] === undefined);
|
|
291
|
+
if (missing.length > 0)
|
|
292
|
+
throw new Error(`model route "${routeId}" is incomplete; missing strengths: ${missing.join(', ')}`);
|
|
293
|
+
for (const strength of MODEL_STRENGTHS) {
|
|
294
|
+
const spec = route.models[strength];
|
|
295
|
+
if (typeof spec !== 'string' || spec.trim() === '')
|
|
296
|
+
throw new Error(`model route "${routeId}" models.${strength} must be a non-empty provider/id ref`);
|
|
297
|
+
const slash = spec.indexOf('/');
|
|
298
|
+
if (slash <= 0 || slash === spec.length - 1)
|
|
299
|
+
throw new Error(`model route "${routeId}" models.${strength} must be provider/id`);
|
|
300
|
+
if (spec.slice(0, slash) !== route.providerId)
|
|
301
|
+
throw new Error(`model route "${routeId}" models.${strength} provider prefix must equal providerId`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
230
304
|
function mergeModelRouting(raw, base) {
|
|
231
305
|
if (raw === undefined)
|
|
232
306
|
return base;
|
|
@@ -679,14 +753,15 @@ function mergeConfig(partial) {
|
|
|
679
753
|
const keybindings = normalizeKeybindings(partial.keybindings);
|
|
680
754
|
const tmux_passthrough = normalizeTmuxPassthrough(partial.tmux_passthrough);
|
|
681
755
|
const modelLadders = mergeModelLadders(partial.modelLadders);
|
|
682
|
-
const
|
|
756
|
+
const routes = resolveModelRoutes(mergeModelRoutePatches(partial.modelRoutes));
|
|
757
|
+
const modelRoutes = routes.complete;
|
|
683
758
|
const modelRouting = mergeModelRouting(partial.modelRouting, undefined);
|
|
684
759
|
const kinds = mergeKinds(partial.kinds);
|
|
685
760
|
const remoteCanvas = mergeRemoteCanvas(partial.remoteCanvas);
|
|
686
761
|
const spawnEnv = mergeSpawnEnv(partial.spawnEnv, defaults.spawnEnv?.allow);
|
|
687
762
|
const bin = normalizeBin(partial.bin);
|
|
688
763
|
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 };
|
|
764
|
+
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 } : {}), ...(Object.keys(routes.legacy).length > 0 ? { legacyModelRoutes: routes.legacy } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
|
|
690
765
|
}
|
|
691
766
|
/** Raw (un-defaulted) partial config for one scope, or null if the scope has
|
|
692
767
|
* no root or no config.json. Used by `readMergedLaunchConfig` to layer
|
|
@@ -775,13 +850,13 @@ export function readMergedLaunchConfig(targetCwd = process.cwd(), targetProfileI
|
|
|
775
850
|
// (ladders/routes/spawnEnv) have no plugin channel.
|
|
776
851
|
let kinds = mergeKinds(userRaw?.kinds, layerPluginKinds('user', scopeRoot('user'), defaults.kinds));
|
|
777
852
|
let modelLadders = mergeModelLadders(userRaw?.modelLadders, defaults.modelLadders);
|
|
778
|
-
let
|
|
853
|
+
let modelRoutePatches = mergeModelRoutePatches(userRaw?.modelRoutes);
|
|
779
854
|
let modelRouting = mergeModelRouting(userRaw?.modelRouting, undefined);
|
|
780
855
|
let spawnEnv = mergeSpawnEnv(userRaw?.spawnEnv, defaults.spawnEnv?.allow);
|
|
781
856
|
if (profileRaw !== null) {
|
|
782
857
|
kinds = mergeKinds(profileRaw.kinds, kinds);
|
|
783
858
|
modelLadders = mergeModelLadders(profileRaw.modelLadders, modelLadders);
|
|
784
|
-
|
|
859
|
+
modelRoutePatches = mergeModelRoutePatches(profileRaw.modelRoutes, modelRoutePatches);
|
|
785
860
|
modelRouting = mergeModelRouting(profileRaw.modelRouting, modelRouting);
|
|
786
861
|
spawnEnv = mergeSpawnEnv(profileRaw.spawnEnv, spawnEnv.allow);
|
|
787
862
|
}
|
|
@@ -790,11 +865,19 @@ export function readMergedLaunchConfig(targetCwd = process.cwd(), targetProfileI
|
|
|
790
865
|
if (raw === null)
|
|
791
866
|
continue;
|
|
792
867
|
modelLadders = mergeModelLadders(raw.modelLadders, modelLadders);
|
|
793
|
-
|
|
868
|
+
modelRoutePatches = mergeModelRoutePatches(raw.modelRoutes, modelRoutePatches);
|
|
794
869
|
modelRouting = mergeModelRouting(raw.modelRouting, modelRouting);
|
|
795
870
|
spawnEnv = mergeSpawnEnv(raw.spawnEnv, spawnEnv.allow);
|
|
796
871
|
}
|
|
797
|
-
|
|
872
|
+
const routes = resolveModelRoutes(modelRoutePatches);
|
|
873
|
+
return {
|
|
874
|
+
kinds,
|
|
875
|
+
modelLadders,
|
|
876
|
+
...(Object.keys(routes.complete).length > 0 ? { modelRoutes: routes.complete } : {}),
|
|
877
|
+
...(Object.keys(routes.legacy).length > 0 ? { legacyModelRoutes: routes.legacy } : {}),
|
|
878
|
+
...(modelRouting !== undefined ? { modelRouting } : {}),
|
|
879
|
+
spawnEnv,
|
|
880
|
+
};
|
|
798
881
|
}
|
|
799
882
|
/** The effective `KindConfig` for one full kind string (top-level, e.g.
|
|
800
883
|
* `developer`, or sub-kind, e.g. `audit/security`), across
|
|
@@ -930,6 +1013,38 @@ export function persistModelLadders(root, patch) {
|
|
|
930
1013
|
atomicWriteJson(lockedPath, { ...raw, modelLadders: next });
|
|
931
1014
|
});
|
|
932
1015
|
}
|
|
1016
|
+
/** Persist one complete route at one exact config root. The route is the only
|
|
1017
|
+
* raw route entry touched; unrelated routes and local ladder defaults remain
|
|
1018
|
+
* absent. Pass `undefined` to remove this scope's entry, which resumes lower
|
|
1019
|
+
* scope inheritance rather than introducing a tombstone. */
|
|
1020
|
+
export function persistModelRoute(root, routeId, route) {
|
|
1021
|
+
if (route !== undefined)
|
|
1022
|
+
assertCompleteModelRoute(routeId, route);
|
|
1023
|
+
else if (routeId.trim() === '')
|
|
1024
|
+
throw new Error('model route id must be a non-empty string');
|
|
1025
|
+
const path = configPathFor(root);
|
|
1026
|
+
ensureDir(root);
|
|
1027
|
+
withConfigPathLock(path, (lockedPath) => {
|
|
1028
|
+
const raw = readJsonIfExists(lockedPath) ?? {};
|
|
1029
|
+
const routes = copyRouteRecord((raw.modelRoutes ?? {}));
|
|
1030
|
+
if (route === undefined)
|
|
1031
|
+
delete routes[routeId];
|
|
1032
|
+
else {
|
|
1033
|
+
const current = routes[routeId];
|
|
1034
|
+
setRouteRecord(routes, routeId, {
|
|
1035
|
+
...(current !== null && typeof current === 'object' && !Array.isArray(current) ? current : {}),
|
|
1036
|
+
...route,
|
|
1037
|
+
models: { ...route.models },
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
const next = { ...raw };
|
|
1041
|
+
if (Object.keys(routes).length === 0)
|
|
1042
|
+
delete next.modelRoutes;
|
|
1043
|
+
else
|
|
1044
|
+
next.modelRoutes = routes;
|
|
1045
|
+
atomicWriteJson(lockedPath, next);
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
933
1048
|
/** Mutate one exact config root. Setup uses this for profile and explicit
|
|
934
1049
|
* project targets that cannot be represented by the user/project Scope enum. */
|
|
935
1050
|
export function updateConfigAtRoot(root, mutate) {
|
|
@@ -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'], []),
|
|
@@ -190,13 +190,13 @@ function setupBindings() {
|
|
|
190
190
|
b('crtr.setup.tab-next', 'Setup', 'Next tab', 'Select the next setup tab.', ['tab', 'right'], 'terminal', SETUP_TAB_CONTEXTS),
|
|
191
191
|
b('crtr.setup.tab-previous', 'Setup', 'Previous tab', 'Select the previous setup tab.', ['shift+tab', 'left'], 'terminal', SETUP_TAB_CONTEXTS),
|
|
192
192
|
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']),
|
|
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', 'setup.models.route-form']),
|
|
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', 'setup.models.route-form']),
|
|
195
195
|
b('crtr.setup.toggle', 'Setup', 'Toggle', 'Toggle the selected setup item.', ['space'], 'terminal', ['setup.list', 'setup.keybindings']),
|
|
196
196
|
b('crtr.setup.details', 'Setup', 'Show details', 'Show details for the selected item.', ['?'], 'terminal', ['setup.list']),
|
|
197
197
|
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'),
|
|
198
|
+
b('crtr.setup.confirm.accept', 'Setup', 'Confirm', 'Accept the setup confirmation.', ['y', 'enter'], 'terminal', ['setup.confirm', 'setup.models.confirm']),
|
|
199
|
+
b('crtr.setup.confirm.back', 'Setup', 'Go back', 'Cancel the setup confirmation.', ['n', 'escape', 'q'], 'terminal', ['setup.confirm', 'setup.models.confirm'], 'dismiss-modal'),
|
|
200
200
|
b('crtr.setup.keybindings.edit', 'Setup', 'Edit binding', 'Edit the selected binding.', ['enter'], 'terminal', ['setup.keybindings']),
|
|
201
201
|
b('crtr.setup.keybindings.commit-edit', 'Setup', 'Commit edit', 'Commit the active binding edit.', ['enter'], 'terminal', ['setup.keybindings.edit']),
|
|
202
202
|
b('crtr.setup.keybindings.cancel-edit', 'Setup', 'Cancel edit', 'Cancel the active binding edit.', ['escape'], 'terminal', ['setup.keybindings.edit'], 'dismiss-modal'),
|
|
@@ -212,10 +212,10 @@ function setupBindings() {
|
|
|
212
212
|
b('crtr.setup.keybindings.filter-clear', 'Setup', 'Clear filter', 'Clear the keybinding filter.', ['escape'], 'terminal', ['setup.keybindings.filter'], 'dismiss-modal'),
|
|
213
213
|
b('crtr.setup.cell-previous', 'Setup', 'Previous field', 'Select the previous field or setup table cell.', ['h'], 'terminal', ['setup.models', 'setup.keybindings']),
|
|
214
214
|
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']),
|
|
215
|
+
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
216
|
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
217
|
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
|
|
218
|
+
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
219
|
];
|
|
220
220
|
}
|
|
221
221
|
export const BINDING_IDS = new Set(BINDING_CATALOG.map((definition) => definition.id));
|
|
@@ -246,4 +246,6 @@ export const SAFETY_REQUIREMENTS = Object.freeze([
|
|
|
246
246
|
{ context: 'setup.models', capability: 'leave-screen' },
|
|
247
247
|
{ context: 'setup.models.picker', capability: 'dismiss-modal' },
|
|
248
248
|
{ context: 'setup.models.dimension-picker', capability: 'dismiss-modal' },
|
|
249
|
+
{ context: 'setup.models.route-form', capability: 'dismiss-modal' },
|
|
250
|
+
{ context: 'setup.models.confirm', capability: 'dismiss-modal' },
|
|
249
251
|
]);
|
|
@@ -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
|
}
|
|
@@ -25,4 +25,4 @@ export declare function docListingDirs(byName: ReadonlyMap<string, MemoryDoc>, n
|
|
|
25
25
|
* `filterDelivered` additionally drops members already exposed at preview rank
|
|
26
26
|
* or higher. A subdirectory always keeps its row — it advertises a subtree, so
|
|
27
27
|
* an omitted preview leaves the bare name. */
|
|
28
|
-
export declare function renderDirListing(byName: ReadonlyMap<string, MemoryDoc>, dir: string, target: ExposureTarget | null, filterDelivered: boolean): string[];
|
|
28
|
+
export declare function renderDirListing(byName: ReadonlyMap<string, MemoryDoc>, dir: string, target: ExposureTarget | null, filterDelivered: boolean, onPreview?: (doc: MemoryDoc) => void): string[];
|
|
@@ -93,7 +93,7 @@ function line(name, suffix) {
|
|
|
93
93
|
* `filterDelivered` additionally drops members already exposed at preview rank
|
|
94
94
|
* or higher. A subdirectory always keeps its row — it advertises a subtree, so
|
|
95
95
|
* an omitted preview leaves the bare name. */
|
|
96
|
-
export function renderDirListing(byName, dir, target, filterDelivered) {
|
|
96
|
+
export function renderDirListing(byName, dir, target, filterDelivered, onPreview) {
|
|
97
97
|
const node = treeFor(byName).node(dir);
|
|
98
98
|
if (node === null)
|
|
99
99
|
return [];
|
|
@@ -102,10 +102,14 @@ export function renderDirListing(byName, dir, target, filterDelivered) {
|
|
|
102
102
|
const suffix = previewSuffix(member.document, target, filterDelivered);
|
|
103
103
|
if (suffix === null)
|
|
104
104
|
continue;
|
|
105
|
+
onPreview?.(member.document);
|
|
105
106
|
lines.push(line(member.name, suffix));
|
|
106
107
|
}
|
|
107
108
|
for (const sub of node.childDirectories) {
|
|
108
|
-
|
|
109
|
+
const suffix = sub.document === null ? null : previewSuffix(sub.document, target, filterDelivered);
|
|
110
|
+
if (sub.document !== null && suffix !== null)
|
|
111
|
+
onPreview?.(sub.document);
|
|
112
|
+
lines.push(line(sub.name, suffix));
|
|
109
113
|
}
|
|
110
114
|
return lines;
|
|
111
115
|
}
|
|
@@ -1,8 +1,62 @@
|
|
|
1
|
+
import { type MemoryDoc, type MemoryTarget } from '../memory-resolver.js';
|
|
1
2
|
import { type ContextExposureState, type ExposureTarget } from './injected-store.js';
|
|
3
|
+
import { type Rung, type SubstrateDoc } from './schema.js';
|
|
2
4
|
import type { NodeConfigSubject } from './subject-fields.js';
|
|
5
|
+
export type DeliveryCause = 'directory listing attached to content delivery' | 'memory-read companion of content delivery';
|
|
6
|
+
export interface AccountedDelivery {
|
|
7
|
+
rung: Rung;
|
|
8
|
+
cause?: DeliveryCause;
|
|
9
|
+
/** Resolver descriptor for accounting-only attachment rows. */
|
|
10
|
+
source?: MemoryDoc;
|
|
11
|
+
}
|
|
12
|
+
/** The renderer's structured delivery ledger. It records precisely the document
|
|
13
|
+
* envelopes and listing lines that rendered, so diagnostics can report the
|
|
14
|
+
* same attachment fixpoint without parsing its text or recreating it. */
|
|
15
|
+
export interface DeliveryAccounting {
|
|
16
|
+
deliveries: Map<string, AccountedDelivery>;
|
|
17
|
+
}
|
|
18
|
+
export declare function deliveryAccounting(): DeliveryAccounting;
|
|
19
|
+
export declare function accountDelivery(accounting: DeliveryAccounting | undefined, doc: Pick<SubstrateDoc, 'path'>, rung: Rung, cause?: DeliveryCause, source?: MemoryDoc): void;
|
|
20
|
+
/** How one content delivery's attachments render, and the ledger + corpus the
|
|
21
|
+
* whole fixpoint runs against. ONE options object flows through every nested
|
|
22
|
+
* companion delivery, so `dirs` accumulates across the fixpoint and a
|
|
23
|
+
* companion inherits its parent's listing mode and memory target. */
|
|
24
|
+
export interface ContentAttachmentOptions {
|
|
25
|
+
/** Subject for companion `memory-read` gate evaluation. */
|
|
26
|
+
subject: NodeConfigSubject | null;
|
|
27
|
+
/** The one exposure ledger this fixpoint reads and registers against. */
|
|
28
|
+
target: ExposureTarget;
|
|
29
|
+
/** The corpus every directory question answers from — the caller's own
|
|
30
|
+
* listing corpus, never an ambient re-resolution. */
|
|
31
|
+
byName: ReadonlyMap<string, MemoryDoc>;
|
|
32
|
+
/** What companion `memory-read` routes plan against. Absent = the ambient
|
|
33
|
+
* cwd/profile target and this session's cached event corpus. */
|
|
34
|
+
memoryTarget?: MemoryTarget;
|
|
35
|
+
/** `blocks` attaches each disclosed listing as its own `<memory-listing>`.
|
|
36
|
+
* `tree` attaches none and accumulates the disclosed directories into
|
|
37
|
+
* `dirs` instead, for a caller that renders those members itself (the boot
|
|
38
|
+
* catalog tree) and registers them as it renders. */
|
|
39
|
+
listings: 'blocks' | 'tree';
|
|
40
|
+
/** Accumulator for `tree` mode: every directory this fixpoint disclosed. */
|
|
41
|
+
dirs?: string[];
|
|
42
|
+
/** An explicit `crtr memory read`: the read doc's OWN directory renders
|
|
43
|
+
* unfiltered and returns as `listing` rather than attaching as a block. */
|
|
44
|
+
explicit?: boolean;
|
|
45
|
+
/** Optional structured mirror of the actual emitted document deliveries. */
|
|
46
|
+
accounting?: DeliveryAccounting;
|
|
47
|
+
/** Why this winner was reached through the current content attachment walk. */
|
|
48
|
+
deliveryCause?: DeliveryCause;
|
|
49
|
+
}
|
|
50
|
+
/** The attachment semantics of a content delivery. Content is read-equivalent:
|
|
51
|
+
* it exposes its own/enclosing directory listings and fires memory-read routes.
|
|
52
|
+
* The exposure ledger makes this finite even when companion routes cycle. */
|
|
53
|
+
export declare function contentReadAttachments(doc: Pick<SubstrateDoc, 'name' | 'path' | 'body'>, opts: ContentAttachmentOptions): {
|
|
54
|
+
blocks: string[];
|
|
55
|
+
listing?: string[];
|
|
56
|
+
};
|
|
3
57
|
/** Surface docs whose `read` entries match a successfully read file, using
|
|
4
58
|
* the caller-supplied daemon snapshot subject. */
|
|
5
|
-
export declare function renderOnReadDocsForSubject(subject: NodeConfigSubject, readFilePath: string, target?: ExposureTarget): string;
|
|
59
|
+
export declare function renderOnReadDocsForSubject(subject: NodeConfigSubject, readFilePath: string, target?: ExposureTarget, accounting?: DeliveryAccounting): string;
|
|
6
60
|
/** Workspace boot docs using the snapshot's cwd/profile instead of a canvas
|
|
7
61
|
* lookup. The shared exposure target prevents a later read from repeating a
|
|
8
62
|
* document already loaded here.
|
|
@@ -11,24 +65,26 @@ export declare function renderOnReadDocsForSubject(subject: NodeConfigSubject, r
|
|
|
11
65
|
* relationship cap BEFORE filtering, ordering, rendering, and exposure
|
|
12
66
|
* registration. A later explicit `crtr memory read` can still raise a document
|
|
13
67
|
* that workspace-open disclosed at a lower rung. */
|
|
14
|
-
export declare function renderWorkspaceOpenDocsForSubject(subject: NodeConfigSubject, cwd: string, profileId: string | null, target?: ExposureTarget): string;
|
|
68
|
+
export declare function renderWorkspaceOpenDocsForSubject(subject: NodeConfigSubject, cwd: string, profileId: string | null, target?: ExposureTarget, accounting?: DeliveryAccounting): string;
|
|
15
69
|
/** Inner `<memory>` blocks for the memory-read event: docs whose
|
|
16
70
|
* `memory-read` entries match the doc a `crtr memory read` just resolved
|
|
17
71
|
* (known by its exact canonical `name` — no mount-stripped or other alias;
|
|
18
72
|
* `excludeRealpath` keeps the resolved doc from delivering itself). Returns
|
|
19
73
|
* unwrapped blocks — the read leaf composes one `<auto-loaded-context>`
|
|
20
74
|
* envelope from these plus the directory listings. */
|
|
21
|
-
export declare function memoryReadDocBlocks(
|
|
75
|
+
export declare function memoryReadDocBlocks(excludeRealpath: string, name: string, opts: Omit<ContentAttachmentOptions, 'byName'> & {
|
|
76
|
+
byName?: ReadonlyMap<string, MemoryDoc>;
|
|
77
|
+
}): string[];
|
|
22
78
|
/** Surface docs whose `command` entries match an executed bash command.
|
|
23
79
|
* The corpus is the resolved cwd/profile set; a command has no file from which
|
|
24
80
|
* to discover enclosing project stores. */
|
|
25
|
-
export declare function renderOnCommandDocsForSubject(subject: NodeConfigSubject, command: string, target?: ExposureTarget): string;
|
|
81
|
+
export declare function renderOnCommandDocsForSubject(subject: NodeConfigSubject, command: string, target?: ExposureTarget, accounting?: DeliveryAccounting): string;
|
|
26
82
|
/** Surface docs whose `pre-command` entries match a bash command that has NOT
|
|
27
83
|
* run yet. The exact twin of the post-execution renderer above, and its empty
|
|
28
84
|
* string is load-bearing: `''` means every matching doc is already exposed at
|
|
29
85
|
* its matching rung or above, which is the release — the caller lets the
|
|
30
86
|
* command through. */
|
|
31
|
-
export declare function renderPreCommandDocsForSubject(subject: NodeConfigSubject, command: string, target?: ExposureTarget): string;
|
|
87
|
+
export declare function renderPreCommandDocsForSubject(subject: NodeConfigSubject, command: string, target?: ExposureTarget, accounting?: DeliveryAccounting): string;
|
|
32
88
|
/** Does ANY doc in this session's corpus carry a `pre-command` entry? The
|
|
33
89
|
* short-circuit a pre-execution caller checks first: with no such doc — the
|
|
34
90
|
* common case — no command need ever pay for a subject lookup. */
|