@openchambery/web 1.19.13-beta.3 → 1.19.13-beta.5
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/assets/{AgentAvatar-mbvX7YlW.js → AgentAvatar-B5MiX004.js} +1 -1
- package/dist/assets/AssistantView-B6kZZdri.js +2 -0
- package/dist/assets/{DiagramView-W0IHFk-1.js → DiagramView-C40ituM2.js} +1 -1
- package/dist/assets/{FadeInOnReveal-moBbL8Ll.js → FadeInOnReveal-UGwClbL_.js} +1 -1
- package/dist/assets/{Icon-CkzzhaLr.js → Icon-ZAKaNHiG.js} +1 -1
- package/dist/assets/{MarkdownRenderer-v7n_D6nL.js → MarkdownRenderer-mcY597G0.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-Buz5LZsa.js → MarkdownRendererImpl-YsTrEeUY.js} +6 -3
- package/dist/assets/MarkstreamRendererImpl-kNx_G9Sd.js +3 -0
- package/dist/assets/MobileDetailNavigation-Uwzh6TWj.js +1 -0
- package/dist/assets/MobileShareBridge-D3oSiSC6.js +4 -0
- package/dist/assets/MobileSurface-B-SWjAj7.js +2 -0
- package/dist/assets/{MultiRunWindow-yWIHgr89.js → MultiRunWindow-LgdUonfa.js} +1 -1
- package/dist/assets/{OnboardingScreen-j0iCyH7f.js → OnboardingScreen-DHKq6y3W.js} +1 -1
- package/dist/assets/{OverlayScrollbar-CDC5B3RH.js → OverlayScrollbar-Bahg_P3r.js} +1 -1
- package/dist/assets/{SettingsView-C3BbLpHT.js → SettingsView-iUiWTKbM.js} +8 -8
- package/dist/assets/{SettingsWindow-b0JWdygM.js → SettingsWindow-DCDEAC1n.js} +1 -1
- package/dist/assets/{TerminalView-C74IvmOt.js → TerminalView-CdZi_UXQ.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BORu8CtR.js → ToolOutputDialog-fZq6UHvy.js} +1 -1
- package/dist/assets/{appThemeRegistry-DOwhB93n.js → appThemeRegistry-DyFotcOM.js} +1 -1
- package/dist/assets/{button-xx1UOBA5.js → button-9R-WgVMl.js} +1 -1
- package/dist/assets/{clipboard-CUasgPiF.js → clipboard-CzXos-TI.js} +1 -1
- package/dist/assets/{collapsible-CACHBRYZ.js → collapsible-Bbz9KiJf.js} +1 -1
- package/dist/assets/{context-menu-CYbm4wBZ.js → context-menu-CSyOhRz8.js} +1 -1
- package/dist/assets/{desktopHosts-DILvrAok.js → desktopHosts-B052a4UK.js} +1 -1
- package/dist/assets/{desktopRelayRestore-t8asCr3m.js → desktopRelayRestore-OjKNXr10.js} +1 -1
- package/dist/assets/{diagram-u0BC9uuY.js → diagram-NdsZih8P.js} +1 -1
- package/dist/assets/{dialog-BwQ3rY_x.js → dialog-U4ML8hxt.js} +1 -1
- package/dist/assets/{es-CRjOwvYM.js → es-BtC17YQH.js} +1 -1
- package/dist/assets/fileMentionTouchSelection-DSKcxgU_.js +4 -0
- package/dist/assets/{fileQueries-Bgly-E8f.js → fileQueries-B_s_B-fH.js} +1 -1
- package/dist/assets/{fr-D9taEN9L.js → fr-DPohCCUO.js} +1 -1
- package/dist/assets/{gitApi-D1mvv14J.js → gitApi-BUubBKri.js} +3 -3
- package/dist/assets/{input-CI730njs.js → input-Cd_1HyQv.js} +1 -1
- package/dist/assets/insertionBoundaries-DeGd-RP9.js +1 -0
- package/dist/assets/{ja-CYQ5aoWp.js → ja-CflPxUNi.js} +1 -1
- package/dist/assets/{ko-BwASV0Q0.js → ko-CuSU80xd.js} +1 -1
- package/dist/assets/main-C6EluFKk.js +169 -0
- package/dist/assets/{main-phpzFgMN.js → main-iFGhn-zU.js} +2 -2
- package/dist/assets/{markdownHeightCache-DEjGKLv8.js → markdownHeightCache-BL8CUQ0M.js} +1 -1
- package/dist/assets/miniChat-Bs_H98PB.js +2 -0
- package/dist/assets/mobile-HfLUO-bG.js +2 -0
- package/dist/assets/{multirun-CYuxJPBX.js → multirun-BABuRg1O.js} +1 -1
- package/dist/assets/{persistence-YbQKzHUQ.js → persistence-DwRBC4zl.js} +2 -2
- package/dist/assets/{pl-DUI1vPil.js → pl-eJvVbQ74.js} +1 -1
- package/dist/assets/platform-DQ5_LNKO.js +2 -0
- package/dist/assets/{projectMeta-Cir66uF3.js → projectMeta-WcxDg0DZ.js} +2 -2
- package/dist/assets/{pt-BR-CZQFJllP.js → pt-BR-DSejAksY.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-BzYqqr9n.js → renderElectronMiniChatApp-Cp-dVXj5.js} +2 -2
- package/dist/assets/renderMobileApp-B6rb6wuc.js +28 -0
- package/dist/assets/{runtimeConfig-BXf9GQ0Q.js → runtimeConfig-88DzHVMT.js} +1 -1
- package/dist/assets/runtimeEndpointReset-CEbKea6x.js +147 -0
- package/dist/assets/sessionChangeSummary-C9071lsK.js +2 -0
- package/dist/assets/sessionLookup-CEw3tCz1.js +1 -0
- package/dist/assets/{sortable-tabs-strip-Dom6Sthu.js → sortable-tabs-strip-eL2whsf8.js} +1 -1
- package/dist/assets/src-B_DG0xy5.css +2 -0
- package/dist/assets/{store-CSnH_QXM.js → store-DHdMj3y4.js} +2 -2
- package/dist/assets/{terminalApi-noa4Cdbh.js → terminalApi-CfgOfnZk.js} +1 -1
- package/dist/assets/{textarea-BfNtUdJE.js → textarea-DE1UrdtK.js} +1 -1
- package/dist/assets/{tooltip-DPxumadF.js → tooltip-DSjr26A_.js} +1 -1
- package/dist/assets/{uk-C1FYZlmA.js → uk-DV9OgOAg.js} +1 -1
- package/dist/assets/{url-CVhsI9kR.js → url-CMQwHq4k.js} +1 -1
- package/dist/assets/{useAppFontEffects-NVyoI_Ei.js → useAppFontEffects-Ca0mASq5.js} +43 -44
- package/dist/assets/{useDesktopWindowControlsLayout-CP3NmMYG.js → useDesktopWindowControlsLayout-B3KJYYwp.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-AUoqSyOJ.js → useEffectiveDirectory-B4l9snxd.js} +1 -1
- package/dist/assets/{useFontPreferences-CB-cGo3I.js → useFontPreferences-DCaY8NbU.js} +1 -1
- package/dist/assets/useMobileNavigationStore-CU2UwmN3.js +413 -0
- package/dist/assets/{useSessionAutoCleanup-ClvQQPqQ.js → useSessionAutoCleanup-B9El9yjX.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-DOk4cAxv.js → useWorkerHighlightedLines-PPhFeDFI.js} +1 -1
- package/dist/assets/{utils-BHTVlVov.js → utils-2ZK8yboQ.js} +1 -1
- package/dist/assets/{vendor-reactuses-core-DDoDDyM9.js → vendor-reactuses-core-BnW6GxJt.js} +1 -1
- package/dist/assets/{zh-CN-C96qP1ip.js → zh-CN-92n_rGTt.js} +1 -1
- package/dist/assets/{zh-TW-BsRk1Rbb.js → zh-TW-CwVxwJCj.js} +1 -1
- package/dist/index.html +8 -8
- package/dist/mini-chat.html +8 -8
- package/dist/mobile.html +8 -8
- package/package.json +1 -1
- package/server/index.js +1 -0
- package/server/lib/assistants/DOCUMENTATION.md +104 -18
- package/server/lib/assistants/assign.js +173 -13
- package/server/lib/assistants/assign.test.js +139 -0
- package/server/lib/assistants/bubbles.js +1 -1
- package/server/lib/assistants/contact-store.js +496 -11
- package/server/lib/assistants/contact-store.test.js +323 -7
- package/server/lib/assistants/contact-tools.js +514 -52
- package/server/lib/assistants/contact-tools.test.js +390 -7
- package/server/lib/assistants/contracts.js +31 -0
- package/server/lib/assistants/harness.js +224 -54
- package/server/lib/assistants/harness.test.js +372 -12
- package/server/lib/assistants/routes.js +3 -0
- package/server/lib/assistants/service.js +1079 -38
- package/server/lib/assistants/service.test.js +1235 -26
- package/server/lib/llm/DOCUMENTATION.md +2 -0
- package/server/lib/llm/completions.js +4 -0
- package/server/lib/llm/completions.test.js +3 -0
- package/server/lib/llm/generate.js +12 -5
- package/server/lib/llm/generate.test.js +40 -0
- package/server/lib/opencode/feature-routes-runtime.js +5 -0
- package/dist/assets/AssistantView-DOZPK9oh.js +0 -2
- package/dist/assets/MarkstreamRendererImpl-D4nUXYpP.js +0 -3
- package/dist/assets/MobileDetailNavigation-Bfs4CTUh.js +0 -1
- package/dist/assets/MobileShareBridge-DtwVLkBL.js +0 -4
- package/dist/assets/MobileSurface-BmYYdY3g.js +0 -2
- package/dist/assets/insertionBoundaries-BBn8YW82.js +0 -4
- package/dist/assets/main-Cd7NwXx5.js +0 -169
- package/dist/assets/miniChat-D6UYe50H.js +0 -2
- package/dist/assets/mobile-w2Ed9I5O.js +0 -2
- package/dist/assets/platform-DzsGqlGW.js +0 -2
- package/dist/assets/renderMobileApp-BvIE86_O.js +0 -28
- package/dist/assets/runtimeEndpointReset-lF5S7U4I.js +0 -147
- package/dist/assets/sessionChangeSummary-DQDZ5HW-.js +0 -1
- package/dist/assets/sessionLookup-CNRDupYq.js +0 -1
- package/dist/assets/src-AA-5nXsF.css +0 -2
- package/dist/assets/useMobileNavigationStore-DANrWekj.js +0 -413
|
@@ -24,6 +24,7 @@ export const ASSIGN_CODES = Object.freeze({
|
|
|
24
24
|
IMAGE_NOT_SUPPORTED: 'image_not_supported',
|
|
25
25
|
NO_PROVIDER: 'no_provider',
|
|
26
26
|
PROMPT_AMBIGUOUS: 'prompt_ambiguous',
|
|
27
|
+
NOT_FOUND: 'not_found',
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
export const PROJECT_REQUIRED_MESSAGE = 'No registered project is configured. Add a project in Settings before assigning work. Do not use assistant-workspaces.';
|
|
@@ -136,7 +137,13 @@ function normalizeProjectRoots(allowedRoots = []) {
|
|
|
136
137
|
const roots = [];
|
|
137
138
|
for (const root of allowedRoots) {
|
|
138
139
|
if (typeof root !== 'string' || !root.trim()) continue;
|
|
139
|
-
|
|
140
|
+
// realpath so /var vs /private/var (and prior resolveAssignDirectory results) still match.
|
|
141
|
+
let resolved = path.resolve(root.trim());
|
|
142
|
+
try {
|
|
143
|
+
resolved = fs.realpathSync(resolved);
|
|
144
|
+
} catch {
|
|
145
|
+
// Keep path.resolve when the root is not yet on disk.
|
|
146
|
+
}
|
|
140
147
|
if (seen.has(resolved)) continue;
|
|
141
148
|
seen.add(resolved);
|
|
142
149
|
roots.push(resolved);
|
|
@@ -146,7 +153,15 @@ function normalizeProjectRoots(allowedRoots = []) {
|
|
|
146
153
|
|
|
147
154
|
export function isManagedAssistantWorkspace(candidate, managedWorkspaceRoot) {
|
|
148
155
|
if (!candidate || !managedWorkspaceRoot) return false;
|
|
149
|
-
|
|
156
|
+
const realpathOrResolve = (value) => {
|
|
157
|
+
const resolved = path.resolve(value);
|
|
158
|
+
try {
|
|
159
|
+
return fs.realpathSync(resolved);
|
|
160
|
+
} catch {
|
|
161
|
+
return resolved;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
return contained(realpathOrResolve(candidate), realpathOrResolve(managedWorkspaceRoot));
|
|
150
165
|
}
|
|
151
166
|
|
|
152
167
|
export function resolveAssignDirectory({
|
|
@@ -177,22 +192,24 @@ export function resolveAssignDirectory({
|
|
|
177
192
|
const resolveExisting = (candidate) => {
|
|
178
193
|
const raw = trim(candidate, 4096);
|
|
179
194
|
if (!raw) return null;
|
|
180
|
-
|
|
181
|
-
rejectManaged(resolved);
|
|
182
|
-
if (!underRoots(resolved)) {
|
|
183
|
-
throw new AssignError(
|
|
184
|
-
ASSIGN_CODES.WORKSPACE_FORBIDDEN,
|
|
185
|
-
'That path is not a registered project. Add it in Settings, then assign again.',
|
|
186
|
-
);
|
|
187
|
-
}
|
|
195
|
+
let resolved = path.resolve(raw);
|
|
188
196
|
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
|
|
189
197
|
throw new AssignError(ASSIGN_CODES.VALIDATION, 'The project directory does not exist.');
|
|
190
198
|
}
|
|
191
199
|
try {
|
|
192
|
-
|
|
200
|
+
resolved = fs.realpathSync(resolved);
|
|
193
201
|
} catch {
|
|
194
202
|
throw new AssignError(ASSIGN_CODES.VALIDATION, 'The project directory does not exist.');
|
|
195
203
|
}
|
|
204
|
+
// Compare realpaths so a prior authoritative resolve cannot fail against non-real roots.
|
|
205
|
+
rejectManaged(resolved);
|
|
206
|
+
if (!underRoots(resolved)) {
|
|
207
|
+
throw new AssignError(
|
|
208
|
+
ASSIGN_CODES.WORKSPACE_FORBIDDEN,
|
|
209
|
+
'That path is not a registered project. Add it in Settings, then assign again.',
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return resolved;
|
|
196
213
|
};
|
|
197
214
|
|
|
198
215
|
let target = resolveExisting(directory) || resolveExisting(projectPath) || resolveExisting(defaultProjectPath);
|
|
@@ -262,17 +279,126 @@ export function hasAssignImageParts(parts = []) {
|
|
|
262
279
|
return sanitizeAssignFileParts(parts).some((part) => String(part.mime).toLowerCase().startsWith('image/'));
|
|
263
280
|
}
|
|
264
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Directory from authoritative OpenCode session payload.
|
|
284
|
+
* Prefer session.directory / path / project.worktree — never invent paths.
|
|
285
|
+
*/
|
|
286
|
+
export function extractSessionDirectory(session) {
|
|
287
|
+
const payload = session?.data && typeof session.data === 'object' && !Array.isArray(session.data)
|
|
288
|
+
? session.data
|
|
289
|
+
: session;
|
|
290
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null;
|
|
291
|
+
return trim(payload.directory, 4096)
|
|
292
|
+
|| trim(payload.path, 4096)
|
|
293
|
+
|| trim(payload.project?.worktree, 4096)
|
|
294
|
+
|| trim(payload.project?.directory, 4096)
|
|
295
|
+
|| trim(payload.project?.path, 4096)
|
|
296
|
+
|| null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Title from session payload or index row. */
|
|
300
|
+
export function extractSessionTitle(session) {
|
|
301
|
+
const payload = session?.data && typeof session.data === 'object' && !Array.isArray(session.data)
|
|
302
|
+
? session.data
|
|
303
|
+
: session;
|
|
304
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null;
|
|
305
|
+
return trim(payload.title, 256) || null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Map OpenCode session + messages into contact watch/card status.
|
|
310
|
+
* Used as the baseline when watching an existing session so an already-terminal
|
|
311
|
+
* idle is not treated as a fresh complete transition.
|
|
312
|
+
* - busy/retry → busy (in-flight)
|
|
313
|
+
* - session.error / last assistant error → error
|
|
314
|
+
* - idle / time.completed / missing session → complete
|
|
315
|
+
* - otherwise busy (keep watching; never invent complete from silence)
|
|
316
|
+
*/
|
|
317
|
+
export function mapSessionToWatchStatus({
|
|
318
|
+
session = null,
|
|
319
|
+
messages = null,
|
|
320
|
+
missing = false,
|
|
321
|
+
} = {}) {
|
|
322
|
+
if (missing) return 'complete';
|
|
323
|
+
const payload = session?.data && typeof session.data === 'object' && !Array.isArray(session.data)
|
|
324
|
+
? session.data
|
|
325
|
+
: session;
|
|
326
|
+
if (payload?.error) return 'error';
|
|
327
|
+
|
|
328
|
+
const statusType = typeof payload?.status?.type === 'string'
|
|
329
|
+
? payload.status.type
|
|
330
|
+
: (typeof payload?.status === 'string'
|
|
331
|
+
? payload.status
|
|
332
|
+
: (typeof payload?.type === 'string' ? payload.type : ''));
|
|
333
|
+
if (statusType === 'busy' || statusType === 'retry') return 'busy';
|
|
334
|
+
if (statusType === 'question') return 'question';
|
|
335
|
+
|
|
336
|
+
const rows = Array.isArray(messages)
|
|
337
|
+
? messages
|
|
338
|
+
: (Array.isArray(messages?.data) ? messages.data : []);
|
|
339
|
+
for (let index = rows.length - 1; index >= 0; index -= 1) {
|
|
340
|
+
const info = rows[index]?.info ?? rows[index];
|
|
341
|
+
if (info?.role !== 'assistant') continue;
|
|
342
|
+
if (info?.error) return 'error';
|
|
343
|
+
if (info?.time?.completed) return 'complete';
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (statusType === 'idle' || payload?.time?.completed) return 'complete';
|
|
348
|
+
// No terminal signal — keep in-flight so later idle can settle once.
|
|
349
|
+
return 'busy';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Normalize a session/message model object to { providerID, modelID }.
|
|
354
|
+
* Accepts `{ providerID, id }` or `{ providerID, modelID }`. Lookup failures stay null.
|
|
355
|
+
*/
|
|
356
|
+
export function normalizeAssignSessionModel(model) {
|
|
357
|
+
if (!model || typeof model !== 'object' || Array.isArray(model)) return null;
|
|
358
|
+
const providerID = trim(model.providerID, 256);
|
|
359
|
+
const modelID = trim(model.modelID, 256) || trim(model.id, 256);
|
|
360
|
+
if (!providerID || !modelID) return null;
|
|
361
|
+
return { providerID, modelID };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Extract the last worker model from a session.get payload and optional messages list.
|
|
366
|
+
* Priority: session.model → newest user/assistant info.model. Never throws.
|
|
367
|
+
*/
|
|
368
|
+
export function extractAssignSessionModel({ session = null, messages = null } = {}) {
|
|
369
|
+
const payload = session?.data && typeof session.data === 'object' && !Array.isArray(session.data)
|
|
370
|
+
? session.data
|
|
371
|
+
: session;
|
|
372
|
+
const fromSession = normalizeAssignSessionModel(payload?.model);
|
|
373
|
+
if (fromSession) return fromSession;
|
|
374
|
+
|
|
375
|
+
const rows = Array.isArray(messages)
|
|
376
|
+
? messages
|
|
377
|
+
: (Array.isArray(messages?.data) ? messages.data : []);
|
|
378
|
+
for (let index = rows.length - 1; index >= 0; index -= 1) {
|
|
379
|
+
const row = rows[index];
|
|
380
|
+
const info = row?.info && typeof row.info === 'object' ? row.info : row;
|
|
381
|
+
const role = info?.role || row?.role;
|
|
382
|
+
if (role !== 'user' && role !== 'assistant') continue;
|
|
383
|
+
const hit = normalizeAssignSessionModel(info?.model || row?.model);
|
|
384
|
+
if (hit) return hit;
|
|
385
|
+
}
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
265
389
|
/**
|
|
266
390
|
* Resolve worker model for assign.
|
|
267
|
-
* - No explicit selection →
|
|
391
|
+
* - No explicit selection → reused session model (catalog match, source session) else assistant default.
|
|
268
392
|
* - Explicit selection → must resolve uniquely against the connected catalog; never silent fallback.
|
|
269
393
|
* - Provided but illegal/blank/overlong/conflicting fields fail closed (validation_error).
|
|
394
|
+
* - Session model missing/not-in-catalog degrades to assistant default (never fails assign).
|
|
270
395
|
*/
|
|
271
396
|
export function resolveAssignWorkerModel({
|
|
272
397
|
providerID,
|
|
273
398
|
modelID,
|
|
274
399
|
model,
|
|
275
400
|
fallback = {},
|
|
401
|
+
sessionModel = null,
|
|
276
402
|
catalog = null,
|
|
277
403
|
} = {}) {
|
|
278
404
|
const explicitProvider = requireProvidedString(providerID, { field: 'providerID', max: 256 });
|
|
@@ -284,6 +410,21 @@ export function resolveAssignWorkerModel({
|
|
|
284
410
|
const fallbackModel = trim(fallback.modelID, 256);
|
|
285
411
|
|
|
286
412
|
if (!hasExplicit) {
|
|
413
|
+
const session = normalizeAssignSessionModel(sessionModel);
|
|
414
|
+
if (session && catalog && Array.isArray(catalog.models)) {
|
|
415
|
+
const hit = catalog.models.find((item) => (
|
|
416
|
+
item?.providerID === session.providerID && item?.modelID === session.modelID
|
|
417
|
+
));
|
|
418
|
+
if (hit) {
|
|
419
|
+
return {
|
|
420
|
+
providerID: session.providerID,
|
|
421
|
+
modelID: session.modelID,
|
|
422
|
+
source: 'session',
|
|
423
|
+
name: typeof hit.name === 'string' ? hit.name : null,
|
|
424
|
+
acceptsImages: hit.acceptsImages === true ? true : hit.acceptsImages === false ? false : null,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
}
|
|
287
428
|
if (!fallbackProvider || !fallbackModel) {
|
|
288
429
|
throw new AssignError(ASSIGN_CODES.VALIDATION, 'Assistant is missing a connected provider/model.');
|
|
289
430
|
}
|
|
@@ -354,7 +495,7 @@ export function resolveAssignWorkerModel({
|
|
|
354
495
|
}
|
|
355
496
|
|
|
356
497
|
const models = catalog.models.filter((entry) => entry && typeof entry === 'object');
|
|
357
|
-
|
|
498
|
+
let matches = models.filter((entry) => {
|
|
358
499
|
const entryProvider = trim(entry.providerID, 256);
|
|
359
500
|
const entryModel = trim(entry.modelID, 256);
|
|
360
501
|
const entryName = trim(entry.name, 512);
|
|
@@ -366,6 +507,7 @@ export function resolveAssignWorkerModel({
|
|
|
366
507
|
return entryProvider === wantedProvider;
|
|
367
508
|
}
|
|
368
509
|
if (!wantedProvider && wantedModel) {
|
|
510
|
+
if (explicitModel) return entryModel === wantedModel;
|
|
369
511
|
const needle = wantedModel.toLowerCase();
|
|
370
512
|
return entryModel.toLowerCase() === needle
|
|
371
513
|
|| (entryName && entryName.toLowerCase() === needle)
|
|
@@ -374,6 +516,19 @@ export function resolveAssignWorkerModel({
|
|
|
374
516
|
return false;
|
|
375
517
|
});
|
|
376
518
|
|
|
519
|
+
// Only the informal `model` alias gets fuzzy matching. Explicit IDs remain exact.
|
|
520
|
+
if (matches.length === 0 && combined && !combined.includes('/') && !explicitModel) {
|
|
521
|
+
const normalize = (value) => trim(value, 512).toLowerCase().replace(/[^\p{L}\p{N}]/gu, '');
|
|
522
|
+
const needle = normalize(combined);
|
|
523
|
+
const eligible = models.filter((entry) => !wantedProvider || entry.providerID === wantedProvider);
|
|
524
|
+
if (needle.length >= 2) {
|
|
525
|
+
const exact = eligible.filter((entry) => normalize(entry.modelID) === needle || normalize(entry.name) === needle);
|
|
526
|
+
matches = exact.length > 0 ? exact : eligible.filter((entry) => (
|
|
527
|
+
normalize(entry.modelID).includes(needle) || normalize(entry.name).includes(needle)
|
|
528
|
+
));
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
377
532
|
if (matches.length === 0) {
|
|
378
533
|
const label = wantedProvider && wantedModel
|
|
379
534
|
? `${wantedProvider}/${wantedModel}`
|
|
@@ -534,6 +689,7 @@ export async function assignSession(input = {}) {
|
|
|
534
689
|
providerID: assistant.providerID,
|
|
535
690
|
modelID: assistant.modelID,
|
|
536
691
|
},
|
|
692
|
+
sessionModel: input.sessionModel,
|
|
537
693
|
catalog: input.catalog,
|
|
538
694
|
});
|
|
539
695
|
}
|
|
@@ -583,6 +739,7 @@ export async function assignSession(input = {}) {
|
|
|
583
739
|
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Worker session APIs are unavailable.');
|
|
584
740
|
}
|
|
585
741
|
|
|
742
|
+
input.signal?.throwIfAborted();
|
|
586
743
|
let targetSessionID = sessionID;
|
|
587
744
|
if (!targetSessionID) {
|
|
588
745
|
let created;
|
|
@@ -603,6 +760,9 @@ export async function assignSession(input = {}) {
|
|
|
603
760
|
}
|
|
604
761
|
}
|
|
605
762
|
|
|
763
|
+
// A cancelled contact must not submit work after a late create response.
|
|
764
|
+
// Retain that session: creation succeeded, and cancellation is not deletion.
|
|
765
|
+
input.signal?.throwIfAborted();
|
|
606
766
|
let prompted;
|
|
607
767
|
let promptError;
|
|
608
768
|
try {
|
|
@@ -8,9 +8,13 @@ import {
|
|
|
8
8
|
assignSession,
|
|
9
9
|
attachmentScopeKey,
|
|
10
10
|
buildAssignParts,
|
|
11
|
+
extractAssignSessionModel,
|
|
12
|
+
extractSessionDirectory,
|
|
11
13
|
hasAssignImageParts,
|
|
12
14
|
isAmbiguousPromptFailure,
|
|
13
15
|
isManagedAssistantWorkspace,
|
|
16
|
+
mapSessionToWatchStatus,
|
|
17
|
+
normalizeAssignSessionModel,
|
|
14
18
|
requireProvidedString,
|
|
15
19
|
resolveAssignDirectory,
|
|
16
20
|
resolveAssignWorkerModel,
|
|
@@ -63,6 +67,22 @@ describe('resolveAssignDirectory', () => {
|
|
|
63
67
|
});
|
|
64
68
|
|
|
65
69
|
describe('requireProvidedString / resolveAssignWorkerModel', () => {
|
|
70
|
+
it('resolves a unique informal alias while keeping explicit ids exact and ambiguity closed', () => {
|
|
71
|
+
const catalog = { models: [
|
|
72
|
+
{ providerID: 'a', modelID: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6' },
|
|
73
|
+
{ providerID: 'a', modelID: 'gpt-5.4', name: 'GPT 5.4' },
|
|
74
|
+
{ providerID: 'b', modelID: 'gpt-5.4-mini', name: 'GPT 5.4 Mini' },
|
|
75
|
+
] }
|
|
76
|
+
expect(resolveAssignWorkerModel({ model: 'sonnet 4.6', catalog }).modelID).toBe('claude-sonnet-4-6')
|
|
77
|
+
expect(resolveAssignWorkerModel({ model: 'gpt54', catalog }).modelID).toBe('gpt-5.4')
|
|
78
|
+
expect(() => resolveAssignWorkerModel({ model: 'gpt', catalog })).toThrow(/Several connected models/)
|
|
79
|
+
expect(() => resolveAssignWorkerModel({ providerID: 'a', modelID: 'sonnet 4.6', catalog })).toThrow(/No connected/)
|
|
80
|
+
expect(() => resolveAssignWorkerModel({ model: 'made-up-model', catalog })).toThrow(/No connected/)
|
|
81
|
+
expect(() => resolveAssignWorkerModel({ modelID: 'GPT 5.4', catalog })).toThrow(/No connected/)
|
|
82
|
+
expect(resolveAssignWorkerModel({ providerID: 'a', model: 'gpt', catalog }).modelID).toBe('gpt-5.4')
|
|
83
|
+
expect(() => resolveAssignWorkerModel({ model: 'sonnet', catalog: { models: [catalog.models[0], { ...catalog.models[0], providerID: 'b' }] } })).toThrow(/Several connected models/)
|
|
84
|
+
})
|
|
85
|
+
|
|
66
86
|
const catalog = {
|
|
67
87
|
models: [
|
|
68
88
|
{ providerID: 'openai', modelID: 'gpt-4o', name: 'GPT-4o', acceptsImages: true },
|
|
@@ -84,6 +104,64 @@ describe('requireProvidedString / resolveAssignWorkerModel', () => {
|
|
|
84
104
|
});
|
|
85
105
|
});
|
|
86
106
|
|
|
107
|
+
it('prefers a catalog-matched session model over the assistant default', () => {
|
|
108
|
+
expect(normalizeAssignSessionModel({ providerID: 'xai', id: 'grok-4.6' })).toEqual({
|
|
109
|
+
providerID: 'xai',
|
|
110
|
+
modelID: 'grok-4.6',
|
|
111
|
+
});
|
|
112
|
+
expect(extractAssignSessionModel({
|
|
113
|
+
session: { model: { providerID: 'xai', id: 'grok-4.6' } },
|
|
114
|
+
})).toEqual({ providerID: 'xai', modelID: 'grok-4.6' });
|
|
115
|
+
expect(extractAssignSessionModel({
|
|
116
|
+
messages: [
|
|
117
|
+
{ info: { role: 'user', model: { providerID: 'openai', modelID: 'gpt-4o' } } },
|
|
118
|
+
{ info: { role: 'assistant', model: { providerID: 'xai', id: 'grok-4.6' } } },
|
|
119
|
+
],
|
|
120
|
+
})).toEqual({ providerID: 'xai', modelID: 'grok-4.6' });
|
|
121
|
+
expect(resolveAssignWorkerModel({
|
|
122
|
+
sessionModel: { providerID: 'xai', modelID: 'grok-4.6' },
|
|
123
|
+
fallback: { providerID: 'p', modelID: 'm' },
|
|
124
|
+
catalog,
|
|
125
|
+
})).toMatchObject({
|
|
126
|
+
providerID: 'xai',
|
|
127
|
+
modelID: 'grok-4.6',
|
|
128
|
+
source: 'session',
|
|
129
|
+
acceptsImages: true,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('degrades to the assistant model when the session model is missing from catalog', () => {
|
|
134
|
+
expect(resolveAssignWorkerModel({
|
|
135
|
+
sessionModel: { providerID: 'missing', modelID: 'gone' },
|
|
136
|
+
fallback: { providerID: 'p', modelID: 'm' },
|
|
137
|
+
catalog,
|
|
138
|
+
})).toEqual({
|
|
139
|
+
providerID: 'p',
|
|
140
|
+
modelID: 'm',
|
|
141
|
+
source: 'assistant',
|
|
142
|
+
name: null,
|
|
143
|
+
acceptsImages: null,
|
|
144
|
+
});
|
|
145
|
+
expect(resolveAssignWorkerModel({
|
|
146
|
+
sessionModel: { providerID: 'xai', modelID: 'grok-4.6' },
|
|
147
|
+
fallback: { providerID: 'p', modelID: 'm' },
|
|
148
|
+
catalog: null,
|
|
149
|
+
}).source).toBe('assistant');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('keeps explicit model fail-closed over a session model', () => {
|
|
153
|
+
expect(resolveAssignWorkerModel({
|
|
154
|
+
model: 'openai/gpt-4o',
|
|
155
|
+
sessionModel: { providerID: 'xai', modelID: 'grok-4.6' },
|
|
156
|
+
fallback: { providerID: 'p', modelID: 'm' },
|
|
157
|
+
catalog,
|
|
158
|
+
})).toMatchObject({
|
|
159
|
+
providerID: 'openai',
|
|
160
|
+
modelID: 'gpt-4o',
|
|
161
|
+
source: 'explicit',
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
87
165
|
it('resolves explicit provider/model against the connected catalog', () => {
|
|
88
166
|
expect(resolveAssignWorkerModel({
|
|
89
167
|
model: 'xai/grok-4.6',
|
|
@@ -149,6 +227,25 @@ describe('requireProvidedString / resolveAssignWorkerModel', () => {
|
|
|
149
227
|
});
|
|
150
228
|
|
|
151
229
|
describe('resolveAssignWorkerVariant', () => {
|
|
230
|
+
it('does not submit or delete a session whose create completes after cancellation', async () => {
|
|
231
|
+
const directory = root();
|
|
232
|
+
const controller = new AbortController();
|
|
233
|
+
const promptExisting = vi.fn();
|
|
234
|
+
const deleteSession = vi.fn();
|
|
235
|
+
const createSession = vi.fn(async () => {
|
|
236
|
+
controller.abort();
|
|
237
|
+
return { data: { id: 'ses_created' } };
|
|
238
|
+
});
|
|
239
|
+
await expect(assignSession({
|
|
240
|
+
prompt: 'Fix', directory, assistant: { providerID: 'p', modelID: 'm' },
|
|
241
|
+
allowedRoots: [directory], managedWorkspaceRoot: path.join(directory, 'assistant-workspaces'),
|
|
242
|
+
createSession, promptExisting, deleteSession, signal: controller.signal,
|
|
243
|
+
})).rejects.toMatchObject({ name: 'AbortError' });
|
|
244
|
+
expect(createSession).toHaveBeenCalledTimes(1);
|
|
245
|
+
expect(promptExisting).not.toHaveBeenCalled();
|
|
246
|
+
expect(deleteSession).not.toHaveBeenCalled();
|
|
247
|
+
});
|
|
248
|
+
|
|
152
249
|
it('reuses assistant variant only for same/default model; drops it cross-model', () => {
|
|
153
250
|
expect(resolveAssignWorkerVariant({
|
|
154
251
|
source: 'assistant',
|
|
@@ -466,4 +563,46 @@ describe('assignSession', () => {
|
|
|
466
563
|
expect(deleteSession).not.toHaveBeenCalled();
|
|
467
564
|
expect(promptExisting).toHaveBeenCalledWith(expect.objectContaining({ sessionID: 'ses_existing' }));
|
|
468
565
|
});
|
|
566
|
+
|
|
567
|
+
it('mapSessionToWatchStatus baselines terminal idle without inventing busy', () => {
|
|
568
|
+
expect(mapSessionToWatchStatus({
|
|
569
|
+
session: { id: 'ses_1', status: { type: 'idle' }, time: { completed: 1 } },
|
|
570
|
+
messages: [{ info: { role: 'assistant', time: { completed: 1 } } }],
|
|
571
|
+
})).toBe('complete');
|
|
572
|
+
expect(mapSessionToWatchStatus({
|
|
573
|
+
session: { id: 'ses_2', status: { type: 'busy' } },
|
|
574
|
+
})).toBe('busy');
|
|
575
|
+
expect(mapSessionToWatchStatus({
|
|
576
|
+
session: { id: 'ses_3', error: { message: 'boom' } },
|
|
577
|
+
})).toBe('error');
|
|
578
|
+
expect(mapSessionToWatchStatus({ missing: true })).toBe('complete');
|
|
579
|
+
expect(extractSessionDirectory({
|
|
580
|
+
directory: '/repo/app',
|
|
581
|
+
project: { worktree: '/other' },
|
|
582
|
+
})).toBe('/repo/app');
|
|
583
|
+
expect(extractSessionDirectory({
|
|
584
|
+
project: { worktree: '/repo/wt' },
|
|
585
|
+
})).toBe('/repo/wt');
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('resolveAssignDirectory accepts a realpath candidate against non-realpath allowed roots', () => {
|
|
589
|
+
const directory = root();
|
|
590
|
+
const project = path.join(directory, 'app');
|
|
591
|
+
fs.mkdirSync(project, { recursive: true });
|
|
592
|
+
const realProject = fs.realpathSync(project);
|
|
593
|
+
// First resolve returns realpath; second call with the same realpath must still pass.
|
|
594
|
+
const first = resolveAssignDirectory({
|
|
595
|
+
directory: project,
|
|
596
|
+
allowedRoots: [directory],
|
|
597
|
+
managedWorkspaceRoot: path.join(directory, 'assistant-workspaces'),
|
|
598
|
+
});
|
|
599
|
+
expect(first).toBe(realProject);
|
|
600
|
+
const second = resolveAssignDirectory({
|
|
601
|
+
directory: realProject,
|
|
602
|
+
projectPath: realProject,
|
|
603
|
+
allowedRoots: [directory],
|
|
604
|
+
managedWorkspaceRoot: path.join(directory, 'assistant-workspaces'),
|
|
605
|
+
});
|
|
606
|
+
expect(second).toBe(realProject);
|
|
607
|
+
});
|
|
469
608
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const SENTENCE = /(?<=[.!?])\s+(?=[A-Z“"‘])/u;
|
|
2
|
-
const CONTACT_PLANNING = /\b(let me think|i should|i need to|i'll call|i will call|the user (said|wants|asked)|actually,?|hmm\.?|assign_session|list_projects|list_sessions|create_assistant|schedule_task|tool call|projectPath)\b/i;
|
|
2
|
+
const CONTACT_PLANNING = /\b(let me think|i should|i need to|i'll call|i will call|the user (said|wants|asked)|actually,?|hmm\.?|assign_session|watch_session|stop_session|list_projects|list_sessions|create_assistant|schedule_task|tool call|projectPath)\b/i;
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* A short spoken contact line (not chain-of-thought). Shown before a tool and kept.
|