@openchambery/web 2.0.0-beta.3 → 2.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AgentAvatar-B_JQoreg.js → AgentAvatar-BBmQXYDq.js} +1 -1
- package/dist/assets/AssistantView-BG8hK4y9.js +3 -0
- package/dist/assets/{DiagramView-BNnda8qO.js → DiagramView-CN0qWNU5.js} +1 -1
- package/dist/assets/{FadeInOnReveal-VYQTSAkr.js → FadeInOnReveal-w_BQ7K7K.js} +1 -1
- package/dist/assets/Icon-B9kZgYZn.js +1 -0
- package/dist/assets/{MarkdownRenderer-CJHSsWin.js → MarkdownRenderer-Ds61--LT.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-DwrHFGUp.js → MarkdownRendererImpl-DOMmiOH0.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-CSnbQMu3.js → MarkstreamRendererImpl-F7MGg5ER.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-BXr6AGLT.js → MobileDetailNavigation-DcmVOjkw.js} +1 -1
- package/dist/assets/{MobileShareBridge-D7OE9OP_.js → MobileShareBridge-C1Fc4ErB.js} +2 -2
- package/dist/assets/MobileSurface-BA7eS5Yd.js +2 -0
- package/dist/assets/{MultiRunWindow-DF3iIFCd.js → MultiRunWindow-BoTnH6Yp.js} +1 -1
- package/dist/assets/{OnboardingScreen-BjHmlwlw.js → OnboardingScreen-BxItVnnO.js} +1 -1
- package/dist/assets/{OverlayScrollbar-DLtWO_Gp.js → OverlayScrollbar-B4e0QD05.js} +1 -1
- package/dist/assets/SettingsView-CjesWNLw.js +25 -0
- package/dist/assets/{SettingsWindow-BI5swspb.js → SettingsWindow-CReMbg-w.js} +1 -1
- package/dist/assets/TerminalView-C64-QYOG.js +8 -0
- package/dist/assets/{ToolOutputDialog-BEWhBcRd.js → ToolOutputDialog-CJu8DNrt.js} +1 -1
- package/dist/assets/{appThemeRegistry-4MR0F7Jz.js → appThemeRegistry-CF4lIxNt.js} +1 -1
- package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
- package/dist/assets/{button-DKuz3L8l.js → button-CwXyux0a.js} +1 -1
- package/dist/assets/collapsible-BZ6_cYJd.js +1 -0
- package/dist/assets/context-menu-DHrRifPE.js +1 -0
- package/dist/assets/{desktopHosts-CoDkOnyS.js → desktopHosts-JGFqhlaM.js} +1 -1
- package/dist/assets/{desktopRelayRestore-DDlVKyOE.js → desktopRelayRestore-CHEwrI93.js} +1 -1
- package/dist/assets/{diagram-D7T8HQgj.js → diagram-BwnPNKWt.js} +1 -1
- package/dist/assets/{dialog-DQYl3W_o.js → dialog-Dp0lQv5K.js} +1 -1
- package/dist/assets/es-DicJ8lKD.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-BZ6ovBYH.js → fileMentionTouchSelection-DPZRxNa2.js} +3 -4
- package/dist/assets/{fileQueries-JrpcsOCs.js → fileQueries-DN6gbLhM.js} +1 -1
- package/dist/assets/fr-lhgVcBW_.js +13 -0
- package/dist/assets/{gitApi-CM42qUed.js → gitApi-D8R7ic-T.js} +3 -3
- package/dist/assets/{imageSource-CIl0FL10.js → imageSource-BMztRu0c.js} +1 -1
- package/dist/assets/{input-CzQy8UhV.js → input-Biw8aNzR.js} +1 -1
- package/dist/assets/insertionBoundaries-DdwCD4S2.js +1 -0
- package/dist/assets/ja-B9EZwOcw.js +15 -0
- package/dist/assets/ko-Dxk-uJRb.js +15 -0
- package/dist/assets/main-BhlwwJ2c.js +169 -0
- package/dist/assets/main-D2BzpnUt.css +1 -0
- package/dist/assets/main-DSPCkT74.js +2 -0
- package/dist/assets/{markdownHeightCache-Cj1I5NDw.js → markdownHeightCache-BOOrGCuG.js} +1 -1
- package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
- package/dist/assets/{mcpQueries-D5mQymgM.js → mcpQueries-CE9tk-Jz.js} +1 -1
- package/dist/assets/miniChat-Ckk9ABql.js +2 -0
- package/dist/assets/mobile-cLMRZqI9.js +2 -0
- package/dist/assets/multirun-DVnFRb1V.js +4 -0
- package/dist/assets/persistence-D0y7226-.js +3 -0
- package/dist/assets/pl-CFhwWLJj.js +15 -0
- package/dist/assets/platform-D_c-T5nO.js +6 -0
- package/dist/assets/projectMeta-CSQgHequ.js +3 -0
- package/dist/assets/pt-BR-Ba6GfZ6J.js +15 -0
- package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-gGFkwoL_.js +2 -0
- package/dist/assets/renderMobileApp-B2w50kAc.js +28 -0
- package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
- package/dist/assets/{runtimeConfig-BIGBRGQI.js → runtimeConfig-BHH1hJDd.js} +1 -1
- package/dist/assets/runtimeEndpointReset-5uJ4sJk0.js +149 -0
- package/dist/assets/{sessionChangeSummary-eIbpSfrO.js → sessionChangeSummary-spf_atO8.js} +2 -2
- package/dist/assets/{sessionLookup-myHe5ltu.js → sessionLookup-BGjjwiCq.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-BQtmHx9r.js → sortable-tabs-strip-CTY_wG46.js} +1 -1
- package/dist/assets/src-BjEyU6if.css +2 -0
- package/dist/assets/store-DmowK-GO.js +15 -0
- package/dist/assets/{terminalApi-BxcdiduX.js → terminalApi-Dhqqp4xi.js} +1 -1
- package/dist/assets/textarea-QxsmcBGj.js +1 -0
- package/dist/assets/{tooltip-Cx5gTbdY.js → tooltip-CpMLl5_k.js} +1 -1
- package/dist/assets/uk-ChchBzy6.js +15 -0
- package/dist/assets/{url-CDHnjlsm.js → url-CGbJbiJq.js} +1 -1
- package/dist/assets/{useAppFontEffects-CNeo0aZs.js → useAppFontEffects-B1h7ab_J.js} +55 -50
- package/dist/assets/{useAssistantUIStore-asNG6K7n.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-Cxsx84NO.js +1 -0
- package/dist/assets/{useEffectiveDirectory-B4LvR8Gt.js → useEffectiveDirectory-DsqhqsJR.js} +1 -1
- package/dist/assets/{useFontPreferences-CUnQ4TVm.js → useFontPreferences-Xw1vSkmC.js} +1 -1
- package/dist/assets/useMobileNavigationStore-B1axS4g-.js +414 -0
- package/dist/assets/{useRuntimeAPIs-DwLtO0c_.js → useRuntimeAPIs-DSLXJybW.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-B5FTG_qf.js +2 -0
- package/dist/assets/{useThemeSystem-BlNUWH47.js → useThemeSystem-DMzWVJaW.js} +1 -1
- package/dist/assets/useUIStore-syVmbuDA.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-CJZbJDW3.js → useWorkerHighlightedLines-DC_Bc-RV.js} +1 -1
- package/dist/assets/{vendor-base-ui-CDjyofeD.js → vendor-base-ui-CnHfUmDN.js} +1 -1
- package/dist/assets/{vendor-cmdk-BFH0UpXX.js → vendor-cmdk-CeAGfrLo.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-accessibility-BrR3AKvM.js → vendor-dnd-kit-accessibility-BY5LhhUB.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-core-CNfdvk6H.js → vendor-dnd-kit-core-CvWxUPdb.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-sortable-DQUaTeGj.js → vendor-dnd-kit-sortable-hHdP9lLb.js} +1 -1
- package/dist/assets/{vendor-framer-motion-4dogsUk3.js → vendor-framer-motion-DFy_BSVd.js} +1 -1
- package/dist/assets/{vendor-legendapp-list-BU7MdPZR.js → vendor-legendapp-list-WE32lwNk.js} +1 -1
- package/dist/assets/{vendor-markstream-react-qHC2FykU.js → vendor-markstream-react-DX55XLGO.js} +3 -3
- package/dist/assets/vendor-opencode-client-C12gfjag.js +12 -0
- package/dist/assets/{vendor-pierre-diffs-CUfqC003.js → vendor-pierre-diffs-DX7XTPG3.js} +1 -1
- package/dist/assets/{vendor-react-C94A0AHl.js → vendor-react-DLPElpoQ.js} +1 -1
- package/dist/assets/{vendor-react-drawio-FVlWqKVE.js → vendor-react-drawio-ECtEwvbc.js} +1 -1
- package/dist/assets/{vendor-reactuses-core-C2A3EjUi.js → vendor-reactuses-core-B8parU1Y.js} +1 -1
- package/dist/assets/{vendor-sonner-DNqHmUQs.js → vendor-sonner-DVzioF3d.js} +1 -1
- package/dist/assets/{vendor-stream-diffs-CwbKsmlv.js → vendor-stream-diffs-DWy3xSmp.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-query-D0iFK57C.js → vendor-tanstack-react-query-D7pys_c9.js} +1 -1
- package/dist/assets/{vendor-tanstack-react-virtual-B2kV6cnT.js → vendor-tanstack-react-virtual-tlDgH6PG.js} +1 -1
- package/dist/assets/{vendor-zustand-BqLKf6ZY.js → vendor-zustand-B4dlGEM2.js} +1 -1
- package/dist/assets/zh-CN-Z_bkUQk5.js +15 -0
- package/dist/assets/zh-TW-CZeTm7kd.js +15 -0
- package/dist/index.html +14 -14
- package/dist/mini-chat.html +14 -14
- package/dist/mobile.html +14 -14
- package/package.json +3 -2
- package/server/index.js +38 -7
- package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
- package/server/lib/browser-provider/contract.js +312 -0
- package/server/lib/browser-provider/host.js +421 -0
- package/server/lib/browser-provider/host.test.js +304 -0
- package/server/lib/browser-provider/routes.js +122 -0
- package/server/lib/browser-provider/routes.test.js +120 -0
- package/server/lib/browser-provider/selection-store.js +43 -0
- package/server/lib/browser-provider/selection-store.test.js +35 -0
- package/server/lib/event-stream/DOCUMENTATION.md +3 -1
- package/server/lib/event-stream/reasoning-projection.js +16 -1
- package/server/lib/event-stream/reasoning-projection.test.js +15 -0
- package/server/lib/notifications/DOCUMENTATION.md +3 -1
- package/server/lib/notifications/runtime.js +128 -31
- package/server/lib/notifications/runtime.test.js +103 -0
- package/server/lib/notifications/template-runtime.js +130 -38
- package/server/lib/notifications/template-runtime.test.js +96 -0
- package/server/lib/opencode/DOCUMENTATION.md +8 -0
- package/server/lib/opencode/config-entity-routes.js +6 -1
- package/server/lib/opencode/config-entity-routes.test.js +25 -0
- package/server/lib/opencode/ensure-cli.d.ts +8 -0
- package/server/lib/opencode/feature-routes-runtime.js +10 -0
- package/server/lib/opencode/feature-routes-runtime.test.js +7 -0
- package/server/lib/opencode/proxy.js +6 -1
- package/server/lib/opencode/proxy.test.js +18 -3
- package/server/lib/opencode/routes.js +30 -0
- package/server/lib/opencode/upgrade-screen.d.ts +30 -0
- package/server/lib/opencode/upgrade-screen.js +299 -0
- package/server/lib/opencode/upgrade-screen.test.js +196 -0
- package/server/lib/session-metadata/DOCUMENTATION.md +27 -17
- package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
- package/server/lib/session-metadata/opencode-session-record.js +316 -0
- package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
- package/server/lib/session-metadata/routes.js +5 -1
- package/server/lib/session-metadata/session-archive.js +1 -1
- package/server/lib/session-metadata/session-metadata-store.d.ts +2 -0
- package/server/lib/session-metadata/session-metadata-store.js +63 -16
- package/server/lib/session-metadata/session-projection.js +26 -2
- package/dist/assets/AssistantView-ClQAfGOT.js +0 -3
- package/dist/assets/Icon-BiEQEcih.js +0 -1
- package/dist/assets/MobileSurface-D2jUkX9l.js +0 -2
- package/dist/assets/SettingsView-BRxELjow.js +0 -25
- package/dist/assets/TerminalView-BU3x8-fr.js +0 -8
- package/dist/assets/collapsible-DXbDynN4.js +0 -3
- package/dist/assets/context-menu-CqolsLCm.js +0 -1
- package/dist/assets/es-CN3E_zXj.js +0 -15
- package/dist/assets/fr-9zZvQZD2.js +0 -13
- package/dist/assets/insertionBoundaries-CtkZ9720.js +0 -1
- package/dist/assets/ja-Dnjk_82N.js +0 -15
- package/dist/assets/ko-BxCEE99C.js +0 -15
- package/dist/assets/main-BILgBI3B.js +0 -169
- package/dist/assets/main-BeveiW6F.js +0 -2
- package/dist/assets/main-qSeeJRW1.css +0 -1
- package/dist/assets/markdownTheme-CKac3wKC.js +0 -1
- package/dist/assets/miniChat-BgrzM3u7.js +0 -2
- package/dist/assets/mobile-DtYoQE8C.js +0 -2
- package/dist/assets/multirun-jy8dEI96.js +0 -4
- package/dist/assets/persistence-C9LQpiOS.js +0 -3
- package/dist/assets/pl-CNNsWnPM.js +0 -15
- package/dist/assets/platform-BQghHRfp.js +0 -6
- package/dist/assets/pt-BR-BZB2DJMp.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +0 -2
- package/dist/assets/renderMobileApp-u3c4GtFC.js +0 -28
- package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
- package/dist/assets/runtimeEndpointReset-D2savkuw.js +0 -147
- package/dist/assets/src-CLN7Z-zx.css +0 -2
- package/dist/assets/store-DLCiZaA6.js +0 -15
- package/dist/assets/textarea-CakKvqFw.js +0 -1
- package/dist/assets/uk-rwQrxeYM.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +0 -1
- package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +0 -414
- package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +0 -2
- package/dist/assets/useUIStore-CQYKJnrg.js +0 -1
- package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
- package/dist/assets/zh-CN-BvmBDnkD.js +0 -15
- package/dist/assets/zh-TW-9wcHTX0y.js +0 -15
|
@@ -13,6 +13,21 @@ import {
|
|
|
13
13
|
} from './reasoning-projection.js';
|
|
14
14
|
|
|
15
15
|
describe('shouldIncludeReasoning / query helpers', () => {
|
|
16
|
+
it('projects v2 list/context and exact snapshots without dropping rows or cursor', () => {
|
|
17
|
+
const row = { id: 'm', type: 'assistant', tokens: { reasoning: 12 }, content: [
|
|
18
|
+
{ type: 'reasoning', text: 'hidden' }, { type: 'text', text: 'visible' },
|
|
19
|
+
] };
|
|
20
|
+
const payload = { data: [row], cursor: { next: 'older' } };
|
|
21
|
+
const projected = projectMessagesPayloadForReasoning(payload, false);
|
|
22
|
+
expect(projected).toEqual({ data: [{ ...row, content: [row.content[1]] }], cursor: payload.cursor });
|
|
23
|
+
expect(projectMessagesPayloadForReasoning(row, false)).toEqual(projected.data[0]);
|
|
24
|
+
expect(projectMessagesPayloadForReasoning(payload, true)).toBe(payload);
|
|
25
|
+
expect(row.content).toHaveLength(2);
|
|
26
|
+
const filter = createReasoningOutboundFilter();
|
|
27
|
+
expect(filter.projectEvent({ type: 'session.reasoning.delta.1', data: { delta: 'hidden' } })).toBeNull();
|
|
28
|
+
const text = { type: 'session.text.delta', data: { delta: 'visible' } };
|
|
29
|
+
expect(filter.projectEvent(text)).toBe(text);
|
|
30
|
+
});
|
|
16
31
|
it('only treats the strict string false as disabled', () => {
|
|
17
32
|
expect(shouldIncludeReasoning(undefined)).toBe(true);
|
|
18
33
|
expect(shouldIncludeReasoning(null)).toBe(true);
|
|
@@ -107,7 +107,9 @@ This module provides notification message preparation utilities for the web serv
|
|
|
107
107
|
- `extractLastMessageText(payload, maxLength?)`
|
|
108
108
|
- `fetchLastAssistantMessageText(sessionId, messageId, maxLength?)`
|
|
109
109
|
- `maybeCacheSessionInfoFromEvent(payload)`
|
|
110
|
-
- `buildTemplateVariables(payload, sessionId)`
|
|
110
|
+
- `buildTemplateVariables(payload, sessionId)` — `session_name` is the session title. OpenCode 2 returns that title on `GET /api/session/:id` as `{ data: { title } }` and on `session.created` / `session.renamed` as `data.title`. Legacy `properties.info.title` still counts. An empty title stays empty so push can fall back to the localized "Session" label. `agent_name` / `model_name` fall back to that same session record when the triggering event has none. `fetchLastAssistantMessageText` accepts a legacy message array and an OpenCode 2 `{ data }` page (`order=desc`).
|
|
111
|
+
- Question prompts come from legacy `question.asked` or OpenCode 2 `form.created` (`data.form`). Permission prompts read `permission.asked` from `properties` or `data` (`action`, `message`, `id`). `form.replied` / `form.cancelled` cancel a pending question notification the same way `permission.replied` cancels a permission notification.
|
|
112
|
+
- iOS APNs and Android FCM share this payload: native banners stay scenario title + session name. Desktop, in-app, and web-push use the resolved template title and body.
|
|
111
113
|
- `getCachedZenModels()`
|
|
112
114
|
|
|
113
115
|
## Constants
|
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
import { readNotificationSessionRecord } from './template-runtime.js';
|
|
2
|
+
|
|
3
|
+
const trimmedText = (value) => (typeof value === 'string' && value.trim() ? value.trim() : '');
|
|
4
|
+
|
|
5
|
+
const readNotificationEventFields = (payload) => {
|
|
6
|
+
const properties = payload?.properties && typeof payload.properties === 'object' && !Array.isArray(payload.properties)
|
|
7
|
+
? payload.properties
|
|
8
|
+
: {};
|
|
9
|
+
const data = payload?.data && typeof payload.data === 'object' && !Array.isArray(payload.data)
|
|
10
|
+
? payload.data
|
|
11
|
+
: {};
|
|
12
|
+
const form = (properties.form && typeof properties.form === 'object' ? properties.form : null)
|
|
13
|
+
|| (data.form && typeof data.form === 'object' ? data.form : null);
|
|
14
|
+
const questions = Array.isArray(properties.questions)
|
|
15
|
+
? properties.questions
|
|
16
|
+
: (Array.isArray(data.questions) ? data.questions : []);
|
|
17
|
+
const firstQuestion = questions[0] && typeof questions[0] === 'object' ? questions[0] : null;
|
|
18
|
+
const field = Array.isArray(form?.fields)
|
|
19
|
+
? form.fields.find((item) => item && typeof item === 'object' && item.hidden !== true)
|
|
20
|
+
: null;
|
|
21
|
+
return {
|
|
22
|
+
requestId: trimmedText(properties.id)
|
|
23
|
+
|| trimmedText(properties.requestID)
|
|
24
|
+
|| trimmedText(properties.requestId)
|
|
25
|
+
|| trimmedText(data.id)
|
|
26
|
+
|| trimmedText(data.requestID)
|
|
27
|
+
|| trimmedText(data.requestId)
|
|
28
|
+
|| trimmedText(form?.id),
|
|
29
|
+
permission: trimmedText(properties.permission) || trimmedText(data.permission) || trimmedText(data.action),
|
|
30
|
+
message: trimmedText(data.message) || trimmedText(properties.message),
|
|
31
|
+
header: trimmedText(firstQuestion?.header) || trimmedText(field?.title) || trimmedText(form?.title),
|
|
32
|
+
questionText: trimmedText(firstQuestion?.question)
|
|
33
|
+
|| trimmedText(field?.description)
|
|
34
|
+
|| trimmedText(field?.title)
|
|
35
|
+
|| trimmedText(form?.title),
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
1
39
|
export const createNotificationTriggerRuntime = (deps) => {
|
|
2
40
|
const {
|
|
3
41
|
readSettingsFromDisk,
|
|
@@ -255,14 +293,14 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
255
293
|
if (!response.ok) {
|
|
256
294
|
return cached;
|
|
257
295
|
}
|
|
258
|
-
const session = await response.json().catch(() => null);
|
|
259
|
-
if (!session
|
|
296
|
+
const session = readNotificationSessionRecord(await response.json().catch(() => null));
|
|
297
|
+
if (!session) {
|
|
260
298
|
return cached;
|
|
261
299
|
}
|
|
262
300
|
|
|
263
301
|
const meta = {
|
|
264
302
|
parentID: normalizeParentID(session.parentID),
|
|
265
|
-
title:
|
|
303
|
+
title: trimmedText(session.title) || null,
|
|
266
304
|
complete: true,
|
|
267
305
|
...metaFromOpenchamber(session.metadata?.openchamber),
|
|
268
306
|
};
|
|
@@ -346,13 +384,24 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
346
384
|
|
|
347
385
|
const extractSessionIdFromPayload = (payload) => {
|
|
348
386
|
if (!payload || typeof payload !== 'object') return null;
|
|
349
|
-
const props = payload.properties
|
|
387
|
+
const props = payload.properties && typeof payload.properties === 'object'
|
|
388
|
+
? payload.properties
|
|
389
|
+
: (payload.data && typeof payload.data === 'object' ? payload.data : null);
|
|
350
390
|
const info = props?.info;
|
|
391
|
+
const data = payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)
|
|
392
|
+
? payload.data
|
|
393
|
+
: null;
|
|
351
394
|
const sessionId =
|
|
352
395
|
info?.sessionID ??
|
|
353
396
|
info?.sessionId ??
|
|
354
397
|
props?.sessionID ??
|
|
355
398
|
props?.sessionId ??
|
|
399
|
+
props?.form?.sessionID ??
|
|
400
|
+
props?.form?.sessionId ??
|
|
401
|
+
data?.sessionID ??
|
|
402
|
+
data?.sessionId ??
|
|
403
|
+
data?.form?.sessionID ??
|
|
404
|
+
data?.form?.sessionId ??
|
|
356
405
|
props?.session ??
|
|
357
406
|
null;
|
|
358
407
|
return typeof sessionId === 'string' && sessionId.length > 0 ? sessionId : null;
|
|
@@ -360,11 +409,32 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
360
409
|
|
|
361
410
|
const extractDirectoryFromPayload = (payload) => {
|
|
362
411
|
if (!payload || typeof payload !== 'object') return undefined;
|
|
363
|
-
const props = payload.properties;
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
412
|
+
const props = payload.properties && typeof payload.properties === 'object' ? payload.properties : {};
|
|
413
|
+
const data = payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data) ? payload.data : {};
|
|
414
|
+
const info = props.info && typeof props.info === 'object' ? props.info : {};
|
|
415
|
+
const dataInfo = data.info && typeof data.info === 'object' ? data.info : {};
|
|
416
|
+
const candidates = [
|
|
417
|
+
props.directory,
|
|
418
|
+
info.directory,
|
|
419
|
+
info.location?.directory,
|
|
420
|
+
payload.location?.directory,
|
|
421
|
+
data.directory,
|
|
422
|
+
data.location?.directory,
|
|
423
|
+
dataInfo.directory,
|
|
424
|
+
dataInfo.location?.directory,
|
|
425
|
+
];
|
|
426
|
+
for (const value of candidates) {
|
|
427
|
+
const directory = trimmedText(value);
|
|
428
|
+
if (directory) return directory;
|
|
429
|
+
}
|
|
430
|
+
return undefined;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const resolvePushSessionName = async (variables, sessionId, directory) => {
|
|
434
|
+
const fromTemplate = trimmedText(variables?.session_name);
|
|
435
|
+
if (fromTemplate) return fromTemplate;
|
|
436
|
+
const meta = await fetchSessionMeta(sessionId, directory);
|
|
437
|
+
return trimmedText(meta?.title);
|
|
368
438
|
};
|
|
369
439
|
|
|
370
440
|
// A session with an ACTIVE goal suppresses per-turn ready notifications;
|
|
@@ -381,7 +451,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
381
451
|
signal: AbortSignal.timeout(2000),
|
|
382
452
|
});
|
|
383
453
|
if (!response.ok) return false;
|
|
384
|
-
const session = await response.json().catch(() => null);
|
|
454
|
+
const session = readNotificationSessionRecord(await response.json().catch(() => null));
|
|
385
455
|
const goal = session?.metadata?.openchamber?.goal;
|
|
386
456
|
return Boolean(goal && typeof goal === 'object' && goal.status === 'active');
|
|
387
457
|
} catch {
|
|
@@ -417,20 +487,31 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
417
487
|
const reason = payload.properties?.reason ?? payload.data?.reason;
|
|
418
488
|
if (reason === 'shutdown') return;
|
|
419
489
|
}
|
|
420
|
-
|
|
421
|
-
|
|
490
|
+
// v2 completion is session.execution.succeeded. session.idle is a deprecated
|
|
491
|
+
// fallback; the ready-notification cooldown below collapses the pair.
|
|
492
|
+
if ((
|
|
493
|
+
payload.type === 'session.idle'
|
|
494
|
+
|| payload.type === 'session.execution.succeeded'
|
|
495
|
+
|| payload.type === 'session.error'
|
|
496
|
+
|| payload.type === 'session.execution.failed'
|
|
497
|
+
) && sessionId) {
|
|
498
|
+
const body = payload.properties && typeof payload.properties === 'object'
|
|
499
|
+
? payload.properties
|
|
500
|
+
: (payload.data && typeof payload.data === 'object' ? payload.data : {});
|
|
501
|
+
const error = body?.error;
|
|
422
502
|
const errorText = typeof error?.message === 'string'
|
|
423
503
|
? error.message
|
|
424
504
|
: typeof error === 'string' ? error : '';
|
|
505
|
+
const failed = payload.type === 'session.error' || payload.type === 'session.execution.failed';
|
|
425
506
|
await maybeSendPushForTrigger({
|
|
426
507
|
...payload,
|
|
427
508
|
type: 'message.updated',
|
|
428
509
|
properties: {
|
|
429
|
-
...payload.properties,
|
|
510
|
+
...(payload.properties && typeof payload.properties === 'object' ? payload.properties : body),
|
|
430
511
|
info: {
|
|
431
512
|
sessionID: sessionId,
|
|
432
513
|
role: 'assistant',
|
|
433
|
-
finish:
|
|
514
|
+
finish: failed ? 'error' : 'stop',
|
|
434
515
|
...(errorText ? { parts: [{ type: 'text', text: errorText }] } : {}),
|
|
435
516
|
},
|
|
436
517
|
},
|
|
@@ -492,12 +573,12 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
492
573
|
const completionTemplate = templates.completion || { title: 'Task completed', message: '{session_name}' };
|
|
493
574
|
|
|
494
575
|
const variables = await buildTemplateVariables(payload, sessionId);
|
|
495
|
-
sessionName =
|
|
576
|
+
sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
|
|
496
577
|
|
|
497
578
|
const messageId = info?.id;
|
|
498
579
|
let lastMessage = extractLastMessageText(payload);
|
|
499
580
|
if (!lastMessage) {
|
|
500
|
-
lastMessage = await fetchLastAssistantMessageText(sessionId, messageId);
|
|
581
|
+
lastMessage = await fetchLastAssistantMessageText(sessionId, messageId, undefined, notificationDirectory);
|
|
501
582
|
}
|
|
502
583
|
|
|
503
584
|
variables.last_message = await prepareNotificationLastMessage({
|
|
@@ -548,7 +629,21 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
548
629
|
return;
|
|
549
630
|
}
|
|
550
631
|
|
|
551
|
-
if (
|
|
632
|
+
if ((
|
|
633
|
+
payload.type === 'form.replied'
|
|
634
|
+
|| payload.type === 'form.cancelled'
|
|
635
|
+
|| payload.type === 'question.replied'
|
|
636
|
+
|| payload.type === 'question.rejected'
|
|
637
|
+
) && sessionId) {
|
|
638
|
+
const existingTimer = pushQuestionDebounceTimers.get(sessionId);
|
|
639
|
+
if (existingTimer) {
|
|
640
|
+
clearTimeout(existingTimer);
|
|
641
|
+
pushQuestionDebounceTimers.delete(sessionId);
|
|
642
|
+
}
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if ((payload.type === 'question.asked' || payload.type === 'form.created') && sessionId) {
|
|
552
647
|
if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
|
|
553
648
|
return;
|
|
554
649
|
}
|
|
@@ -569,9 +664,9 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
569
664
|
return;
|
|
570
665
|
}
|
|
571
666
|
|
|
572
|
-
const
|
|
573
|
-
const header =
|
|
574
|
-
const questionText =
|
|
667
|
+
const prompt = readNotificationEventFields(payload);
|
|
668
|
+
const header = prompt.header;
|
|
669
|
+
const questionText = prompt.questionText;
|
|
575
670
|
|
|
576
671
|
let title = 'Needs your answer';
|
|
577
672
|
let body = '';
|
|
@@ -579,7 +674,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
579
674
|
|
|
580
675
|
try {
|
|
581
676
|
const variables = await buildTemplateVariables(payload, sessionId);
|
|
582
|
-
sessionName =
|
|
677
|
+
sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
|
|
583
678
|
variables.last_message = questionText || header || '';
|
|
584
679
|
|
|
585
680
|
const templates = settings.notificationTemplates || {};
|
|
@@ -630,8 +725,8 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
630
725
|
}
|
|
631
726
|
|
|
632
727
|
if (payload.type === 'permission.replied' && sessionId) {
|
|
633
|
-
const requestId = payload.
|
|
634
|
-
const requestKey =
|
|
728
|
+
const requestId = readNotificationEventFields(payload).requestId;
|
|
729
|
+
const requestKey = requestId ? `${sessionId}:${requestId}` : null;
|
|
635
730
|
const pendingNotification = pushPermissionDebounceTimers.get(sessionId);
|
|
636
731
|
if (!pendingNotification) {
|
|
637
732
|
return;
|
|
@@ -651,9 +746,10 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
651
746
|
if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
|
|
652
747
|
return;
|
|
653
748
|
}
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
const
|
|
749
|
+
const permissionFields = readNotificationEventFields(payload);
|
|
750
|
+
const requestId = permissionFields.requestId;
|
|
751
|
+
const permission = permissionFields.permission;
|
|
752
|
+
const requestKey = requestId ? `${sessionId}:${requestId}` : null;
|
|
657
753
|
if (requestKey && notifiedPermissionRequests.has(requestKey)) {
|
|
658
754
|
return;
|
|
659
755
|
}
|
|
@@ -691,11 +787,12 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
691
787
|
return;
|
|
692
788
|
}
|
|
693
789
|
|
|
694
|
-
const sessionTitle = payload.properties?.sessionTitle;
|
|
790
|
+
const sessionTitle = payload.properties?.sessionTitle ?? payload.data?.sessionTitle;
|
|
695
791
|
const permissionText = typeof permission === 'string' && permission.length > 0 ? permission : '';
|
|
792
|
+
const detail = permissionFields.message;
|
|
696
793
|
const fallbackMessage = typeof sessionTitle === 'string' && sessionTitle.trim().length > 0
|
|
697
794
|
? sessionTitle.trim()
|
|
698
|
-
: permissionText || 'Agent is waiting for your approval';
|
|
795
|
+
: detail || permissionText || 'Agent is waiting for your approval';
|
|
699
796
|
|
|
700
797
|
let title = 'Needs permission';
|
|
701
798
|
let body = '';
|
|
@@ -703,7 +800,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
703
800
|
|
|
704
801
|
try {
|
|
705
802
|
const variables = await buildTemplateVariables(payload, sessionId);
|
|
706
|
-
sessionName =
|
|
803
|
+
sessionName = await resolvePushSessionName(variables, sessionId, notificationDirectory);
|
|
707
804
|
variables.last_message = fallbackMessage;
|
|
708
805
|
|
|
709
806
|
const templates = settings.notificationTemplates || {};
|
|
@@ -771,8 +868,8 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
771
868
|
signal: AbortSignal.timeout(2000),
|
|
772
869
|
});
|
|
773
870
|
if (response.ok) {
|
|
774
|
-
const session = await response.json().catch(() => null);
|
|
775
|
-
|
|
871
|
+
const session = readNotificationSessionRecord(await response.json().catch(() => null));
|
|
872
|
+
sessionName = trimmedText(session?.title);
|
|
776
873
|
}
|
|
777
874
|
} catch {
|
|
778
875
|
// Session name is presentation sugar for the mobile push — never block on it.
|
|
@@ -457,6 +457,17 @@ describe('contact turn notifications', () => {
|
|
|
457
457
|
});
|
|
458
458
|
|
|
459
459
|
describe('notification trigger live activity end', () => {
|
|
460
|
+
it('treats session.execution.succeeded as completion, including a v2 data envelope', async () => {
|
|
461
|
+
vi.stubGlobal('fetch', vi.fn(async () => rootSessionResponse('ses_done')));
|
|
462
|
+
const { runtime, sendLiveActivityEnd, emitDesktopNotification } = createRuntime();
|
|
463
|
+
await runtime.maybeSendPushForTrigger({
|
|
464
|
+
type: 'session.execution.succeeded',
|
|
465
|
+
data: { sessionID: 'ses_done' },
|
|
466
|
+
});
|
|
467
|
+
expect(sendLiveActivityEnd).toHaveBeenCalledWith({ sessionId: 'ses_done', status: 'complete' });
|
|
468
|
+
expect(emitDesktopNotification).toHaveBeenCalled();
|
|
469
|
+
});
|
|
470
|
+
|
|
460
471
|
it('ends the live activity on top-level completion and error', async () => {
|
|
461
472
|
vi.stubGlobal('fetch', vi.fn(async () => rootSessionResponse()));
|
|
462
473
|
const { runtime, sendLiveActivityEnd } = createRuntime();
|
|
@@ -615,6 +626,97 @@ describe('notification event toggles', () => {
|
|
|
615
626
|
expect(sendPushToAllUiSessions).not.toHaveBeenCalled();
|
|
616
627
|
});
|
|
617
628
|
|
|
629
|
+
it('notifies on an OpenCode 2 question form and a permission request', async () => {
|
|
630
|
+
vi.useFakeTimers();
|
|
631
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
632
|
+
data: { id: 'ses_ask', parentID: null, title: '发布说明', metadata: {} },
|
|
633
|
+
})));
|
|
634
|
+
const resolveNotificationTemplate = vi.fn((template, variables) => (
|
|
635
|
+
typeof template === 'string'
|
|
636
|
+
? template.replace(/\{(\w+)\}/g, (_match, key) => variables?.[key] ?? '')
|
|
637
|
+
: template
|
|
638
|
+
));
|
|
639
|
+
const { runtime, emitDesktopNotification, sendApnsToAllUiSessions } = createRuntime({
|
|
640
|
+
buildTemplateVariables: vi.fn(async () => ({ session_name: '' })),
|
|
641
|
+
resolveNotificationTemplate,
|
|
642
|
+
readSettingsFromDisk: vi.fn(async () => ({
|
|
643
|
+
...defaultSettings,
|
|
644
|
+
notificationTemplates: {
|
|
645
|
+
question: { title: 'Needs your answer', message: '{last_message}' },
|
|
646
|
+
},
|
|
647
|
+
})),
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
await runtime.maybeSendPushForTrigger({
|
|
651
|
+
type: 'form.created',
|
|
652
|
+
data: {
|
|
653
|
+
form: {
|
|
654
|
+
id: 'frm_1',
|
|
655
|
+
sessionID: 'ses_ask',
|
|
656
|
+
title: 'Questions',
|
|
657
|
+
metadata: { kind: 'question' },
|
|
658
|
+
fields: [{ key: 'q0', title: '确认入口', description: '从哪个入口触发?' }],
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
});
|
|
662
|
+
await vi.runAllTimersAsync();
|
|
663
|
+
|
|
664
|
+
expect(emitDesktopNotification).toHaveBeenCalledWith(expect.objectContaining({
|
|
665
|
+
kind: 'question',
|
|
666
|
+
body: '从哪个入口触发?',
|
|
667
|
+
}));
|
|
668
|
+
expect(sendApnsToAllUiSessions).toHaveBeenCalledWith(
|
|
669
|
+
expect.objectContaining({ type: 'question', sessionName: '发布说明' }),
|
|
670
|
+
{ requireNoSse: true },
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
emitDesktopNotification.mockClear();
|
|
674
|
+
sendApnsToAllUiSessions.mockClear();
|
|
675
|
+
await runtime.maybeSendPushForTrigger({
|
|
676
|
+
type: 'permission.asked',
|
|
677
|
+
data: {
|
|
678
|
+
id: 'per_1',
|
|
679
|
+
sessionID: 'ses_ask',
|
|
680
|
+
action: 'edit',
|
|
681
|
+
message: '修改 README',
|
|
682
|
+
resources: ['README.md'],
|
|
683
|
+
},
|
|
684
|
+
});
|
|
685
|
+
await vi.runAllTimersAsync();
|
|
686
|
+
|
|
687
|
+
expect(emitDesktopNotification).toHaveBeenCalledWith(expect.objectContaining({
|
|
688
|
+
kind: 'permission',
|
|
689
|
+
body: '修改 README',
|
|
690
|
+
}));
|
|
691
|
+
expect(sendApnsToAllUiSessions).toHaveBeenCalledWith(
|
|
692
|
+
expect.objectContaining({ type: 'permission', sessionName: '发布说明' }),
|
|
693
|
+
{ requireNoSse: true },
|
|
694
|
+
);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('uses the wrapped OpenCode 2 session title when the template has none', async () => {
|
|
698
|
+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({
|
|
699
|
+
data: {
|
|
700
|
+
id: 'ses_done',
|
|
701
|
+
parentID: null,
|
|
702
|
+
title: '发布说明',
|
|
703
|
+
metadata: {},
|
|
704
|
+
},
|
|
705
|
+
})));
|
|
706
|
+
const { runtime, sendApnsToAllUiSessions } = createRuntime({
|
|
707
|
+
buildTemplateVariables: vi.fn(async () => ({ session_name: '' })),
|
|
708
|
+
});
|
|
709
|
+
await runtime.maybeSendPushForTrigger({
|
|
710
|
+
type: 'session.execution.succeeded',
|
|
711
|
+
location: { directory: '/repo' },
|
|
712
|
+
data: { sessionID: 'ses_done' },
|
|
713
|
+
});
|
|
714
|
+
expect(sendApnsToAllUiSessions).toHaveBeenCalledWith(
|
|
715
|
+
expect.objectContaining({ type: 'ready', sessionName: '发布说明' }),
|
|
716
|
+
{ requireNoSse: true },
|
|
717
|
+
);
|
|
718
|
+
});
|
|
719
|
+
|
|
618
720
|
it('still sends goal settle push by default', async () => {
|
|
619
721
|
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ id: 'ses_goal', title: 'Goal run' })));
|
|
620
722
|
const { runtime, sendPushToAllUiSessions } = createRuntime();
|
|
@@ -626,6 +728,7 @@ describe('notification event toggles', () => {
|
|
|
626
728
|
});
|
|
627
729
|
expect(sendPushToAllUiSessions).toHaveBeenCalledTimes(1);
|
|
628
730
|
expect(sendPushToAllUiSessions.mock.calls[0][0]).toMatchObject({ tag: 'goal-ses_goal' });
|
|
731
|
+
expect(sendPushToAllUiSessions.mock.calls[0][0].data).toMatchObject({ sessionName: 'Goal run' });
|
|
629
732
|
});
|
|
630
733
|
});
|
|
631
734
|
|