@openchambery/web 1.18.2-beta.16 → 1.18.2-beta.18
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/{AssistantView-5b0FsCQa.js → AssistantView-BjECeCP-.js} +1 -1
- package/dist/assets/{ChatContainer-QtcnvWoA.js → ChatContainer-BYOJGW70.js} +4 -4
- package/dist/assets/{DiagramView-BoWXd8X4.js → DiagramView-y5N3UVYC.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-0Gq_Uqs_.js → MarkdownRendererImpl-C_42Xy0b.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-CBWFnBIu.js → MobileDetailNavigation-CESuP4mM.js} +1 -1
- package/dist/assets/{MobileShareBridge-CTg5aHEn.js → MobileShareBridge-DteabHid.js} +1 -1
- package/dist/assets/{MobileSurface-Dw6Ina7A.js → MobileSurface-U-yc1m0-.js} +1 -1
- package/dist/assets/{MultiRunWindow-De0C4bCt.js → MultiRunWindow-CCF0PXlx.js} +1 -1
- package/dist/assets/{OnboardingScreen-7mFH7iBm.js → OnboardingScreen-CYeRZznl.js} +1 -1
- package/dist/assets/{OverlayScrollbar-DzX65Ew5.js → OverlayScrollbar-C5kI44g7.js} +1 -1
- package/dist/assets/{SettingsView-Bs2kVAA1.js → SettingsView-DZb1RZlV.js} +1 -1
- package/dist/assets/{SettingsWindow-axr7ewCq.js → SettingsWindow-DRQK5GqM.js} +1 -1
- package/dist/assets/{TerminalView-B_5ml6J4.js → TerminalView-Ynxbvy5-.js} +1 -1
- package/dist/assets/{ToolOutputDialog-CR5Ifk2Z.js → ToolOutputDialog-Cav2sifO.js} +1 -1
- package/dist/assets/{appThemeRegistry-ih8IT_2E.js → appThemeRegistry-C_UnqMeo.js} +2 -2
- package/dist/assets/{clipboard-Cm4VHQ3n.js → clipboard-BnElHSnO.js} +1 -1
- package/dist/assets/{context-menu-DyfEPvnE.js → context-menu-D00De8AC.js} +1 -1
- package/dist/assets/{dialog-ruyemRYh.js → dialog-COrLBm9O.js} +1 -1
- package/dist/assets/{es-W_z2kSYf.js → es-DIVYdoxi.js} +1 -1
- package/dist/assets/{fr-MYuf2QNQ.js → fr-CNTlVypq.js} +1 -1
- package/dist/assets/{gitApi-BrvHOAWU.js → gitApi-BSovla63.js} +1 -1
- package/dist/assets/{insertionBoundaries-BW7YHXi6.js → insertionBoundaries-BzxCFqY7.js} +1 -1
- package/dist/assets/{ja-DBkl4NXl.js → ja-C_eW5ZM8.js} +1 -1
- package/dist/assets/{ko-DwSsHsxa.js → ko-CE3cOv7v.js} +1 -1
- package/dist/assets/{main-BBKtt1Y9.js → main-BaqqdTzl.js} +4 -4
- package/dist/assets/{main-fdHtRqqY.js → main-yYt_TrZD.js} +2 -2
- package/dist/assets/{miniChat-AFFBcZuq.js → miniChat-DeS-YbIL.js} +2 -2
- package/dist/assets/{mobile-BjoYS6yy.js → mobile-Cg1Lw3I_.js} +2 -2
- package/dist/assets/{multirun-BUCHfy2k.js → multirun-CjmiAnFl.js} +1 -1
- package/dist/assets/{pl-BGLpnSSE.js → pl-BZKZOE99.js} +1 -1
- package/dist/assets/{projectMeta-GWaQA3Nh.js → projectMeta-BQJHLcsI.js} +1 -1
- package/dist/assets/{pt-BR-DuEh49lp.js → pt-BR-DHDgp8S3.js} +1 -1
- package/dist/assets/{radio-DLT8WkB1.js → radio-BpKBzfEa.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-mk15OpZl.js → renderElectronMiniChatApp-DNRlOH_8.js} +2 -2
- package/dist/assets/renderMobileApp-zMbsHo-j.js +23 -0
- package/dist/assets/{runtimeEndpointReset-C-zHA9mH.js → runtimeEndpointReset-_cbs-WPN.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-B8ZHvoDm.js → sortable-tabs-strip-CQ6Jhk4W.js} +1 -1
- package/dist/assets/src-DcPZ_rWS.css +2 -0
- package/dist/assets/{store-Cb8Q6EtD.js → store-CUWqzRt7.js} +2 -2
- package/dist/assets/{uk-CUoSz-Q8.js → uk-CfQK6cz1.js} +1 -1
- package/dist/assets/{useAppFontEffects-BmoxCfbH.js → useAppFontEffects-CvGJXvOL.js} +2 -2
- package/dist/assets/{useDesktopWindowControlsLayout-BLOLzV-B.js → useDesktopWindowControlsLayout-BztJfpeS.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-DYoU9MD8.js → useEffectiveDirectory-BNxfJJ4B.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-DpFAK6sE.js → useMobileNavigationStore-te_AdqSf.js} +5 -5
- package/dist/assets/{useSessionAutoCleanup-B5wYiGWJ.js → useSessionAutoCleanup-DDcW7p4V.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-DkDOqMcg.js → useWorkerHighlightedLines-HT_ad5F8.js} +1 -1
- package/dist/assets/{zh-CN-CJP5FWCK.js → zh-CN-DvY2fKWp.js} +1 -1
- package/dist/assets/{zh-TW-B_2h1yuo.js → zh-TW-eWe0lm3-.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/mini-chat.html +2 -2
- package/dist/mobile.html +2 -2
- package/package.json +1 -1
- package/server/index.js +7 -0
- 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/session-index/DOCUMENTATION.md +6 -5
- package/server/lib/session-index/service.js +1 -0
- package/server/lib/session-index/service.test.js +7 -2
- package/server/lib/session-title/DOCUMENTATION.md +3 -2
- package/server/lib/session-title/runtime.js +1 -0
- package/server/lib/session-title/runtime.test.js +6 -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/renderMobileApp-1_-KjQ6K.js +0 -23
- package/dist/assets/src-C8jn0x69.css +0 -2
package/dist/index.html
CHANGED
|
@@ -552,7 +552,7 @@
|
|
|
552
552
|
opacity: 0.72;
|
|
553
553
|
}
|
|
554
554
|
</style>
|
|
555
|
-
<script type="module" crossorigin src="/assets/main-
|
|
555
|
+
<script type="module" crossorigin src="/assets/main-yYt_TrZD.js"></script>
|
|
556
556
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
557
557
|
<link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
|
|
558
558
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
|
|
@@ -568,7 +568,7 @@
|
|
|
568
568
|
<link rel="modulepreload" crossorigin href="/assets/gitApiHttp-BxGAFLTf.js">
|
|
569
569
|
<link rel="modulepreload" crossorigin href="/assets/terminalApi-DpiAJlil.js">
|
|
570
570
|
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BXCt7ZsQ.js">
|
|
571
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
571
|
+
<link rel="stylesheet" crossorigin href="/assets/src-DcPZ_rWS.css">
|
|
572
572
|
</head>
|
|
573
573
|
<body class="h-full bg-background text-foreground">
|
|
574
574
|
<!-- Keep the startup layer outside #root. React replaces every child of
|
package/dist/mini-chat.html
CHANGED
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
}
|
|
75
75
|
</style>
|
|
76
76
|
|
|
77
|
-
<script type="module" crossorigin src="/assets/miniChat-
|
|
77
|
+
<script type="module" crossorigin src="/assets/miniChat-DeS-YbIL.js"></script>
|
|
78
78
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
79
79
|
<link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
|
|
80
80
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
<link rel="modulepreload" crossorigin href="/assets/gitApiHttp-BxGAFLTf.js">
|
|
91
91
|
<link rel="modulepreload" crossorigin href="/assets/terminalApi-DpiAJlil.js">
|
|
92
92
|
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BXCt7ZsQ.js">
|
|
93
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
93
|
+
<link rel="stylesheet" crossorigin href="/assets/src-DcPZ_rWS.css">
|
|
94
94
|
</head>
|
|
95
95
|
<body class="h-full bg-background text-foreground">
|
|
96
96
|
<div id="root" class="h-full">
|
package/dist/mobile.html
CHANGED
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
})();
|
|
64
64
|
</script>
|
|
65
65
|
|
|
66
|
-
<script type="module" crossorigin src="/assets/mobile-
|
|
66
|
+
<script type="module" crossorigin src="/assets/mobile-Cg1Lw3I_.js"></script>
|
|
67
67
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
68
68
|
<link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
|
|
69
69
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
<link rel="modulepreload" crossorigin href="/assets/gitApiHttp-BxGAFLTf.js">
|
|
80
80
|
<link rel="modulepreload" crossorigin href="/assets/terminalApi-DpiAJlil.js">
|
|
81
81
|
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BXCt7ZsQ.js">
|
|
82
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
82
|
+
<link rel="stylesheet" crossorigin href="/assets/src-DcPZ_rWS.css">
|
|
83
83
|
</head>
|
|
84
84
|
<body class="h-full bg-background text-foreground">
|
|
85
85
|
<div id="root" class="h-full"></div>
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -1714,6 +1714,13 @@ async function main(options = {}) {
|
|
|
1714
1714
|
} catch {
|
|
1715
1715
|
// best-effort shutdown of the dictation worker
|
|
1716
1716
|
}
|
|
1717
|
+
try {
|
|
1718
|
+
if (smallModelServiceInstance?.stop) {
|
|
1719
|
+
await smallModelServiceInstance.stop();
|
|
1720
|
+
}
|
|
1721
|
+
} catch {
|
|
1722
|
+
// best-effort cleanup of the small-model temp OpenCode directory
|
|
1723
|
+
}
|
|
1717
1724
|
return gracefulShutdown({
|
|
1718
1725
|
exitProcess: shutdownOptions.exitProcess ?? false,
|
|
1719
1726
|
forceCloseConnections: shutdownOptions.forceCloseConnections === true,
|
|
@@ -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
|
+
});
|
|
@@ -13,11 +13,12 @@ newest 20 root-session summaries per runtime and directory. It never stores
|
|
|
13
13
|
messages, attachments, permissions, provider data, or model metadata.
|
|
14
14
|
Sessions titled `smartfetch-secondary` are temporary SmartFetch model calls;
|
|
15
15
|
the index excludes them from every snapshot and clears prior summaries when a
|
|
16
|
-
matching session update arrives. System-owned sessions are excluded by
|
|
17
|
-
authoritative metadata only: a non-empty
|
|
18
|
-
`metadata.openchamber.assistant.assistantID
|
|
19
|
-
`metadata.openchamber.scheduledTask.taskID
|
|
20
|
-
|
|
16
|
+
matching session update arrives. System-owned sessions are excluded by
|
|
17
|
+
authoritative metadata only: a non-empty
|
|
18
|
+
`metadata.openchamber.assistant.assistantID`,
|
|
19
|
+
`metadata.openchamber.scheduledTask.taskID`, or
|
|
20
|
+
`metadata.openchamber.smallModel.purpose`. Title prefixes are human labels and
|
|
21
|
+
never participate in this filter. Metadata is the ownership/isolation signal;
|
|
21
22
|
`time.archived` is archive state; `time.pinned` is OpenChamber-owned pin state
|
|
22
23
|
(persisted as `pinned_at`, rescued across directory rebuilds, never overwritten
|
|
23
24
|
by live OpenCode upserts); titles are for recognition. The index does
|
|
@@ -15,6 +15,7 @@ const isSystemSession = (session) => {
|
|
|
15
15
|
if (!openchamber || typeof openchamber !== 'object') return false;
|
|
16
16
|
if (nonEmptySystemID(openchamber.assistant?.assistantID)) return true;
|
|
17
17
|
if (nonEmptySystemID(openchamber.scheduledTask?.taskID)) return true;
|
|
18
|
+
if (nonEmptySystemID(openchamber.smallModel?.purpose)) return true;
|
|
18
19
|
return false;
|
|
19
20
|
};
|
|
20
21
|
|
|
@@ -68,7 +68,7 @@ describe('Electron session index', () => {
|
|
|
68
68
|
service.close();
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
it('excludes Assistant and
|
|
71
|
+
it('excludes Assistant, Scheduled, and smallModel system sessions from ordinary sidebar summaries', () => {
|
|
72
72
|
const runtimeRef = { value: 'http://runtime-a.test' };
|
|
73
73
|
const service = createService(runtimeRef);
|
|
74
74
|
const assistantSession = {
|
|
@@ -79,11 +79,15 @@ describe('Electron session index', () => {
|
|
|
79
79
|
...session('ses_scheduled', 99),
|
|
80
80
|
metadata: { openchamber: { scheduledTask: { taskID: 'task_1' } } },
|
|
81
81
|
};
|
|
82
|
+
const smallModelSession = {
|
|
83
|
+
...session('ses_small_model', 97),
|
|
84
|
+
metadata: { openchamber: { smallModel: { purpose: 'session-title' } } },
|
|
85
|
+
};
|
|
82
86
|
const ordinary = session('ses_ordinary', 98);
|
|
83
87
|
|
|
84
88
|
service.replaceDirectory({
|
|
85
89
|
directory: '/repo',
|
|
86
|
-
sessions: [assistantSession, scheduledSession, ordinary],
|
|
90
|
+
sessions: [assistantSession, scheduledSession, smallModelSession, ordinary],
|
|
87
91
|
cursor: null,
|
|
88
92
|
hasMore: false,
|
|
89
93
|
});
|
|
@@ -91,6 +95,7 @@ describe('Electron session index', () => {
|
|
|
91
95
|
|
|
92
96
|
service.upsert(assistantSession);
|
|
93
97
|
service.upsert(scheduledSession);
|
|
98
|
+
service.upsert(smallModelSession);
|
|
94
99
|
expect(service.snapshot().directories[0].sessions.map((item) => item.id)).toEqual(['ses_ordinary']);
|
|
95
100
|
service.close();
|
|
96
101
|
});
|
|
@@ -31,8 +31,9 @@ throttle (`TITLE_THROTTLE_MS`) when a refresh is armed.
|
|
|
31
31
|
3. On fire:
|
|
32
32
|
- Skip when Settings → Chat → session title refresh is off.
|
|
33
33
|
- Skip system-owned sessions whose metadata carries a non-empty
|
|
34
|
-
`openchamber.assistant.assistantID
|
|
35
|
-
|
|
34
|
+
`openchamber.assistant.assistantID`, `openchamber.scheduledTask.taskID`, or
|
|
35
|
+
`openchamber.smallModel.purpose`
|
|
36
|
+
(fixed Assistant / scheduled / small-model titles must not be rewritten). Early
|
|
36
37
|
`session.created` and smart-title request extraction skip the same
|
|
37
38
|
metadata when present so timers are not armed.
|
|
38
39
|
- Skip sub-agent sessions (`parentID`), multi-run/fusion structural titles,
|
|
@@ -165,6 +165,7 @@ export const isSystemOwnedSession = (sessionOrInfo) => {
|
|
|
165
165
|
if (!openchamber || typeof openchamber !== 'object') return false;
|
|
166
166
|
if (typeof openchamber.assistant?.assistantID === 'string' && openchamber.assistant.assistantID) return true;
|
|
167
167
|
if (typeof openchamber.scheduledTask?.taskID === 'string' && openchamber.scheduledTask.taskID) return true;
|
|
168
|
+
if (typeof openchamber.smallModel?.purpose === 'string' && openchamber.smallModel.purpose) return true;
|
|
168
169
|
return false;
|
|
169
170
|
};
|
|
170
171
|
|
|
@@ -232,10 +232,16 @@ describe('session-title helpers', () => {
|
|
|
232
232
|
expect(isSystemOwnedSession({
|
|
233
233
|
metadata: { openchamber: { scheduledTask: { taskID: 'task_1' } } },
|
|
234
234
|
})).toBe(true);
|
|
235
|
+
expect(isSystemOwnedSession({
|
|
236
|
+
metadata: { openchamber: { smallModel: { purpose: 'session-title' } } },
|
|
237
|
+
})).toBe(true);
|
|
235
238
|
expect(isSystemOwnedSession({
|
|
236
239
|
title: '[Assistant] Looks system',
|
|
237
240
|
metadata: { openchamber: { assistant: { name: 'no-id' } } },
|
|
238
241
|
})).toBe(false);
|
|
242
|
+
expect(isSystemOwnedSession({
|
|
243
|
+
metadata: { openchamber: { smallModel: { purpose: '' } } },
|
|
244
|
+
})).toBe(false);
|
|
239
245
|
expect(isSystemOwnedSession({ title: 'Ordinary' })).toBe(false);
|
|
240
246
|
});
|
|
241
247
|
|