@openchambery/web 1.19.10-beta.1 → 1.19.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AssistantView-7SZQ12Js.js → AssistantView-BsSP5fIS.js} +1 -1
- package/dist/assets/{DiagramView-D5WUE1De.js → DiagramView-Dx6Bt2V6.js} +1 -1
- package/dist/assets/MarkdownRenderer-BkAWQ2GV.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-uH_cfmYV.js → MarkdownRendererImpl-Ds8Ssyq-.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-BAEStQuf.js → MarkstreamRendererImpl-De3BKIuP.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-DfZmPtql.js → MobileDetailNavigation-BXKxAdE_.js} +1 -1
- package/dist/assets/{MobileShareBridge-CSJXojSq.js → MobileShareBridge-ziYucHOE.js} +1 -1
- package/dist/assets/MobileSurface-CVKw8mGv.js +2 -0
- package/dist/assets/{MultiRunWindow-DM_LKkMR.js → MultiRunWindow-CNEIMi0b.js} +1 -1
- package/dist/assets/{OnboardingScreen-C3-wzeSX.js → OnboardingScreen-DS2nugLX.js} +1 -1
- package/dist/assets/{SettingsView-DLzL3jV2.js → SettingsView-gH4Hr-Ci.js} +2 -2
- package/dist/assets/{SettingsWindow-CtF8i9jw.js → SettingsWindow-DywJoMmg.js} +1 -1
- package/dist/assets/{TerminalView-D_XjSm4c.js → TerminalView-DMCgudnq.js} +1 -1
- package/dist/assets/{ToolOutputDialog-FB6tTD3j.js → ToolOutputDialog-DS0sIkYs.js} +1 -1
- package/dist/assets/{appThemeRegistry-QBlzmiVt.js → appThemeRegistry-CPmXzUpv.js} +1 -1
- package/dist/assets/desktopHosts-Dc2PS66M.js +1 -0
- package/dist/assets/desktopRelayRestore-T9y0orqa.js +1 -0
- package/dist/assets/{gitApi-Dhf22VRf.js → gitApi-B7Z4cSkO.js} +1 -1
- package/dist/assets/{insertionBoundaries-DPXJs_6g.js → insertionBoundaries-BqKrCN3T.js} +1 -1
- package/dist/assets/{main-rP7OpJ90.js → main-BK_cIfNI.js} +22 -22
- package/dist/assets/{main-DDkeV7EJ.js → main-gw6pD8eo.js} +2 -2
- package/dist/assets/{miniChat-DbpkRapr.js → miniChat-BuKi9XeD.js} +2 -2
- package/dist/assets/{mobile-BHzD2N9R.js → mobile-C24ys3se.js} +2 -2
- package/dist/assets/{multirun-Bj0sq61V.js → multirun-CekxHJb2.js} +1 -1
- package/dist/assets/{projectMeta-CWZHL9ES.js → projectMeta-4yuBfnoI.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-Cvq7gvEF.js → renderElectronMiniChatApp-DPannb7B.js} +2 -2
- package/dist/assets/{renderMobileApp-Bw-DYhhE.js → renderMobileApp-B_rMJh_N.js} +4 -4
- package/dist/assets/{runtimeConfig-DI2F7QqO.js → runtimeConfig-Qm8Qi_ur.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-BaZvZoAh.js → runtimeEndpointReset-abc-B3J7.js} +1 -1
- package/dist/assets/{sessionLookup-C0X2jjwR.js → sessionLookup-cWAHYgXp.js} +1 -1
- package/dist/assets/{useAppFontEffects-BzioX7oh.js → useAppFontEffects-BC7Olwie.js} +5 -5
- package/dist/assets/{useDesktopWindowControlsLayout-sxoPqjgO.js → useDesktopWindowControlsLayout-D06g5Qdz.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-D36udiNq.js → useEffectiveDirectory-SoAZb6s1.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-DYJ6I8FV.js → useMobileNavigationStore-Btd4QQdS.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-CLoSRLcC.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-bL-mmXJM.js → useWorkerHighlightedLines-Cu4ICr-m.js} +1 -1
- package/dist/index.html +4 -4
- package/dist/mini-chat.html +4 -4
- package/dist/mobile.html +4 -4
- package/package.json +1 -1
- package/server/lib/assistants/DOCUMENTATION.md +21 -10
- package/server/lib/assistants/contact-store.js +90 -5
- package/server/lib/assistants/contact-store.test.js +127 -2
- package/server/lib/assistants/contact-tools.js +107 -24
- package/server/lib/assistants/contact-tools.test.js +45 -3
- package/server/lib/assistants/harness.js +15 -5
- package/server/lib/assistants/harness.test.js +65 -3
- package/server/lib/assistants/service.js +76 -10
- package/server/lib/assistants/service.test.js +270 -11
- package/server/lib/llm/DOCUMENTATION.md +6 -0
- package/server/lib/llm/generate.js +4 -1
- package/server/lib/llm/generate.test.js +6 -1
- package/server/lib/opencode/DOCUMENTATION.md +1 -0
- package/server/lib/opencode/openchamber-routes.js +3 -0
- package/server/lib/opencode/openchamber-routes.test.js +43 -0
- package/server/lib/relay/DOCUMENTATION.md +1 -0
- package/dist/assets/MarkdownRenderer-CqYd9loP.js +0 -2
- package/dist/assets/MobileSurface-CmFXo_Zx.js +0 -2
- package/dist/assets/desktopHosts-CWxex1c3.js +0 -1
- package/dist/assets/desktopRelayRestore-9EbBYQ9s.js +0 -1
- package/dist/assets/useSessionAutoCleanup-7L5-suil.js +0 -1
|
@@ -5,6 +5,10 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
6
|
import { createAssistantsService } from './service.js';
|
|
7
7
|
import { assistantContractFixtures } from './contracts.js';
|
|
8
|
+
import {
|
|
9
|
+
CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE,
|
|
10
|
+
NEW_CONVERSATION_CONFIRM_BUBBLE,
|
|
11
|
+
} from './contact-tools.js';
|
|
8
12
|
|
|
9
13
|
const require = createRequire(import.meta.url);
|
|
10
14
|
const root = () => fs.mkdtempSync(path.join(os.tmpdir(), 'assistants-'));
|
|
@@ -97,7 +101,7 @@ describe('assistants service', () => {
|
|
|
97
101
|
service.close();
|
|
98
102
|
});
|
|
99
103
|
|
|
100
|
-
it('new_conversation clears
|
|
104
|
+
it('new_conversation clears LLM memory while keeping the full transcript', async () => {
|
|
101
105
|
const directory = root();
|
|
102
106
|
let creates = 0;
|
|
103
107
|
let lastHistory = null;
|
|
@@ -109,7 +113,7 @@ describe('assistants service', () => {
|
|
|
109
113
|
if (userText === '开新对话') {
|
|
110
114
|
const tool = tools.find((item) => item.name === 'new_conversation');
|
|
111
115
|
const result = await tool.execute('reset_1', {});
|
|
112
|
-
return { text: result.content[0].text, bubbles: [result.content[0].text] };
|
|
116
|
+
return { text: result.content[0].text, bubbles: [result.content[0].text], reset: true };
|
|
113
117
|
}
|
|
114
118
|
return { text: `reply:${userText}`, bubbles: [`reply:${userText}`] };
|
|
115
119
|
},
|
|
@@ -125,13 +129,16 @@ describe('assistants service', () => {
|
|
|
125
129
|
expect(creates).toBe(1);
|
|
126
130
|
const page = service.contactMessages(assistant.id, { limit: 50 });
|
|
127
131
|
expect(page.messages.map((message) => ({ role: message.role, text: message.text }))).toEqual([
|
|
132
|
+
{ role: 'user', text: 'remember this secret' },
|
|
133
|
+
{ role: 'assistant', text: 'reply:remember this secret' },
|
|
134
|
+
{ role: 'user', text: 'and this too' },
|
|
135
|
+
{ role: 'assistant', text: 'reply:and this too' },
|
|
128
136
|
{ role: 'user', text: '开新对话' },
|
|
129
|
-
{ role: 'assistant', text:
|
|
137
|
+
{ role: 'assistant', text: NEW_CONVERSATION_CONFIRM_BUBBLE },
|
|
130
138
|
]);
|
|
131
139
|
await settleSend(service, assistant.id, { messageID: 'fresh_1', parts: [{ type: 'text', text: 'what did I say before?' }] });
|
|
132
140
|
expect(lastHistory.map((item) => item.content)).toEqual([
|
|
133
|
-
|
|
134
|
-
'Started a new conversation. Previous contact messages are cleared.',
|
|
141
|
+
NEW_CONVERSATION_CONFIRM_BUBBLE,
|
|
135
142
|
]);
|
|
136
143
|
expect(lastHistory.some((item) => item.content.includes('remember this secret'))).toBe(false);
|
|
137
144
|
service.close();
|
|
@@ -144,13 +151,14 @@ describe('assistants service', () => {
|
|
|
144
151
|
if (userText === '开新对话') {
|
|
145
152
|
await tools.find((item) => item.name === 'new_conversation').execute('reset_leftover', {});
|
|
146
153
|
return {
|
|
147
|
-
text:
|
|
154
|
+
text: `${NEW_CONVERSATION_CONFIRM_BUBBLE}\n\nI still see your dot.png and note.txt.`,
|
|
148
155
|
bubbles: [
|
|
149
|
-
|
|
156
|
+
NEW_CONVERSATION_CONFIRM_BUBBLE,
|
|
150
157
|
'I still see your dot.png and note.txt.',
|
|
151
158
|
'Those attachments are still in context.',
|
|
152
159
|
],
|
|
153
160
|
cards: [{ type: 'card', cardType: 'session', sessionID: 'ses_stale', directory: '/repo', title: 'Old', status: 'busy' }],
|
|
161
|
+
reset: true,
|
|
154
162
|
};
|
|
155
163
|
}
|
|
156
164
|
return { text: `reply:${userText}`, bubbles: [`reply:${userText}`] };
|
|
@@ -168,30 +176,281 @@ describe('assistants service', () => {
|
|
|
168
176
|
await settleSend(service, assistant.id, { messageID: 'reset_leftover', parts: [{ type: 'text', text: '开新对话' }] });
|
|
169
177
|
const page = service.contactMessages(assistant.id, { limit: 50 });
|
|
170
178
|
expect(page.messages.map((message) => ({ role: message.role, text: message.text }))).toEqual([
|
|
179
|
+
{ role: 'user', text: 'look at these' },
|
|
180
|
+
{ role: 'assistant', text: 'reply:look at these' },
|
|
171
181
|
{ role: 'user', text: '开新对话' },
|
|
172
|
-
{ role: 'assistant', text:
|
|
182
|
+
{ role: 'assistant', text: NEW_CONVERSATION_CONFIRM_BUBBLE },
|
|
173
183
|
]);
|
|
174
184
|
expect(page.messages.some((message) => message.text.includes('dot.png') || message.text.includes('note.txt'))).toBe(false);
|
|
175
|
-
expect(page.messages.
|
|
185
|
+
expect(page.messages.filter((message) => message.cards?.length > 0)).toHaveLength(0);
|
|
176
186
|
service.close();
|
|
177
187
|
});
|
|
178
188
|
|
|
179
|
-
it('resetContact
|
|
189
|
+
it('clear_chat_history and resetContact delete the transcript without createNew', async () => {
|
|
180
190
|
const directory = root();
|
|
181
191
|
let creates = 0;
|
|
182
192
|
const service = setup(directory, {
|
|
183
193
|
create: async () => ({ data: { id: `ses_${++creates}` } }),
|
|
194
|
+
}, {
|
|
195
|
+
runContactTurn: async ({ userText, tools }) => {
|
|
196
|
+
if (userText === '清空聊天记录' || userText === '清除聊天记录') {
|
|
197
|
+
const tool = tools.find((item) => item.name === 'clear_chat_history');
|
|
198
|
+
const result = await tool.execute('wipe_1', {});
|
|
199
|
+
if (result.details?.error) {
|
|
200
|
+
return { text: result.content[0].text, bubbles: [result.content[0].text] };
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
text: result.content[0].text,
|
|
204
|
+
bubbles: [result.content[0].text],
|
|
205
|
+
reset: true,
|
|
206
|
+
historyCleared: true,
|
|
207
|
+
messages: [{
|
|
208
|
+
role: 'toolResult',
|
|
209
|
+
toolName: 'clear_chat_history',
|
|
210
|
+
details: result.details,
|
|
211
|
+
}],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return { text: `reply:${userText}`, bubbles: [`reply:${userText}`] };
|
|
215
|
+
},
|
|
184
216
|
});
|
|
185
217
|
const assistant = service.createAssistant(assistantInput);
|
|
186
218
|
await service.ensure(assistant.id);
|
|
187
219
|
await settleSend(service, assistant.id, { messageID: 'keep_1', parts: [{ type: 'text', text: 'hello' }] });
|
|
188
220
|
expect(service.contactMessages(assistant.id, { limit: 50 }).messages.length).toBeGreaterThan(0);
|
|
189
|
-
expect(service.resetContact(assistant.id)).toEqual({
|
|
221
|
+
expect(service.resetContact(assistant.id)).toEqual({
|
|
222
|
+
assistantID: assistant.id,
|
|
223
|
+
reset: true,
|
|
224
|
+
historyCleared: true,
|
|
225
|
+
});
|
|
190
226
|
expect(creates).toBe(1);
|
|
191
227
|
expect(service.contactMessages(assistant.id, { limit: 50 })).toMatchObject({
|
|
192
228
|
messages: [],
|
|
193
229
|
complete: true,
|
|
194
230
|
});
|
|
231
|
+
await settleSend(service, assistant.id, { messageID: 'again_1', parts: [{ type: 'text', text: 'hello again' }] });
|
|
232
|
+
await settleSend(service, assistant.id, { messageID: 'wipe_1', parts: [{ type: 'text', text: '清空聊天记录' }] });
|
|
233
|
+
expect(service.contactMessages(assistant.id, { limit: 50 }).messages.map((message) => ({
|
|
234
|
+
role: message.role,
|
|
235
|
+
text: message.text,
|
|
236
|
+
}))).toEqual([
|
|
237
|
+
{ role: 'user', text: '清空聊天记录' },
|
|
238
|
+
{ role: 'assistant', text: CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE },
|
|
239
|
+
]);
|
|
240
|
+
await settleSend(service, assistant.id, { messageID: 'again_2', parts: [{ type: 'text', text: 'hello third' }] });
|
|
241
|
+
await settleSend(service, assistant.id, { messageID: 'wipe_2', parts: [{ type: 'text', text: '清除聊天记录' }] });
|
|
242
|
+
expect(service.contactMessages(assistant.id, { limit: 50 }).messages.map((message) => ({
|
|
243
|
+
role: message.role,
|
|
244
|
+
text: message.text,
|
|
245
|
+
}))).toEqual([
|
|
246
|
+
{ role: 'user', text: '清除聊天记录' },
|
|
247
|
+
{ role: 'assistant', text: CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE },
|
|
248
|
+
]);
|
|
249
|
+
service.close();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('rejects malicious clear_chat_history without explicit wipe intent and keeps transcript', async () => {
|
|
253
|
+
const directory = root();
|
|
254
|
+
const wipeAttempts = [];
|
|
255
|
+
const service = setup(directory, {}, {
|
|
256
|
+
runContactTurn: async ({ userText, tools }) => {
|
|
257
|
+
if (userText === '清除记忆' || userText === '不要清除聊天记录') {
|
|
258
|
+
const tool = tools.find((item) => item.name === 'clear_chat_history');
|
|
259
|
+
const result = await tool.execute(`malicious_${userText}`, {});
|
|
260
|
+
wipeAttempts.push({
|
|
261
|
+
userText,
|
|
262
|
+
error: result.details?.error || null,
|
|
263
|
+
historyCleared: result.details?.historyCleared === true,
|
|
264
|
+
});
|
|
265
|
+
return {
|
|
266
|
+
text: `blocked:${userText}`,
|
|
267
|
+
bubbles: [`blocked:${userText}`],
|
|
268
|
+
messages: [{
|
|
269
|
+
role: 'toolResult',
|
|
270
|
+
toolName: 'clear_chat_history',
|
|
271
|
+
details: result.details,
|
|
272
|
+
}],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return { text: `reply:${userText}`, bubbles: [`reply:${userText}`] };
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
const assistant = service.createAssistant(assistantInput);
|
|
279
|
+
await settleSend(service, assistant.id, { messageID: 'seed_keep', parts: [{ type: 'text', text: 'keep-me' }] });
|
|
280
|
+
await settleSend(service, assistant.id, { messageID: 'bad_memory', parts: [{ type: 'text', text: '清除记忆' }] });
|
|
281
|
+
await settleSend(service, assistant.id, { messageID: 'bad_negation', parts: [{ type: 'text', text: '不要清除聊天记录' }] });
|
|
282
|
+
expect(wipeAttempts).toEqual([
|
|
283
|
+
{ userText: '清除记忆', error: 'validation_error', historyCleared: false },
|
|
284
|
+
{ userText: '不要清除聊天记录', error: 'validation_error', historyCleared: false },
|
|
285
|
+
]);
|
|
286
|
+
const texts = service.contactMessages(assistant.id, { limit: 50 }).messages.map((message) => message.text);
|
|
287
|
+
expect(texts).toContain('keep-me');
|
|
288
|
+
expect(texts).toContain('清除记忆');
|
|
289
|
+
expect(texts).toContain('不要清除聊天记录');
|
|
290
|
+
expect(texts).not.toContain(CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE);
|
|
291
|
+
service.close();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('loads LLM history at turn start so queued turns respect a prior clear-memory', async () => {
|
|
295
|
+
const directory = root();
|
|
296
|
+
const histories = [];
|
|
297
|
+
let releaseClear;
|
|
298
|
+
const clearGate = new Promise((resolve) => { releaseClear = resolve; });
|
|
299
|
+
const service = setup(directory, {}, {
|
|
300
|
+
runContactTurn: async ({ history, userText, tools }) => {
|
|
301
|
+
histories.push({ userText, history: history.map((item) => item.content) });
|
|
302
|
+
if (userText === '开新对话') {
|
|
303
|
+
await tools.find((item) => item.name === 'new_conversation').execute('queued_clear', {});
|
|
304
|
+
releaseClear();
|
|
305
|
+
return { text: 'cleared', bubbles: ['cleared'], reset: true };
|
|
306
|
+
}
|
|
307
|
+
if (userText === 'queued after clear') {
|
|
308
|
+
await clearGate;
|
|
309
|
+
}
|
|
310
|
+
return { text: `reply:${userText}`, bubbles: [`reply:${userText}`] };
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
const assistant = service.createAssistant(assistantInput);
|
|
314
|
+
await settleSend(service, assistant.id, { messageID: 'seed_1', parts: [{ type: 'text', text: 'seed secret' }] });
|
|
315
|
+
const clearSend = service.send(assistant.id, { messageID: 'clear_q', parts: [{ type: 'text', text: '开新对话' }] });
|
|
316
|
+
const queuedSend = service.send(assistant.id, { messageID: 'queued_q', parts: [{ type: 'text', text: 'queued after clear' }] });
|
|
317
|
+
await Promise.all([clearSend, queuedSend]);
|
|
318
|
+
await service.whenContactTurnSettled('clear_q');
|
|
319
|
+
await service.whenContactTurnSettled('queued_q');
|
|
320
|
+
const queuedHistory = histories.find((entry) => entry.userText === 'queued after clear')?.history || [];
|
|
321
|
+
expect(queuedHistory.some((content) => content.includes('seed secret'))).toBe(false);
|
|
322
|
+
expect(service.contactMessages(assistant.id, { limit: 50 }).messages.some((message) => message.text === 'seed secret')).toBe(true);
|
|
323
|
+
service.close();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('clear-memory watermark does not permanently exclude later-admitted queued users', async () => {
|
|
327
|
+
const directory = root();
|
|
328
|
+
const histories = [];
|
|
329
|
+
let releaseClear;
|
|
330
|
+
const clearGate = new Promise((resolve) => { releaseClear = resolve; });
|
|
331
|
+
const service = setup(directory, {}, {
|
|
332
|
+
runContactTurn: async ({ history, userText, tools }) => {
|
|
333
|
+
histories.push({ userText, history: history.map((item) => item.content) });
|
|
334
|
+
if (userText === '开新对话') {
|
|
335
|
+
await tools.find((item) => item.name === 'new_conversation').execute('waterline_clear', {});
|
|
336
|
+
releaseClear();
|
|
337
|
+
return { text: NEW_CONVERSATION_CONFIRM_BUBBLE, bubbles: [NEW_CONVERSATION_CONFIRM_BUBBLE], reset: true };
|
|
338
|
+
}
|
|
339
|
+
if (userText === 'queued keep me') {
|
|
340
|
+
await clearGate;
|
|
341
|
+
}
|
|
342
|
+
return { text: `reply:${userText}`, bubbles: [`reply:${userText}`] };
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
const assistant = service.createAssistant(assistantInput);
|
|
346
|
+
await settleSend(service, assistant.id, { messageID: 'seed_w', parts: [{ type: 'text', text: 'seed secret' }] });
|
|
347
|
+
const clearSend = service.send(assistant.id, { messageID: 'clear_w', parts: [{ type: 'text', text: '开新对话' }] });
|
|
348
|
+
const queuedSend = service.send(assistant.id, { messageID: 'queued_w', parts: [{ type: 'text', text: 'queued keep me' }] });
|
|
349
|
+
await Promise.all([clearSend, queuedSend]);
|
|
350
|
+
await service.whenContactTurnSettled('clear_w');
|
|
351
|
+
await service.whenContactTurnSettled('queued_w');
|
|
352
|
+
await settleSend(service, assistant.id, { messageID: 'follow_w', parts: [{ type: 'text', text: 'follow after queue' }] });
|
|
353
|
+
const followHistory = histories.find((entry) => entry.userText === 'follow after queue')?.history || [];
|
|
354
|
+
expect(followHistory.some((content) => content.includes('seed secret'))).toBe(false);
|
|
355
|
+
expect(followHistory.some((content) => content.includes('queued keep me'))).toBe(true);
|
|
356
|
+
expect(followHistory.some((content) => content.includes('reply:queued keep me'))).toBe(true);
|
|
357
|
+
service.close();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('does not inject later-admitted queued user messages into the current turn history', async () => {
|
|
361
|
+
const directory = root();
|
|
362
|
+
const histories = [];
|
|
363
|
+
let releaseFirst;
|
|
364
|
+
const firstGate = new Promise((resolve) => { releaseFirst = resolve; });
|
|
365
|
+
const service = setup(directory, {}, {
|
|
366
|
+
runContactTurn: async ({ history, userText }) => {
|
|
367
|
+
histories.push({ userText, history: history.map((item) => item.content) });
|
|
368
|
+
if (userText === 'first turn') {
|
|
369
|
+
await firstGate;
|
|
370
|
+
}
|
|
371
|
+
return { text: `reply:${userText}`, bubbles: [`reply:${userText}`] };
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
const assistant = service.createAssistant(assistantInput);
|
|
375
|
+
await settleSend(service, assistant.id, { messageID: 'seed_hist', parts: [{ type: 'text', text: 'seed prior' }] });
|
|
376
|
+
const firstSend = service.send(assistant.id, { messageID: 'first_q', parts: [{ type: 'text', text: 'first turn' }] });
|
|
377
|
+
const queuedSend = service.send(assistant.id, { messageID: 'queued_later', parts: [{ type: 'text', text: 'queued later secret' }] });
|
|
378
|
+
await Promise.all([firstSend, queuedSend]);
|
|
379
|
+
// Let the first lane turn observe history while the queued user row is already admitted.
|
|
380
|
+
releaseFirst();
|
|
381
|
+
await service.whenContactTurnSettled('first_q');
|
|
382
|
+
await service.whenContactTurnSettled('queued_later');
|
|
383
|
+
const firstHistory = histories.find((entry) => entry.userText === 'first turn')?.history || [];
|
|
384
|
+
expect(firstHistory.some((content) => content.includes('seed prior'))).toBe(true);
|
|
385
|
+
expect(firstHistory.some((content) => content.includes('queued later secret'))).toBe(false);
|
|
386
|
+
const queuedHistory = histories.find((entry) => entry.userText === 'queued later secret')?.history || [];
|
|
387
|
+
expect(queuedHistory.some((content) => content.includes('first turn'))).toBe(true);
|
|
388
|
+
expect(queuedHistory.some((content) => content.includes('reply:first turn'))).toBe(true);
|
|
389
|
+
service.close();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('persists clear-memory across restart and isolates assistants', async () => {
|
|
393
|
+
const directory = root();
|
|
394
|
+
const service = setup(directory, {}, {
|
|
395
|
+
runContactTurn: async ({ userText, tools }) => {
|
|
396
|
+
if (userText === '开新对话') {
|
|
397
|
+
await tools.find((item) => item.name === 'new_conversation').execute('persist_clear', {});
|
|
398
|
+
return { text: 'cleared', bubbles: ['cleared'], reset: true };
|
|
399
|
+
}
|
|
400
|
+
return { text: `reply:${userText}`, bubbles: [`reply:${userText}`] };
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
const first = service.createAssistant(assistantInput);
|
|
404
|
+
const second = service.createAssistant({ ...assistantInput, name: 'B' });
|
|
405
|
+
await settleSend(service, first.id, { messageID: 'a1', parts: [{ type: 'text', text: 'alpha secret' }] });
|
|
406
|
+
await settleSend(service, second.id, { messageID: 'b1', parts: [{ type: 'text', text: 'beta secret' }] });
|
|
407
|
+
await settleSend(service, first.id, { messageID: 'a_clear', parts: [{ type: 'text', text: '开新对话' }] });
|
|
408
|
+
expect(service.clearContactMemory(second.id)).toMatchObject({
|
|
409
|
+
assistantID: second.id,
|
|
410
|
+
memoryCleared: true,
|
|
411
|
+
reset: true,
|
|
412
|
+
});
|
|
413
|
+
service.close();
|
|
414
|
+
const histories = [];
|
|
415
|
+
const restarted = setup(directory, {}, {
|
|
416
|
+
runContactTurn: async ({ history, userText }) => {
|
|
417
|
+
histories.push({ assistantHint: userText, history: history.map((item) => item.content) });
|
|
418
|
+
return { text: `reply:${userText}`, bubbles: [`reply:${userText}`] };
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
const assistants = restarted.snapshot().assistants;
|
|
422
|
+
const a = assistants.find((item) => item.name === 'A');
|
|
423
|
+
const b = assistants.find((item) => item.name === 'B');
|
|
424
|
+
await settleSend(restarted, a.id, { messageID: 'a_next', parts: [{ type: 'text', text: 'after restart a' }] });
|
|
425
|
+
await settleSend(restarted, b.id, { messageID: 'b_next', parts: [{ type: 'text', text: 'after restart b' }] });
|
|
426
|
+
const aHistory = histories.find((entry) => entry.assistantHint === 'after restart a')?.history || [];
|
|
427
|
+
const bHistory = histories.find((entry) => entry.assistantHint === 'after restart b')?.history || [];
|
|
428
|
+
expect(aHistory.some((content) => content.includes('alpha secret'))).toBe(false);
|
|
429
|
+
expect(bHistory.some((content) => content.includes('beta secret'))).toBe(false);
|
|
430
|
+
expect(restarted.contactMessages(a.id, { limit: 50 }).messages.some((message) => message.text === 'alpha secret')).toBe(true);
|
|
431
|
+
expect(restarted.contactMessages(b.id, { limit: 50 }).messages.some((message) => message.text === 'beta secret')).toBe(true);
|
|
432
|
+
restarted.close();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('exposes clearContactMemory without wiping GET transcript rows', async () => {
|
|
436
|
+
const directory = root();
|
|
437
|
+
let lastHistory = null;
|
|
438
|
+
const service = setup(directory, {}, {
|
|
439
|
+
runContactTurn: async ({ history, userText }) => {
|
|
440
|
+
lastHistory = history;
|
|
441
|
+
return { text: `reply:${userText}`, bubbles: [`reply:${userText}`] };
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
const assistant = service.createAssistant(assistantInput);
|
|
445
|
+
await settleSend(service, assistant.id, { messageID: 'api_1', parts: [{ type: 'text', text: 'visible forever' }] });
|
|
446
|
+
expect(service.clearContactMemory(assistant.id)).toMatchObject({
|
|
447
|
+
assistantID: assistant.id,
|
|
448
|
+
reset: true,
|
|
449
|
+
memoryCleared: true,
|
|
450
|
+
});
|
|
451
|
+
expect(service.contactMessages(assistant.id, { limit: 50 }).messages.some((message) => message.text === 'visible forever')).toBe(true);
|
|
452
|
+
await settleSend(service, assistant.id, { messageID: 'api_2', parts: [{ type: 'text', text: 'next' }] });
|
|
453
|
+
expect(lastHistory.some((item) => item.content.includes('visible forever'))).toBe(false);
|
|
195
454
|
service.close();
|
|
196
455
|
});
|
|
197
456
|
|
|
@@ -83,6 +83,12 @@ an empty success. The 502 body includes `{ error, message }`.
|
|
|
83
83
|
|
|
84
84
|
## Ownership
|
|
85
85
|
|
|
86
|
+
The hidden generator agent explicitly describes application-owned tool calls:
|
|
87
|
+
it emits `openchamber-tool` JSON for the contact harness to execute and receives
|
|
88
|
+
the results on subsequent requests. Native OpenCode tool permissions remain
|
|
89
|
+
denied. Capability statements and execution errors must follow the supplied
|
|
90
|
+
application tool catalog and actual results.
|
|
91
|
+
|
|
86
92
|
The Assistant contact harness owns system prompt, OpenChamber transcript,
|
|
87
93
|
bubble splitting, and OpenChamber API tools (`assign_session`). Those tools
|
|
88
94
|
deliver through contact **cards**, not this completions payload. The gateway
|
|
@@ -15,7 +15,10 @@ permissions:
|
|
|
15
15
|
effect: deny
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
You
|
|
18
|
+
You generate responses for an application-owned assistant. Follow the supplied system instructions and response format.
|
|
19
|
+
The application executes its registered tools, including openchamber-tool JSON fences in your response, and supplies their results on the next request.
|
|
20
|
+
Emit the requested application tool call when an action requires one. Your native tool permissions describe this generator process; the application's supplied tool catalog describes the assistant's capabilities.
|
|
21
|
+
Report execution and failures from supplied tool results only.
|
|
19
22
|
`;
|
|
20
23
|
|
|
21
24
|
const isMissing = (result) =>
|
|
@@ -72,7 +72,12 @@ describe('generateOpenCodeText', () => {
|
|
|
72
72
|
modelID: 'gpt-5-nano',
|
|
73
73
|
messages: [{ role: 'user', content: 'hi' }],
|
|
74
74
|
clientFactory: createOpencodeClient,
|
|
75
|
-
ensureTempDirectory: async () =>
|
|
75
|
+
ensureTempDirectory: async ({ agentMarkdown }) => {
|
|
76
|
+
expect(agentMarkdown).toContain('openchamber-tool JSON fences')
|
|
77
|
+
expect(agentMarkdown).toContain('effect: deny')
|
|
78
|
+
expect(agentMarkdown).toContain('Report execution and failures from supplied tool results only.')
|
|
79
|
+
return '/tmp/openchamber-llm'
|
|
80
|
+
},
|
|
76
81
|
detect: async () => ({ available: false, mode: 'throwaway-session' }),
|
|
77
82
|
})
|
|
78
83
|
|
|
@@ -352,6 +352,7 @@ When adding or changing Host HTTP APIs that mobile/desktop clients reach over Pr
|
|
|
352
352
|
- `registerOpenChamberRoutes(app, dependencies)`: registers OpenChamber endpoints:
|
|
353
353
|
- `GET /api/openchamber/update-check`
|
|
354
354
|
- `POST /api/openchamber/update-install`
|
|
355
|
+
- Standalone web/CLI package installer. Electron-hosted servers return `403` before package-manager work; their owning desktop application manages updates. The desktop instance menu offers this action only for SSH-managed hosts, excluding Relay/imported connections.
|
|
355
356
|
- `GET /api/zen/models`
|
|
356
357
|
- session-index routes (see `session-index/DOCUMENTATION.md`)
|
|
357
358
|
- transcript-cache routes under `/api/openchamber/transcript-cache` (see `transcript-cache/DOCUMENTATION.md`)
|
|
@@ -60,6 +60,9 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
app.post('/api/openchamber/update-install', async (_req, res) => {
|
|
63
|
+
if (process.versions?.electron) {
|
|
64
|
+
return res.status(403).json({ error: 'Update this instance from its desktop application.' });
|
|
65
|
+
}
|
|
63
66
|
try {
|
|
64
67
|
const { spawn: spawnChild } = await import('child_process');
|
|
65
68
|
const {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import request from 'supertest';
|
|
4
|
+
import { registerOpenChamberRoutes } from './openchamber-routes.js';
|
|
5
|
+
import { checkForUpdates } from '../package-manager.js';
|
|
6
|
+
|
|
7
|
+
vi.mock('../session-index/routes.js', () => ({ registerSessionIndexRoutes: vi.fn() }));
|
|
8
|
+
vi.mock('../transcript-cache/routes.js', () => ({ registerTranscriptCacheRoutes: vi.fn() }));
|
|
9
|
+
vi.mock('../config-sync/routes.js', () => ({ registerConfigSyncRoutes: vi.fn() }));
|
|
10
|
+
vi.mock('../package-manager.js', () => ({
|
|
11
|
+
checkForUpdates: vi.fn(),
|
|
12
|
+
getUpdateCommand: vi.fn(),
|
|
13
|
+
detectPackageManagerDetails: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe('remote update installation runtime boundary', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
vi.mocked(checkForUpdates).mockResolvedValue({ available: false });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const createApp = (versions) => {
|
|
23
|
+
const app = express();
|
|
24
|
+
registerOpenChamberRoutes(app, { express, process: { versions, env: {} } });
|
|
25
|
+
return app;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
it('rejects the web installer in an Electron-hosted server before any update work', async () => {
|
|
29
|
+
const response = await request(createApp({ electron: '40.0.0' }))
|
|
30
|
+
.post('/api/openchamber/update-install');
|
|
31
|
+
expect(response.status).toBe(403);
|
|
32
|
+
expect(response.body.error).toMatch(/desktop application/i);
|
|
33
|
+
expect(checkForUpdates).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('preserves the standalone web server update path', async () => {
|
|
37
|
+
const response = await request(createApp({ node: '24.0.0' }))
|
|
38
|
+
.post('/api/openchamber/update-install');
|
|
39
|
+
expect(response.status).toBe(400);
|
|
40
|
+
expect(response.body.error).toBe('No update available');
|
|
41
|
+
expect(checkForUpdates).toHaveBeenCalledOnce();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -61,6 +61,7 @@ See also `packages/web/server/lib/opencode/DOCUMENTATION.md` (“Catalog / OpenC
|
|
|
61
61
|
- The tunnel is **transport only**. The OpenChamber server still authenticates every tunneled request exactly as it authenticates a direct remote client. The relay path grants reachability, not authorization.
|
|
62
62
|
- A paired **desktop** client is a device-fleet operator: through the tunnel it can create pairing links, list every paired device, and revoke other devices (parity with the SSH attach flow, where the connecting desktop authenticates as the operator). Mobile (and any other) paired clients stay scoped to their own record. Re-pointing the Host relay endpoint set from a remote bearer remains forbidden — a paired desktop may name any endpoint the host already has, but adding/removing endpoints (including `relayUrl` in pairing requests for a new domain) is an owner-UI / local-desktop-shell action.
|
|
63
63
|
- Clients carry their normal credential. HTTP and SSE requests authenticate with the client's bearer token (a header). **WebSocket upgrades cannot send headers**, so they authenticate with a short-lived URL-scoped token minted beforehand and passed as a query parameter. This asymmetry is important when adding new WebSocket features (see the skill).
|
|
64
|
+
- Desktop switch preflight calls `/auth/session` with the saved bearer over a pinned tunnel before adopting it. `/health` and cached reachability badges provide transport information only. Failed or timed-out probes close their temporary tunnel and preserve the active runtime; successful switch probes transfer ownership of the authenticated tunnel to the runtime.
|
|
64
65
|
- The host authenticates itself to the relay with a signed handshake using its long-lived signing key.
|
|
65
66
|
- Enabling the relay is explicit opt-in and disabled by default; disabling it severs all relay reachability immediately.
|
|
66
67
|
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/MarkstreamRendererImpl-BAEStQuf.js","assets/rolldown-runtime-C0FnF6B9.js","assets/vendor-base-ui-CnHfUmDN.js","assets/vendor-react-DLPElpoQ.js","assets/vendor-markstream-react-DX55XLGO.js","assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js","assets/vendor-markstream-core-Cm34iPLL.js","assets/vendor-floating-ui-dom-BHhLtF5l.js","assets/vendor-floating-ui-core-Co1I1lFV.js","assets/vendor-reactuses-core-DDoDDyM9.js","assets/vendor-lodash-es-DfLbG5Af.js","assets/vendor-js-cookie-hSOiFTtX.js","assets/useMobileNavigationStore-DYJ6I8FV.js","assets/useUIStore-CuaA8-Hp.js","assets/vendor-zustand-B4dlGEM2.js","assets/persistence-DdUac14I.js","assets/vendor-pathe-vBmvQE8f.js","assets/vendor-tanstack-query-core-BWz0ose6.js","assets/platform-B4KKLnZ5.js","assets/vendor-opencode-sdk-C07HQypa.js","assets/vendor-tanstack-react-query-D7pys_c9.js","assets/vendor-sonner-DVzioF3d.js","assets/clipboard-DJ4nQEuY.js","assets/store-BDFdnk6l.js","assets/useAssistantUIStore-me6s63b2.js","assets/vendor-fuse.js-DB5wDJbI.js","assets/utils-BiK3tBGM.js","assets/vendor-class-variance-authority-BLUaYARZ.js","assets/vendor-tailwind-merge-BicNWLTg.js","assets/useThemeSystem-DMzWVJaW.js","assets/useRuntimeAPIs-DSLXJybW.js","assets/url-Pk6tQ1EZ.js","assets/useEffectiveDirectory-D36udiNq.js","assets/markdownHeightCache-CYDlisxr.js","assets/MarkdownRendererImpl-uH_cfmYV.js","assets/vendor-katex--B5dHUyk.js","assets/markdownTheme-DyJ4Gfcj.js","assets/vendor-pierre-diffs-DX7XTPG3.js","assets/vendor-hast-util-to-html-B0p1k5Xa.js","assets/vendor-character-entities-legacy-Wut6vMuj.js","assets/vendor-character-entities-html4-DlN_yAFd.js","assets/vendor-ccount-CyGsA0u7.js","assets/vendor-comma-separated-tokens-B9ouxNNS.js","assets/vendor-oniguruma-to-es-CrHzjjSU.js","assets/vendor-oniguruma-parser-D2DIvhSP.js","assets/vendor-diff-Dq7CZNim.js","assets/vendor-lru_map-DpDied8H.js","assets/markdown-worker-CTsrtpPh.js","assets/FadeInOnReveal-CTlNE4Hg.js","assets/vendor-morphdom-BbKiNDAu.js","assets/vendor-beautiful-mermaid-DykqIK5D.js","assets/vendor-dompurify-CeJNOzik.js","assets/vendor-marked-WVekxjAN.js","assets/vendor-remend-C3cXc-tf.js","assets/MarkstreamRendererImpl-DKhVy3nF.css"])))=>i.map(i=>d[i]);
|
|
2
|
-
import{o as e}from"./rolldown-runtime-C0FnF6B9.js";import{a as t}from"./vendor-zustand-B4dlGEM2.js";import{nt as n,rt as r}from"./vendor-base-ui-CnHfUmDN.js";import{o as i}from"./vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js";import{n as a}from"./vendor-react-DLPElpoQ.js";import{t as o}from"./utils-BiK3tBGM.js";import{a as s,n as c,t as l}from"./markdownHeightCache-CYDlisxr.js";var u=a(),d=e(r(),1),f=`openchamber:chunk-import-reload`,p=250,m=3e4,h=[/Importing a module script failed/i,/Failed to fetch dynamically imported module/i,/error loading dynamically imported module/i,/Loading chunk \S+ failed/i,/ChunkLoadError/i];function g(e){if(e instanceof Error)return`${e.name}\n${e.message}\n${e.stack??``}`;if(typeof e==`string`)return e;try{return JSON.stringify(e)}catch{return String(e)}}function _(e){let t=g(e);return h.some(e=>e.test(t))}function v(e){return`1.19.10-beta.1:${g(e).slice(0,500)}`}function y(e){if(typeof window>`u`)return;let t=Date.now(),n=v(e),r=null;try{let e=window.sessionStorage.getItem(f);if(e)try{r=JSON.parse(e)}catch{r=null}}catch{return}let i=typeof r?.timestamp==`number`?r.timestamp:0;if(!(r?.signature===n&&t-i<m)){try{window.sessionStorage.setItem(f,JSON.stringify({signature:n,timestamp:t}))}catch{return}console.error(`[refresh-debug] chunk-import-reload`,{error:g(e).slice(0,1e3)}),window.setTimeout(()=>{window.location.reload()},0)}}async function b(e,t={}){let n=t.retries??1,r;for(let t=0;t<=n;t+=1)try{return await e()}catch(e){if(r=e,!_(e)||t>=n)break;await new Promise(e=>setTimeout(e,p*(t+1)))}throw _(r)&&y(r),r}var x=e=>(0,d.lazy)(()=>b(e)),S=(e,t)=>{try{let n=t??(typeof localStorage>`u`?null:localStorage);return n?n.getItem(e)===`1`:!1}catch{return!1}},C=(e,t)=>{try{let n=t??(typeof localStorage>`u`?null:localStorage);return!n||n.getItem(e)!==`0`}catch{return!0}},w=(e,t)=>{try{typeof localStorage<`u`&&localStorage.setItem(e,t?`1`:`0`)}catch{}},T=`oc:legend-timeline`,E=e=>S(T,e),D=`oc:markstream-react`,O=e=>C(D,e),k=t(e=>({legendTimelineEnabled:E(),setLegendTimelineEnabled:t=>{w(T,t),e({legendTimelineEnabled:t})},markstreamReactEnabled:O(),setMarkstreamReactEnabled:t=>{w(D,t),e({markstreamReactEnabled:t})}})),A=null,j=()=>(A??=i(()=>import(`./MarkstreamRendererImpl-BAEStQuf.js`),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54])).catch(e=>{throw A=null,e}),A),M=()=>{j().catch(()=>void 0)},N=null,P=()=>(N??=i(()=>import(`./MarkdownRendererImpl-uH_cfmYV.js`).then(e=>e.n),__vite__mapDeps([34,1,13,14,2,15,16,3,17,18,19,5,35,9,10,11,22,23,12,20,21,24,25,26,27,28,29,36,37,38,39,40,41,42,43,44,45,46,30,31,47,33,32,48,49,50,51,52,53])).catch(e=>{throw N=null,e}),N),F=()=>{P().catch(()=>void 0),O()&&M()},I=class extends d.Component{state={hasError:!1};static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(e){console.warn(`[markstream-react] falling back to the current Markdown renderer`,e)}render(){return this.state.hasError?this.props.fallback:this.props.children}},L=d.createContext(!0),R=()=>d.useContext(L),z=n(),B=x(()=>P().then(e=>({default:e.MarkdownRenderer}))),V=x(()=>j().then(e=>({default:e.MarkstreamRenderer}))),H=x(()=>P().then(e=>({default:e.SimpleMarkdownRenderer}))),U=e=>e===`tool`?`markdown-content markdown-tool`:e===`reasoning`?`markdown-content markdown-reasoning`:`markdown-content leading-relaxed`,W=e=>{let t=(0,u.c)(15),n=k(q),r=typeof e.content==`string`?e.content:``,i=typeof e.variant==`string`?e.variant:`assistant`,a;t[0]!==r||t[1]!==n||t[2]!==e.reserveMeasuredHeight||t[3]!==i?(a=e.reserveMeasuredHeight?c(l(r,n?`markstream:${i}`:i)):void 0,t[0]=r,t[1]=n,t[2]=e.reserveMeasuredHeight,t[3]=i,t[4]=a):a=t[4];let d=a,f;t[5]!==e.className||t[6]!==e.variant?(f=o(`relative break-words w-full min-w-0`,U(e.variant),typeof e.className==`string`?e.className:void 0),t[5]=e.className,t[6]=e.variant,t[7]=f):f=t[7];let p;t[8]!==r||t[9]!==e.animated||t[10]!==d?(p=(0,z.jsx)(s,{animated:e.animated,content:r,reservedHeight:d}),t[8]=r,t[9]=e.animated,t[10]=d,t[11]=p):p=t[11];let m;return t[12]!==f||t[13]!==p?(m=(0,z.jsx)(`div`,{className:f,"aria-busy":`true`,"data-markdown-hydration":`deferred`,"data-markdown-ready":`false`,children:p}),t[12]=f,t[13]=p,t[14]=m):m=t[14],m},G=e=>{let t=(0,u.c)(9),n=R(),r=k(J);if(!n&&e.isStreaming!==!0){let n;return t[0]===e?n=t[1]:(n=(0,z.jsx)(W,{...e,animated:!1,reserveMeasuredHeight:!0}),t[0]=e,t[1]=n),n}let i;t[2]===e?i=t[3]:(i=(0,z.jsx)(d.Suspense,{fallback:(0,z.jsx)(W,{...e,reserveMeasuredHeight:!0}),children:(0,z.jsx)(B,{...e})}),t[2]=e,t[3]=i);let a=i;if(!r)return a;let o;t[4]===e?o=t[5]:(o=(0,z.jsx)(d.Suspense,{fallback:(0,z.jsx)(W,{...e,reserveMeasuredHeight:!0}),children:(0,z.jsx)(V,{...e})}),t[4]=e,t[5]=o);let s;return t[6]!==a||t[7]!==o?(s=(0,z.jsx)(I,{fallback:a,children:o}),t[6]=a,t[7]=o,t[8]=s):s=t[8],s},K=e=>{let t=(0,u.c)(4);if(!R()){let n;return t[0]===e?n=t[1]:(n=(0,z.jsx)(W,{...e,animated:!1}),t[0]=e,t[1]=n),n}let n;return t[2]===e?n=t[3]:(n=(0,z.jsx)(d.Suspense,{fallback:(0,z.jsx)(W,{...e}),children:(0,z.jsx)(H,{...e})}),t[2]=e,t[3]=n),n};function q(e){return e.markstreamReactEnabled}function J(e){return e.markstreamReactEnabled}export{k as a,F as i,K as n,x as o,L as r,G as t};
|