@openchambery/web 1.18.2-beta.2 → 1.18.2-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-B2kZjgaf.js → AgentAvatar-B4fj9PIe.js} +1 -1
- package/dist/assets/{AssistantView-DvoyOGkx.js → AssistantView-C00aiEHK.js} +1 -1
- package/dist/assets/{ChatContainer-D6yfuigV.js → ChatContainer-BDFeDKu-.js} +39 -39
- package/dist/assets/{DiagramView-MGGo9iME.js → DiagramView-CUfSFI3e.js} +1 -1
- package/dist/assets/{FadeInOnReveal-aeWlaqVB.js → FadeInOnReveal-CKKU6fKI.js} +1 -1
- package/dist/assets/{Icon-AXqa-fYB.js → Icon-DiIuWOuC.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-jVOkZkyX.js → MarkdownRendererImpl-Cen6E9Lj.js} +2 -2
- package/dist/assets/{MobileDetailNavigation-Q2IzHqGA.js → MobileDetailNavigation-DILi0tN7.js} +1 -1
- package/dist/assets/{MobileShareBridge-apXM34Lb.js → MobileShareBridge-PZMRiieI.js} +1 -1
- package/dist/assets/MobileSurface-BLYcR0tO.js +2 -0
- package/dist/assets/{MultiRunWindow-B-l_O-bd.js → MultiRunWindow-D_2mWZyv.js} +1 -1
- package/dist/assets/{OnboardingScreen-CUsgwt4y.js → OnboardingScreen-CYeRZznl.js} +1 -1
- package/dist/assets/{OverlayScrollbar-D6NySAxi.js → OverlayScrollbar-BgMLtMIq.js} +1 -1
- package/dist/assets/SettingsView-CbDBfQp1.js +25 -0
- package/dist/assets/{SettingsWindow-DfVgTQ4I.js → SettingsWindow-Br7XLcSd.js} +1 -1
- package/dist/assets/{TerminalView-DNyjfkvT.js → TerminalView-CM5jRKlj.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DLG1iThm.js → ToolOutputDialog-D3U6aTZ4.js} +1 -1
- package/dist/assets/appThemeRegistry-CDxBJNNB.js +2 -0
- package/dist/assets/assistant-guide-hero-BeUHQcxE.jpg +0 -0
- package/dist/assets/{button-CDvMVaiO.js → button-DaAoVF4X.js} +1 -1
- package/dist/assets/{clipboard-BW7nk_Cx.js → clipboard-BnElHSnO.js} +1 -1
- package/dist/assets/{collapsible-C2aamDvD.js → collapsible-BmuQu0dq.js} +1 -1
- package/dist/assets/{context-menu-BaxmodD8.js → context-menu-DBOMervd.js} +1 -1
- package/dist/assets/{desktopHosts-CBoBpJ1Z.js → desktopHosts-ZK_vkdzL.js} +1 -1
- package/dist/assets/{desktopRelayRestore-l2R9muux.js → desktopRelayRestore-CewM6-g5.js} +1 -1
- package/dist/assets/{diagram-CEon0BQs.js → diagram-DAE4CZy2.js} +1 -1
- package/dist/assets/{dialog-DjYfj1Sn.js → dialog-COrLBm9O.js} +1 -1
- package/dist/assets/{es-5Lhudh0n.js → es-DIVYdoxi.js} +1 -1
- package/dist/assets/{fileQueries-CE_DaZWQ.js → fileQueries-CmWYnRVz.js} +1 -1
- package/dist/assets/{fr-Q_UhFmKT.js → fr-CNTlVypq.js} +2 -2
- package/dist/assets/{gitApi-BYxg_qGJ.js → gitApi-BQT3QXD2.js} +1 -1
- package/dist/assets/gitApiHttp-BxGAFLTf.js +2 -0
- package/dist/assets/{input-k9DGGyZ6.js → input-XiL2-2gG.js} +1 -1
- package/dist/assets/insertionBoundaries-9a0RuIDc.js +1 -0
- package/dist/assets/{ja-CzMPz7bT.js → ja-C_eW5ZM8.js} +1 -1
- package/dist/assets/{ko-B-EslUsR.js → ko-CE3cOv7v.js} +1 -1
- package/dist/assets/{main-LDphyc-v.js → main-BGfX81Y9.js} +20 -20
- package/dist/assets/{main-CO6z5yRB.js → main-CEqSDnE2.js} +2 -2
- package/dist/assets/{markdown-worker-DkeWiER6.js → markdown-worker-BDyY0vt6.js} +1 -1
- package/dist/assets/{markdownTheme-DNT93i5L.js → markdownTheme-D-aLQZaM.js} +1 -1
- package/dist/assets/{miniChat-BZQwTZpx.js → miniChat-CKucSAhg.js} +2 -2
- package/dist/assets/mobile-C4bTWKry.js +2 -0
- package/dist/assets/{multirun-ZlwlkIy5.js → multirun-DeNtSkgb.js} +1 -1
- package/dist/assets/{persistence-DPNMIbF5.js → persistence-D1lIuyqO.js} +1 -1
- package/dist/assets/{pl-Dnea6Ppc.js → pl-BZKZOE99.js} +1 -1
- package/dist/assets/projectMeta-BunN9pH5.js +3 -0
- package/dist/assets/{pt-BR-Ca6QTem7.js → pt-BR-DHDgp8S3.js} +1 -1
- package/dist/assets/{radio-CJuhbQ5a.js → radio-Bziafaje.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-D8-FDgWE.js +2 -0
- package/dist/assets/renderMobileApp-CdU0aJ-e.js +23 -0
- package/dist/assets/{runtimeConfig-BtWi74Pm.js → runtimeConfig-BXCt7ZsQ.js} +1 -1
- package/dist/assets/runtimeEndpointReset-CK1GhIsm.js +145 -0
- package/dist/assets/{sortable-tabs-strip-D8JHUI0z.js → sortable-tabs-strip-pWlBorwQ.js} +1 -1
- package/dist/assets/src-8kQFmtjw.css +2 -0
- package/dist/assets/{store-wa1ersRf.js → store-CUWqzRt7.js} +2 -2
- package/dist/assets/{terminalApi-tB3qLcfa.js → terminalApi-DpiAJlil.js} +1 -1
- package/dist/assets/{tooltip-DMNR5rN0.js → tooltip-BLugzWq-.js} +1 -1
- package/dist/assets/{uk-xGc9Pzge.js → uk-CfQK6cz1.js} +1 -1
- package/dist/assets/{url-7MqoLs9X.js → url-DMs_PgIC.js} +1 -1
- package/dist/assets/useAppFontEffects-IEUQDvE8.js +7 -0
- package/dist/assets/useDesktopWindowControlsLayout-MlqJX89k.js +1 -0
- package/dist/assets/{useEffectiveDirectory-aUC3zzl_.js → useEffectiveDirectory-GbhkdO6m.js} +1 -1
- package/dist/assets/{useFontPreferences-CIurCeNv.js → useFontPreferences-92Qg-98H.js} +1 -1
- package/dist/assets/useMobileNavigationStore-DQHF_ETu.js +476 -0
- package/dist/assets/useSessionAutoCleanup-BuU_S_Sh.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-CiYQjBe6.js → useWorkerHighlightedLines-Ca6OgNHF.js} +1 -1
- package/dist/assets/{utils-Co19EJEL.js → utils-BxJEIWv5.js} +1 -1
- package/dist/assets/{zh-CN-sL4ktGB3.js → zh-CN-DvY2fKWp.js} +1 -1
- package/dist/assets/{zh-TW-Cfxps4gq.js → zh-TW-eWe0lm3-.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.d.ts +6 -0
- package/server/index.js +13 -0
- package/server/lib/config-sync/DOCUMENTATION.md +70 -0
- package/server/lib/config-sync/config-sync.test.js +197 -0
- package/server/lib/config-sync/constants.js +39 -0
- package/server/lib/config-sync/contract.js +130 -0
- package/server/lib/config-sync/credential-auth.js +39 -0
- package/server/lib/config-sync/engine.js +108 -0
- package/server/lib/config-sync/index.js +67 -0
- package/server/lib/config-sync/local-backup.js +171 -0
- package/server/lib/config-sync/plan.js +318 -0
- package/server/lib/config-sync/receiver.js +330 -0
- package/server/lib/config-sync/routes.js +141 -0
- package/server/lib/config-sync/routes.test.js +154 -0
- package/server/lib/config-sync/scripts.js +264 -0
- package/server/lib/config-sync/selections.js +112 -0
- package/server/lib/config-sync/tar.js +35 -0
- package/server/lib/config-sync/target-id.js +24 -0
- package/server/lib/event-stream/DOCUMENTATION.md +2 -2
- package/server/lib/event-stream/diff-summary.js +39 -11
- package/server/lib/event-stream/diff-summary.test.js +73 -6
- package/server/lib/event-stream/protocol.test.js +70 -7
- package/server/lib/notifications/DOCUMENTATION.md +2 -2
- package/server/lib/notifications/runtime.js +68 -27
- package/server/lib/notifications/runtime.test.js +176 -0
- package/server/lib/opencode/DOCUMENTATION.md +4 -1
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/core-routes.js +1 -0
- package/server/lib/opencode/feature-routes-runtime.test.js +10 -1
- package/server/lib/opencode/openchamber-routes.js +7 -0
- package/server/lib/opencode/settings-runtime.js +82 -51
- package/server/lib/opencode/settings-runtime.test.js +42 -0
- package/server/lib/relay/DOCUMENTATION.md +1 -1
- package/server/lib/session-index/DOCUMENTATION.md +12 -6
- package/server/lib/session-index/routes.js +34 -0
- package/server/lib/session-index/routes.test.js +76 -0
- package/server/lib/session-index/service.js +53 -4
- package/server/lib/session-index/service.test.js +72 -2
- package/server/lib/session-title/DOCUMENTATION.md +19 -14
- package/server/lib/session-title/runtime.js +8 -8
- package/server/lib/session-title/runtime.test.js +231 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +26 -6
- package/server/lib/session-turn-pages/changes.service.js +156 -0
- package/server/lib/session-turn-pages/changes.service.test.js +188 -0
- package/server/lib/session-turn-pages/reconcile.routes.test.js +52 -0
- package/server/lib/session-turn-pages/reconcile.service.js +3 -1
- package/server/lib/session-turn-pages/reconcile.service.test.js +44 -0
- package/server/lib/session-turn-pages/routes.js +273 -12
- package/server/lib/session-turn-pages/routes.test.js +387 -0
- package/server/lib/session-turn-pages/service.js +31 -7
- package/server/lib/session-turn-pages/service.test.js +40 -0
- package/server/lib/small-model/DOCUMENTATION.md +93 -38
- package/server/lib/small-model/catalog.js +10 -2
- package/server/lib/small-model/catalog.test.js +3 -1
- package/server/lib/small-model/index.js +52 -28
- package/server/lib/small-model/opencode-session.js +339 -0
- package/server/lib/small-model/opencode-session.test.js +177 -0
- package/server/lib/small-model/resolve.js +116 -5
- package/server/lib/small-model/resolve.test.js +76 -10
- package/server/lib/small-model/summary-settings.test.js +60 -3
- package/dist/assets/MobileSurface-htGkf7Ua.js +0 -2
- package/dist/assets/SettingsView-DxMpVLlt.js +0 -25
- package/dist/assets/appThemeRegistry-CgfOR_0L.js +0 -2
- package/dist/assets/gitApiHttp-D8j1qRH4.js +0 -2
- package/dist/assets/insertionBoundaries-DzXMTleb.js +0 -1
- package/dist/assets/mobile-BktO3_4R.js +0 -2
- package/dist/assets/projectMeta-v-LP2mzz.js +0 -3
- package/dist/assets/renderElectronMiniChatApp-Db8uO-6q.js +0 -2
- package/dist/assets/renderMobileApp-BCV81Ryl.js +0 -23
- package/dist/assets/runtimeEndpointReset-Dg3DMy9v.js +0 -145
- package/dist/assets/src-oHrYIqHF.css +0 -2
- package/dist/assets/useAppFontEffects-CM0VKsdo.js +0 -7
- package/dist/assets/useDesktopWindowControlsLayout-Cq-r4Hcb.js +0 -1
- package/dist/assets/useMobileNavigationStore-DnzT4jO2.js +0 -475
- package/dist/assets/useSessionAutoCleanup-DxZeceKO.js +0 -1
|
@@ -223,7 +223,7 @@ describe('event stream protocol helpers', () => {
|
|
|
223
223
|
expect(rawPayload.length).toBeLessThan(64 * 1024);
|
|
224
224
|
});
|
|
225
225
|
|
|
226
|
-
it('
|
|
226
|
+
it('projects message.updated summary.diffs to L1 slim list + markers on outbound frames', () => {
|
|
227
227
|
let rawPayload = null;
|
|
228
228
|
const socket = {
|
|
229
229
|
readyState: 1,
|
|
@@ -246,6 +246,8 @@ describe('event stream protocol helpers', () => {
|
|
|
246
246
|
additions: 2,
|
|
247
247
|
deletions: 1,
|
|
248
248
|
patch: '@@ heavy patch body @@\n' + 'y'.repeat(1000),
|
|
249
|
+
before: 'old',
|
|
250
|
+
after: 'new',
|
|
249
251
|
}],
|
|
250
252
|
},
|
|
251
253
|
},
|
|
@@ -254,13 +256,74 @@ describe('event stream protocol helpers', () => {
|
|
|
254
256
|
|
|
255
257
|
expect(sent).toBe(true);
|
|
256
258
|
const frame = JSON.parse(rawPayload);
|
|
257
|
-
expect(frame.payload.properties.info.summary
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
259
|
+
expect(frame.payload.properties.info.summary).toEqual({
|
|
260
|
+
diffs: [{
|
|
261
|
+
file: 'a.ts',
|
|
262
|
+
status: 'modified',
|
|
263
|
+
additions: 2,
|
|
264
|
+
deletions: 1,
|
|
265
|
+
}],
|
|
266
|
+
diffCount: 1,
|
|
267
|
+
hasDiffs: true,
|
|
268
|
+
});
|
|
263
269
|
expect(JSON.stringify(frame)).not.toContain('patch');
|
|
270
|
+
expect(JSON.stringify(frame)).not.toContain('"before"');
|
|
271
|
+
expect(JSON.stringify(frame)).not.toContain('"after"');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('keeps L1 message.updated serialization under 256KiB for a 463-file / ~14MB patch summary', () => {
|
|
275
|
+
let rawPayload = null;
|
|
276
|
+
const socket = {
|
|
277
|
+
readyState: 1,
|
|
278
|
+
send(payload) {
|
|
279
|
+
rawPayload = payload;
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const patchBody = 'P'.repeat(Math.floor((14 * 1024 * 1024) / 463));
|
|
284
|
+
const diffs = Array.from({ length: 463 }, (_, i) => ({
|
|
285
|
+
file: `src/generated/file-${i}.ts`,
|
|
286
|
+
status: 'modified',
|
|
287
|
+
additions: i % 7,
|
|
288
|
+
deletions: i % 3,
|
|
289
|
+
patch: `${patchBody}-${i}`,
|
|
290
|
+
before: `before-${i}`,
|
|
291
|
+
after: `after-${i}`,
|
|
292
|
+
from: `from-${i}`,
|
|
293
|
+
to: `to-${i}`,
|
|
294
|
+
}));
|
|
295
|
+
|
|
296
|
+
const sent = sendMessageStreamWsEvent(socket, {
|
|
297
|
+
type: 'message.updated',
|
|
298
|
+
properties: {
|
|
299
|
+
info: {
|
|
300
|
+
id: 'msg_huge',
|
|
301
|
+
sessionID: 'ses_huge',
|
|
302
|
+
role: 'assistant',
|
|
303
|
+
summary: { title: 'huge', diffs },
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(sent).toBe(true);
|
|
309
|
+
const frame = JSON.parse(rawPayload);
|
|
310
|
+
const summary = frame.payload.properties.info.summary;
|
|
311
|
+
expect(summary.diffCount).toBe(463);
|
|
312
|
+
expect(summary.hasDiffs).toBe(true);
|
|
313
|
+
expect(summary.diffs).toHaveLength(463);
|
|
314
|
+
expect(summary.diffs[0]).toEqual({
|
|
315
|
+
file: 'src/generated/file-0.ts',
|
|
316
|
+
status: 'modified',
|
|
317
|
+
additions: 0,
|
|
318
|
+
deletions: 0,
|
|
319
|
+
});
|
|
320
|
+
expect(rawPayload).not.toContain(patchBody.slice(0, 64));
|
|
321
|
+
expect(rawPayload).not.toContain('"patch"');
|
|
322
|
+
expect(rawPayload).not.toContain('"before"');
|
|
323
|
+
expect(rawPayload).not.toContain('"after"');
|
|
324
|
+
expect(rawPayload).not.toContain('"from"');
|
|
325
|
+
expect(rawPayload).not.toContain('"to"');
|
|
326
|
+
expect(Buffer.byteLength(rawPayload, 'utf8')).toBeLessThan(256 * 1024);
|
|
264
327
|
});
|
|
265
328
|
|
|
266
329
|
it('leaves ordinary session.status events unchanged', () => {
|
|
@@ -45,8 +45,8 @@ This module provides notification message preparation utilities for the web serv
|
|
|
45
45
|
- Returned API:
|
|
46
46
|
- `maybeSendPushForTrigger(payload)`
|
|
47
47
|
- Owns:
|
|
48
|
-
- top-level completion/question/permission trigger routing; completion is the sole task-status notification and child sessions are suppressed
|
|
49
|
-
- session
|
|
48
|
+
- top-level completion/question/permission trigger routing; completion is the sole task-status notification and child sessions plus small-model system sessions (`metadata.openchamber.smallModel.purpose`) are suppressed
|
|
49
|
+
- session meta cache for child-session and small-model suppression
|
|
50
50
|
- template resolution and fallback behavior
|
|
51
51
|
- native notification fanout and web push payload fanout
|
|
52
52
|
- push suppression while any fresh UI visibility heartbeat reports a focused client
|
|
@@ -111,8 +111,8 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
111
111
|
const notifiedPermissionRequests = new Set();
|
|
112
112
|
const lastReadyNotificationAt = new Map();
|
|
113
113
|
|
|
114
|
-
const
|
|
115
|
-
const
|
|
114
|
+
const sessionMetaCache = new Map();
|
|
115
|
+
const SESSION_META_CACHE_TTL_MS = 60 * 1000;
|
|
116
116
|
|
|
117
117
|
// Sessions where the client has enabled Permission Auto-Accept. Mirrored
|
|
118
118
|
// from the client-side permissionStore via POST /api/notifications/auto-accept
|
|
@@ -136,43 +136,67 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
136
136
|
return `/?session=${encodeURIComponent(sessionId)}`;
|
|
137
137
|
};
|
|
138
138
|
|
|
139
|
-
const
|
|
139
|
+
const getSessionMetaCacheKey = (sessionId, directory) => `${directory || ''}\0${sessionId}`;
|
|
140
140
|
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
const normalizeParentID = (value) => (
|
|
142
|
+
typeof value === 'string' && value.length > 0 ? value : null
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const normalizeSmallModelPurpose = (value) => (
|
|
146
|
+
typeof value === 'string' && value.length > 0 ? value : null
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const getCachedSessionMeta = (sessionId, directory) => {
|
|
150
|
+
const cacheKey = getSessionMetaCacheKey(sessionId, directory);
|
|
151
|
+
const entry = sessionMetaCache.get(cacheKey);
|
|
144
152
|
if (!entry) return undefined;
|
|
145
|
-
if (Date.now() - entry.at >
|
|
146
|
-
|
|
153
|
+
if (Date.now() - entry.at > SESSION_META_CACHE_TTL_MS) {
|
|
154
|
+
sessionMetaCache.delete(cacheKey);
|
|
147
155
|
return undefined;
|
|
148
156
|
}
|
|
149
|
-
return entry
|
|
157
|
+
return entry;
|
|
150
158
|
};
|
|
151
159
|
|
|
152
|
-
const
|
|
153
|
-
|
|
160
|
+
const setCachedSessionMeta = (sessionId, directory, meta) => {
|
|
161
|
+
const previous = getCachedSessionMeta(sessionId, directory);
|
|
162
|
+
sessionMetaCache.set(getSessionMetaCacheKey(sessionId, directory), {
|
|
163
|
+
parentID: meta.parentID !== undefined ? meta.parentID : (previous?.parentID ?? null),
|
|
164
|
+
smallModelPurpose: meta.smallModelPurpose !== undefined
|
|
165
|
+
? meta.smallModelPurpose
|
|
166
|
+
: (previous?.smallModelPurpose ?? null),
|
|
167
|
+
at: Date.now(),
|
|
168
|
+
});
|
|
154
169
|
};
|
|
155
170
|
|
|
156
171
|
const getParentIdFromPayload = (payload) => {
|
|
157
172
|
if (!payload || typeof payload !== 'object') return undefined;
|
|
158
173
|
if (payload.type !== 'session.created' && payload.type !== 'session.updated') return undefined;
|
|
159
|
-
|
|
160
|
-
|
|
174
|
+
return normalizeParentID(payload.properties?.info?.parentID);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const getSmallModelPurposeFromPayload = (payload) => {
|
|
178
|
+
if (!payload || typeof payload !== 'object') return undefined;
|
|
179
|
+
if (payload.type !== 'session.created' && payload.type !== 'session.updated') return undefined;
|
|
180
|
+
return normalizeSmallModelPurpose(payload.properties?.info?.metadata?.openchamber?.smallModel?.purpose);
|
|
161
181
|
};
|
|
162
182
|
|
|
163
|
-
const
|
|
183
|
+
const maybeCacheSessionMetaFromPayload = (payload) => {
|
|
164
184
|
const sessionId = extractSessionIdFromPayload(payload);
|
|
165
185
|
if (typeof sessionId !== 'string' || sessionId.length === 0) return;
|
|
166
186
|
const directory = extractDirectoryFromPayload(payload);
|
|
167
187
|
const parentID = getParentIdFromPayload(payload);
|
|
168
|
-
|
|
169
|
-
|
|
188
|
+
const smallModelPurpose = getSmallModelPurposeFromPayload(payload);
|
|
189
|
+
if (parentID === undefined && smallModelPurpose === undefined) return;
|
|
190
|
+
setCachedSessionMeta(sessionId, directory, {
|
|
191
|
+
...(parentID !== undefined ? { parentID } : {}),
|
|
192
|
+
...(smallModelPurpose !== undefined ? { smallModelPurpose } : {}),
|
|
193
|
+
});
|
|
170
194
|
};
|
|
171
195
|
|
|
172
|
-
const
|
|
196
|
+
const fetchSessionMeta = async (sessionId, directory) => {
|
|
173
197
|
if (!sessionId) return undefined;
|
|
174
198
|
|
|
175
|
-
const cached =
|
|
199
|
+
const cached = getCachedSessionMeta(sessionId, directory);
|
|
176
200
|
if (cached !== undefined) return cached;
|
|
177
201
|
|
|
178
202
|
try {
|
|
@@ -194,16 +218,22 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
194
218
|
return undefined;
|
|
195
219
|
}
|
|
196
220
|
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
:
|
|
200
|
-
|
|
201
|
-
|
|
221
|
+
const meta = {
|
|
222
|
+
parentID: normalizeParentID(session.parentID),
|
|
223
|
+
smallModelPurpose: normalizeSmallModelPurpose(session.metadata?.openchamber?.smallModel?.purpose),
|
|
224
|
+
};
|
|
225
|
+
setCachedSessionMeta(sessionId, directory, meta);
|
|
226
|
+
return meta;
|
|
202
227
|
} catch {
|
|
203
228
|
return undefined;
|
|
204
229
|
}
|
|
205
230
|
};
|
|
206
231
|
|
|
232
|
+
const fetchSessionParentId = async (sessionId, directory) => {
|
|
233
|
+
const meta = await fetchSessionMeta(sessionId, directory);
|
|
234
|
+
return meta?.parentID;
|
|
235
|
+
};
|
|
236
|
+
|
|
207
237
|
// Mirrors client-side autoRespondsPermission: a session auto-accepts if it
|
|
208
238
|
// OR any ancestor is flagged. Walks the parent chain via fetchSessionParentId.
|
|
209
239
|
const isSessionAutoAccepting = async (sessionId, directory) => {
|
|
@@ -225,6 +255,17 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
225
255
|
return typeof parentID === 'string' && parentID.length > 0;
|
|
226
256
|
};
|
|
227
257
|
|
|
258
|
+
const isSmallModelSession = async (sessionId, directory) => {
|
|
259
|
+
const meta = await fetchSessionMeta(sessionId, directory);
|
|
260
|
+
return typeof meta?.smallModelPurpose === 'string' && meta.smallModelPurpose.length > 0;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const shouldSkipSystemSessionNotification = async (sessionId, directory) => {
|
|
264
|
+
if (await isSubtaskSession(sessionId, directory)) return true;
|
|
265
|
+
if (await isSmallModelSession(sessionId, directory)) return true;
|
|
266
|
+
return false;
|
|
267
|
+
};
|
|
268
|
+
|
|
228
269
|
const extractSessionIdFromPayload = (payload) => {
|
|
229
270
|
if (!payload || typeof payload !== 'object') return null;
|
|
230
271
|
const props = payload.properties;
|
|
@@ -309,7 +350,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
309
350
|
return;
|
|
310
351
|
}
|
|
311
352
|
|
|
312
|
-
|
|
353
|
+
maybeCacheSessionMetaFromPayload(payload);
|
|
313
354
|
|
|
314
355
|
const sessionId = extractSessionIdFromPayload(payload);
|
|
315
356
|
const notificationDirectory = extractDirectoryFromPayload(payload);
|
|
@@ -340,7 +381,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
340
381
|
sessionId
|
|
341
382
|
&& info?.role === 'assistant'
|
|
342
383
|
&& (info?.finish === 'stop' || info?.finish === 'error')
|
|
343
|
-
&& await
|
|
384
|
+
&& await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)
|
|
344
385
|
) {
|
|
345
386
|
return;
|
|
346
387
|
}
|
|
@@ -433,7 +474,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
433
474
|
}
|
|
434
475
|
|
|
435
476
|
if (payload.type === 'question.asked' && sessionId) {
|
|
436
|
-
if (await
|
|
477
|
+
if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
|
|
437
478
|
return;
|
|
438
479
|
}
|
|
439
480
|
const existingTimer = pushQuestionDebounceTimers.get(sessionId);
|
|
@@ -534,7 +575,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
534
575
|
}
|
|
535
576
|
|
|
536
577
|
if (payload.type === 'permission.asked' && sessionId) {
|
|
537
|
-
if (await
|
|
578
|
+
if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
|
|
538
579
|
return;
|
|
539
580
|
}
|
|
540
581
|
const requestId = payload.properties?.id ?? payload.properties?.requestID ?? payload.properties?.requestId;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createNotificationTriggerRuntime } from './runtime.js';
|
|
4
|
+
|
|
5
|
+
const defaultSettings = {
|
|
6
|
+
notifyOnCompletion: true,
|
|
7
|
+
notifyOnQuestion: true,
|
|
8
|
+
notificationMode: 'always',
|
|
9
|
+
nativeNotificationsEnabled: true,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const createRuntime = (overrides = {}) => {
|
|
13
|
+
const emitDesktopNotification = vi.fn(() => true);
|
|
14
|
+
const broadcastUiNotification = vi.fn();
|
|
15
|
+
const sendPushToAllUiSessions = vi.fn(async () => {});
|
|
16
|
+
const sendApnsToAllUiSessions = vi.fn(async () => {});
|
|
17
|
+
const runtime = createNotificationTriggerRuntime({
|
|
18
|
+
readSettingsFromDisk: vi.fn(async () => defaultSettings),
|
|
19
|
+
prepareNotificationLastMessage: vi.fn(async ({ message }) => message || ''),
|
|
20
|
+
buildTemplateVariables: vi.fn(async () => ({ session_name: 'Session' })),
|
|
21
|
+
extractLastMessageText: vi.fn(() => ''),
|
|
22
|
+
fetchLastAssistantMessageText: vi.fn(async () => ''),
|
|
23
|
+
resolveNotificationTemplate: vi.fn((template) => template),
|
|
24
|
+
shouldApplyResolvedTemplateMessage: vi.fn(() => true),
|
|
25
|
+
emitDesktopNotification,
|
|
26
|
+
broadcastUiNotification,
|
|
27
|
+
sendPushToAllUiSessions,
|
|
28
|
+
sendApnsToAllUiSessions,
|
|
29
|
+
isAnyInteractiveClientVisible: () => false,
|
|
30
|
+
buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
|
|
31
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
32
|
+
getIsWindowFocused: () => false,
|
|
33
|
+
...overrides,
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
runtime,
|
|
37
|
+
emitDesktopNotification,
|
|
38
|
+
broadcastUiNotification,
|
|
39
|
+
sendPushToAllUiSessions,
|
|
40
|
+
sendApnsToAllUiSessions,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
45
|
+
status,
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
vi.unstubAllGlobals();
|
|
51
|
+
vi.useRealTimers();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('notification trigger runtime smallModel suppression', () => {
|
|
55
|
+
it('skips ready notifications for smallModel sessions', async () => {
|
|
56
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
57
|
+
id: 'ses_small',
|
|
58
|
+
parentID: null,
|
|
59
|
+
metadata: { openchamber: { smallModel: { purpose: 'session-title' } } },
|
|
60
|
+
})));
|
|
61
|
+
|
|
62
|
+
const { runtime, emitDesktopNotification, sendPushToAllUiSessions } = createRuntime();
|
|
63
|
+
await runtime.maybeSendPushForTrigger({
|
|
64
|
+
type: 'message.updated',
|
|
65
|
+
properties: {
|
|
66
|
+
directory: '/repo',
|
|
67
|
+
info: {
|
|
68
|
+
id: 'msg_1',
|
|
69
|
+
sessionID: 'ses_small',
|
|
70
|
+
role: 'assistant',
|
|
71
|
+
finish: 'stop',
|
|
72
|
+
mode: 'build',
|
|
73
|
+
modelID: 'small',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(emitDesktopNotification).not.toHaveBeenCalled();
|
|
79
|
+
expect(sendPushToAllUiSessions).not.toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('skips ready notifications synthesized from session.idle for smallModel sessions', async () => {
|
|
83
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
84
|
+
id: 'ses_small',
|
|
85
|
+
parentID: null,
|
|
86
|
+
metadata: { openchamber: { smallModel: { purpose: 'commit' } } },
|
|
87
|
+
})));
|
|
88
|
+
|
|
89
|
+
const { runtime, emitDesktopNotification, sendPushToAllUiSessions } = createRuntime();
|
|
90
|
+
await runtime.maybeSendPushForTrigger({
|
|
91
|
+
type: 'session.idle',
|
|
92
|
+
properties: {
|
|
93
|
+
directory: '/repo',
|
|
94
|
+
sessionID: 'ses_small',
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(emitDesktopNotification).not.toHaveBeenCalled();
|
|
99
|
+
expect(sendPushToAllUiSessions).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('skips question notifications for smallModel sessions', async () => {
|
|
103
|
+
vi.useFakeTimers();
|
|
104
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
105
|
+
id: 'ses_small',
|
|
106
|
+
parentID: null,
|
|
107
|
+
metadata: { openchamber: { smallModel: { purpose: 'session-title' } } },
|
|
108
|
+
})));
|
|
109
|
+
|
|
110
|
+
const { runtime, emitDesktopNotification, sendPushToAllUiSessions } = createRuntime();
|
|
111
|
+
await runtime.maybeSendPushForTrigger({
|
|
112
|
+
type: 'question.asked',
|
|
113
|
+
properties: {
|
|
114
|
+
directory: '/repo',
|
|
115
|
+
sessionID: 'ses_small',
|
|
116
|
+
questions: [{ header: 'Input needed', question: 'Continue?' }],
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
await vi.runAllTimersAsync();
|
|
120
|
+
|
|
121
|
+
expect(emitDesktopNotification).not.toHaveBeenCalled();
|
|
122
|
+
expect(sendPushToAllUiSessions).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('skips permission notifications for smallModel sessions', async () => {
|
|
126
|
+
vi.useFakeTimers();
|
|
127
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
128
|
+
id: 'ses_small',
|
|
129
|
+
parentID: null,
|
|
130
|
+
metadata: { openchamber: { smallModel: { purpose: 'session-title' } } },
|
|
131
|
+
})));
|
|
132
|
+
|
|
133
|
+
const { runtime, emitDesktopNotification, sendPushToAllUiSessions } = createRuntime();
|
|
134
|
+
await runtime.maybeSendPushForTrigger({
|
|
135
|
+
type: 'permission.asked',
|
|
136
|
+
properties: {
|
|
137
|
+
directory: '/repo',
|
|
138
|
+
sessionID: 'ses_small',
|
|
139
|
+
id: 'perm_1',
|
|
140
|
+
permission: 'edit',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
await vi.runAllTimersAsync();
|
|
144
|
+
|
|
145
|
+
expect(emitDesktopNotification).not.toHaveBeenCalled();
|
|
146
|
+
expect(sendPushToAllUiSessions).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('still notifies ordinary root sessions', async () => {
|
|
150
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
151
|
+
id: 'ses_root',
|
|
152
|
+
parentID: null,
|
|
153
|
+
title: 'Ordinary',
|
|
154
|
+
metadata: {},
|
|
155
|
+
})));
|
|
156
|
+
|
|
157
|
+
const { runtime, emitDesktopNotification, sendPushToAllUiSessions } = createRuntime();
|
|
158
|
+
await runtime.maybeSendPushForTrigger({
|
|
159
|
+
type: 'message.updated',
|
|
160
|
+
properties: {
|
|
161
|
+
directory: '/repo',
|
|
162
|
+
info: {
|
|
163
|
+
id: 'msg_2',
|
|
164
|
+
sessionID: 'ses_root',
|
|
165
|
+
role: 'assistant',
|
|
166
|
+
finish: 'stop',
|
|
167
|
+
mode: 'build',
|
|
168
|
+
modelID: 'gpt',
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(emitDesktopNotification).toHaveBeenCalledTimes(1);
|
|
174
|
+
expect(sendPushToAllUiSessions).toHaveBeenCalledTimes(1);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -28,7 +28,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
28
28
|
- `packages/web/server/lib/opencode/static-routes-runtime.js`: static asset/SPA fallback route registration and manifest route wiring.
|
|
29
29
|
- `packages/web/server/lib/opencode/feature-routes-runtime.js`: feature route composition runtime for dynamic import-backed config/skill/provider route registration.
|
|
30
30
|
- `packages/web/server/lib/conversations/`: combined create-session-and-prompt orchestration (OpenChamber-owned, registered before generic proxy).
|
|
31
|
-
- `packages/web/server/lib/session-turn-pages/`: turn-window session message pagination (`GET /api/openchamber/sessions/:sessionID/messages`)
|
|
31
|
+
- `packages/web/server/lib/session-turn-pages/`: turn-window session message pagination (`GET /api/openchamber/sessions/:sessionID/messages`), anchor reconcile (`GET .../messages/reconcile`), Changes L2/L3 (`GET .../changes`), and exact message L1 projection (`GET /api/session/:sessionID/message/:messageID`); loops official OpenCode `session.messages` for turn pages and head→anchor gap recovery with continuation/budgets/`resetRequired`; all registered before generic proxy. L1 first-packet / SSE / exact-message paths keep `summary.diffs` as a slim `{ file, status?, additions, deletions }` list plus additive `diffCount`/`hasDiffs` (patch bodies stay on L3). See `session-turn-pages/DOCUMENTATION.md`.
|
|
32
32
|
- `packages/web/server/lib/opencode/opencode-resolution-runtime.js`: OpenCode binary resolution snapshot runtime for settings routes and diagnostics.
|
|
33
33
|
- `packages/web/server/lib/opencode/tunnel-wiring-runtime.js`: active loopback-port wiring shared by pairing/LAN URLs and the private relay host.
|
|
34
34
|
- `packages/web/server/lib/opencode/startup-pipeline-runtime.js`: server startup tail orchestration runtime for terminal/proxy/static/start-listen flow.
|
|
@@ -184,13 +184,16 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
184
184
|
|
|
185
185
|
## Public exports (settings-runtime.js)
|
|
186
186
|
- `createSettingsRuntime(dependencies)`: creates settings lifecycle runtime for read/migrate/persist concerns.
|
|
187
|
+
- Optional dependency `runExclusivePersist` / `getRunExclusivePersist`, plus returned `setRunExclusivePersist(fn)`, let an in-process host (Electron) replace the default `persistSettingsLock` with a shared exclusive runner so desktop main/ssh writers and web settings persistence serialize on one chain.
|
|
187
188
|
- Returned API:
|
|
188
189
|
- `readSettingsFromDisk()`
|
|
189
190
|
- `readSettingsFromDiskMigrated()`
|
|
190
191
|
- `writeSettingsToDisk(settings)`
|
|
191
192
|
- `persistSettings(changes)`
|
|
193
|
+
- `setRunExclusivePersist(fn)`
|
|
192
194
|
- Persistent permission auto-accept policy is stored under `permissionAutoAccept`; execution ownership lives in `lib/permission-auto-accept/`.
|
|
193
195
|
- One-shot compact-chat defaults migration: when disk marker `compactChatDefaultsMigrationVersion` is missing, rewrite legacy/absent `chatRenderMode`/`activityRenderMode`/`showTurnChangedFiles` to `sorted`/`collapsed`/`true` and persist marker `1` (marker stays on disk; response allowlist still hides it). Marker already `1` preserves user values; `persistSettings` runs the same migration before the first write.
|
|
196
|
+
- `startWebUiServer({ settingsPersistLock })` late-binds that shared runner after module load.
|
|
194
197
|
|
|
195
198
|
## Public exports (settings-helpers.js)
|
|
196
199
|
- `createSettingsHelpers(dependencies)`: creates settings helper runtime for settings request/response shaping.
|
|
@@ -30,6 +30,7 @@ export const createBootstrapRuntime = (dependencies) => {
|
|
|
30
30
|
getServerId,
|
|
31
31
|
getServerLabel,
|
|
32
32
|
readSettingsFromDiskMigrated,
|
|
33
|
+
persistSettings,
|
|
33
34
|
normalizeTunnelSessionTtlMs,
|
|
34
35
|
authorizeManagedOpenCodeBridgeRequest,
|
|
35
36
|
sayTTSCapability,
|
|
@@ -147,6 +148,7 @@ export const createBootstrapRuntime = (dependencies) => {
|
|
|
147
148
|
__dirname,
|
|
148
149
|
openchamberDataDir,
|
|
149
150
|
readSettingsFromDiskMigrated,
|
|
151
|
+
persistSettings,
|
|
150
152
|
fetchFreeZenModels,
|
|
151
153
|
getCachedZenModels,
|
|
152
154
|
sessionIndexService,
|
|
@@ -1127,6 +1127,7 @@ export const registerCommonRequestMiddleware = (app, dependencies) => {
|
|
|
1127
1127
|
} else if (
|
|
1128
1128
|
(req.method === 'PUT' && req.path.match(/^\/api\/openchamber\/message-queue\/attachments\/uploads\/[^/]+\/?$/))
|
|
1129
1129
|
|| (req.method === 'PUT' && req.path.match(/^\/api\/fs\/prompt-attachments\/[^/]+\/?$/))
|
|
1130
|
+
|| (req.method === 'PUT' && req.path.match(/^\/api\/openchamber\/config-sync\/put\/[^/]+\/?$/))
|
|
1130
1131
|
) {
|
|
1131
1132
|
// Binary attachment uploads stream as the request body. Parsing JSON here
|
|
1132
1133
|
// would buffer the entire payload and block the relay tunnel.
|
|
@@ -27,10 +27,19 @@ describe('feature routes runtime composition', () => {
|
|
|
27
27
|
const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
|
|
28
28
|
expect(source).toContain("import { registerSessionTurnPageRoutes } from '../session-turn-pages/routes.js';");
|
|
29
29
|
expect(source).toMatch(/registerSessionTurnPageRoutes\(app, \{[\s\S]*buildOpenCodeUrl,[\s\S]*getOpenCodeAuthHeaders,/);
|
|
30
|
-
// OpenChamber-owned turn-page/reconcile registration
|
|
30
|
+
// OpenChamber-owned turn-page/reconcile/changes/exact-message registration stays before end of feature composition.
|
|
31
31
|
expect(source).toContain('// OpenChamber-owned turn-window messages API — must register before generic proxy.');
|
|
32
32
|
const turnIdx = source.indexOf('registerSessionTurnPageRoutes(app,');
|
|
33
33
|
expect(turnIdx).toBeGreaterThan(-1);
|
|
34
|
+
// Exact message + Changes live inside registerSessionTurnPageRoutes (same Host composition Electron reuses).
|
|
35
|
+
const routesSource = await fs.readFile(new URL('../session-turn-pages/routes.js', import.meta.url), 'utf8');
|
|
36
|
+
expect(routesSource).toContain("app.get('/api/session/:sessionID/message/:messageID'");
|
|
37
|
+
expect(routesSource).toContain("app.get('/api/openchamber/sessions/:sessionID/changes'");
|
|
38
|
+
const exactIdx = routesSource.indexOf("app.get('/api/session/:sessionID/message/:messageID'");
|
|
39
|
+
const changesIdx = routesSource.indexOf("app.get('/api/openchamber/sessions/:sessionID/changes'");
|
|
40
|
+
const messagesIdx = routesSource.indexOf("app.get('/api/openchamber/sessions/:sessionID/messages'");
|
|
41
|
+
expect(exactIdx).toBeGreaterThan(messagesIdx);
|
|
42
|
+
expect(changesIdx).toBeGreaterThan(exactIdx);
|
|
34
43
|
});
|
|
35
44
|
|
|
36
45
|
it('removes broken SSE clients while continuing worktree topology broadcasts', () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { registerSessionIndexRoutes } from '../session-index/routes.js';
|
|
2
2
|
import { registerTranscriptCacheRoutes } from '../transcript-cache/routes.js';
|
|
3
3
|
import { registerDesktopHostRoutes } from '../desktop-hosts/routes.js';
|
|
4
|
+
import { registerConfigSyncRoutes } from '../config-sync/routes.js';
|
|
4
5
|
|
|
5
6
|
export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
6
7
|
const {
|
|
@@ -11,6 +12,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
11
12
|
__dirname,
|
|
12
13
|
openchamberDataDir,
|
|
13
14
|
readSettingsFromDiskMigrated,
|
|
15
|
+
persistSettings,
|
|
14
16
|
fetchFreeZenModels,
|
|
15
17
|
getCachedZenModels,
|
|
16
18
|
sessionIndexService,
|
|
@@ -31,6 +33,11 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
31
33
|
getPairingSession,
|
|
32
34
|
express,
|
|
33
35
|
});
|
|
36
|
+
registerConfigSyncRoutes(app, {
|
|
37
|
+
readSettingsFromDiskMigrated,
|
|
38
|
+
persistSettings,
|
|
39
|
+
express,
|
|
40
|
+
});
|
|
34
41
|
|
|
35
42
|
app.get('/api/openchamber/update-check', async (req, res) => {
|
|
36
43
|
try {
|