@openchambery/web 1.19.13 → 1.19.15-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AgentAvatar-mbvX7YlW.js → AgentAvatar-m1jNq4bX.js} +1 -1
- package/dist/assets/AssistantView-D5rEZ-Ss.js +2 -0
- package/dist/assets/{DiagramView-BdI4PT2H.js → DiagramView-DaB9Sb7S.js} +1 -1
- package/dist/assets/{FadeInOnReveal-moBbL8Ll.js → FadeInOnReveal-UGwClbL_.js} +1 -1
- package/dist/assets/Icon-D1BcR8Tz.js +1 -0
- package/dist/assets/MarkdownRenderer-emLeoNjI.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-2ewbRZE2.js → MarkdownRendererImpl-BWCAPlqn.js} +3 -3
- package/dist/assets/{MarkstreamRendererImpl-DXewO7S_.js → MarkstreamRendererImpl-BD_NFKUv.js} +2 -2
- package/dist/assets/MobileDetailNavigation-DqXVAIap.js +1 -0
- package/dist/assets/MobileShareBridge-Bk2fdkJk.js +4 -0
- package/dist/assets/MobileSurface-C4mpcOCh.js +2 -0
- package/dist/assets/{MultiRunWindow-C8ukIdLA.js → MultiRunWindow-rs49_K1E.js} +1 -1
- package/dist/assets/{OnboardingScreen-j0iCyH7f.js → OnboardingScreen-bIWeMLVx.js} +1 -1
- package/dist/assets/{OverlayScrollbar-CDC5B3RH.js → OverlayScrollbar-Bahg_P3r.js} +1 -1
- package/dist/assets/SettingsView-b8LUjoad.js +25 -0
- package/dist/assets/{SettingsWindow-CSePZ_V8.js → SettingsWindow-D8JFlPdA.js} +1 -1
- package/dist/assets/{TerminalView-BmSIrZ81.js → TerminalView-DCd-Godi.js} +1 -1
- package/dist/assets/{ToolOutputDialog-2BYaWLo1.js → ToolOutputDialog-B50L_nUP.js} +1 -1
- package/dist/assets/{appThemeRegistry-Dk4gBaOu.js → appThemeRegistry-BWtCaTMe.js} +1 -1
- package/dist/assets/{button-xx1UOBA5.js → button-9R-WgVMl.js} +1 -1
- package/dist/assets/{clipboard-CUasgPiF.js → clipboard-GUUNlWIv.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-DhMGbF_B.js} +1 -1
- package/dist/assets/{es-CRjOwvYM.js → es-BQb6I8gl.js} +2 -2
- package/dist/assets/fileMentionTouchSelection-DKYkoypI.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-BjGouC_l.js} +2 -2
- package/dist/assets/{gitApi-DtPf-h-O.js → gitApi-D4PyYgLZ.js} +3 -3
- package/dist/assets/{input-CI730njs.js → input-Cd_1HyQv.js} +1 -1
- package/dist/assets/insertionBoundaries-Bnnx03Wj.js +1 -0
- package/dist/assets/{ja-CYQ5aoWp.js → ja-Cw9zliYY.js} +2 -2
- package/dist/assets/{ko-BwASV0Q0.js → ko-Dyz0Dk9b.js} +2 -2
- package/dist/assets/main-BRiQTNFX.js +169 -0
- package/dist/assets/{main-Ci3JmtpW.js → main-CnY0LWNc.js} +2 -2
- package/dist/assets/{markdownHeightCache-DEjGKLv8.js → markdownHeightCache-BL8CUQ0M.js} +1 -1
- package/dist/assets/miniChat-B0pw_mUs.js +2 -0
- package/dist/assets/mobile-BKHmxwXz.js +2 -0
- package/dist/assets/{multirun-CmaTeZON.js → multirun-BQKqVfFT.js} +1 -1
- package/dist/assets/{persistence-YbQKzHUQ.js → persistence-DwRBC4zl.js} +2 -2
- package/dist/assets/{pl-DUI1vPil.js → pl-C7EY-dTH.js} +1 -1
- package/dist/assets/platform-DQ5_LNKO.js +2 -0
- package/dist/assets/{projectMeta-cw9nrwpr.js → projectMeta-BvG8sdtC.js} +2 -2
- package/dist/assets/{pt-BR-CZQFJllP.js → pt-BR-B1uHpjFW.js} +2 -2
- package/dist/assets/renderElectronMiniChatApp-E5a0yy7E.js +2 -0
- package/dist/assets/renderMobileApp-DT0L4Ah6.js +28 -0
- package/dist/assets/{runtimeConfig-DI8l0VyZ.js → runtimeConfig-utWzrDwa.js} +1 -1
- package/dist/assets/runtimeEndpointReset-DE6lwHyg.js +147 -0
- package/dist/assets/sessionChangeSummary-BG7l2heP.js +2 -0
- package/dist/assets/sessionLookup-12zVOR0J.js +1 -0
- package/dist/assets/{sortable-tabs-strip-Dom6Sthu.js → sortable-tabs-strip-dYW6SH62.js} +1 -1
- package/dist/assets/src-CFUi4riW.css +2 -0
- package/dist/assets/{store-CSnH_QXM.js → store-ClnJdJSj.js} +3 -3
- package/dist/assets/{terminalApi-noa4Cdbh.js → terminalApi-CfgOfnZk.js} +1 -1
- package/dist/assets/{textarea-BfNtUdJE.js → textarea-B9CmN13Z.js} +1 -1
- package/dist/assets/{tooltip-DPxumadF.js → tooltip-DSjr26A_.js} +1 -1
- package/dist/assets/{uk-C1FYZlmA.js → uk-DgG8v2md.js} +2 -2
- package/dist/assets/{url-CVhsI9kR.js → url-CMQwHq4k.js} +1 -1
- package/dist/assets/{useAppFontEffects-BDFUND4v.js → useAppFontEffects-BhYuP4L2.js} +43 -44
- package/dist/assets/useDesktopWindowControlsLayout-DPNMRkrI.js +1 -0
- package/dist/assets/{useEffectiveDirectory-e9HjIqGz.js → useEffectiveDirectory-Ji8AKgzY.js} +1 -1
- package/dist/assets/{useFontPreferences-CB-cGo3I.js → useFontPreferences-DCaY8NbU.js} +1 -1
- package/dist/assets/useMobileNavigationStore-BYdpuwL7.js +413 -0
- package/dist/assets/useSessionAutoCleanup-CADZCvZL.js +2 -0
- package/dist/assets/{useWorkerHighlightedLines-DTGE0CDs.js → useWorkerHighlightedLines-CuG88Lsj.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-C6OFKjeT.js} +2 -2
- package/dist/assets/{zh-TW-BsRk1Rbb.js → zh-TW-BVP99aMr.js} +2 -2
- 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 +120 -22
- package/server/lib/assistants/assign.js +117 -12
- package/server/lib/assistants/assign.test.js +79 -0
- package/server/lib/assistants/bubbles.js +1 -1
- package/server/lib/assistants/contact-store.js +520 -11
- package/server/lib/assistants/contact-store.test.js +362 -12
- package/server/lib/assistants/contact-tools.js +305 -54
- package/server/lib/assistants/contact-tools.test.js +239 -7
- package/server/lib/assistants/contracts.js +31 -0
- package/server/lib/assistants/harness.js +295 -132
- package/server/lib/assistants/harness.test.js +662 -10
- package/server/lib/assistants/pi-tools.js +2 -0
- package/server/lib/assistants/pi-tools.test.js +8 -0
- package/server/lib/assistants/routes.js +3 -0
- package/server/lib/assistants/service.js +861 -79
- package/server/lib/assistants/service.test.js +1234 -8
- package/server/lib/llm/DOCUMENTATION.md +10 -3
- package/server/lib/llm/completions.js +4 -0
- package/server/lib/llm/completions.test.js +3 -0
- package/server/lib/llm/generate.js +57 -11
- package/server/lib/llm/generate.test.js +92 -0
- package/server/lib/opencode/feature-routes-runtime.js +5 -0
- package/dist/assets/AssistantView-CKmIS1cS.js +0 -2
- package/dist/assets/Icon-CkzzhaLr.js +0 -1
- package/dist/assets/MarkdownRenderer-CLQ4Z8Z0.js +0 -2
- package/dist/assets/MobileDetailNavigation-BGaAyuBH.js +0 -1
- package/dist/assets/MobileShareBridge-BAZoyeIP.js +0 -4
- package/dist/assets/MobileSurface-BVb3xNWt.js +0 -2
- package/dist/assets/SettingsView-CkQ61pbr.js +0 -25
- package/dist/assets/insertionBoundaries-BMhzPFft.js +0 -4
- package/dist/assets/main-BjeOhdcG.js +0 -169
- package/dist/assets/miniChat-BYrjYEXJ.js +0 -2
- package/dist/assets/mobile-BVMNnPK2.js +0 -2
- package/dist/assets/platform-DzsGqlGW.js +0 -2
- package/dist/assets/renderElectronMiniChatApp-COwia8Cs.js +0 -2
- package/dist/assets/renderMobileApp-CXwwpY1t.js +0 -28
- package/dist/assets/runtimeEndpointReset-BkAQXmXJ.js +0 -147
- package/dist/assets/sessionChangeSummary-DQDZ5HW-.js +0 -1
- package/dist/assets/sessionLookup-Imc995rW.js +0 -1
- package/dist/assets/src-AA-5nXsF.css +0 -2
- package/dist/assets/useDesktopWindowControlsLayout-C5hZD1co.js +0 -1
- package/dist/assets/useMobileNavigationStore-CYbDxRTM.js +0 -413
- package/dist/assets/useSessionAutoCleanup-BGy9_AhN.js +0 -2
|
@@ -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,6 +279,76 @@ 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
|
+
|
|
265
352
|
/**
|
|
266
353
|
* Normalize a session/message model object to { providerID, modelID }.
|
|
267
354
|
* Accepts `{ providerID, id }` or `{ providerID, modelID }`. Lookup failures stay null.
|
|
@@ -408,7 +495,7 @@ export function resolveAssignWorkerModel({
|
|
|
408
495
|
}
|
|
409
496
|
|
|
410
497
|
const models = catalog.models.filter((entry) => entry && typeof entry === 'object');
|
|
411
|
-
|
|
498
|
+
let matches = models.filter((entry) => {
|
|
412
499
|
const entryProvider = trim(entry.providerID, 256);
|
|
413
500
|
const entryModel = trim(entry.modelID, 256);
|
|
414
501
|
const entryName = trim(entry.name, 512);
|
|
@@ -420,6 +507,7 @@ export function resolveAssignWorkerModel({
|
|
|
420
507
|
return entryProvider === wantedProvider;
|
|
421
508
|
}
|
|
422
509
|
if (!wantedProvider && wantedModel) {
|
|
510
|
+
if (explicitModel) return entryModel === wantedModel;
|
|
423
511
|
const needle = wantedModel.toLowerCase();
|
|
424
512
|
return entryModel.toLowerCase() === needle
|
|
425
513
|
|| (entryName && entryName.toLowerCase() === needle)
|
|
@@ -428,6 +516,19 @@ export function resolveAssignWorkerModel({
|
|
|
428
516
|
return false;
|
|
429
517
|
});
|
|
430
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
|
+
|
|
431
532
|
if (matches.length === 0) {
|
|
432
533
|
const label = wantedProvider && wantedModel
|
|
433
534
|
? `${wantedProvider}/${wantedModel}`
|
|
@@ -638,6 +739,7 @@ export async function assignSession(input = {}) {
|
|
|
638
739
|
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Worker session APIs are unavailable.');
|
|
639
740
|
}
|
|
640
741
|
|
|
742
|
+
input.signal?.throwIfAborted();
|
|
641
743
|
let targetSessionID = sessionID;
|
|
642
744
|
if (!targetSessionID) {
|
|
643
745
|
let created;
|
|
@@ -658,6 +760,9 @@ export async function assignSession(input = {}) {
|
|
|
658
760
|
}
|
|
659
761
|
}
|
|
660
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();
|
|
661
766
|
let prompted;
|
|
662
767
|
let promptError;
|
|
663
768
|
try {
|
|
@@ -9,9 +9,11 @@ import {
|
|
|
9
9
|
attachmentScopeKey,
|
|
10
10
|
buildAssignParts,
|
|
11
11
|
extractAssignSessionModel,
|
|
12
|
+
extractSessionDirectory,
|
|
12
13
|
hasAssignImageParts,
|
|
13
14
|
isAmbiguousPromptFailure,
|
|
14
15
|
isManagedAssistantWorkspace,
|
|
16
|
+
mapSessionToWatchStatus,
|
|
15
17
|
normalizeAssignSessionModel,
|
|
16
18
|
requireProvidedString,
|
|
17
19
|
resolveAssignDirectory,
|
|
@@ -65,6 +67,22 @@ describe('resolveAssignDirectory', () => {
|
|
|
65
67
|
});
|
|
66
68
|
|
|
67
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
|
+
|
|
68
86
|
const catalog = {
|
|
69
87
|
models: [
|
|
70
88
|
{ providerID: 'openai', modelID: 'gpt-4o', name: 'GPT-4o', acceptsImages: true },
|
|
@@ -209,6 +227,25 @@ describe('requireProvidedString / resolveAssignWorkerModel', () => {
|
|
|
209
227
|
});
|
|
210
228
|
|
|
211
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
|
+
|
|
212
249
|
it('reuses assistant variant only for same/default model; drops it cross-model', () => {
|
|
213
250
|
expect(resolveAssignWorkerVariant({
|
|
214
251
|
source: 'assistant',
|
|
@@ -526,4 +563,46 @@ describe('assignSession', () => {
|
|
|
526
563
|
expect(deleteSession).not.toHaveBeenCalled();
|
|
527
564
|
expect(promptExisting).toHaveBeenCalledWith(expect.objectContaining({ sessionID: 'ses_existing' }));
|
|
528
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
|
+
});
|
|
529
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.
|