@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
|
@@ -1,5 +1,75 @@
|
|
|
1
|
+
import { projectSessionMessageRecords } from '../session-turn-pages/session-message-projection.js';
|
|
1
2
|
import { summarizeText as summarizeSharedText } from '../text/summarization.js';
|
|
2
3
|
|
|
4
|
+
const trimmedText = (value) => (typeof value === 'string' && value.trim() ? value.trim() : '');
|
|
5
|
+
|
|
6
|
+
const isSessionRecord = (value) => {
|
|
7
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
8
|
+
return typeof value.id === 'string'
|
|
9
|
+
|| typeof value.sessionID === 'string'
|
|
10
|
+
|| typeof value.title === 'string'
|
|
11
|
+
|| typeof value.parentID === 'string'
|
|
12
|
+
|| (value.metadata && typeof value.metadata === 'object');
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* OpenCode 2 `GET /api/session/:id` returns `{ data: Session }`.
|
|
17
|
+
* Legacy responses and tests still return the session object itself.
|
|
18
|
+
* @param {unknown} payload
|
|
19
|
+
* @returns {object | null}
|
|
20
|
+
*/
|
|
21
|
+
export const readNotificationSessionRecord = (payload) => {
|
|
22
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null;
|
|
23
|
+
if (isSessionRecord(payload.data)) return payload.data;
|
|
24
|
+
return isSessionRecord(payload) ? payload : null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const TITLE_EVENT_TYPES = new Set(['session.created', 'session.updated', 'session.renamed']);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Title carried by a session lifecycle event.
|
|
31
|
+
* v2 puts it on `data.title` (`session.created` / `session.renamed`).
|
|
32
|
+
* Legacy events put it on `properties.info.title`.
|
|
33
|
+
* @param {object | null | undefined} payload
|
|
34
|
+
* @returns {{ sessionId: string, title: string } | null}
|
|
35
|
+
*/
|
|
36
|
+
export const readSessionTitleFromEvent = (payload) => {
|
|
37
|
+
if (!payload || typeof payload !== 'object' || !TITLE_EVENT_TYPES.has(payload.type)) return null;
|
|
38
|
+
const bodies = [payload.properties, payload.data].filter((body) => body && typeof body === 'object' && !Array.isArray(body));
|
|
39
|
+
for (const body of bodies) {
|
|
40
|
+
const info = body.info && typeof body.info === 'object' ? body.info : null;
|
|
41
|
+
const title = trimmedText(info?.title) || trimmedText(body.title);
|
|
42
|
+
const sessionId = trimmedText(info?.id)
|
|
43
|
+
|| trimmedText(info?.sessionID)
|
|
44
|
+
|| trimmedText(body.sessionID)
|
|
45
|
+
|| trimmedText(body.id);
|
|
46
|
+
if (title && sessionId) return { sessionId, title };
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const readNotificationDirectory = (payload) => {
|
|
52
|
+
const properties = payload?.properties && typeof payload.properties === 'object' ? payload.properties : {};
|
|
53
|
+
const data = payload?.data && typeof payload.data === 'object' && !Array.isArray(payload.data) ? payload.data : {};
|
|
54
|
+
const info = properties.info && typeof properties.info === 'object' ? properties.info : {};
|
|
55
|
+
const dataInfo = data.info && typeof data.info === 'object' ? data.info : {};
|
|
56
|
+
const candidates = [
|
|
57
|
+
properties.directory,
|
|
58
|
+
info.directory,
|
|
59
|
+
info.location?.directory,
|
|
60
|
+
payload?.location?.directory,
|
|
61
|
+
data.directory,
|
|
62
|
+
data.location?.directory,
|
|
63
|
+
dataInfo.directory,
|
|
64
|
+
dataInfo.location?.directory,
|
|
65
|
+
];
|
|
66
|
+
for (const value of candidates) {
|
|
67
|
+
const directory = trimmedText(value);
|
|
68
|
+
if (directory) return directory;
|
|
69
|
+
}
|
|
70
|
+
return '';
|
|
71
|
+
};
|
|
72
|
+
|
|
3
73
|
export const createNotificationTemplateRuntime = (deps) => {
|
|
4
74
|
const {
|
|
5
75
|
readSettingsFromDisk,
|
|
@@ -133,37 +203,45 @@ export const createNotificationTemplateRuntime = (deps) => {
|
|
|
133
203
|
return '';
|
|
134
204
|
};
|
|
135
205
|
|
|
136
|
-
const fetchLastAssistantMessageText = async (sessionId, messageId, maxLength = NOTIFICATION_BODY_MAX_CHARS) => {
|
|
206
|
+
const fetchLastAssistantMessageText = async (sessionId, messageId, maxLength = NOTIFICATION_BODY_MAX_CHARS, directory) => {
|
|
137
207
|
if (!sessionId) return '';
|
|
138
208
|
|
|
139
209
|
try {
|
|
140
|
-
const
|
|
141
|
-
const
|
|
210
|
+
const base = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}/message`, '');
|
|
211
|
+
const params = new URLSearchParams({ limit: '5', order: 'desc' });
|
|
212
|
+
const scopedDirectory = trimmedText(directory);
|
|
213
|
+
if (scopedDirectory) params.set('directory', scopedDirectory);
|
|
214
|
+
const response = await fetch(`${base}?${params.toString()}`, {
|
|
142
215
|
method: 'GET',
|
|
143
216
|
headers: {
|
|
144
217
|
Accept: 'application/json',
|
|
145
|
-
...getOpenCodeAuthHeaders(),
|
|
218
|
+
...(typeof getOpenCodeAuthHeaders === 'function' ? getOpenCodeAuthHeaders() : {}),
|
|
146
219
|
},
|
|
147
220
|
signal: AbortSignal.timeout(3000),
|
|
148
221
|
});
|
|
149
222
|
|
|
150
223
|
if (!response.ok) return '';
|
|
151
224
|
|
|
152
|
-
const
|
|
153
|
-
|
|
225
|
+
const payload = await response.json().catch(() => null);
|
|
226
|
+
// Legacy lists are a chronological array. OpenCode 2 returns `{ data }`
|
|
227
|
+
// and `order=desc` puts the newest turn first.
|
|
228
|
+
const newestFirst = !Array.isArray(payload);
|
|
229
|
+
const records = Array.isArray(payload)
|
|
230
|
+
? payload
|
|
231
|
+
: (Array.isArray(payload?.data) ? payload.data : null);
|
|
232
|
+
if (!records) return '';
|
|
233
|
+
const projected = projectSessionMessageRecords(records);
|
|
234
|
+
const messages = newestFirst ? projected : [...projected].reverse();
|
|
154
235
|
|
|
155
236
|
let target = null;
|
|
156
237
|
if (messageId) {
|
|
157
238
|
target = messages.find((message) => message?.info?.id === messageId && message?.info?.role === 'assistant');
|
|
158
239
|
}
|
|
159
240
|
if (!target) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
241
|
+
target = messages.find((message) => (
|
|
242
|
+
message?.info?.role === 'assistant'
|
|
243
|
+
&& extractTextFromParts(message.parts, maxLength).length > 0
|
|
244
|
+
));
|
|
167
245
|
}
|
|
168
246
|
|
|
169
247
|
if (!target || !Array.isArray(target.parts)) return '';
|
|
@@ -175,9 +253,9 @@ export const createNotificationTemplateRuntime = (deps) => {
|
|
|
175
253
|
};
|
|
176
254
|
|
|
177
255
|
const cacheSessionTitle = (sessionId, title) => {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
256
|
+
const id = trimmedText(sessionId);
|
|
257
|
+
const name = trimmedText(title);
|
|
258
|
+
if (id && name) sessionTitleCache.set(id, name);
|
|
181
259
|
};
|
|
182
260
|
|
|
183
261
|
const getCachedSessionTitle = (sessionId) => {
|
|
@@ -185,15 +263,12 @@ export const createNotificationTemplateRuntime = (deps) => {
|
|
|
185
263
|
};
|
|
186
264
|
|
|
187
265
|
const maybeCacheSessionInfoFromEvent = (payload) => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const info = payload.properties?.info;
|
|
192
|
-
if (!info || typeof info !== 'object') return;
|
|
193
|
-
cacheSessionTitle(info.id, info.title);
|
|
266
|
+
const titled = readSessionTitleFromEvent(payload);
|
|
267
|
+
if (!titled) return;
|
|
268
|
+
cacheSessionTitle(titled.sessionId, titled.title);
|
|
194
269
|
};
|
|
195
270
|
|
|
196
|
-
const fetchSessionInfo = async (sessionId) => {
|
|
271
|
+
const fetchSessionInfo = async (sessionId, directory) => {
|
|
197
272
|
if (!sessionId) return null;
|
|
198
273
|
|
|
199
274
|
const cached = sessionInfoCache.get(sessionId);
|
|
@@ -202,20 +277,24 @@ export const createNotificationTemplateRuntime = (deps) => {
|
|
|
202
277
|
}
|
|
203
278
|
|
|
204
279
|
try {
|
|
205
|
-
const
|
|
280
|
+
const base = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}`, '');
|
|
281
|
+
const url = directory ? `${base}?directory=${encodeURIComponent(directory)}` : base;
|
|
206
282
|
const response = await fetch(url, {
|
|
207
283
|
method: 'GET',
|
|
208
|
-
headers: {
|
|
284
|
+
headers: {
|
|
285
|
+
Accept: 'application/json',
|
|
286
|
+
...(typeof getOpenCodeAuthHeaders === 'function' ? getOpenCodeAuthHeaders() : {}),
|
|
287
|
+
},
|
|
209
288
|
signal: AbortSignal.timeout(2000),
|
|
210
289
|
});
|
|
211
290
|
if (!response.ok) {
|
|
212
291
|
console.warn(`[Notification] fetchSessionInfo: ${response.status} for session ${sessionId}`);
|
|
213
292
|
return null;
|
|
214
293
|
}
|
|
215
|
-
const
|
|
216
|
-
if (
|
|
217
|
-
sessionInfoCache.set(sessionId, { data, at: Date.now() });
|
|
218
|
-
return
|
|
294
|
+
const session = readNotificationSessionRecord(await response.json().catch(() => null));
|
|
295
|
+
if (session) {
|
|
296
|
+
sessionInfoCache.set(sessionId, { data: session, at: Date.now() });
|
|
297
|
+
return session;
|
|
219
298
|
}
|
|
220
299
|
return null;
|
|
221
300
|
} catch (error) {
|
|
@@ -227,7 +306,11 @@ export const createNotificationTemplateRuntime = (deps) => {
|
|
|
227
306
|
const buildTemplateVariables = async (payload, sessionId) => {
|
|
228
307
|
const info = payload?.properties?.info || {};
|
|
229
308
|
|
|
230
|
-
|
|
309
|
+
const fromEvent = readSessionTitleFromEvent(payload);
|
|
310
|
+
let sessionTitle = trimmedText(payload?.properties?.sessionTitle)
|
|
311
|
+
|| trimmedText(payload?.properties?.session?.title)
|
|
312
|
+
|| trimmedText(info.sessionTitle)
|
|
313
|
+
|| (fromEvent && (!sessionId || fromEvent.sessionId === sessionId) ? fromEvent.title : '');
|
|
231
314
|
|
|
232
315
|
if (!sessionTitle && sessionId) {
|
|
233
316
|
const cached = getCachedSessionTitle(sessionId);
|
|
@@ -238,25 +321,34 @@ export const createNotificationTemplateRuntime = (deps) => {
|
|
|
238
321
|
|
|
239
322
|
let sessionInfo = null;
|
|
240
323
|
if (!sessionTitle && sessionId) {
|
|
241
|
-
sessionInfo = await fetchSessionInfo(sessionId);
|
|
242
|
-
|
|
243
|
-
|
|
324
|
+
sessionInfo = await fetchSessionInfo(sessionId, readNotificationDirectory(payload));
|
|
325
|
+
const fetchedTitle = trimmedText(sessionInfo?.title);
|
|
326
|
+
if (fetchedTitle) {
|
|
327
|
+
sessionTitle = fetchedTitle;
|
|
244
328
|
cacheSessionTitle(sessionId, sessionTitle);
|
|
245
329
|
}
|
|
246
330
|
}
|
|
247
331
|
|
|
332
|
+
const eventAgent = trimmedText(info.agent) || trimmedText(info.mode);
|
|
333
|
+
const eventModel = trimmedText(info.modelID)
|
|
334
|
+
|| trimmedText(info.model?.modelID)
|
|
335
|
+
|| trimmedText(info.model?.id);
|
|
336
|
+
if ((!eventAgent || !eventModel) && sessionId && !sessionInfo) {
|
|
337
|
+
sessionInfo = await fetchSessionInfo(sessionId, readNotificationDirectory(payload));
|
|
338
|
+
}
|
|
339
|
+
|
|
248
340
|
const agentName = (() => {
|
|
249
|
-
const mode =
|
|
250
|
-
? info.agent.trim()
|
|
251
|
-
: (typeof info.mode === 'string' ? info.mode.trim() : '');
|
|
341
|
+
const mode = eventAgent || trimmedText(sessionInfo?.agent) || trimmedText(sessionInfo?.mode);
|
|
252
342
|
if (!mode) return 'Agent';
|
|
253
343
|
return mode.split(/[-_\s]+/).filter(Boolean)
|
|
254
344
|
.map((token) => token.charAt(0).toUpperCase() + token.slice(1)).join(' ');
|
|
255
345
|
})();
|
|
256
346
|
|
|
257
347
|
const modelName = (() => {
|
|
258
|
-
const raw =
|
|
259
|
-
|
|
348
|
+
const raw = eventModel
|
|
349
|
+
|| trimmedText(sessionInfo?.model?.modelID)
|
|
350
|
+
|| trimmedText(sessionInfo?.model?.id)
|
|
351
|
+
|| trimmedText(sessionInfo?.modelID);
|
|
260
352
|
if (!raw) return 'Assistant';
|
|
261
353
|
return raw.split(/[-_]+/).filter(Boolean)
|
|
262
354
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
|
|
@@ -82,3 +82,99 @@ describe('notification template message extraction', () => {
|
|
|
82
82
|
await expect(runtime.fetchLastAssistantMessageText('session-1', 'msg-1')).resolves.toBe('final answer');
|
|
83
83
|
});
|
|
84
84
|
});
|
|
85
|
+
|
|
86
|
+
describe('notification session titles', () => {
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
globalThis.fetch = originalFetch;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('reads the OpenCode 2 session title from the data envelope', async () => {
|
|
92
|
+
const runtime = createNotificationTemplateRuntime({
|
|
93
|
+
readSettingsFromDisk: async () => ({}),
|
|
94
|
+
buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
|
|
95
|
+
getOpenCodeAuthHeaders: () => ({ authorization: 'Basic test' }),
|
|
96
|
+
resolveGitBinaryForSpawn: () => 'git',
|
|
97
|
+
});
|
|
98
|
+
globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({
|
|
99
|
+
data: { id: 'ses_1', title: '发布说明' },
|
|
100
|
+
})));
|
|
101
|
+
|
|
102
|
+
const variables = await runtime.buildTemplateVariables({
|
|
103
|
+
type: 'message.updated',
|
|
104
|
+
location: { directory: '/repo' },
|
|
105
|
+
properties: { info: { sessionID: 'ses_1', role: 'assistant', finish: 'stop' } },
|
|
106
|
+
}, 'ses_1');
|
|
107
|
+
|
|
108
|
+
expect(variables.session_name).toBe('发布说明');
|
|
109
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
110
|
+
'http://opencode.test/session/ses_1?directory=%2Frepo',
|
|
111
|
+
expect.objectContaining({
|
|
112
|
+
headers: expect.objectContaining({ authorization: 'Basic test' }),
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('keeps a title learned from session.renamed when the session fetch fails', async () => {
|
|
118
|
+
const runtime = createRuntime();
|
|
119
|
+
globalThis.fetch = vi.fn(async () => new Response(null, { status: 401 }));
|
|
120
|
+
runtime.maybeCacheSessionInfoFromEvent({
|
|
121
|
+
type: 'session.renamed',
|
|
122
|
+
data: { sessionID: 'ses_1', title: '修通知标题' },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const variables = await runtime.buildTemplateVariables({
|
|
126
|
+
type: 'session.execution.succeeded',
|
|
127
|
+
data: { sessionID: 'ses_1' },
|
|
128
|
+
}, 'ses_1');
|
|
129
|
+
|
|
130
|
+
expect(variables.session_name).toBe('修通知标题');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('reads the latest assistant text from an OpenCode 2 message page', async () => {
|
|
134
|
+
const runtime = createRuntime();
|
|
135
|
+
globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({
|
|
136
|
+
data: [
|
|
137
|
+
{
|
|
138
|
+
id: 'msg_new',
|
|
139
|
+
sessionID: 'ses_1',
|
|
140
|
+
type: 'assistant',
|
|
141
|
+
agent: 'build',
|
|
142
|
+
model: { id: 'gpt-5', providerID: 'openai' },
|
|
143
|
+
content: [
|
|
144
|
+
{ type: 'reasoning', text: 'private' },
|
|
145
|
+
{ type: 'text', text: '发布说明已写好' },
|
|
146
|
+
],
|
|
147
|
+
time: { created: 2 },
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'msg_old',
|
|
151
|
+
sessionID: 'ses_1',
|
|
152
|
+
type: 'assistant',
|
|
153
|
+
content: [{ type: 'text', text: 'older' }],
|
|
154
|
+
finish: 'stop',
|
|
155
|
+
time: { created: 1 },
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
})));
|
|
159
|
+
|
|
160
|
+
await expect(runtime.fetchLastAssistantMessageText('ses_1')).resolves.toBe('发布说明已写好');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('fills agent and model names from the OpenCode 2 session record', async () => {
|
|
164
|
+
const runtime = createRuntime();
|
|
165
|
+
globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({
|
|
166
|
+
data: { id: 'ses_1', title: '发布说明', agent: 'build', model: { id: 'gpt-5', providerID: 'openai' } },
|
|
167
|
+
})));
|
|
168
|
+
|
|
169
|
+
const variables = await runtime.buildTemplateVariables({
|
|
170
|
+
type: 'session.execution.succeeded',
|
|
171
|
+
data: { sessionID: 'ses_1' },
|
|
172
|
+
}, 'ses_1');
|
|
173
|
+
|
|
174
|
+
expect(variables).toMatchObject({
|
|
175
|
+
session_name: '发布说明',
|
|
176
|
+
agent_name: 'Build',
|
|
177
|
+
model_name: 'Gpt 5',
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -15,6 +15,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
15
15
|
- `packages/web/server/lib/opencode/managed-capabilities-runtime.js`: managed-child scheduled-task resources, config injection, rotating bridge identity, and bridge authorization.
|
|
16
16
|
- `packages/web/server/lib/opencode/env-runtime.js`: OpenCode CLI/binary resolution and shell environment runtime.
|
|
17
17
|
- `packages/web/server/lib/opencode/ensure-cli.js`: detect local `opencode` version and install the pinned official V2 into the OpenChamber data dir when missing or too old. Never uses a packaged/bundled binary.
|
|
18
|
+
- `packages/web/server/lib/opencode/upgrade-screen.js`: startup gate for a detected OpenCode below `2.0.15`. It does not own process lifecycle. `GET /api/opencode/compatibility` classifies the version already detected by startup. `POST /api/opencode/install-required` installs `max(pin, 2.0.15)` through `installPinnedOpenCode2Cli`, points the managed binary at that cache, and restarts through the existing lifecycle hooks. Success requires the running serve to report `2.0.15` or newer. External serves are not replaced. Failure returns `upgraded: false` and the error; it does not claim the previous process was restored.
|
|
18
19
|
- `packages/web/server/lib/opencode/env-config.js`: OpenCode-related environment variable parsing and validation (host/port/hostname).
|
|
19
20
|
- `packages/web/server/lib/opencode/hmr-state-runtime.js`: HMR-persistent runtime state initialization, auth-state bootstrap, and HMR sync helpers.
|
|
20
21
|
- `packages/web/server/lib/opencode/bootstrap-runtime.js`: base app bootstrap runtime for status/auth/tts/notification/OpenChamber route wiring.
|
|
@@ -30,6 +31,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
30
31
|
- `packages/web/server/lib/opencode/static-routes-runtime.js`: static asset/SPA fallback route registration and manifest route wiring. The SPA fallback sends `index.html` relative to `root`, so deep links keep working when the install lives under a dot directory.
|
|
31
32
|
- `packages/web/server/lib/opencode/feature-routes-runtime.js`: feature route composition runtime for dynamic import-backed config/skill/provider route registration.
|
|
32
33
|
- `packages/web/server/lib/conversations/`: combined create-session-and-prompt orchestration (OpenChamber-owned, registered before generic proxy).
|
|
34
|
+
- `packages/web/server/lib/browser-provider/`: host for an installed extension that declares `browser`. Lists reachable providers, sends one agent browser action to the selected provider, and refuses when none is selected. Does not drive the in-app browser pane. See `browser-provider/DOCUMENTATION.md`.
|
|
33
35
|
- `packages/web/server/lib/session-turn-pages/`: turn-window session message pagination (`GET /api/openchamber/sessions/:sessionID/messages`), anchor reconcile (`GET .../messages/reconcile`), Changes L2/L3 (`GET .../changes`), and exact message L1 projection (`GET /api/session/:sessionID/message/:messageID`); loops official OpenCode `session.messages` for turn pages and head→anchor gap recovery with continuation/budgets/`resetRequired`; all registered before generic proxy. L1 first-packet / SSE / exact-message paths keep `summary.diffs` as a slim `{ file, status?, additions, deletions }` list plus additive `diffCount`/`hasDiffs` (patch bodies stay on L3). See `session-turn-pages/DOCUMENTATION.md`.
|
|
34
36
|
- `packages/web/server/lib/opencode/opencode-resolution-runtime.js`: OpenCode binary resolution snapshot runtime for settings routes and diagnostics.
|
|
35
37
|
- `packages/web/server/lib/opencode/tunnel-wiring-runtime.js`: active loopback-port wiring shared by pairing/LAN URLs and the private relay host.
|
|
@@ -87,6 +89,8 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
87
89
|
- `PUT /api/behavior/agents-md`
|
|
88
90
|
- `POST /api/opencode/upgrade` (ticket 12): installs the pinned/target `@opencode/cli` into the **owned cache** at `~/.config/openchamber/opencode-cli/<version>/`. Managed owned-cache processes restart on that binary; official shared services use that exact binary's `service restart`, then adopt its registration again. Success requires a fresh running-serve version probe and runtime contract verification. Arbitrary external servers, private processes using global CLI, and private bundled binaries return HTTP 409; user installs are never overwritten. Concurrent upgrades are single-flight (`operation` state). Active sessions require `confirmActiveTasks`/`force`. Failure preserves cached binaries, restores the previous binary selection, and reports the failed phase; it does not claim the old process was restored. Shared-service recovery can subsequently re-ensure the registered service.
|
|
89
91
|
- `GET /api/opencode/upgrade-status`: returns `currentVersion`/`serveVersion`/`cliVersion`, `targetVersion`/`latestVersion` (pin), `supplySource`, `ownership`, `canManage`, `management`, `guidance`, `contract`, and `operation`. Official shared services report `ownership: 'shared-service'`, `processOwnership: 'shared'`, and `canManage: true`. `available` is true only when `canManage` and target > current; unmanaged global/external runtimes report `available: null` plus manual guidance.
|
|
92
|
+
- `GET /api/opencode/compatibility`: startup upgrade-screen status. `state` is `compatible` at OpenCode `2.0.15` or newer, `incompatible` when a detected version is older or not 2.x, and `unavailable` when no version could be read. `canInstall` is true only for a local (managed or bundled) runtime this platform can install into the owned cache. External serves are reported and not replaced.
|
|
93
|
+
- `POST /api/opencode/install-required`: installs the upgrade-screen target (`2.0.15`, or the pin when the pin is newer) into the owned cache and restarts through the existing lifecycle. `upgraded: true` only after the running serve verifies at or above that target. Failure is `upgraded: false` plus `error`. Concurrent installs are single-flight.
|
|
90
94
|
- `GET /api/opencode/contract` (ticket 11): runtime contract admission for the **running serve** (not health alone). Dimensions: reachable / authenticated / protocolCompatible / executionAllowed / capabilities / migration / CLI↔serve mismatch. Floor `2.0.12` (acceptable OpenCode 2.x at or above); below-min 2.x and 1.x block execution with upgrade guidance; no upper version cap.
|
|
91
95
|
- `GET /api/opencode/health`: probes managed OpenCode `GET /api/info` (Basic auth, 4s timeout). `healthy` is reachability/version shape via `evaluateOpenCodeHealthBody` (admits official 2.x `ServerInfo` and classic `{ healthy: true, version }`; rejects 1.x / missing / `healthy: false`). Response also includes `executionAllowed`, `protocolCompatible`, and `contract` so clients do not treat health success as full execution semantics. Upstream non-OK responses pass through status with `{ healthy: false, error, contract }`; transport failures return HTTP 503.
|
|
92
96
|
- `GET /api/opencode/version`: serve + CLI versions, mismatch flag, and contract snapshot
|
|
@@ -305,6 +309,10 @@ configuration reload uses official SDK `location.reload`. The historical
|
|
|
305
309
|
`/api/config/reload` host/bridge endpoint remains reserved for existing process
|
|
306
310
|
startup/recovery/binary-change callers, not ordinary Settings mutations.
|
|
307
311
|
|
|
312
|
+
Command catalog SDK fetch forwarding preserves the generated request options,
|
|
313
|
+
including authentication headers, and combines caller cancellation with its
|
|
314
|
+
eight-second request deadline.
|
|
315
|
+
|
|
308
316
|
`config-hot-reload.integration.test.js` is opt-in via
|
|
309
317
|
`OPENCHAMBER_TEST_OPENCODE_BINARY=<absolute-v2-cli>`. It starts a separate loopback
|
|
310
318
|
server with isolated HOME/XDG/config/database, checks global command changes,
|
|
@@ -531,7 +531,12 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
|
|
531
531
|
const client = createOpenCodeClient({
|
|
532
532
|
baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
|
|
533
533
|
headers: getOpenCodeAuthHeaders(),
|
|
534
|
-
fetch: (request) => fetch(request, {
|
|
534
|
+
fetch: (request, init) => fetch(request, {
|
|
535
|
+
...init,
|
|
536
|
+
signal: init?.signal
|
|
537
|
+
? AbortSignal.any([init.signal, AbortSignal.timeout(8_000)])
|
|
538
|
+
: AbortSignal.timeout(8_000),
|
|
539
|
+
}),
|
|
535
540
|
});
|
|
536
541
|
const response = await client.command.list({ location: { directory } });
|
|
537
542
|
if (!Array.isArray(response?.data)) {
|
|
@@ -69,6 +69,31 @@ describe('V2 configuration persistence', () => {
|
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
describe('config entity command metadata route', () => {
|
|
72
|
+
it('preserves real SDK authentication and request options when fetching the catalog', async () => {
|
|
73
|
+
const { OpenCode: realOpenCode } = await vi.importActual('@opencode/client');
|
|
74
|
+
createOpencodeClient.mockImplementation(realOpenCode.make);
|
|
75
|
+
const upstream = vi.spyOn(globalThis, 'fetch').mockImplementation(async (_input, init) => {
|
|
76
|
+
if (new Headers(init?.headers).get('Authorization') !== 'Basic example') {
|
|
77
|
+
return new Response('Unauthorized', { status: 401 });
|
|
78
|
+
}
|
|
79
|
+
return Response.json({ data: [{ name: 'review', description: 'Review changes' }] });
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
const app = express();
|
|
83
|
+
app.use(express.json());
|
|
84
|
+
registerConfigEntityRoutes(app, createDependencies(() => ({ md: { exists: false }, json: { exists: false } })));
|
|
85
|
+
const response = await request(app).post('/api/config/commands/metadata').send({ catalog: true });
|
|
86
|
+
expect(response.status).toBe(200);
|
|
87
|
+
expect(response.body.commands).toEqual([expect.objectContaining({ name: 'review' })]);
|
|
88
|
+
const [url, init] = upstream.mock.calls[0];
|
|
89
|
+
expect(new URL(url).pathname).toBe('/api/command');
|
|
90
|
+
expect(init.method).toBe('GET');
|
|
91
|
+
expect(init.signal).toBeInstanceOf(AbortSignal);
|
|
92
|
+
} finally {
|
|
93
|
+
upstream.mockRestore();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
72
97
|
it('returns metadata for many commands through one request', async () => {
|
|
73
98
|
const app = express();
|
|
74
99
|
app.use(express.json());
|
|
@@ -19,6 +19,9 @@ import { registerSessionTurnPageRoutes } from '../session-turn-pages/routes.js';
|
|
|
19
19
|
import { registerAssistantRoutes } from '../assistants/routes.js';
|
|
20
20
|
import { registerLlmRoutes } from '../llm/routes.js';
|
|
21
21
|
import { registerMessageQueueRoutes } from '../message-queue/routes.js';
|
|
22
|
+
import { getBrowserProviderHost } from '../browser-provider/host.js';
|
|
23
|
+
import { registerBrowserProviderRoutes } from '../browser-provider/routes.js';
|
|
24
|
+
import { createBrowserProviderSelectionStore } from '../browser-provider/selection-store.js';
|
|
22
25
|
import { registerSkillRoutes } from './skill-routes.js';
|
|
23
26
|
import { registerPluginRoutes } from './plugin-routes.js';
|
|
24
27
|
import { getNpmInfo, clearCache as clearNpmCache } from './npm-registry.js';
|
|
@@ -513,6 +516,13 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
513
516
|
openchamberUserConfigRoot,
|
|
514
517
|
openchamberDataDir,
|
|
515
518
|
});
|
|
519
|
+
const browserProviderHost = getBrowserProviderHost();
|
|
520
|
+
if (typeof openchamberDataDir === 'string' && openchamberDataDir) {
|
|
521
|
+
browserProviderHost.attachSelectionStore(createBrowserProviderSelectionStore(
|
|
522
|
+
path.join(openchamberDataDir, 'browser-provider-selection.json'),
|
|
523
|
+
));
|
|
524
|
+
}
|
|
525
|
+
registerBrowserProviderRoutes(app, { host: browserProviderHost });
|
|
516
526
|
};
|
|
517
527
|
|
|
518
528
|
return {
|
|
@@ -54,6 +54,13 @@ describe('feature routes runtime composition', () => {
|
|
|
54
54
|
expect(source).toMatch(/registerScheduledTaskRoutes\(app, \{[\s\S]*runHistoryStore,/);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
it('registers browser provider routes on the shared host before proxy composition', async () => {
|
|
58
|
+
const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
|
|
59
|
+
expect(source).toContain("import { registerBrowserProviderRoutes } from '../browser-provider/routes.js';");
|
|
60
|
+
expect(source).toContain('registerBrowserProviderRoutes(app, { host: browserProviderHost });');
|
|
61
|
+
expect(source).toContain('createBrowserProviderSelectionStore(');
|
|
62
|
+
});
|
|
63
|
+
|
|
57
64
|
it('registers message queue routes with the injected service before proxy composition', async () => {
|
|
58
65
|
const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
|
|
59
66
|
expect(source).toContain("import { registerMessageQueueRoutes } from '../message-queue/routes.js';");
|
|
@@ -957,7 +957,9 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
957
957
|
req.path === '/opencode/version' ||
|
|
958
958
|
req.path === '/opencode/contract' ||
|
|
959
959
|
req.path === '/opencode/upgrade-status' ||
|
|
960
|
-
req.path === '/opencode/upgrade'
|
|
960
|
+
req.path === '/opencode/upgrade' ||
|
|
961
|
+
req.path === '/opencode/compatibility' ||
|
|
962
|
+
req.path === '/opencode/install-required'
|
|
961
963
|
) {
|
|
962
964
|
return next();
|
|
963
965
|
}
|
|
@@ -1207,6 +1209,9 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
1207
1209
|
app.get('/api/session/:sessionID/message', (req, res, next) => {
|
|
1208
1210
|
return forwardSessionMessagesListRequest(req, res, next);
|
|
1209
1211
|
});
|
|
1212
|
+
app.get('/api/session/:sessionID/context', (req, res, next) => {
|
|
1213
|
+
return forwardSessionMessagesListRequest(req, res, next);
|
|
1214
|
+
});
|
|
1210
1215
|
|
|
1211
1216
|
app.get('/api/global/event', forwardSseRequest);
|
|
1212
1217
|
app.get('/api/event', forwardSseRequest);
|
|
@@ -460,6 +460,21 @@ describe('registerOpenCodeProxy reasoning projection routes', () => {
|
|
|
460
460
|
expect(JSON.stringify(res.body)).not.toContain('hidden');
|
|
461
461
|
});
|
|
462
462
|
|
|
463
|
+
it.each(['message', 'context'])('filters native v2 %s before outbound transport', async (surface) => {
|
|
464
|
+
const upstreamUrls = [];
|
|
465
|
+
globalThis.fetch = vi.fn(async (url) => {
|
|
466
|
+
upstreamUrls.push(String(url));
|
|
467
|
+
return new Response(JSON.stringify({ data: [{ id: 'm', type: 'assistant', content: [
|
|
468
|
+
{ type: 'reasoning', text: 'hidden' }, { type: 'text', text: 'visible' },
|
|
469
|
+
] }], cursor: { next: 'older' } }), { headers: { 'content-type': 'application/json' } });
|
|
470
|
+
});
|
|
471
|
+
const res = await request(mountProxy()).get(`/api/session/ses_1/${surface}?includeReasoning=false`);
|
|
472
|
+
expect(res.status).toBe(200);
|
|
473
|
+
expect(res.body.data[0].content).toEqual([{ type: 'text', text: 'visible' }]);
|
|
474
|
+
expect(res.body.cursor.next).toBe('older');
|
|
475
|
+
expect(upstreamUrls[0]).not.toContain('includeReasoning');
|
|
476
|
+
});
|
|
477
|
+
|
|
463
478
|
it('keeps reasoning parts on session.messages list when includeReasoning is omitted', async () => {
|
|
464
479
|
globalThis.fetch = vi.fn(async () => ({
|
|
465
480
|
ok: true,
|
|
@@ -503,7 +518,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
|
|
|
503
518
|
return app;
|
|
504
519
|
};
|
|
505
520
|
|
|
506
|
-
it('merges
|
|
521
|
+
it('merges side-store gaps onto the OpenCode record without letting the side row win conflicts', async () => {
|
|
507
522
|
globalThis.fetch = vi.fn(async (url) => {
|
|
508
523
|
const path = String(url).split('?')[0];
|
|
509
524
|
if (path.endsWith('/api/session') || path.endsWith('/session')) {
|
|
@@ -551,7 +566,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
|
|
|
551
566
|
id: 'ses_1',
|
|
552
567
|
metadata: {
|
|
553
568
|
fromOpenCode: true,
|
|
554
|
-
shared: '
|
|
569
|
+
shared: 'theirs',
|
|
555
570
|
openchamber: { goal: { status: 'active' } },
|
|
556
571
|
},
|
|
557
572
|
});
|
|
@@ -563,7 +578,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
|
|
|
563
578
|
id: 'ses_1',
|
|
564
579
|
metadata: {
|
|
565
580
|
fromOpenCode: true,
|
|
566
|
-
shared: '
|
|
581
|
+
shared: 'theirs',
|
|
567
582
|
openchamber: { goal: { status: 'active' } },
|
|
568
583
|
},
|
|
569
584
|
});
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
readOpenCode2BinaryVersion,
|
|
12
12
|
resolveOpenChamberDataDir,
|
|
13
13
|
} from './ensure-cli.js';
|
|
14
|
+
import { registerUpgradeScreenRoutes } from './upgrade-screen.js';
|
|
14
15
|
import { evaluateRuntimeContract } from './runtime-contract.js';
|
|
15
16
|
import {
|
|
16
17
|
buildUpgradeStatusSnapshot,
|
|
@@ -253,6 +254,35 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
253
254
|
return fallback;
|
|
254
255
|
};
|
|
255
256
|
|
|
257
|
+
registerUpgradeScreenRoutes(app, {
|
|
258
|
+
resolveOwnership: resolveOwnershipContext,
|
|
259
|
+
readServeVersion: async () => {
|
|
260
|
+
const probe = await readOpenCodeCurrentVersion().catch(() => ({ currentVersion: null }));
|
|
261
|
+
return probe.currentVersion || null;
|
|
262
|
+
},
|
|
263
|
+
readCliVersion: () => {
|
|
264
|
+
const fromRuntime = typeof getOpenCodeCliVersion === 'function' ? getOpenCodeCliVersion() : null;
|
|
265
|
+
if (fromRuntime) return fromRuntime;
|
|
266
|
+
const binaryPath = typeof getResolvedOpenCodeBinary === 'function' ? getResolvedOpenCodeBinary() : null;
|
|
267
|
+
return binaryPath ? readOpenCode2BinaryVersion(binaryPath) : null;
|
|
268
|
+
},
|
|
269
|
+
readConfiguredBinary: async () => {
|
|
270
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
271
|
+
return typeof settings?.opencodeBinary === 'string' ? settings.opencodeBinary : null;
|
|
272
|
+
},
|
|
273
|
+
persistBinary: async (binaryPath) => {
|
|
274
|
+
await persistSettings({ opencodeBinary: binaryPath });
|
|
275
|
+
},
|
|
276
|
+
getResolvedBinary: getResolvedOpenCodeBinary,
|
|
277
|
+
getResolvedBinarySource: getResolvedOpenCodeBinarySource,
|
|
278
|
+
forceBinary: typeof forceResolvedOpenCodeBinary === 'function' ? forceResolvedOpenCodeBinary : null,
|
|
279
|
+
restart: typeof restartOpenCode === 'function' ? restartOpenCode : null,
|
|
280
|
+
waitReady: typeof waitForOpenCodeReady === 'function' ? waitForOpenCodeReady : null,
|
|
281
|
+
isSharedService: () => getIsSharedOpenCodeService() === true,
|
|
282
|
+
install: (options) => installPinnedOpenCode2Cli({ ...options, dataDir: resolveDataDir() }),
|
|
283
|
+
readBinaryVersion: readOpenCode2BinaryVersion,
|
|
284
|
+
});
|
|
285
|
+
|
|
256
286
|
app.get('/api/config/settings', async (req, res) => {
|
|
257
287
|
try {
|
|
258
288
|
const settings = await readSettingsFromDiskMigrated();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const REQUIRED_OPENCODE_VERSION: string;
|
|
2
|
+
|
|
3
|
+
export type UpgradeScreen = {
|
|
4
|
+
state: 'unavailable' | 'compatible' | 'incompatible';
|
|
5
|
+
version: string | null;
|
|
6
|
+
installation: 'managed' | 'external' | 'bundled';
|
|
7
|
+
minimumVersion: string;
|
|
8
|
+
targetVersion: string;
|
|
9
|
+
canInstall: boolean;
|
|
10
|
+
guidance: string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function describeUpgradeScreen(input?: {
|
|
14
|
+
version?: string | null;
|
|
15
|
+
installation?: 'managed' | 'external' | 'bundled';
|
|
16
|
+
platformCanInstall?: boolean;
|
|
17
|
+
guidance?: string | null;
|
|
18
|
+
pin?: string;
|
|
19
|
+
}): UpgradeScreen;
|
|
20
|
+
|
|
21
|
+
export function platformSupportsUpgradeScreenInstall(platform?: string, arch?: string): boolean;
|
|
22
|
+
|
|
23
|
+
export function installRequiredOpenCode(deps: object): Promise<{
|
|
24
|
+
ok: true;
|
|
25
|
+
upgraded: true;
|
|
26
|
+
version: string;
|
|
27
|
+
targetVersion: string;
|
|
28
|
+
error: null;
|
|
29
|
+
errorCode: null;
|
|
30
|
+
}>;
|