@openchambery/web 1.19.7-beta.2 → 1.19.7-beta.21
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-DI2zs2WH.js +1 -0
- package/dist/assets/AssistantView-sV1PVQiM.js +2 -0
- package/dist/assets/DiagramView-D2hqP5VV.js +1 -0
- package/dist/assets/FadeInOnReveal-BWJcUp5P.js +1 -0
- package/dist/assets/Icon-4My8YZwt.js +1 -0
- package/dist/assets/MarkdownLoadingSkeleton-Vezjvk9B.js +3 -0
- package/dist/assets/MarkdownRenderer-x091Z--3.js +2 -0
- package/dist/assets/MarkdownRendererImpl-DML8BSro.js +7 -0
- package/dist/assets/MarkstreamRendererImpl-BK-HM2k3.js +1 -0
- package/dist/assets/{MarkstreamRendererImpl-CR0fBG5w.css → MarkstreamRendererImpl-Ca7RATbs.css} +1 -1
- package/dist/assets/MobileDetailNavigation-DcxkWiks.js +1 -0
- package/dist/assets/MobileShareBridge-TAGcOiyb.js +4 -0
- package/dist/assets/MobileSurface-zqlZzEFQ.js +2 -0
- package/dist/assets/{MultiRunWindow-khpnWyZO.js → MultiRunWindow-iH1d9S14.js} +1 -1
- package/dist/assets/OnboardingScreen-BNEtPmvw.js +1 -0
- package/dist/assets/OverlayScrollbar-BZB21Yp_.js +2 -0
- package/dist/assets/SettingsView-CzW23DZA.js +25 -0
- package/dist/assets/{SettingsWindow-Dx5rU9v5.js → SettingsWindow-DZXCWZid.js} +1 -1
- package/dist/assets/TerminalView-pU0wpD8s.js +8 -0
- package/dist/assets/ToolOutputDialog-Bt7JChFG.js +10 -0
- package/dist/assets/appThemeRegistry-BI7WR2Wo.js +1 -0
- package/dist/assets/{button-z0GOBtWi.js → button-Or0daT9o.js} +1 -1
- package/dist/assets/clipboard-BXxLshVq.js +1 -0
- package/dist/assets/{collapsible-D9zJRpeB.js → collapsible-C-kzSGkq.js} +1 -1
- package/dist/assets/context-menu-Dpjla5p4.js +1 -0
- package/dist/assets/desktopHosts-BzSE0sK7.js +1 -0
- package/dist/assets/desktopRelayRestore-BaKjk1kQ.js +1 -0
- package/dist/assets/{diagram-CoEqmReY.js → diagram-D0mT3mo1.js} +1 -1
- package/dist/assets/{dialog-HCE4eLUW.js → dialog-BkhHBTd4.js} +1 -1
- package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +1 -0
- package/dist/assets/{es-D9KcV5TX.js → es-BHqnSjoZ.js} +1 -1
- package/dist/assets/fileQueries-DFY6Kiy1.js +1 -0
- package/dist/assets/fileReferenceActions-Di_DoDex.js +1 -0
- package/dist/assets/{fr-DPA9qUl3.js → fr-BfrlJY9i.js} +1 -1
- package/dist/assets/{gitApi-DNTMAa3F.js → gitApi-CeFszLvk.js} +3 -3
- package/dist/assets/{input-BG3VWTIF.js → input-BUsGnzwe.js} +1 -1
- package/dist/assets/insertionBoundaries-DDJNusn2.js +4 -0
- package/dist/assets/{ja-Dsif3NoD.js → ja-DP-7qt-x.js} +1 -1
- package/dist/assets/{ko-Dixx4Ctg.js → ko-E2B5vGQP.js} +1 -1
- package/dist/assets/main-CjnOsKDL.js +169 -0
- package/dist/assets/main-j-Ij13jc.js +2 -0
- package/dist/assets/markdown-worker-BuveaH3O.js +1 -0
- package/dist/assets/markdownTheme-CVS01wHf.js +2 -0
- package/dist/assets/miniChat-DgxavREE.js +2 -0
- package/dist/assets/mobile-C0rwPMPM.js +2 -0
- package/dist/assets/multirun-ot7-bKg7.js +4 -0
- package/dist/assets/outsideFileGrants-BB4GPvCq.js +1 -0
- package/dist/assets/path-utils-jGozTg9d.js +1 -0
- package/dist/assets/persistence-CSZp1CNB.js +3 -0
- package/dist/assets/{pl-Bj78ZGgf.js → pl-r8X5gRM5.js} +1 -1
- package/dist/assets/platform-CenlWqxu.js +2 -0
- package/dist/assets/projectMeta-3yOEc0oI.js +3 -0
- package/dist/assets/{pt-BR-CFL3xSAf.js → pt-BR-DcpdXCf5.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-BPNvFA_N.js +2 -0
- package/dist/assets/renderMobileApp-CNog29Xy.js +28 -0
- package/dist/assets/runtimeConfig-D28kNEYO.js +2 -0
- package/dist/assets/runtimeEndpointReset-CorvFIRk.js +147 -0
- package/dist/assets/sessionChangeSummary-CX2MJIkc.js +1 -0
- package/dist/assets/sessionLookup-CbatbQf7.js +1 -0
- package/dist/assets/sortable-tabs-strip-C5gGnikj.js +1 -0
- package/dist/assets/src-Af9cMFKF.css +2 -0
- package/dist/assets/{store-DzpbXWX1.js → store-BDFdnk6l.js} +2 -2
- package/dist/assets/terminalApi-CIIs9vFx.js +1 -0
- package/dist/assets/textarea-CIO34TZz.js +1 -0
- package/dist/assets/toolHelpers-DEvVP9U0.js +2 -0
- package/dist/assets/{tooltip-CtH61mQh.js → tooltip-BSrJYYYc.js} +1 -1
- package/dist/assets/{uk-BY60Ub3Z.js → uk-Di0vl9V-.js} +1 -1
- package/dist/assets/url-CE_gAfNa.js +1 -0
- package/dist/assets/{ChatContainer-CnOrdAgs.js → useAppFontEffects-CDpeZWK4.js} +44 -39
- package/dist/assets/useAssistantUIStore-BHacrxe_.js +1 -0
- package/dist/assets/useDesktopWindowControlsLayout-D-xPByG0.js +1 -0
- package/dist/assets/useEffectiveDirectory-Dp-tez_r.js +1 -0
- package/dist/assets/useFontPreferences-BxOHKuVw.js +1 -0
- package/dist/assets/useMobileNavigationStore-DE1Pe9A9.js +412 -0
- package/dist/assets/useSessionAutoCleanup-jNJ_WC38.js +1 -0
- package/dist/assets/useUIStore-CqqO11ZI.js +1 -0
- package/dist/assets/useWorkerHighlightedLines-MDnGj3yv.js +7 -0
- package/dist/assets/utils-B0mQXN9L.js +1 -0
- package/dist/assets/{vendor-markstream-react-CwvbEg9R.js → vendor-markstream-react-C3K8YH0d.js} +4 -4
- package/dist/assets/{zh-CN-Cox1LaOW.js → zh-CN-coFCfeQE.js} +1 -1
- package/dist/assets/{zh-TW-B6RU0mxa.js → zh-TW-oDRZ1-yb.js} +1 -1
- package/dist/index.html +9 -11
- package/dist/mini-chat.html +9 -11
- package/dist/mobile.html +15 -17
- package/package.json +2 -1
- package/server/index.js +2 -0
- package/server/lib/assistant-delivery-parts.js +3 -1
- package/server/lib/assistant-delivery-parts.test.js +11 -0
- package/server/lib/assistants/DOCUMENTATION.md +76 -3
- package/server/lib/assistants/assign.js +225 -0
- package/server/lib/assistants/assign.test.js +118 -0
- package/server/lib/assistants/bubbles.js +45 -0
- package/server/lib/assistants/bubbles.test.js +36 -0
- package/server/lib/assistants/cards.js +148 -0
- package/server/lib/assistants/cards.test.js +116 -0
- package/server/lib/assistants/contact-store.js +284 -0
- package/server/lib/assistants/contact-store.test.js +131 -0
- package/server/lib/assistants/contact-tools.js +761 -0
- package/server/lib/assistants/contact-tools.test.js +328 -0
- package/server/lib/assistants/contracts.js +20 -1
- package/server/lib/assistants/harness.js +551 -0
- package/server/lib/assistants/harness.test.js +607 -0
- package/server/lib/assistants/routes.js +44 -4
- package/server/lib/assistants/service.js +930 -17
- package/server/lib/assistants/service.test.js +1163 -45
- package/server/lib/llm/DOCUMENTATION.md +84 -0
- package/server/lib/llm/catalog.js +100 -0
- package/server/lib/llm/catalog.test.js +78 -0
- package/server/lib/llm/completions.js +151 -0
- package/server/lib/llm/completions.test.js +127 -0
- package/server/lib/llm/generate.js +570 -0
- package/server/lib/llm/generate.test.js +459 -0
- package/server/lib/llm/routes.js +47 -0
- package/server/lib/llm/temp-directory.js +37 -0
- package/server/lib/notifications/APNS.md +23 -17
- package/server/lib/notifications/DOCUMENTATION.md +5 -4
- package/server/lib/notifications/apns-runtime.js +139 -20
- package/server/lib/notifications/apns-runtime.test.js +66 -7
- package/server/lib/notifications/apns-titles.js +62 -62
- package/server/lib/notifications/apns-titles.test.js +3 -1
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +177 -86
- package/server/lib/notifications/runtime.test.js +122 -0
- package/server/lib/opencode/feature-routes-runtime.js +30 -0
- package/server/lib/opencode/feature-routes-runtime.test.js +20 -0
- package/server/lib/opencode/settings-runtime.js +18 -2
- package/server/lib/session-goal/DOCUMENTATION.md +8 -1
- package/server/lib/session-goal/runtime.js +20 -0
- package/server/lib/session-index/DOCUMENTATION.md +3 -1
- package/server/lib/session-index/service.js +10 -4
- package/server/lib/session-index/service.test.js +37 -0
- package/server/lib/session-title/runtime.js +1 -0
- package/server/lib/session-title/runtime.test.js +11 -0
- package/dist/assets/AgentAvatar-ziUs4sw3.js +0 -1
- package/dist/assets/AssistantView-DF4yXFgS.js +0 -1
- package/dist/assets/ContextPanel-Cg_uCHzJ.js +0 -285
- package/dist/assets/DiagramView-DYh3Zn2y.js +0 -1
- package/dist/assets/FadeInOnReveal-BAlsRFJH.js +0 -1
- package/dist/assets/Icon-CLpn1mIz.js +0 -1
- package/dist/assets/MarkdownRendererImpl-QzUEpBEm.js +0 -7
- package/dist/assets/MarkstreamRendererImpl-CXrfeUia.js +0 -1
- package/dist/assets/MobileDetailNavigation-DJXEYfq4.js +0 -1
- package/dist/assets/MobileShareBridge-CliVo1YH.js +0 -4
- package/dist/assets/MobileSurface-9Xh6WQkY.js +0 -2
- package/dist/assets/OnboardingScreen-CJYyR54B.js +0 -1
- package/dist/assets/OverlayScrollbar-C-jZ8ITG.js +0 -2
- package/dist/assets/SettingsView-DyCZehFG.js +0 -25
- package/dist/assets/TerminalView-XSALfUaH.js +0 -8
- package/dist/assets/ToolOutputDialog-BfuXazW9.js +0 -10
- package/dist/assets/appThemeRegistry-DDWrniMM.js +0 -2
- package/dist/assets/clipboard-Bz0Zm9VJ.js +0 -1
- package/dist/assets/desktop-CfLlmafK.js +0 -1
- package/dist/assets/desktopHosts-C1Ns3DUu.js +0 -1
- package/dist/assets/desktopRelayRestore-LBt0CBOj.js +0 -1
- package/dist/assets/fileQueries-Dnn15WsA.js +0 -1
- package/dist/assets/insertionBoundaries-DMxuUnL_.js +0 -4
- package/dist/assets/main-B9HVtgXE.js +0 -2
- package/dist/assets/main-ZNyIs3Sn.js +0 -29
- package/dist/assets/markdown-worker-y3ewaqAK.js +0 -4
- package/dist/assets/markdownTheme-TdhQOZSN.js +0 -2
- package/dist/assets/miniChat-q0T0WGkZ.js +0 -2
- package/dist/assets/mobile-DEduvLJj.js +0 -2
- package/dist/assets/multirun-DU-wrxlG.js +0 -4
- package/dist/assets/persistence-CmjKImz2.js +0 -3
- package/dist/assets/platform-skqcMARt.js +0 -2
- package/dist/assets/projectMeta-DNWx6Uva.js +0 -3
- package/dist/assets/radio-BEonIX1k.js +0 -1
- package/dist/assets/renderElectronMiniChatApp--JYWY41L.js +0 -2
- package/dist/assets/renderMobileApp-B0Ix_uNo.js +0 -25
- package/dist/assets/runtime-switch-BkpTTbEi.js +0 -1
- package/dist/assets/runtimeConfig-BTDIG7mX.js +0 -2
- package/dist/assets/runtimeEndpointReset-CZQCYtVI.js +0 -4
- package/dist/assets/shikiHighlight-C9Cx56Qg.js +0 -1
- package/dist/assets/sortable-tabs-strip-Bj8N0VWf.js +0 -1
- package/dist/assets/src-2eFAiBLq.css +0 -2
- package/dist/assets/terminalApi-CNqqwjfG.js +0 -1
- package/dist/assets/url-KeihbE0d.js +0 -1
- package/dist/assets/useAppFontEffects-CKLSamae.js +0 -7
- package/dist/assets/useAssistantUIStore-DaplPyb0.js +0 -1
- package/dist/assets/useDesktopWindowControlsLayout-DJSkFfhT.js +0 -1
- package/dist/assets/useEffectiveDirectory-B3rbY1Kv.js +0 -1
- package/dist/assets/useFontPreferences-D8emhcNv.js +0 -1
- package/dist/assets/useMobileNavigationStore-CAvVUItx.js +0 -412
- package/dist/assets/useSessionAutoCleanup-DxNgqJ8s.js +0 -1
- package/dist/assets/useUIStore-B7KJS5G7.js +0 -1
- package/dist/assets/useWorkerHighlightedLines-DiDF68LR.js +0 -7
- package/dist/assets/utils-CTL16t2f.js +0 -1
|
@@ -47,9 +47,20 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
47
47
|
return pendingPushTags.size;
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
const stringifyApnsData = (data) => {
|
|
51
|
+
if (!data || typeof data !== 'object') return undefined;
|
|
52
|
+
const next = {};
|
|
53
|
+
for (const [key, value] of Object.entries(data)) {
|
|
54
|
+
if (typeof value !== 'string' || value.length === 0) continue;
|
|
55
|
+
next[key] = value;
|
|
56
|
+
}
|
|
57
|
+
return Object.keys(next).length > 0 ? next : undefined;
|
|
58
|
+
};
|
|
59
|
+
|
|
50
60
|
// Generic notification for native push (per the mobile design): a fixed, scenario-based
|
|
51
61
|
// title + the session name as the body. No model/project/message content crosses the relay.
|
|
52
62
|
// Title strings are localized per device token locale inside sendApnsToAllUiSessions.
|
|
63
|
+
// Contact turns opt out via preserveAlert — those keep nickname + spoken text.
|
|
53
64
|
const toApnsGenericPayload = (payload) => {
|
|
54
65
|
const data = payload?.data && typeof payload.data === 'object' ? payload.data : {};
|
|
55
66
|
const sessionName = typeof data.sessionName === 'string' && data.sessionName.trim().length > 0
|
|
@@ -66,15 +77,39 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
66
77
|
};
|
|
67
78
|
};
|
|
68
79
|
|
|
80
|
+
const toApnsContactPayload = (payload) => {
|
|
81
|
+
const data = payload?.data && typeof payload.data === 'object' ? payload.data : {};
|
|
82
|
+
const title = typeof payload?.title === 'string' && payload.title.trim()
|
|
83
|
+
? payload.title.trim()
|
|
84
|
+
: 'Assistant';
|
|
85
|
+
const body = typeof payload?.body === 'string' ? payload.body : '';
|
|
86
|
+
return {
|
|
87
|
+
title,
|
|
88
|
+
body,
|
|
89
|
+
badge: trackPushAndCountBadge(typeof payload?.tag === 'string' ? payload.tag : undefined),
|
|
90
|
+
tag: payload?.tag,
|
|
91
|
+
data: stringifyApnsData({
|
|
92
|
+
assistantID: data.assistantID,
|
|
93
|
+
url: typeof data.assistantID === 'string' && data.assistantID.trim()
|
|
94
|
+
? `openchamber://assistant/${encodeURIComponent(data.assistantID.trim())}`
|
|
95
|
+
: (typeof data.url === 'string' ? data.url : undefined),
|
|
96
|
+
}),
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
69
100
|
// Fan a notification out to every delivery channel: browser web-push (full templated
|
|
70
|
-
// payload) and native iOS APNs (generic model-based text).
|
|
71
|
-
// a failure in one channel must not block the other.
|
|
72
|
-
// never suppresses delivery — every subscribed
|
|
73
|
-
|
|
101
|
+
// payload) and native iOS APNs (generic model-based text, unless preserveAlert).
|
|
102
|
+
// Both share the dedup tag; a failure in one channel must not block the other.
|
|
103
|
+
// Visibility on any other client never suppresses delivery — every subscribed
|
|
104
|
+
// surface gets the push.
|
|
105
|
+
const fanoutPush = (payload, options = {}) => Promise.all([
|
|
74
106
|
Promise.resolve(sendPushToAllUiSessions?.(payload, options)).catch((error) => {
|
|
75
107
|
console.warn('[Push] web-push fanout failed:', error?.message ?? error);
|
|
76
108
|
}),
|
|
77
|
-
Promise.resolve(sendApnsToAllUiSessions?.(
|
|
109
|
+
Promise.resolve(sendApnsToAllUiSessions?.(
|
|
110
|
+
options.preserveAlert === true ? toApnsContactPayload(payload) : toApnsGenericPayload(payload),
|
|
111
|
+
options,
|
|
112
|
+
)).catch((error) => {
|
|
78
113
|
console.warn('[APNs] fanout failed:', error?.message ?? error);
|
|
79
114
|
}),
|
|
80
115
|
]);
|
|
@@ -97,6 +132,8 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
97
132
|
|
|
98
133
|
const sessionMetaCache = new Map();
|
|
99
134
|
const SESSION_META_CACHE_TTL_MS = 60 * 1000;
|
|
135
|
+
const HIDDEN_SESSION_TITLES = new Set(['smartfetch-secondary']);
|
|
136
|
+
const nonEmptySystemID = (value) => typeof value === 'string' && value.length > 0;
|
|
100
137
|
|
|
101
138
|
// Sessions where the client has enabled Permission Auto-Accept. Mirrored
|
|
102
139
|
// from the client-side permissionStore via POST /api/notifications/auto-accept
|
|
@@ -148,32 +185,47 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
148
185
|
smallModelPurpose: meta.smallModelPurpose !== undefined
|
|
149
186
|
? meta.smallModelPurpose
|
|
150
187
|
: (previous?.smallModelPurpose ?? null),
|
|
188
|
+
assistantID: meta.assistantID !== undefined ? meta.assistantID : (previous?.assistantID ?? null),
|
|
189
|
+
assignedFrom: meta.assignedFrom !== undefined ? meta.assignedFrom : (previous?.assignedFrom ?? null),
|
|
190
|
+
scheduledTaskID: meta.scheduledTaskID !== undefined
|
|
191
|
+
? meta.scheduledTaskID
|
|
192
|
+
: (previous?.scheduledTaskID ?? null),
|
|
193
|
+
title: meta.title !== undefined ? meta.title : (previous?.title ?? null),
|
|
194
|
+
complete: meta.complete === true || previous?.complete === true,
|
|
151
195
|
at: Date.now(),
|
|
152
196
|
});
|
|
153
197
|
};
|
|
154
198
|
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
199
|
+
const metaFromOpenchamber = (openchamber, extras = {}) => {
|
|
200
|
+
const assignedFrom = typeof openchamber?.assigned?.from === 'string'
|
|
201
|
+
? openchamber.assigned.from
|
|
202
|
+
: null;
|
|
203
|
+
return {
|
|
204
|
+
smallModelPurpose: normalizeSmallModelPurpose(openchamber?.smallModel?.purpose),
|
|
205
|
+
assistantID: nonEmptySystemID(openchamber?.assistant?.assistantID)
|
|
206
|
+
? openchamber.assistant.assistantID
|
|
207
|
+
: null,
|
|
208
|
+
assignedFrom,
|
|
209
|
+
scheduledTaskID: nonEmptySystemID(openchamber?.scheduledTask?.taskID)
|
|
210
|
+
? openchamber.scheduledTask.taskID
|
|
211
|
+
: null,
|
|
212
|
+
...extras,
|
|
213
|
+
};
|
|
165
214
|
};
|
|
166
215
|
|
|
167
216
|
const maybeCacheSessionMetaFromPayload = (payload) => {
|
|
217
|
+
if (!payload || typeof payload !== 'object') return;
|
|
218
|
+
if (payload.type !== 'session.created' && payload.type !== 'session.updated') return;
|
|
219
|
+
const info = payload.properties?.info;
|
|
220
|
+
if (!info || typeof info !== 'object') return;
|
|
168
221
|
const sessionId = extractSessionIdFromPayload(payload);
|
|
169
222
|
if (typeof sessionId !== 'string' || sessionId.length === 0) return;
|
|
170
223
|
const directory = extractDirectoryFromPayload(payload);
|
|
171
|
-
const parentID = getParentIdFromPayload(payload);
|
|
172
|
-
const smallModelPurpose = getSmallModelPurposeFromPayload(payload);
|
|
173
|
-
if (parentID === undefined && smallModelPurpose === undefined) return;
|
|
174
224
|
setCachedSessionMeta(sessionId, directory, {
|
|
175
|
-
|
|
176
|
-
|
|
225
|
+
parentID: normalizeParentID(info.parentID),
|
|
226
|
+
title: typeof info.title === 'string' ? info.title : null,
|
|
227
|
+
complete: true,
|
|
228
|
+
...metaFromOpenchamber(info.metadata?.openchamber),
|
|
177
229
|
});
|
|
178
230
|
};
|
|
179
231
|
|
|
@@ -181,7 +233,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
181
233
|
if (!sessionId) return undefined;
|
|
182
234
|
|
|
183
235
|
const cached = getCachedSessionMeta(sessionId, directory);
|
|
184
|
-
if (cached
|
|
236
|
+
if (cached?.complete) return cached;
|
|
185
237
|
|
|
186
238
|
try {
|
|
187
239
|
const base = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}`, '');
|
|
@@ -195,21 +247,23 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
195
247
|
signal: AbortSignal.timeout(2000),
|
|
196
248
|
});
|
|
197
249
|
if (!response.ok) {
|
|
198
|
-
return
|
|
250
|
+
return cached;
|
|
199
251
|
}
|
|
200
252
|
const session = await response.json().catch(() => null);
|
|
201
253
|
if (!session || typeof session !== 'object') {
|
|
202
|
-
return
|
|
254
|
+
return cached;
|
|
203
255
|
}
|
|
204
256
|
|
|
205
257
|
const meta = {
|
|
206
258
|
parentID: normalizeParentID(session.parentID),
|
|
207
|
-
|
|
259
|
+
title: typeof session.title === 'string' ? session.title : null,
|
|
260
|
+
complete: true,
|
|
261
|
+
...metaFromOpenchamber(session.metadata?.openchamber),
|
|
208
262
|
};
|
|
209
263
|
setCachedSessionMeta(sessionId, directory, meta);
|
|
210
|
-
return meta;
|
|
264
|
+
return getCachedSessionMeta(sessionId, directory) ?? meta;
|
|
211
265
|
} catch {
|
|
212
|
-
return
|
|
266
|
+
return cached;
|
|
213
267
|
}
|
|
214
268
|
};
|
|
215
269
|
|
|
@@ -234,20 +288,25 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
234
288
|
return false;
|
|
235
289
|
};
|
|
236
290
|
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
291
|
+
const isHiddenFromNavSession = (meta) => {
|
|
292
|
+
if (!meta) return false;
|
|
293
|
+
if (typeof meta.parentID === 'string' && meta.parentID.length > 0) return true;
|
|
294
|
+
if (typeof meta.smallModelPurpose === 'string' && meta.smallModelPurpose.length > 0) return true;
|
|
295
|
+
if (typeof meta.assistantID === 'string' && meta.assistantID.length > 0 && meta.assignedFrom !== 'contact') {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
if (typeof meta.scheduledTaskID === 'string' && meta.scheduledTaskID.length > 0) return true;
|
|
299
|
+
if (typeof meta.title === 'string' && HIDDEN_SESSION_TITLES.has(meta.title)) return true;
|
|
300
|
+
return false;
|
|
245
301
|
};
|
|
246
302
|
|
|
303
|
+
// Only sessions that can appear as sidebar roots get ordinary push. Child /
|
|
304
|
+
// subagent sessions, archived Assistant bindings, scheduled-task sessions,
|
|
305
|
+
// small-model system sessions, and SmartFetch secondaries are excluded.
|
|
306
|
+
// Contact-assigned worker sessions stay visible and still notify.
|
|
247
307
|
const shouldSkipSystemSessionNotification = async (sessionId, directory) => {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return false;
|
|
308
|
+
const meta = await fetchSessionMeta(sessionId, directory);
|
|
309
|
+
return isHiddenFromNavSession(meta);
|
|
251
310
|
};
|
|
252
311
|
|
|
253
312
|
const extractSessionIdFromPayload = (payload) => {
|
|
@@ -273,40 +332,6 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
273
332
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
274
333
|
};
|
|
275
334
|
|
|
276
|
-
const formatMode = (raw) => {
|
|
277
|
-
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
278
|
-
const normalized = value.length > 0 ? value : 'agent';
|
|
279
|
-
return normalized
|
|
280
|
-
.split(/[-_\s]+/)
|
|
281
|
-
.filter(Boolean)
|
|
282
|
-
.map((token) => token.charAt(0).toUpperCase() + token.slice(1))
|
|
283
|
-
.join(' ');
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
const formatModelId = (raw) => {
|
|
287
|
-
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
288
|
-
if (!value) {
|
|
289
|
-
return 'Assistant';
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const tokens = value.split(/[-_]+/).filter(Boolean);
|
|
293
|
-
const result = [];
|
|
294
|
-
for (let i = 0; i < tokens.length; i += 1) {
|
|
295
|
-
const current = tokens[i];
|
|
296
|
-
const next = tokens[i + 1];
|
|
297
|
-
if (/^\d+$/.test(current) && next && /^\d+$/.test(next)) {
|
|
298
|
-
result.push(`${current}.${next}`);
|
|
299
|
-
i += 1;
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
result.push(current);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return result
|
|
306
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
307
|
-
.join(' ');
|
|
308
|
-
};
|
|
309
|
-
|
|
310
335
|
// A session with an ACTIVE goal suppresses per-turn ready notifications;
|
|
311
336
|
// the session-goal runtime sends its own notification when the goal
|
|
312
337
|
// settles. Fetch failures fall through to normal notification behavior.
|
|
@@ -338,6 +363,20 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
338
363
|
|
|
339
364
|
const sessionId = extractSessionIdFromPayload(payload);
|
|
340
365
|
const notificationDirectory = extractDirectoryFromPayload(payload);
|
|
366
|
+
if ((payload.type === 'session.updated' || payload.type === 'session.created') && sessionId) {
|
|
367
|
+
const title = payload.properties?.info?.title;
|
|
368
|
+
if (
|
|
369
|
+
typeof title === 'string'
|
|
370
|
+
&& title.trim()
|
|
371
|
+
&& !(await shouldSkipSystemSessionNotification(sessionId, notificationDirectory))
|
|
372
|
+
) {
|
|
373
|
+
try {
|
|
374
|
+
await sendLiveActivityEnd?.({ sessionId, title: title.trim() });
|
|
375
|
+
} catch (error) {
|
|
376
|
+
console.warn('[Live Activity] title update failed:', error?.message ?? error);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
341
380
|
if ((payload.type === 'session.idle' || payload.type === 'session.error') && sessionId) {
|
|
342
381
|
const error = payload.properties?.error;
|
|
343
382
|
const errorText = typeof error?.message === 'string'
|
|
@@ -404,13 +443,13 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
404
443
|
}
|
|
405
444
|
lastReadyNotificationAt.set(sessionId, now);
|
|
406
445
|
|
|
407
|
-
let title =
|
|
408
|
-
let body =
|
|
446
|
+
let title = 'Task completed';
|
|
447
|
+
let body = '';
|
|
409
448
|
let sessionName = '';
|
|
410
449
|
|
|
411
450
|
try {
|
|
412
451
|
const templates = settings.notificationTemplates || {};
|
|
413
|
-
const completionTemplate = templates.completion || { title: '
|
|
452
|
+
const completionTemplate = templates.completion || { title: 'Task completed', message: '{session_name}' };
|
|
414
453
|
|
|
415
454
|
const variables = await buildTemplateVariables(payload, sessionId);
|
|
416
455
|
sessionName = typeof variables.session_name === 'string' ? variables.session_name : sessionName;
|
|
@@ -430,8 +469,10 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
430
469
|
const resolvedBody = resolveNotificationTemplate(completionTemplate.message, variables);
|
|
431
470
|
if (resolvedTitle) title = resolvedTitle;
|
|
432
471
|
if (shouldApplyResolvedTemplateMessage(completionTemplate.message, resolvedBody, variables)) body = resolvedBody;
|
|
472
|
+
if (!body) body = sessionName || resolvedBody || 'Session';
|
|
433
473
|
} catch (error) {
|
|
434
474
|
console.warn('[Notification] Template resolution failed, using defaults:', error?.message || error);
|
|
475
|
+
if (!body) body = sessionName || 'Session';
|
|
435
476
|
}
|
|
436
477
|
|
|
437
478
|
if (settings.nativeNotificationsEnabled) {
|
|
@@ -492,12 +533,8 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
492
533
|
const header = typeof firstQuestion?.header === 'string' ? firstQuestion.header.trim() : '';
|
|
493
534
|
const questionText = typeof firstQuestion?.question === 'string' ? firstQuestion.question.trim() : '';
|
|
494
535
|
|
|
495
|
-
let title =
|
|
496
|
-
|
|
497
|
-
: /build\s*agent/i.test(header)
|
|
498
|
-
? 'Switch to build mode'
|
|
499
|
-
: header || 'Input needed';
|
|
500
|
-
let body = questionText || 'Agent is waiting for your response';
|
|
536
|
+
let title = 'Needs your answer';
|
|
537
|
+
let body = '';
|
|
501
538
|
let sessionName = '';
|
|
502
539
|
|
|
503
540
|
try {
|
|
@@ -506,14 +543,16 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
506
543
|
variables.last_message = questionText || header || '';
|
|
507
544
|
|
|
508
545
|
const templates = settings.notificationTemplates || {};
|
|
509
|
-
const questionTemplate = templates.question || { title: '
|
|
546
|
+
const questionTemplate = templates.question || { title: 'Needs your answer', message: '{session_name}' };
|
|
510
547
|
|
|
511
548
|
const resolvedTitle = resolveNotificationTemplate(questionTemplate.title, variables);
|
|
512
549
|
const resolvedBody = resolveNotificationTemplate(questionTemplate.message, variables);
|
|
513
550
|
if (resolvedTitle) title = resolvedTitle;
|
|
514
551
|
if (shouldApplyResolvedTemplateMessage(questionTemplate.message, resolvedBody, variables)) body = resolvedBody;
|
|
552
|
+
if (!body) body = sessionName || resolvedBody || 'Session';
|
|
515
553
|
} catch (error) {
|
|
516
554
|
console.warn('[Notification] Question template resolution failed, using defaults:', error?.message || error);
|
|
555
|
+
if (!body) body = sessionName || 'Session';
|
|
517
556
|
}
|
|
518
557
|
|
|
519
558
|
if (settings.nativeNotificationsEnabled) {
|
|
@@ -618,8 +657,8 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
618
657
|
? sessionTitle.trim()
|
|
619
658
|
: permissionText || 'Agent is waiting for your approval';
|
|
620
659
|
|
|
621
|
-
let title = '
|
|
622
|
-
let body =
|
|
660
|
+
let title = 'Needs permission';
|
|
661
|
+
let body = '';
|
|
623
662
|
let sessionName = '';
|
|
624
663
|
|
|
625
664
|
try {
|
|
@@ -628,14 +667,14 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
628
667
|
variables.last_message = fallbackMessage;
|
|
629
668
|
|
|
630
669
|
const templates = settings.notificationTemplates || {};
|
|
631
|
-
const questionTemplate = templates.question || { title: '
|
|
670
|
+
const questionTemplate = templates.question || { title: 'Needs permission', message: '{session_name}' };
|
|
632
671
|
|
|
633
|
-
const resolvedTitle = resolveNotificationTemplate(questionTemplate.title, variables);
|
|
634
672
|
const resolvedBody = resolveNotificationTemplate(questionTemplate.message, variables);
|
|
635
|
-
if (resolvedTitle) title = resolvedTitle;
|
|
636
673
|
if (shouldApplyResolvedTemplateMessage(questionTemplate.message, resolvedBody, variables)) body = resolvedBody;
|
|
674
|
+
if (!body) body = sessionName || fallbackMessage || 'Session';
|
|
637
675
|
} catch (error) {
|
|
638
676
|
console.warn('[Notification] Permission template resolution failed, using defaults:', error?.message || error);
|
|
677
|
+
if (!body) body = sessionName || fallbackMessage || 'Session';
|
|
639
678
|
}
|
|
640
679
|
|
|
641
680
|
if (settings.nativeNotificationsEnabled) {
|
|
@@ -713,6 +752,57 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
713
752
|
);
|
|
714
753
|
};
|
|
715
754
|
|
|
755
|
+
const contactNotificationTitle = (name) => {
|
|
756
|
+
if (typeof name !== 'string' || !name.trim()) return 'Assistant';
|
|
757
|
+
const stripped = name.replace(/^(?:\p{Extended_Pictographic}|\p{Regional_Indicator}|[\uFE0F\u200D])+\s*/u, '').trim();
|
|
758
|
+
return stripped || name.trim();
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
const sendContactTurnNotification = async ({ assistantID, name, body, status } = {}) => {
|
|
762
|
+
if (typeof assistantID !== 'string' || !assistantID.trim()) return;
|
|
763
|
+
const settings = await readSettingsFromDisk();
|
|
764
|
+
if (settings.notifyOnCompletion === false) return;
|
|
765
|
+
|
|
766
|
+
const title = contactNotificationTitle(name);
|
|
767
|
+
const rawBody = typeof body === 'string' ? body : '';
|
|
768
|
+
let text = '';
|
|
769
|
+
try {
|
|
770
|
+
text = await prepareNotificationLastMessage({ message: rawBody, settings });
|
|
771
|
+
} catch {
|
|
772
|
+
text = '';
|
|
773
|
+
}
|
|
774
|
+
if (!text) text = status === 'error' ? 'Something went wrong' : 'New message';
|
|
775
|
+
const tag = `contact-${assistantID.trim()}`;
|
|
776
|
+
|
|
777
|
+
if (settings.nativeNotificationsEnabled) {
|
|
778
|
+
const notificationPayload = {
|
|
779
|
+
title,
|
|
780
|
+
body: text,
|
|
781
|
+
tag,
|
|
782
|
+
kind: 'contact',
|
|
783
|
+
assistantID: assistantID.trim(),
|
|
784
|
+
requireHidden: false,
|
|
785
|
+
};
|
|
786
|
+
const desktopNotificationDelivered = emitDesktopNotification(notificationPayload);
|
|
787
|
+
broadcastUiNotification(notificationPayload, { desktopNotificationDelivered });
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const assistantPath = `/assistant/${encodeURIComponent(assistantID.trim())}`;
|
|
791
|
+
await fanoutPush(
|
|
792
|
+
{
|
|
793
|
+
title,
|
|
794
|
+
body: text,
|
|
795
|
+
tag,
|
|
796
|
+
data: {
|
|
797
|
+
url: assistantPath,
|
|
798
|
+
assistantID: assistantID.trim(),
|
|
799
|
+
type: 'contact',
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
{ requireNoSse: true, preserveAlert: true },
|
|
803
|
+
);
|
|
804
|
+
};
|
|
805
|
+
|
|
716
806
|
return {
|
|
717
807
|
maybeSendPushForTrigger,
|
|
718
808
|
setAutoAcceptSession,
|
|
@@ -720,5 +810,6 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
720
810
|
setGetIsSessionAutoAccepting,
|
|
721
811
|
clearPendingPushBadge,
|
|
722
812
|
sendGoalSettlePush,
|
|
813
|
+
sendContactTurnNotification,
|
|
723
814
|
};
|
|
724
815
|
};
|
|
@@ -175,6 +175,58 @@ describe('notification trigger runtime smallModel suppression', () => {
|
|
|
175
175
|
expect(emitDesktopNotification).toHaveBeenCalledTimes(1);
|
|
176
176
|
expect(sendPushToAllUiSessions).toHaveBeenCalledTimes(1);
|
|
177
177
|
});
|
|
178
|
+
|
|
179
|
+
it('skips ready notifications for child sessions', async () => {
|
|
180
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
181
|
+
id: 'ses_child',
|
|
182
|
+
parentID: 'ses_parent',
|
|
183
|
+
title: 'Fixer',
|
|
184
|
+
metadata: {},
|
|
185
|
+
})));
|
|
186
|
+
const { runtime, emitDesktopNotification, sendPushToAllUiSessions } = createRuntime();
|
|
187
|
+
await runtime.maybeSendPushForTrigger(completionPayload('ses_child'));
|
|
188
|
+
expect(emitDesktopNotification).not.toHaveBeenCalled();
|
|
189
|
+
expect(sendPushToAllUiSessions).not.toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('skips ready notifications for Assistant binding sessions', async () => {
|
|
193
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
194
|
+
id: 'ses_binding',
|
|
195
|
+
parentID: null,
|
|
196
|
+
title: '大小白',
|
|
197
|
+
metadata: { openchamber: { assistant: { assistantID: 'asst_1', name: '大小白' } } },
|
|
198
|
+
})));
|
|
199
|
+
const { runtime, emitDesktopNotification, sendPushToAllUiSessions } = createRuntime();
|
|
200
|
+
await runtime.maybeSendPushForTrigger(completionPayload('ses_binding'));
|
|
201
|
+
expect(emitDesktopNotification).not.toHaveBeenCalled();
|
|
202
|
+
expect(sendPushToAllUiSessions).not.toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('skips ready notifications for scheduled-task sessions', async () => {
|
|
206
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
207
|
+
id: 'ses_sched',
|
|
208
|
+
parentID: null,
|
|
209
|
+
title: 'Nightly',
|
|
210
|
+
metadata: { openchamber: { scheduledTask: { taskID: 'task_1' } } },
|
|
211
|
+
})));
|
|
212
|
+
const { runtime, emitDesktopNotification, sendPushToAllUiSessions } = createRuntime();
|
|
213
|
+
await runtime.maybeSendPushForTrigger(completionPayload('ses_sched'));
|
|
214
|
+
expect(emitDesktopNotification).not.toHaveBeenCalled();
|
|
215
|
+
expect(sendPushToAllUiSessions).not.toHaveBeenCalled();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('still notifies contact-assigned worker sessions that appear in the sidebar', async () => {
|
|
219
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
220
|
+
id: 'ses_worker',
|
|
221
|
+
parentID: null,
|
|
222
|
+
title: 'Assigned work',
|
|
223
|
+
metadata: { openchamber: { assigned: { from: 'contact', assistantID: 'asst_1', name: '大小白' } } },
|
|
224
|
+
})));
|
|
225
|
+
const { runtime, emitDesktopNotification, sendPushToAllUiSessions } = createRuntime();
|
|
226
|
+
await runtime.maybeSendPushForTrigger(completionPayload('ses_worker'));
|
|
227
|
+
expect(emitDesktopNotification).toHaveBeenCalledTimes(1);
|
|
228
|
+
expect(sendPushToAllUiSessions).toHaveBeenCalledTimes(1);
|
|
229
|
+
});
|
|
178
230
|
});
|
|
179
231
|
|
|
180
232
|
const rootSessionResponse = () => jsonResponse({
|
|
@@ -199,6 +251,54 @@ const completionPayload = (sessionId = 'ses_root', finish = 'stop') => ({
|
|
|
199
251
|
},
|
|
200
252
|
});
|
|
201
253
|
|
|
254
|
+
describe('contact turn notifications', () => {
|
|
255
|
+
it('sends an SMS-style title and body when a contact turn completes', async () => {
|
|
256
|
+
const { runtime, emitDesktopNotification, broadcastUiNotification, sendPushToAllUiSessions, sendApnsToAllUiSessions } = createRuntime();
|
|
257
|
+
await runtime.sendContactTurnNotification({
|
|
258
|
+
assistantID: 'asst_1',
|
|
259
|
+
name: '大小白',
|
|
260
|
+
body: '我去找一下',
|
|
261
|
+
status: 'complete',
|
|
262
|
+
});
|
|
263
|
+
expect(emitDesktopNotification).toHaveBeenCalledWith(expect.objectContaining({
|
|
264
|
+
title: '大小白',
|
|
265
|
+
body: '我去找一下',
|
|
266
|
+
tag: 'contact-asst_1',
|
|
267
|
+
kind: 'contact',
|
|
268
|
+
assistantID: 'asst_1',
|
|
269
|
+
}));
|
|
270
|
+
expect(broadcastUiNotification).toHaveBeenCalled();
|
|
271
|
+
expect(sendPushToAllUiSessions).toHaveBeenCalledWith(expect.objectContaining({
|
|
272
|
+
title: '大小白',
|
|
273
|
+
body: '我去找一下',
|
|
274
|
+
data: expect.objectContaining({ assistantID: 'asst_1', type: 'contact', url: '/assistant/asst_1' }),
|
|
275
|
+
}), expect.objectContaining({ requireNoSse: true, preserveAlert: true }));
|
|
276
|
+
expect(sendApnsToAllUiSessions).toHaveBeenCalledWith(expect.objectContaining({
|
|
277
|
+
title: '大小白',
|
|
278
|
+
body: '我去找一下',
|
|
279
|
+
data: expect.objectContaining({
|
|
280
|
+
assistantID: 'asst_1',
|
|
281
|
+
url: 'openchamber://assistant/asst_1',
|
|
282
|
+
}),
|
|
283
|
+
}), expect.objectContaining({ preserveAlert: true }));
|
|
284
|
+
expect(sendApnsToAllUiSessions.mock.calls[0][0].type).toBeUndefined();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('skips contact notifications when completion notices are disabled', async () => {
|
|
288
|
+
const { runtime, emitDesktopNotification, sendPushToAllUiSessions } = createRuntime({
|
|
289
|
+
readSettingsFromDisk: vi.fn(async () => ({ ...defaultSettings, notifyOnCompletion: false })),
|
|
290
|
+
});
|
|
291
|
+
await runtime.sendContactTurnNotification({
|
|
292
|
+
assistantID: 'asst_1',
|
|
293
|
+
name: '大小白',
|
|
294
|
+
body: 'hi',
|
|
295
|
+
status: 'complete',
|
|
296
|
+
});
|
|
297
|
+
expect(emitDesktopNotification).not.toHaveBeenCalled();
|
|
298
|
+
expect(sendPushToAllUiSessions).not.toHaveBeenCalled();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
202
302
|
describe('notification trigger live activity end', () => {
|
|
203
303
|
it('ends the live activity on top-level completion and error', async () => {
|
|
204
304
|
vi.stubGlobal('fetch', vi.fn(async () => rootSessionResponse()));
|
|
@@ -232,6 +332,28 @@ describe('notification trigger live activity end', () => {
|
|
|
232
332
|
expect(sendApnsToAllUiSessions).toHaveBeenCalledTimes(1);
|
|
233
333
|
});
|
|
234
334
|
|
|
335
|
+
it('does not patch live activity titles for child sessions', async () => {
|
|
336
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
337
|
+
id: 'ses_child',
|
|
338
|
+
parentID: 'ses_parent',
|
|
339
|
+
metadata: {},
|
|
340
|
+
})));
|
|
341
|
+
const { runtime, sendLiveActivityEnd } = createRuntime();
|
|
342
|
+
await runtime.maybeSendPushForTrigger({
|
|
343
|
+
type: 'session.updated',
|
|
344
|
+
properties: {
|
|
345
|
+
directory: '/repo',
|
|
346
|
+
info: {
|
|
347
|
+
id: 'ses_child',
|
|
348
|
+
sessionID: 'ses_child',
|
|
349
|
+
parentID: 'ses_parent',
|
|
350
|
+
title: 'Fixer',
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
expect(sendLiveActivityEnd).not.toHaveBeenCalled();
|
|
355
|
+
});
|
|
356
|
+
|
|
235
357
|
it('suppresses live activity end for child and small-model sessions', async () => {
|
|
236
358
|
const fetchMock = vi.fn(async (url) => {
|
|
237
359
|
if (String(url).includes('ses_child')) {
|
|
@@ -15,6 +15,7 @@ import { registerScheduledTaskToolRoute } from '../scheduled-tasks/managed-tool-
|
|
|
15
15
|
import { registerConversationRoutes } from '../conversations/routes.js';
|
|
16
16
|
import { registerSessionTurnPageRoutes } from '../session-turn-pages/routes.js';
|
|
17
17
|
import { registerAssistantRoutes } from '../assistants/routes.js';
|
|
18
|
+
import { registerLlmRoutes } from '../llm/routes.js';
|
|
18
19
|
import { registerMessageQueueRoutes } from '../message-queue/routes.js';
|
|
19
20
|
import { registerSkillRoutes } from './skill-routes.js';
|
|
20
21
|
import { registerPluginRoutes } from './plugin-routes.js';
|
|
@@ -126,6 +127,8 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
126
127
|
broadcastGlobalUiEvent,
|
|
127
128
|
globalMessageStreamHub,
|
|
128
129
|
getServerId,
|
|
130
|
+
sessionIndexService,
|
|
131
|
+
notifyContactTurnComplete,
|
|
129
132
|
} = routeDependencies;
|
|
130
133
|
|
|
131
134
|
registerSettingsUtilityRoutes(app, {
|
|
@@ -215,6 +218,15 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
215
218
|
getOpenChamberEventClients,
|
|
216
219
|
writeSseEvent,
|
|
217
220
|
});
|
|
221
|
+
// Contact turn lifecycle / bubble deltas — same SSE bus, not revision watermark.
|
|
222
|
+
const broadcastContactTurnEvent = createOpenChamberEventBroadcaster({
|
|
223
|
+
getOpenChamberEventClients,
|
|
224
|
+
writeSseEvent,
|
|
225
|
+
});
|
|
226
|
+
registerLlmRoutes(app, {
|
|
227
|
+
buildOpenCodeUrl,
|
|
228
|
+
getOpenCodeAuthHeaders,
|
|
229
|
+
});
|
|
218
230
|
assistantRoutesRuntime = registerAssistantRoutes(app, {
|
|
219
231
|
openchamberDataDir,
|
|
220
232
|
dbPath: path.join(openchamberDataDir, 'assistants.sqlite'),
|
|
@@ -222,12 +234,30 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
222
234
|
getOpenCodeAuthHeaders,
|
|
223
235
|
getServerId,
|
|
224
236
|
getAllowedRoots: () => assistantAllowedRoots,
|
|
237
|
+
listProjects: async () => {
|
|
238
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
239
|
+
return sanitizeProjects(settings?.projects ?? [])
|
|
240
|
+
.filter((project) => project && typeof project.id === 'string' && typeof project.path === 'string')
|
|
241
|
+
.map((project) => ({
|
|
242
|
+
id: project.id,
|
|
243
|
+
path: project.path,
|
|
244
|
+
...(typeof project.label === 'string' && project.label.trim()
|
|
245
|
+
? { label: project.label.trim() }
|
|
246
|
+
: {}),
|
|
247
|
+
}));
|
|
248
|
+
},
|
|
249
|
+
listScheduledTasks: (projectID) => projectConfigRuntime.listScheduledTasks(projectID),
|
|
250
|
+
sessionIndexService,
|
|
251
|
+
upsertScheduledTask: (projectID, task) => projectConfigRuntime.upsertScheduledTask(projectID, task),
|
|
252
|
+
syncScheduledTaskProject: (projectID) => scheduledTasksRuntime.syncProject(projectID),
|
|
225
253
|
refreshAllowedRoots: refreshAssistantAllowedRoots,
|
|
226
254
|
globalEventHub: globalMessageStreamHub,
|
|
227
255
|
onRevisionTip: (tip) => broadcastAssistantRevisionTip({
|
|
228
256
|
type: 'openchamber:assistants-changed',
|
|
229
257
|
properties: tip,
|
|
230
258
|
}),
|
|
259
|
+
onContactTurnEvent: (event) => broadcastContactTurnEvent(event),
|
|
260
|
+
onContactTurnComplete: (event) => notifyContactTurnComplete?.(event),
|
|
231
261
|
});
|
|
232
262
|
messageQueueRuntime?.setAssistantDeliveryService?.(assistantRoutesRuntime.service);
|
|
233
263
|
|
|
@@ -3,6 +3,26 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
3
3
|
import { createWorktreeTopologyBroadcaster } from './feature-routes-runtime.js';
|
|
4
4
|
|
|
5
5
|
describe('feature routes runtime composition', () => {
|
|
6
|
+
it('wires scheduled-task upsert into assistant contact tools', async () => {
|
|
7
|
+
const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
|
|
8
|
+
expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*upsertScheduledTask:[\s\S]*projectConfigRuntime\.upsertScheduledTask/);
|
|
9
|
+
expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*syncScheduledTaskProject:[\s\S]*scheduledTasksRuntime\.syncProject/);
|
|
10
|
+
expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*listProjects:/);
|
|
11
|
+
expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*listScheduledTasks:[\s\S]*projectConfigRuntime\.listScheduledTasks/);
|
|
12
|
+
expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*sessionIndexService/);
|
|
13
|
+
expect(source).toMatch(/label: project\.label\.trim\(\)/);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('fans contact turn events out on the OpenChamber SSE bus separately from revision tips', async () => {
|
|
17
|
+
const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
|
|
18
|
+
expect(source).toMatch(/const broadcastContactTurnEvent = createOpenChamberEventBroadcaster\(/);
|
|
19
|
+
expect(source).toMatch(/onContactTurnEvent: \(event\) => broadcastContactTurnEvent\(event\)/);
|
|
20
|
+
expect(source).toMatch(/onContactTurnComplete: \(event\) => notifyContactTurnComplete\?\.\(event\)/);
|
|
21
|
+
expect(source).toMatch(/onRevisionTip: \(tip\) => broadcastAssistantRevisionTip\(\{\s*type: 'openchamber:assistants-changed'/);
|
|
22
|
+
// Contact turn envelopes stay off the assistants-changed revision watermark path.
|
|
23
|
+
expect(source).not.toMatch(/onRevisionTip:[\s\S]*contact-turn/);
|
|
24
|
+
});
|
|
25
|
+
|
|
6
26
|
it('registers the managed scheduled-task tool route with its required dependencies', async () => {
|
|
7
27
|
const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
|
|
8
28
|
expect(source).toContain("import { registerScheduledTaskToolRoute } from '../scheduled-tasks/managed-tool-route.js';");
|