@openchambery/web 1.18.7-beta.1 → 1.18.7-beta.3
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-CVTgW_rx.js +1 -0
- package/dist/assets/AssistantView-CvQ91w2s.js +1 -0
- package/dist/assets/{ChatContainer-Dnxdg6He.js → ChatContainer-CERrtaB-.js} +41 -43
- package/dist/assets/ContextPanel-CLz4r1G-.js +285 -0
- package/dist/assets/{DiagramView-BplbsR8l.js → DiagramView-DhV4Zthj.js} +1 -1
- package/dist/assets/{FadeInOnReveal-By2F9vcI.js → FadeInOnReveal-Cy3sf2M5.js} +1 -1
- package/dist/assets/{Icon-DpjYnXxn.js → Icon-BLWVqBee.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-EP8W7j6_.js → MarkdownRendererImpl-B4qwUu4q.js} +2 -2
- package/dist/assets/{MobileDetailNavigation-CYq_8ZAq.js → MobileDetailNavigation-CuWgEThi.js} +1 -1
- package/dist/assets/{MobileShareBridge-VmA8Qdh0.js → MobileShareBridge-C3nay5Sq.js} +2 -2
- package/dist/assets/MobileSurface-D7v_ZkYO.js +2 -0
- package/dist/assets/{MultiRunWindow-F6NGMY5U.js → MultiRunWindow-Cq8yyiqm.js} +1 -1
- package/dist/assets/{OnboardingScreen-BKmuT7AL.js → OnboardingScreen-CTNi2zuA.js} +1 -1
- package/dist/assets/OverlayScrollbar-GQgJusKL.js +2 -0
- package/dist/assets/SettingsView-CViKCsxJ.js +25 -0
- package/dist/assets/{SettingsWindow-Z4sr2ZBF.js → SettingsWindow-C6vwy4hH.js} +1 -1
- package/dist/assets/{TerminalView-DnOowqR4.js → TerminalView-C1aX22ka.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BKJJN6ZR.js → ToolOutputDialog-BrOqj1sB.js} +4 -4
- package/dist/assets/appThemeRegistry-BbqImxSo.js +2 -0
- package/dist/assets/{button-C7OXr4ae.js → button-CeCkEpvn.js} +1 -1
- package/dist/assets/{clipboard-DWuULPzo.js → clipboard-BXC1vZwE.js} +1 -1
- package/dist/assets/{collapsible-DaEya6b6.js → collapsible-B-aagADh.js} +1 -1
- package/dist/assets/{desktopHosts-Txp4xOVQ.js → desktopHosts-BrrYPqKq.js} +1 -1
- package/dist/assets/{desktopRelayRestore-C_FSEHKM.js → desktopRelayRestore-DR60DATT.js} +1 -1
- package/dist/assets/{diagram-CHfginjS.js → diagram-Cxma_IKI.js} +1 -1
- package/dist/assets/dialog-wraJHZQ2.js +1 -0
- package/dist/assets/es-DQqFAaWT.js +15 -0
- package/dist/assets/{fileQueries-BrUKKgPW.js → fileQueries-BJKLdYos.js} +1 -1
- package/dist/assets/fr-CNfGRpc1.js +13 -0
- package/dist/assets/{gitApi-CtHhG2UX.js → gitApi-B4z4eSZV.js} +1 -1
- package/dist/assets/{input-BzYA603C.js → input-CyG05w-V.js} +1 -1
- package/dist/assets/insertionBoundaries-DCYAK5W1.js +4 -0
- package/dist/assets/ja-CQqehnVn.js +15 -0
- package/dist/assets/ko-kmwi8UJD.js +15 -0
- package/dist/assets/main-A8oe9r34.js +29 -0
- package/dist/assets/{main-YMWxpyqS.js → main-BJBKpmw3.js} +2 -2
- package/dist/assets/{markdown-worker-C3uVXiOS.js → markdown-worker-DDCUZkz4.js} +1 -1
- package/dist/assets/{markdownTheme-B89dbj3N.js → markdownTheme-H7_7lbuh.js} +1 -1
- package/dist/assets/miniChat-BObOyFSV.js +2 -0
- package/dist/assets/mobile-Ba390Cmc.js +2 -0
- package/dist/assets/multirun-CMDYOrmD.js +4 -0
- package/dist/assets/{persistence-6sclr7B6.js → persistence-DE9RnVcA.js} +2 -2
- package/dist/assets/pl-Blv5o2QA.js +15 -0
- package/dist/assets/platform-MjT8iqKB.js +2 -0
- package/dist/assets/projectMeta-uWBWFG5A.js +3 -0
- package/dist/assets/pt-BR-BK74t-Bd.js +15 -0
- package/dist/assets/radio-CEXUd4RC.js +1 -0
- package/dist/assets/renderElectronMiniChatApp-Bpgb2E78.js +2 -0
- package/dist/assets/renderMobileApp-BMwRozEC.js +23 -0
- package/dist/assets/runtimeConfig-hHuyauKZ.js +2 -0
- package/dist/assets/runtimeEndpointReset-CmMI7Xp1.js +4 -0
- package/dist/assets/shikiHighlight-C-kTQvqd.js +1 -0
- package/dist/assets/{sortable-tabs-strip-nLsPC06f.js → sortable-tabs-strip-BdbKDDAP.js} +1 -1
- package/dist/assets/src-CgFGBE_A.css +2 -0
- package/dist/assets/store-UweBeN2y.js +15 -0
- package/dist/assets/{terminalApi-BpPeXWIu.js → terminalApi-Bnx0KvaV.js} +1 -1
- package/dist/assets/{tooltip-D_orJMYd.js → tooltip-B__x-HDJ.js} +1 -1
- package/dist/assets/uk-6I-KXy3R.js +15 -0
- package/dist/assets/{url-BsaBG5-1.js → url-CTuw7Rjf.js} +1 -1
- package/dist/assets/{useAppFontEffects-DPE9SuwC.js → useAppFontEffects-8iAE0Yrj.js} +5 -5
- package/dist/assets/{useAssistantUIStore-BHsHXCKE.js → useAssistantUIStore-D2K7ksGj.js} +1 -1
- package/dist/assets/{useDesktopWindowControlsLayout-evJk53fe.js → useDesktopWindowControlsLayout--oz1VAPt.js} +1 -1
- package/dist/assets/useEffectiveDirectory-DmoqXaa4.js +1 -0
- package/dist/assets/{useFontPreferences-ckzpXQaJ.js → useFontPreferences-CWZnNRjz.js} +1 -1
- package/dist/assets/useMobileNavigationStore-BxbEX-es.js +412 -0
- package/dist/assets/useSessionAutoCleanup-BwlJlhY7.js +1 -0
- package/dist/assets/{useUIStore-DzwCfRqr.js → useUIStore-DOxLVeGb.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-ClEzCydD.js → useWorkerHighlightedLines-B21D9MOD.js} +1 -1
- package/dist/assets/{utils-B1EK5zKa.js → utils-C2R0YoC5.js} +1 -1
- package/dist/assets/vendor-legendapp-list-WE32lwNk.js +1 -0
- package/dist/assets/zh-CN-DwlzS4yK.js +15 -0
- package/dist/assets/zh-TW-p0VriyfT.js +15 -0
- package/dist/index.html +9 -9
- package/dist/mini-chat.html +9 -9
- package/dist/mobile.html +9 -9
- package/package.json +3 -2
- package/server/index.js +23 -20
- package/server/lib/config-sync/DOCUMENTATION.md +6 -7
- package/server/lib/config-sync/engine.js +0 -8
- package/server/lib/config-sync/index.js +0 -7
- package/server/lib/config-sync/receiver.js +0 -35
- package/server/lib/config-sync/routes.js +8 -41
- package/server/lib/config-sync/routes.test.js +1 -38
- package/server/lib/notifications/APNS.md +88 -38
- package/server/lib/notifications/DOCUMENTATION.md +11 -4
- package/server/lib/notifications/apns-runtime.js +793 -78
- package/server/lib/notifications/apns-runtime.test.js +922 -0
- package/server/lib/notifications/apns-titles.js +212 -0
- package/server/lib/notifications/apns-titles.test.js +55 -0
- package/server/lib/notifications/routes.js +57 -1
- package/server/lib/notifications/runtime.js +20 -17
- package/server/lib/notifications/runtime.test.js +108 -0
- package/server/lib/opencode/DOCUMENTATION.md +1 -1
- package/server/lib/opencode/bootstrap-runtime.js +6 -0
- package/server/lib/opencode/bootstrap-runtime.test.js +21 -0
- package/server/lib/opencode/openchamber-routes.js +1 -7
- package/server/lib/opencode/routes.js +24 -5
- package/server/lib/opencode/routes.test.js +78 -1
- package/server/lib/opencode/settings-helpers.js +0 -3
- package/server/lib/opencode/shutdown-runtime.js +0 -2
- package/server/lib/relay/DOCUMENTATION.md +2 -2
- package/server/lib/relay/service.js +45 -3
- package/server/lib/relay/service.test.js +117 -0
- package/server/lib/relay/signing-key.js +1 -1
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +27 -2
- package/server/lib/scheduled-tasks/runtime.js +271 -3
- package/server/lib/scheduled-tasks/runtime.test.js +308 -0
- package/server/lib/session-goal/DOCUMENTATION.md +3 -3
- package/server/lib/session-goal/runtime.js +3 -3
- package/server/lib/session-title/DOCUMENTATION.md +1 -2
- package/server/lib/small-model/DOCUMENTATION.md +2 -2
- package/dist/assets/AgentAvatar-DDcjamcJ.js +0 -1
- package/dist/assets/AssistantView-BsQRWYlz.js +0 -1
- package/dist/assets/MobileSurface-D7aKyOLV.js +0 -2
- package/dist/assets/OverlayScrollbar-Ch3J6ASR.js +0 -2
- package/dist/assets/SettingsView-5b0tAHTL.js +0 -25
- package/dist/assets/appThemeRegistry-Dfucen00.js +0 -2
- package/dist/assets/context-menu-in4KRVok.js +0 -1
- package/dist/assets/dialog-DPS0W9eS.js +0 -1
- package/dist/assets/es-BX0Vm3Ac.js +0 -15
- package/dist/assets/fr-CuPzXTYx.js +0 -13
- package/dist/assets/insertionBoundaries--0yrL837.js +0 -1
- package/dist/assets/ja-DGtzjrFO.js +0 -15
- package/dist/assets/ko-DZ9Di0je.js +0 -15
- package/dist/assets/main-B9klnJvc.js +0 -172
- package/dist/assets/miniChat-D6inDYgY.js +0 -2
- package/dist/assets/mobile-DRu5smYb.js +0 -2
- package/dist/assets/multirun-CVRJ-r1z.js +0 -4
- package/dist/assets/pl-BuCO3qb1.js +0 -15
- package/dist/assets/platform-CbeLpKPk.js +0 -2
- package/dist/assets/projectMeta-Cr4ghXIQ.js +0 -3
- package/dist/assets/pt-BR-Brf4RMsw.js +0 -15
- package/dist/assets/radio-C9Qe2mW1.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-CCpXytpP.js +0 -2
- package/dist/assets/renderMobileApp-BmAEbSuq.js +0 -23
- package/dist/assets/runtimeConfig-JC8_XlMh.js +0 -2
- package/dist/assets/runtimeEndpointReset-Cgefx6dv.js +0 -147
- package/dist/assets/src-CpetTO35.css +0 -2
- package/dist/assets/store-DQY8Fkb0.js +0 -15
- package/dist/assets/uk-C-wmaPTi.js +0 -15
- package/dist/assets/useEffectiveDirectory-Bdc1_8wI.js +0 -1
- package/dist/assets/useMobileNavigationStore-BNyyqZch.js +0 -475
- package/dist/assets/useSessionAutoCleanup-C-NCLDvZ.js +0 -1
- package/dist/assets/zh-CN-DueRrIql.js +0 -15
- package/dist/assets/zh-TW-6875X80V.js +0 -15
- package/server/lib/config-sync/credential-auth.js +0 -39
- package/server/lib/session-assist/DOCUMENTATION.md +0 -62
- package/server/lib/session-assist/runtime.js +0 -376
- package/server/lib/session-assist/runtime.test.js +0 -114
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// Localized scenario titles for native push (APNs / FCM). Keys match UI locales.
|
|
2
|
+
// Titles stay content-free (scenario only); session name is the body separately.
|
|
3
|
+
|
|
4
|
+
export const APNS_DEFAULT_LOCALE = 'en';
|
|
5
|
+
|
|
6
|
+
export const APNS_LOCALES = Object.freeze([
|
|
7
|
+
'en',
|
|
8
|
+
'fr',
|
|
9
|
+
'zh-CN',
|
|
10
|
+
'zh-TW',
|
|
11
|
+
'uk',
|
|
12
|
+
'es',
|
|
13
|
+
'pt-BR',
|
|
14
|
+
'ko',
|
|
15
|
+
'pl',
|
|
16
|
+
'ja',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
/** @typedef {'ready'|'error'|'question'|'permission'|'goal_complete'|'goal_blocked'|'goal_budget'|'update'} ApnsTitleType */
|
|
20
|
+
|
|
21
|
+
const TITLES = Object.freeze({
|
|
22
|
+
en: Object.freeze({
|
|
23
|
+
ready: 'Agent response is ready',
|
|
24
|
+
error: 'Agent hit an error',
|
|
25
|
+
question: 'Agent needs your input',
|
|
26
|
+
permission: 'Agent needs permission',
|
|
27
|
+
goal_complete: 'Goal complete',
|
|
28
|
+
goal_blocked: 'Goal blocked',
|
|
29
|
+
goal_budget: 'Goal reached its token budget',
|
|
30
|
+
update: 'Agent update',
|
|
31
|
+
session: 'Session',
|
|
32
|
+
}),
|
|
33
|
+
fr: Object.freeze({
|
|
34
|
+
ready: "La réponse de l'agent est prête",
|
|
35
|
+
error: "L'agent a rencontré une erreur",
|
|
36
|
+
question: "L'agent a besoin de votre saisie",
|
|
37
|
+
permission: "L'agent a besoin d'une autorisation",
|
|
38
|
+
goal_complete: 'Objectif terminé',
|
|
39
|
+
goal_blocked: 'Objectif bloqué',
|
|
40
|
+
goal_budget: "L'objectif a atteint son budget de tokens",
|
|
41
|
+
update: "Mise à jour de l'agent",
|
|
42
|
+
session: 'Session',
|
|
43
|
+
}),
|
|
44
|
+
'zh-CN': Object.freeze({
|
|
45
|
+
ready: '智能体回复已就绪',
|
|
46
|
+
error: '智能体遇到错误',
|
|
47
|
+
question: '智能体需要你的输入',
|
|
48
|
+
permission: '智能体需要权限',
|
|
49
|
+
goal_complete: '目标已完成',
|
|
50
|
+
goal_blocked: '目标受阻',
|
|
51
|
+
goal_budget: '目标已达 token 预算',
|
|
52
|
+
update: '智能体更新',
|
|
53
|
+
session: '会话',
|
|
54
|
+
}),
|
|
55
|
+
'zh-TW': Object.freeze({
|
|
56
|
+
ready: '智能體回覆已就緒',
|
|
57
|
+
error: '智能體遇到錯誤',
|
|
58
|
+
question: '智能體需要你的輸入',
|
|
59
|
+
permission: '智能體需要權限',
|
|
60
|
+
goal_complete: '目標已完成',
|
|
61
|
+
goal_blocked: '目標受阻',
|
|
62
|
+
goal_budget: '目標已達 token 預算',
|
|
63
|
+
update: '智能體更新',
|
|
64
|
+
session: '工作階段',
|
|
65
|
+
}),
|
|
66
|
+
uk: Object.freeze({
|
|
67
|
+
ready: 'Відповідь агента готова',
|
|
68
|
+
error: 'Агент зіткнувся з помилкою',
|
|
69
|
+
question: 'Агенту потрібне ваше введення',
|
|
70
|
+
permission: 'Агенту потрібен дозвіл',
|
|
71
|
+
goal_complete: 'Цілі досягнуто',
|
|
72
|
+
goal_blocked: 'Цілі заблоковано',
|
|
73
|
+
goal_budget: 'Ціль досягла ліміту токенів',
|
|
74
|
+
update: 'Оновлення агента',
|
|
75
|
+
session: 'Сесія',
|
|
76
|
+
}),
|
|
77
|
+
es: Object.freeze({
|
|
78
|
+
ready: 'La respuesta del agente está lista',
|
|
79
|
+
error: 'El agente encontró un error',
|
|
80
|
+
question: 'El agente necesita tu entrada',
|
|
81
|
+
permission: 'El agente necesita permiso',
|
|
82
|
+
goal_complete: 'Objetivo completado',
|
|
83
|
+
goal_blocked: 'Objetivo bloqueado',
|
|
84
|
+
goal_budget: 'El objetivo alcanzó su presupuesto de tokens',
|
|
85
|
+
update: 'Actualización del agente',
|
|
86
|
+
session: 'Sesión',
|
|
87
|
+
}),
|
|
88
|
+
'pt-BR': Object.freeze({
|
|
89
|
+
ready: 'A resposta do agente está pronta',
|
|
90
|
+
error: 'O agente encontrou um erro',
|
|
91
|
+
question: 'O agente precisa da sua entrada',
|
|
92
|
+
permission: 'O agente precisa de permissão',
|
|
93
|
+
goal_complete: 'Objetivo concluído',
|
|
94
|
+
goal_blocked: 'Objetivo bloqueado',
|
|
95
|
+
goal_budget: 'O objetivo atingiu o orçamento de tokens',
|
|
96
|
+
update: 'Atualização do agente',
|
|
97
|
+
session: 'Sessão',
|
|
98
|
+
}),
|
|
99
|
+
ko: Object.freeze({
|
|
100
|
+
ready: '에이전트 응답이 준비되었습니다',
|
|
101
|
+
error: '에이전트에서 오류가 발생했습니다',
|
|
102
|
+
question: '에이전트가 입력을 요청합니다',
|
|
103
|
+
permission: '에이전트가 권한이 필요합니다',
|
|
104
|
+
goal_complete: '목표 완료',
|
|
105
|
+
goal_blocked: '목표 차단됨',
|
|
106
|
+
goal_budget: '목표가 토큰 예산에 도달했습니다',
|
|
107
|
+
update: '에이전트 업데이트',
|
|
108
|
+
session: '세션',
|
|
109
|
+
}),
|
|
110
|
+
pl: Object.freeze({
|
|
111
|
+
ready: 'Odpowiedź agenta jest gotowa',
|
|
112
|
+
error: 'Agent napotkał błąd',
|
|
113
|
+
question: 'Agent potrzebuje Twojego wejścia',
|
|
114
|
+
permission: 'Agent potrzebuje uprawnienia',
|
|
115
|
+
goal_complete: 'Cel ukończony',
|
|
116
|
+
goal_blocked: 'Cel zablokowany',
|
|
117
|
+
goal_budget: 'Cel osiągnął budżet tokenów',
|
|
118
|
+
update: 'Aktualizacja agenta',
|
|
119
|
+
session: 'Sesja',
|
|
120
|
+
}),
|
|
121
|
+
ja: Object.freeze({
|
|
122
|
+
ready: 'エージェントの応答が準備できました',
|
|
123
|
+
error: 'エージェントでエラーが発生しました',
|
|
124
|
+
question: 'エージェントが入力を求めています',
|
|
125
|
+
permission: 'エージェントが許可を求めています',
|
|
126
|
+
goal_complete: '目標完了',
|
|
127
|
+
goal_blocked: '目標がブロックされました',
|
|
128
|
+
goal_budget: '目標がトークン予算に達しました',
|
|
129
|
+
update: 'エージェントの更新',
|
|
130
|
+
session: 'セッション',
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const matchSupportedLocale = (value) => {
|
|
135
|
+
if (!value || typeof value !== 'string') return undefined;
|
|
136
|
+
const normalized = value.toLowerCase().replace(/_/g, '-');
|
|
137
|
+
if (
|
|
138
|
+
normalized === 'zh-tw'
|
|
139
|
+
|| normalized.startsWith('zh-tw-')
|
|
140
|
+
|| normalized === 'zh-hk'
|
|
141
|
+
|| normalized.startsWith('zh-hk-')
|
|
142
|
+
|| normalized === 'zh-mo'
|
|
143
|
+
|| normalized.startsWith('zh-mo-')
|
|
144
|
+
|| normalized === 'zh-hant'
|
|
145
|
+
|| normalized.startsWith('zh-hant-')
|
|
146
|
+
) {
|
|
147
|
+
return 'zh-TW';
|
|
148
|
+
}
|
|
149
|
+
if (normalized.startsWith('zh')) return 'zh-CN';
|
|
150
|
+
if (normalized.startsWith('en')) return 'en';
|
|
151
|
+
if (normalized === 'fr' || normalized.startsWith('fr-')) return 'fr';
|
|
152
|
+
if (normalized === 'uk' || normalized.startsWith('uk-') || normalized === 'ua' || normalized.startsWith('ua-')) {
|
|
153
|
+
return 'uk';
|
|
154
|
+
}
|
|
155
|
+
if (normalized === 'es' || normalized.startsWith('es-')) return 'es';
|
|
156
|
+
if (normalized === 'pt' || normalized.startsWith('pt-')) return 'pt-BR';
|
|
157
|
+
if (normalized === 'ko' || normalized.startsWith('ko-')) return 'ko';
|
|
158
|
+
if (normalized === 'ja' || normalized.startsWith('ja-')) return 'ja';
|
|
159
|
+
if (normalized === 'pl' || normalized.startsWith('pl-')) return 'pl';
|
|
160
|
+
return undefined;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export const normalizeApnsLocale = (value) => matchSupportedLocale(value) ?? APNS_DEFAULT_LOCALE;
|
|
164
|
+
|
|
165
|
+
const titlesFor = (locale) => TITLES[normalizeApnsLocale(locale)] || TITLES[APNS_DEFAULT_LOCALE];
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {string|undefined|null} type
|
|
169
|
+
* @param {string|undefined|null} locale
|
|
170
|
+
*/
|
|
171
|
+
export const resolveApnsTitle = (type, locale) => {
|
|
172
|
+
const dict = titlesFor(locale);
|
|
173
|
+
if (typeof type === 'string' && Object.prototype.hasOwnProperty.call(dict, type) && type !== 'session') {
|
|
174
|
+
return dict[type];
|
|
175
|
+
}
|
|
176
|
+
return dict.update;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {string|undefined|null} locale
|
|
181
|
+
*/
|
|
182
|
+
export const resolveApnsSessionFallback = (locale) => titlesFor(locale).session;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Localize a generic native-push payload for one device locale.
|
|
186
|
+
* `payload.type` selects the scenario title; `payload.sessionName` (or non-empty body) is the body.
|
|
187
|
+
* Callers that already pass a fixed `title`/`body` (tests / legacy) keep that text when `type` is absent.
|
|
188
|
+
*
|
|
189
|
+
* @param {{ type?: string, title?: string, body?: string, sessionName?: string, badge?: number, tag?: string, data?: object }} payload
|
|
190
|
+
* @param {string|undefined|null} locale
|
|
191
|
+
*/
|
|
192
|
+
export const localizeApnsPayload = (payload, locale) => {
|
|
193
|
+
const type = typeof payload?.type === 'string' ? payload.type : undefined;
|
|
194
|
+
const sessionName = typeof payload?.sessionName === 'string' && payload.sessionName.trim().length > 0
|
|
195
|
+
? payload.sessionName.trim()
|
|
196
|
+
: '';
|
|
197
|
+
const legacyBody = typeof payload?.body === 'string' && payload.body.trim().length > 0
|
|
198
|
+
? payload.body.trim()
|
|
199
|
+
: '';
|
|
200
|
+
const title = type
|
|
201
|
+
? resolveApnsTitle(type, locale)
|
|
202
|
+
: (typeof payload?.title === 'string' && payload.title.length > 0
|
|
203
|
+
? payload.title
|
|
204
|
+
: resolveApnsTitle('update', locale));
|
|
205
|
+
return {
|
|
206
|
+
title,
|
|
207
|
+
body: sessionName || legacyBody || resolveApnsSessionFallback(locale),
|
|
208
|
+
badge: payload?.badge,
|
|
209
|
+
tag: payload?.tag,
|
|
210
|
+
data: payload?.data,
|
|
211
|
+
};
|
|
212
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
APNS_DEFAULT_LOCALE,
|
|
5
|
+
APNS_LOCALES,
|
|
6
|
+
localizeApnsPayload,
|
|
7
|
+
normalizeApnsLocale,
|
|
8
|
+
resolveApnsSessionFallback,
|
|
9
|
+
resolveApnsTitle,
|
|
10
|
+
} from './apns-titles.js';
|
|
11
|
+
|
|
12
|
+
describe('apns-titles', () => {
|
|
13
|
+
it('normalizes BCP-47 tags onto supported app locales', () => {
|
|
14
|
+
expect(normalizeApnsLocale('zh-Hans-CN')).toBe('zh-CN');
|
|
15
|
+
expect(normalizeApnsLocale('zh_TW')).toBe('zh-TW');
|
|
16
|
+
expect(normalizeApnsLocale('pt-PT')).toBe('pt-BR');
|
|
17
|
+
expect(normalizeApnsLocale('ja-JP')).toBe('ja');
|
|
18
|
+
expect(normalizeApnsLocale('nope')).toBe(APNS_DEFAULT_LOCALE);
|
|
19
|
+
expect(normalizeApnsLocale(undefined)).toBe(APNS_DEFAULT_LOCALE);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('resolves every scenario type for every supported locale', () => {
|
|
23
|
+
const types = ['ready', 'error', 'question', 'permission', 'goal_complete', 'goal_blocked', 'goal_budget', 'update'];
|
|
24
|
+
for (const locale of APNS_LOCALES) {
|
|
25
|
+
for (const type of types) {
|
|
26
|
+
const title = resolveApnsTitle(type, locale);
|
|
27
|
+
expect(typeof title).toBe('string');
|
|
28
|
+
expect(title.length).toBeGreaterThan(0);
|
|
29
|
+
}
|
|
30
|
+
expect(resolveApnsSessionFallback(locale).length).toBeGreaterThan(0);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('localizes payload title by type and keeps session name as body', () => {
|
|
35
|
+
expect(localizeApnsPayload({ type: 'question', sessionName: 'Build fix' }, 'zh-CN')).toEqual({
|
|
36
|
+
title: '智能体需要你的输入',
|
|
37
|
+
body: 'Build fix',
|
|
38
|
+
badge: undefined,
|
|
39
|
+
tag: undefined,
|
|
40
|
+
data: undefined,
|
|
41
|
+
});
|
|
42
|
+
expect(localizeApnsPayload({ type: 'ready' }, 'fr').body).toBe('Session');
|
|
43
|
+
expect(localizeApnsPayload({ type: 'ready' }, 'zh-CN').body).toBe('会话');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('keeps explicit legacy title/body when type is absent', () => {
|
|
47
|
+
expect(localizeApnsPayload({ title: 'Custom', body: 'Body' }, 'ja')).toEqual({
|
|
48
|
+
title: 'Custom',
|
|
49
|
+
body: 'Body',
|
|
50
|
+
badge: undefined,
|
|
51
|
+
tag: undefined,
|
|
52
|
+
data: undefined,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -22,6 +22,17 @@ const parsePushUnsubscribeBody = (body) => {
|
|
|
22
22
|
return { endpoint: endpoint.trim() };
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
const MAX_LIVE_ACTIVITY_TOKEN_LENGTH = 512;
|
|
26
|
+
const MAX_LIVE_ACTIVITY_ACTIVITY_ID_LENGTH = 128;
|
|
27
|
+
const MAX_LIVE_ACTIVITY_SESSION_ID_LENGTH = 128;
|
|
28
|
+
|
|
29
|
+
const parseLimitedPushId = (value, maxLength) => {
|
|
30
|
+
if (typeof value !== 'string') return '';
|
|
31
|
+
const trimmed = value.trim();
|
|
32
|
+
if (trimmed.length === 0 || trimmed.length > maxLength) return '';
|
|
33
|
+
return trimmed;
|
|
34
|
+
};
|
|
35
|
+
|
|
25
36
|
export const NOTIFICATION_SSE_HEARTBEAT_INTERVAL_MS = 20_000;
|
|
26
37
|
|
|
27
38
|
export const registerNotificationRoutes = (app, dependencies) => {
|
|
@@ -37,6 +48,8 @@ export const registerNotificationRoutes = (app, dependencies) => {
|
|
|
37
48
|
removePushSubscription,
|
|
38
49
|
addOrUpdateApnsToken,
|
|
39
50
|
removeApnsToken,
|
|
51
|
+
addOrUpdateLiveActivityToken,
|
|
52
|
+
removeLiveActivityToken,
|
|
40
53
|
updateUiVisibility,
|
|
41
54
|
clearPendingPushBadge,
|
|
42
55
|
isUiVisible,
|
|
@@ -162,8 +175,9 @@ export const registerNotificationRoutes = (app, dependencies) => {
|
|
|
162
175
|
}
|
|
163
176
|
|
|
164
177
|
const platform = req.body?.platform === 'android' ? 'android' : 'ios';
|
|
178
|
+
const locale = typeof req.body?.locale === 'string' ? req.body.locale.trim() : undefined;
|
|
165
179
|
if (typeof addOrUpdateApnsToken === 'function') {
|
|
166
|
-
await addOrUpdateApnsToken(uiToken, deviceToken, req.headers['user-agent'], platform);
|
|
180
|
+
await addOrUpdateApnsToken(uiToken, deviceToken, req.headers['user-agent'], platform, locale);
|
|
167
181
|
}
|
|
168
182
|
return res.json({ ok: true });
|
|
169
183
|
});
|
|
@@ -187,6 +201,48 @@ export const registerNotificationRoutes = (app, dependencies) => {
|
|
|
187
201
|
return res.json({ ok: true });
|
|
188
202
|
});
|
|
189
203
|
|
|
204
|
+
app.post('/api/push/live-activity-token', async (req, res) => {
|
|
205
|
+
await ensureSessionWatcher();
|
|
206
|
+
|
|
207
|
+
const uiToken = uiAuthController?.ensureSessionToken
|
|
208
|
+
? await uiAuthController.ensureSessionToken(req, res)
|
|
209
|
+
: getUiSessionTokenFromRequest(req);
|
|
210
|
+
if (!uiToken) {
|
|
211
|
+
return res.status(401).json({ error: 'UI session missing' });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const token = parseLimitedPushId(req.body?.token, MAX_LIVE_ACTIVITY_TOKEN_LENGTH);
|
|
215
|
+
const activityId = parseLimitedPushId(req.body?.activityId, MAX_LIVE_ACTIVITY_ACTIVITY_ID_LENGTH);
|
|
216
|
+
const sessionId = parseLimitedPushId(req.body?.sessionId, MAX_LIVE_ACTIVITY_SESSION_ID_LENGTH);
|
|
217
|
+
if (!token || !activityId || !sessionId) {
|
|
218
|
+
return res.status(400).json({ error: 'Invalid body' });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (typeof addOrUpdateLiveActivityToken === 'function') {
|
|
222
|
+
await addOrUpdateLiveActivityToken(uiToken, token, activityId, sessionId);
|
|
223
|
+
}
|
|
224
|
+
return res.json({ ok: true });
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
app.delete('/api/push/live-activity-token', async (req, res) => {
|
|
228
|
+
const uiToken = uiAuthController?.ensureSessionToken
|
|
229
|
+
? await uiAuthController.ensureSessionToken(req, res)
|
|
230
|
+
: getUiSessionTokenFromRequest(req);
|
|
231
|
+
if (!uiToken) {
|
|
232
|
+
return res.status(401).json({ error: 'UI session missing' });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const token = parseLimitedPushId(req.body?.token, MAX_LIVE_ACTIVITY_TOKEN_LENGTH);
|
|
236
|
+
if (!token) {
|
|
237
|
+
return res.status(400).json({ error: 'Invalid body' });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (typeof removeLiveActivityToken === 'function') {
|
|
241
|
+
await removeLiveActivityToken(uiToken, token);
|
|
242
|
+
}
|
|
243
|
+
return res.json({ ok: true });
|
|
244
|
+
});
|
|
245
|
+
|
|
190
246
|
app.post('/api/push/visibility', async (req, res) => {
|
|
191
247
|
const uiToken = uiAuthController?.ensureSessionToken
|
|
192
248
|
? await uiAuthController.ensureSessionToken(req, res)
|
|
@@ -11,6 +11,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
11
11
|
broadcastUiNotification,
|
|
12
12
|
sendPushToAllUiSessions,
|
|
13
13
|
sendApnsToAllUiSessions,
|
|
14
|
+
sendLiveActivityEnd,
|
|
14
15
|
isAnyInteractiveClientVisible,
|
|
15
16
|
buildOpenCodeUrl,
|
|
16
17
|
getOpenCodeAuthHeaders,
|
|
@@ -49,24 +50,16 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
49
50
|
|
|
50
51
|
// Generic notification for native push (per the mobile design): a fixed, scenario-based
|
|
51
52
|
// title + the session name as the body. No model/project/message content crosses the relay.
|
|
52
|
-
|
|
53
|
-
ready: 'Agent response is ready',
|
|
54
|
-
error: 'Agent hit an error',
|
|
55
|
-
question: 'Agent needs your input',
|
|
56
|
-
permission: 'Agent needs permission',
|
|
57
|
-
goal_complete: 'Goal complete',
|
|
58
|
-
goal_blocked: 'Goal blocked',
|
|
59
|
-
goal_budget: 'Goal reached its token budget',
|
|
60
|
-
};
|
|
61
|
-
|
|
53
|
+
// Title strings are localized per device token locale inside sendApnsToAllUiSessions.
|
|
62
54
|
const toApnsGenericPayload = (payload) => {
|
|
63
55
|
const data = payload?.data && typeof payload.data === 'object' ? payload.data : {};
|
|
64
56
|
const sessionName = typeof data.sessionName === 'string' && data.sessionName.trim().length > 0
|
|
65
57
|
? data.sessionName.trim()
|
|
66
|
-
: '
|
|
58
|
+
: '';
|
|
59
|
+
const type = typeof data.type === 'string' ? data.type : undefined;
|
|
67
60
|
return {
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
type,
|
|
62
|
+
sessionName,
|
|
70
63
|
badge: trackPushAndCountBadge(typeof payload?.tag === 'string' ? payload.tag : undefined),
|
|
71
64
|
tag: payload?.tag,
|
|
72
65
|
// sessionId is forwarded so a tapped push can deep-link; it is an opaque id, not content.
|
|
@@ -377,14 +370,24 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
377
370
|
|
|
378
371
|
if (payload.type === 'message.updated') {
|
|
379
372
|
const info = payload.properties?.info;
|
|
380
|
-
|
|
373
|
+
const isAssistantTerminal = Boolean(
|
|
381
374
|
sessionId
|
|
382
375
|
&& info?.role === 'assistant'
|
|
383
|
-
&& (info?.finish === 'stop' || info?.finish === 'error')
|
|
384
|
-
|
|
385
|
-
) {
|
|
376
|
+
&& (info?.finish === 'stop' || info?.finish === 'error'),
|
|
377
|
+
);
|
|
378
|
+
if (isAssistantTerminal && await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
|
|
386
379
|
return;
|
|
387
380
|
}
|
|
381
|
+
if (isAssistantTerminal) {
|
|
382
|
+
try {
|
|
383
|
+
await sendLiveActivityEnd?.({
|
|
384
|
+
sessionId,
|
|
385
|
+
status: info.finish === 'error' ? 'error' : 'complete',
|
|
386
|
+
});
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.warn('[Live Activity] end failed:', error?.message ?? error);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
388
391
|
if (info?.role === 'assistant' && info?.finish === 'stop' && sessionId) {
|
|
389
392
|
const settings = await readSettingsFromDisk();
|
|
390
393
|
|
|
@@ -14,6 +14,7 @@ const createRuntime = (overrides = {}) => {
|
|
|
14
14
|
const broadcastUiNotification = vi.fn();
|
|
15
15
|
const sendPushToAllUiSessions = vi.fn(async () => {});
|
|
16
16
|
const sendApnsToAllUiSessions = vi.fn(async () => {});
|
|
17
|
+
const sendLiveActivityEnd = vi.fn(async () => {});
|
|
17
18
|
const runtime = createNotificationTriggerRuntime({
|
|
18
19
|
readSettingsFromDisk: vi.fn(async () => defaultSettings),
|
|
19
20
|
prepareNotificationLastMessage: vi.fn(async ({ message }) => message || ''),
|
|
@@ -26,6 +27,7 @@ const createRuntime = (overrides = {}) => {
|
|
|
26
27
|
broadcastUiNotification,
|
|
27
28
|
sendPushToAllUiSessions,
|
|
28
29
|
sendApnsToAllUiSessions,
|
|
30
|
+
sendLiveActivityEnd,
|
|
29
31
|
isAnyInteractiveClientVisible: () => false,
|
|
30
32
|
buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
|
|
31
33
|
getOpenCodeAuthHeaders: () => ({}),
|
|
@@ -38,6 +40,7 @@ const createRuntime = (overrides = {}) => {
|
|
|
38
40
|
broadcastUiNotification,
|
|
39
41
|
sendPushToAllUiSessions,
|
|
40
42
|
sendApnsToAllUiSessions,
|
|
43
|
+
sendLiveActivityEnd,
|
|
41
44
|
};
|
|
42
45
|
};
|
|
43
46
|
|
|
@@ -174,3 +177,108 @@ describe('notification trigger runtime smallModel suppression', () => {
|
|
|
174
177
|
expect(sendPushToAllUiSessions).toHaveBeenCalledTimes(1);
|
|
175
178
|
});
|
|
176
179
|
});
|
|
180
|
+
|
|
181
|
+
const rootSessionResponse = () => jsonResponse({
|
|
182
|
+
id: 'ses_root',
|
|
183
|
+
parentID: null,
|
|
184
|
+
title: 'Ordinary',
|
|
185
|
+
metadata: {},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const completionPayload = (sessionId = 'ses_root', finish = 'stop') => ({
|
|
189
|
+
type: 'message.updated',
|
|
190
|
+
properties: {
|
|
191
|
+
directory: '/repo',
|
|
192
|
+
info: {
|
|
193
|
+
id: 'msg_2',
|
|
194
|
+
sessionID: sessionId,
|
|
195
|
+
role: 'assistant',
|
|
196
|
+
finish,
|
|
197
|
+
mode: 'build',
|
|
198
|
+
modelID: 'gpt',
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('notification trigger live activity end', () => {
|
|
204
|
+
it('ends the live activity on top-level completion and error', async () => {
|
|
205
|
+
vi.stubGlobal('fetch', vi.fn(async () => rootSessionResponse()));
|
|
206
|
+
const { runtime, sendLiveActivityEnd } = createRuntime();
|
|
207
|
+
await runtime.maybeSendPushForTrigger(completionPayload('ses_root', 'stop'));
|
|
208
|
+
await runtime.maybeSendPushForTrigger({
|
|
209
|
+
type: 'session.error',
|
|
210
|
+
properties: { directory: '/repo', sessionID: 'ses_err', error: 'boom' },
|
|
211
|
+
});
|
|
212
|
+
expect(sendLiveActivityEnd).toHaveBeenNthCalledWith(1, { sessionId: 'ses_root', status: 'complete' });
|
|
213
|
+
expect(sendLiveActivityEnd).toHaveBeenNthCalledWith(2, { sessionId: 'ses_err', status: 'error' });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('still ends the live activity when ordinary notifications are disabled', async () => {
|
|
217
|
+
vi.stubGlobal('fetch', vi.fn(async () => rootSessionResponse()));
|
|
218
|
+
const { runtime, sendLiveActivityEnd, sendPushToAllUiSessions, sendApnsToAllUiSessions } = createRuntime({
|
|
219
|
+
readSettingsFromDisk: vi.fn(async () => ({ ...defaultSettings, notifyOnCompletion: false })),
|
|
220
|
+
});
|
|
221
|
+
await runtime.maybeSendPushForTrigger(completionPayload());
|
|
222
|
+
expect(sendLiveActivityEnd).toHaveBeenCalledWith({ sessionId: 'ses_root', status: 'complete' });
|
|
223
|
+
expect(sendPushToAllUiSessions).not.toHaveBeenCalled();
|
|
224
|
+
expect(sendApnsToAllUiSessions).not.toHaveBeenCalled();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('ends the live activity even when an interactive client is visible', async () => {
|
|
228
|
+
vi.stubGlobal('fetch', vi.fn(async () => rootSessionResponse()));
|
|
229
|
+
const { runtime, sendLiveActivityEnd, sendApnsToAllUiSessions } = createRuntime({
|
|
230
|
+
isAnyInteractiveClientVisible: () => true,
|
|
231
|
+
});
|
|
232
|
+
await runtime.maybeSendPushForTrigger(completionPayload());
|
|
233
|
+
expect(sendLiveActivityEnd).toHaveBeenCalledWith({ sessionId: 'ses_root', status: 'complete' });
|
|
234
|
+
expect(sendApnsToAllUiSessions).not.toHaveBeenCalled();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('suppresses live activity end for child and small-model sessions', async () => {
|
|
238
|
+
const fetchMock = vi.fn(async (url) => {
|
|
239
|
+
if (String(url).includes('ses_child')) {
|
|
240
|
+
return jsonResponse({ id: 'ses_child', parentID: 'ses_parent', metadata: {} });
|
|
241
|
+
}
|
|
242
|
+
return jsonResponse({
|
|
243
|
+
id: 'ses_small',
|
|
244
|
+
parentID: null,
|
|
245
|
+
metadata: { openchamber: { smallModel: { purpose: 'session-title' } } },
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
249
|
+
const { runtime, sendLiveActivityEnd, sendPushToAllUiSessions } = createRuntime();
|
|
250
|
+
await runtime.maybeSendPushForTrigger(completionPayload('ses_child'));
|
|
251
|
+
await runtime.maybeSendPushForTrigger(completionPayload('ses_small', 'error'));
|
|
252
|
+
expect(sendLiveActivityEnd).not.toHaveBeenCalled();
|
|
253
|
+
expect(sendPushToAllUiSessions).not.toHaveBeenCalled();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('isolates live activity end failure from ordinary push', async () => {
|
|
257
|
+
vi.stubGlobal('fetch', vi.fn(async () => rootSessionResponse()));
|
|
258
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
259
|
+
const sendLiveActivityEnd = vi.fn(async () => {
|
|
260
|
+
throw new Error('live-activity failed');
|
|
261
|
+
});
|
|
262
|
+
const { runtime, sendPushToAllUiSessions, sendApnsToAllUiSessions } = createRuntime({
|
|
263
|
+
sendLiveActivityEnd,
|
|
264
|
+
});
|
|
265
|
+
try {
|
|
266
|
+
await runtime.maybeSendPushForTrigger(completionPayload());
|
|
267
|
+
expect(sendLiveActivityEnd).toHaveBeenCalledTimes(1);
|
|
268
|
+
expect(sendPushToAllUiSessions).toHaveBeenCalledTimes(1);
|
|
269
|
+
expect(sendApnsToAllUiSessions).toHaveBeenCalledTimes(1);
|
|
270
|
+
} finally {
|
|
271
|
+
warn.mockRestore();
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('repeats terminal delivery on duplicate completion events', async () => {
|
|
276
|
+
vi.stubGlobal('fetch', vi.fn(async () => rootSessionResponse()));
|
|
277
|
+
const { runtime, sendLiveActivityEnd } = createRuntime();
|
|
278
|
+
await runtime.maybeSendPushForTrigger(completionPayload());
|
|
279
|
+
await runtime.maybeSendPushForTrigger(completionPayload());
|
|
280
|
+
expect(sendLiveActivityEnd).toHaveBeenCalledTimes(2);
|
|
281
|
+
expect(sendLiveActivityEnd).toHaveBeenNthCalledWith(1, { sessionId: 'ses_root', status: 'complete' });
|
|
282
|
+
expect(sendLiveActivityEnd).toHaveBeenNthCalledWith(2, { sessionId: 'ses_root', status: 'complete' });
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -83,7 +83,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
83
83
|
- `GET /api/config/opencode-resolution`
|
|
84
84
|
- `GET /api/behavior/agents-md`: returns an authoritative empty document only when the file is absent (`ENOENT`); permission and I/O failures return HTTP 500.
|
|
85
85
|
- `PUT /api/behavior/agents-md`
|
|
86
|
-
- `POST /api/opencode/upgrade` (proxies OpenCode upgrade
|
|
86
|
+
- `POST /api/opencode/upgrade` (proxies OpenCode `POST /global/upgrade` with a required semantic version `target`, then restarts managed OpenCode so the new binary is active)
|
|
87
87
|
- `GET /api/opencode/upgrade-status`
|
|
88
88
|
- `POST /api/opencode/directory`
|
|
89
89
|
- `GET /api/provider/:providerId/source`
|
|
@@ -43,6 +43,9 @@ export const createBootstrapRuntime = (dependencies) => {
|
|
|
43
43
|
removePushSubscription,
|
|
44
44
|
addOrUpdateApnsToken,
|
|
45
45
|
removeApnsToken,
|
|
46
|
+
addOrUpdateLiveActivityToken,
|
|
47
|
+
removeLiveActivityToken,
|
|
48
|
+
sendLiveActivityEnd,
|
|
46
49
|
updateUiVisibility,
|
|
47
50
|
clearPendingPushBadge,
|
|
48
51
|
isUiVisible,
|
|
@@ -119,6 +122,9 @@ export const createBootstrapRuntime = (dependencies) => {
|
|
|
119
122
|
removePushSubscription,
|
|
120
123
|
addOrUpdateApnsToken,
|
|
121
124
|
removeApnsToken,
|
|
125
|
+
addOrUpdateLiveActivityToken,
|
|
126
|
+
removeLiveActivityToken,
|
|
127
|
+
sendLiveActivityEnd,
|
|
122
128
|
updateUiVisibility,
|
|
123
129
|
clearPendingPushBadge,
|
|
124
130
|
isUiVisible,
|
|
@@ -16,4 +16,25 @@ describe('bootstrap runtime', () => {
|
|
|
16
16
|
});
|
|
17
17
|
expect(registerAuthAndAccessRoutes).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ authorizeManagedOpenCodeBridgeRequest }));
|
|
18
18
|
});
|
|
19
|
+
|
|
20
|
+
it('passes live activity register, unregister, and end into notification route registration', () => {
|
|
21
|
+
const registerNotificationRoutes = vi.fn();
|
|
22
|
+
const runtime = createBootstrapRuntime({
|
|
23
|
+
createUiAuth: () => ({ enabled: false }), registerServerStatusRoutes: vi.fn(), registerCommonRequestMiddleware: vi.fn(), registerAuthAndAccessRoutes: vi.fn(),
|
|
24
|
+
registerTtsRoutes: vi.fn(), registerNotificationRoutes, registerOpenChamberRoutes: vi.fn(), express: {},
|
|
25
|
+
});
|
|
26
|
+
const addOrUpdateLiveActivityToken = vi.fn();
|
|
27
|
+
const removeLiveActivityToken = vi.fn();
|
|
28
|
+
const sendLiveActivityEnd = vi.fn();
|
|
29
|
+
runtime.setupBaseRoutes({}, {
|
|
30
|
+
process: {}, openchamberVersion: 'test', runtimeName: 'test', serverStartedAt: 0, gracefulShutdown: vi.fn(), getHealthSnapshot: vi.fn(), verboseRequestLogs: false, uiPassword: null,
|
|
31
|
+
tunnelAuthController: {}, remoteClientAuthRuntime: {}, clientPairingRuntime: {}, getRelayPairingCandidate: vi.fn(), reconcileRelay: vi.fn(), getPairingTransports: vi.fn(), getDirectCandidateUrls: vi.fn(), getServerId: vi.fn(), getServerLabel: vi.fn(), readSettingsFromDiskMigrated: vi.fn(), normalizeTunnelSessionTtlMs: vi.fn(), authorizeManagedOpenCodeBridgeRequest: vi.fn(),
|
|
32
|
+
sayTTSCapability: vi.fn(), ensurePushInitialized: vi.fn(), ensureGlobalWatcherStarted: vi.fn(), getOrCreateVapidKeys: vi.fn(), getUiSessionTokenFromRequest: vi.fn(), writeSettingsToDisk: vi.fn(), addOrUpdatePushSubscription: vi.fn(), removePushSubscription: vi.fn(), addOrUpdateApnsToken: vi.fn(), removeApnsToken: vi.fn(), addOrUpdateLiveActivityToken, removeLiveActivityToken, sendLiveActivityEnd, updateUiVisibility: vi.fn(), clearPendingPushBadge: vi.fn(), isUiVisible: vi.fn(), getUiNotificationClients: vi.fn(), writeSseEvent: vi.fn(), sessionRuntime: { getSessionActivitySnapshot: vi.fn(), getSessionStateSnapshot: vi.fn(), getSessionAttentionSnapshot: vi.fn(), getSessionState: vi.fn(), getSessionAttentionState: vi.fn(), markSessionViewed: vi.fn(), markSessionUnviewed: vi.fn(), markUserMessageSent: vi.fn() }, setPushInitialized: vi.fn(), fs: {}, os: {}, path: {}, server: {}, __dirname: '', openchamberDataDir: '', fetchFreeZenModels: vi.fn(), getCachedZenModels: vi.fn(), setAutoAcceptSession: vi.fn(), sessionIndexService: {}, sessionIndexSyncRuntime: {},
|
|
33
|
+
});
|
|
34
|
+
expect(registerNotificationRoutes).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
|
|
35
|
+
addOrUpdateLiveActivityToken,
|
|
36
|
+
removeLiveActivityToken,
|
|
37
|
+
sendLiveActivityEnd,
|
|
38
|
+
}));
|
|
39
|
+
});
|
|
19
40
|
});
|
|
@@ -10,8 +10,6 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
10
10
|
server,
|
|
11
11
|
__dirname,
|
|
12
12
|
openchamberDataDir,
|
|
13
|
-
readSettingsFromDiskMigrated,
|
|
14
|
-
persistSettings,
|
|
15
13
|
fetchFreeZenModels,
|
|
16
14
|
getCachedZenModels,
|
|
17
15
|
sessionIndexService,
|
|
@@ -22,11 +20,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
22
20
|
|
|
23
21
|
registerSessionIndexRoutes(app, { sessionIndexService, sessionIndexSyncRuntime });
|
|
24
22
|
registerTranscriptCacheRoutes(app, { transcriptCacheService });
|
|
25
|
-
registerConfigSyncRoutes(app, {
|
|
26
|
-
readSettingsFromDiskMigrated,
|
|
27
|
-
persistSettings,
|
|
28
|
-
express,
|
|
29
|
-
});
|
|
23
|
+
registerConfigSyncRoutes(app, { express });
|
|
30
24
|
|
|
31
25
|
app.get('/api/openchamber/update-check', async (req, res) => {
|
|
32
26
|
try {
|