@openchambery/web 1.18.2-beta.2 → 1.18.2-beta.21
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-B2kZjgaf.js → AgentAvatar-B4fj9PIe.js} +1 -1
- package/dist/assets/{AssistantView-DvoyOGkx.js → AssistantView-C00aiEHK.js} +1 -1
- package/dist/assets/{ChatContainer-D6yfuigV.js → ChatContainer-BDFeDKu-.js} +39 -39
- package/dist/assets/{DiagramView-MGGo9iME.js → DiagramView-CUfSFI3e.js} +1 -1
- package/dist/assets/{FadeInOnReveal-aeWlaqVB.js → FadeInOnReveal-CKKU6fKI.js} +1 -1
- package/dist/assets/{Icon-AXqa-fYB.js → Icon-DiIuWOuC.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-jVOkZkyX.js → MarkdownRendererImpl-Cen6E9Lj.js} +2 -2
- package/dist/assets/{MobileDetailNavigation-Q2IzHqGA.js → MobileDetailNavigation-DILi0tN7.js} +1 -1
- package/dist/assets/{MobileShareBridge-apXM34Lb.js → MobileShareBridge-PZMRiieI.js} +1 -1
- package/dist/assets/MobileSurface-BLYcR0tO.js +2 -0
- package/dist/assets/{MultiRunWindow-B-l_O-bd.js → MultiRunWindow-D_2mWZyv.js} +1 -1
- package/dist/assets/{OnboardingScreen-CUsgwt4y.js → OnboardingScreen-CYeRZznl.js} +1 -1
- package/dist/assets/{OverlayScrollbar-D6NySAxi.js → OverlayScrollbar-BgMLtMIq.js} +1 -1
- package/dist/assets/SettingsView-CbDBfQp1.js +25 -0
- package/dist/assets/{SettingsWindow-DfVgTQ4I.js → SettingsWindow-Br7XLcSd.js} +1 -1
- package/dist/assets/{TerminalView-DNyjfkvT.js → TerminalView-CM5jRKlj.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DLG1iThm.js → ToolOutputDialog-D3U6aTZ4.js} +1 -1
- package/dist/assets/appThemeRegistry-CDxBJNNB.js +2 -0
- package/dist/assets/assistant-guide-hero-BeUHQcxE.jpg +0 -0
- package/dist/assets/{button-CDvMVaiO.js → button-DaAoVF4X.js} +1 -1
- package/dist/assets/{clipboard-BW7nk_Cx.js → clipboard-BnElHSnO.js} +1 -1
- package/dist/assets/{collapsible-C2aamDvD.js → collapsible-BmuQu0dq.js} +1 -1
- package/dist/assets/{context-menu-BaxmodD8.js → context-menu-DBOMervd.js} +1 -1
- package/dist/assets/{desktopHosts-CBoBpJ1Z.js → desktopHosts-ZK_vkdzL.js} +1 -1
- package/dist/assets/{desktopRelayRestore-l2R9muux.js → desktopRelayRestore-CewM6-g5.js} +1 -1
- package/dist/assets/{diagram-CEon0BQs.js → diagram-DAE4CZy2.js} +1 -1
- package/dist/assets/{dialog-DjYfj1Sn.js → dialog-COrLBm9O.js} +1 -1
- package/dist/assets/{es-5Lhudh0n.js → es-DIVYdoxi.js} +1 -1
- package/dist/assets/{fileQueries-CE_DaZWQ.js → fileQueries-CmWYnRVz.js} +1 -1
- package/dist/assets/{fr-Q_UhFmKT.js → fr-CNTlVypq.js} +2 -2
- package/dist/assets/{gitApi-BYxg_qGJ.js → gitApi-BQT3QXD2.js} +1 -1
- package/dist/assets/gitApiHttp-BxGAFLTf.js +2 -0
- package/dist/assets/{input-k9DGGyZ6.js → input-XiL2-2gG.js} +1 -1
- package/dist/assets/insertionBoundaries-9a0RuIDc.js +1 -0
- package/dist/assets/{ja-CzMPz7bT.js → ja-C_eW5ZM8.js} +1 -1
- package/dist/assets/{ko-B-EslUsR.js → ko-CE3cOv7v.js} +1 -1
- package/dist/assets/{main-LDphyc-v.js → main-BGfX81Y9.js} +20 -20
- package/dist/assets/{main-CO6z5yRB.js → main-CEqSDnE2.js} +2 -2
- package/dist/assets/{markdown-worker-DkeWiER6.js → markdown-worker-BDyY0vt6.js} +1 -1
- package/dist/assets/{markdownTheme-DNT93i5L.js → markdownTheme-D-aLQZaM.js} +1 -1
- package/dist/assets/{miniChat-BZQwTZpx.js → miniChat-CKucSAhg.js} +2 -2
- package/dist/assets/mobile-C4bTWKry.js +2 -0
- package/dist/assets/{multirun-ZlwlkIy5.js → multirun-DeNtSkgb.js} +1 -1
- package/dist/assets/{persistence-DPNMIbF5.js → persistence-D1lIuyqO.js} +1 -1
- package/dist/assets/{pl-Dnea6Ppc.js → pl-BZKZOE99.js} +1 -1
- package/dist/assets/projectMeta-BunN9pH5.js +3 -0
- package/dist/assets/{pt-BR-Ca6QTem7.js → pt-BR-DHDgp8S3.js} +1 -1
- package/dist/assets/{radio-CJuhbQ5a.js → radio-Bziafaje.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-D8-FDgWE.js +2 -0
- package/dist/assets/renderMobileApp-CdU0aJ-e.js +23 -0
- package/dist/assets/{runtimeConfig-BtWi74Pm.js → runtimeConfig-BXCt7ZsQ.js} +1 -1
- package/dist/assets/runtimeEndpointReset-CK1GhIsm.js +145 -0
- package/dist/assets/{sortable-tabs-strip-D8JHUI0z.js → sortable-tabs-strip-pWlBorwQ.js} +1 -1
- package/dist/assets/src-8kQFmtjw.css +2 -0
- package/dist/assets/{store-wa1ersRf.js → store-CUWqzRt7.js} +2 -2
- package/dist/assets/{terminalApi-tB3qLcfa.js → terminalApi-DpiAJlil.js} +1 -1
- package/dist/assets/{tooltip-DMNR5rN0.js → tooltip-BLugzWq-.js} +1 -1
- package/dist/assets/{uk-xGc9Pzge.js → uk-CfQK6cz1.js} +1 -1
- package/dist/assets/{url-7MqoLs9X.js → url-DMs_PgIC.js} +1 -1
- package/dist/assets/useAppFontEffects-IEUQDvE8.js +7 -0
- package/dist/assets/useDesktopWindowControlsLayout-MlqJX89k.js +1 -0
- package/dist/assets/{useEffectiveDirectory-aUC3zzl_.js → useEffectiveDirectory-GbhkdO6m.js} +1 -1
- package/dist/assets/{useFontPreferences-CIurCeNv.js → useFontPreferences-92Qg-98H.js} +1 -1
- package/dist/assets/useMobileNavigationStore-DQHF_ETu.js +476 -0
- package/dist/assets/useSessionAutoCleanup-BuU_S_Sh.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-CiYQjBe6.js → useWorkerHighlightedLines-Ca6OgNHF.js} +1 -1
- package/dist/assets/{utils-Co19EJEL.js → utils-BxJEIWv5.js} +1 -1
- package/dist/assets/{zh-CN-sL4ktGB3.js → zh-CN-DvY2fKWp.js} +1 -1
- package/dist/assets/{zh-TW-Cfxps4gq.js → zh-TW-eWe0lm3-.js} +1 -1
- package/dist/index.html +8 -8
- package/dist/mini-chat.html +8 -8
- package/dist/mobile.html +8 -8
- package/package.json +1 -1
- package/server/index.d.ts +6 -0
- package/server/index.js +13 -0
- package/server/lib/config-sync/DOCUMENTATION.md +70 -0
- package/server/lib/config-sync/config-sync.test.js +197 -0
- package/server/lib/config-sync/constants.js +39 -0
- package/server/lib/config-sync/contract.js +130 -0
- package/server/lib/config-sync/credential-auth.js +39 -0
- package/server/lib/config-sync/engine.js +108 -0
- package/server/lib/config-sync/index.js +67 -0
- package/server/lib/config-sync/local-backup.js +171 -0
- package/server/lib/config-sync/plan.js +318 -0
- package/server/lib/config-sync/receiver.js +330 -0
- package/server/lib/config-sync/routes.js +141 -0
- package/server/lib/config-sync/routes.test.js +154 -0
- package/server/lib/config-sync/scripts.js +264 -0
- package/server/lib/config-sync/selections.js +112 -0
- package/server/lib/config-sync/tar.js +35 -0
- package/server/lib/config-sync/target-id.js +24 -0
- package/server/lib/event-stream/DOCUMENTATION.md +2 -2
- package/server/lib/event-stream/diff-summary.js +39 -11
- package/server/lib/event-stream/diff-summary.test.js +73 -6
- package/server/lib/event-stream/protocol.test.js +70 -7
- package/server/lib/notifications/DOCUMENTATION.md +2 -2
- package/server/lib/notifications/runtime.js +68 -27
- package/server/lib/notifications/runtime.test.js +176 -0
- package/server/lib/opencode/DOCUMENTATION.md +4 -1
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/core-routes.js +1 -0
- package/server/lib/opencode/feature-routes-runtime.test.js +10 -1
- package/server/lib/opencode/openchamber-routes.js +7 -0
- package/server/lib/opencode/settings-runtime.js +82 -51
- package/server/lib/opencode/settings-runtime.test.js +42 -0
- package/server/lib/relay/DOCUMENTATION.md +1 -1
- package/server/lib/session-index/DOCUMENTATION.md +12 -6
- package/server/lib/session-index/routes.js +34 -0
- package/server/lib/session-index/routes.test.js +76 -0
- package/server/lib/session-index/service.js +53 -4
- package/server/lib/session-index/service.test.js +72 -2
- package/server/lib/session-title/DOCUMENTATION.md +19 -14
- package/server/lib/session-title/runtime.js +8 -8
- package/server/lib/session-title/runtime.test.js +231 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +26 -6
- package/server/lib/session-turn-pages/changes.service.js +156 -0
- package/server/lib/session-turn-pages/changes.service.test.js +188 -0
- package/server/lib/session-turn-pages/reconcile.routes.test.js +52 -0
- package/server/lib/session-turn-pages/reconcile.service.js +3 -1
- package/server/lib/session-turn-pages/reconcile.service.test.js +44 -0
- package/server/lib/session-turn-pages/routes.js +273 -12
- package/server/lib/session-turn-pages/routes.test.js +387 -0
- package/server/lib/session-turn-pages/service.js +31 -7
- package/server/lib/session-turn-pages/service.test.js +40 -0
- package/server/lib/small-model/DOCUMENTATION.md +93 -38
- package/server/lib/small-model/catalog.js +10 -2
- package/server/lib/small-model/catalog.test.js +3 -1
- package/server/lib/small-model/index.js +52 -28
- package/server/lib/small-model/opencode-session.js +339 -0
- package/server/lib/small-model/opencode-session.test.js +177 -0
- package/server/lib/small-model/resolve.js +116 -5
- package/server/lib/small-model/resolve.test.js +76 -10
- package/server/lib/small-model/summary-settings.test.js +60 -3
- package/dist/assets/MobileSurface-htGkf7Ua.js +0 -2
- package/dist/assets/SettingsView-DxMpVLlt.js +0 -25
- package/dist/assets/appThemeRegistry-CgfOR_0L.js +0 -2
- package/dist/assets/gitApiHttp-D8j1qRH4.js +0 -2
- package/dist/assets/insertionBoundaries-DzXMTleb.js +0 -1
- package/dist/assets/mobile-BktO3_4R.js +0 -2
- package/dist/assets/projectMeta-v-LP2mzz.js +0 -3
- package/dist/assets/renderElectronMiniChatApp-Db8uO-6q.js +0 -2
- package/dist/assets/renderMobileApp-BCV81Ryl.js +0 -23
- package/dist/assets/runtimeEndpointReset-Dg3DMy9v.js +0 -145
- package/dist/assets/src-oHrYIqHF.css +0 -2
- package/dist/assets/useAppFontEffects-CM0VKsdo.js +0 -7
- package/dist/assets/useDesktopWindowControlsLayout-Cq-r4Hcb.js +0 -1
- package/dist/assets/useMobileNavigationStore-DnzT4jO2.js +0 -475
- package/dist/assets/useSessionAutoCleanup-DxZeceKO.js +0 -1
|
@@ -320,6 +320,122 @@ describe('registerSessionTurnPageRoutes', () => {
|
|
|
320
320
|
expect(part.state.output).toBeUndefined();
|
|
321
321
|
});
|
|
322
322
|
|
|
323
|
+
it('projects message summary.diffs to L1 slim list + markers before serializing a turn-page response', async () => {
|
|
324
|
+
const patch = 'diff-body-'.repeat(20_000);
|
|
325
|
+
const loadPage = vi.fn(async () => ({
|
|
326
|
+
ok: true,
|
|
327
|
+
records: [{
|
|
328
|
+
info: {
|
|
329
|
+
id: 'msg_u1',
|
|
330
|
+
role: 'user',
|
|
331
|
+
summary: {
|
|
332
|
+
diffs: [{
|
|
333
|
+
file: 'src/large.ts',
|
|
334
|
+
status: 'modified',
|
|
335
|
+
additions: 9,
|
|
336
|
+
deletions: 3,
|
|
337
|
+
patch,
|
|
338
|
+
before: 'old',
|
|
339
|
+
after: 'new',
|
|
340
|
+
}],
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
parts: [],
|
|
344
|
+
}],
|
|
345
|
+
turnCount: 1,
|
|
346
|
+
cursor: null,
|
|
347
|
+
complete: true,
|
|
348
|
+
}));
|
|
349
|
+
const { app, route } = registry();
|
|
350
|
+
registerSessionTurnPageRoutes(app, { sessionTurnPageService: { loadPage } });
|
|
351
|
+
const res = response();
|
|
352
|
+
|
|
353
|
+
await route('GET', ROUTE)({
|
|
354
|
+
params: { sessionID: 'ses_1' },
|
|
355
|
+
query: { turns: '3' },
|
|
356
|
+
headers: {},
|
|
357
|
+
}, res);
|
|
358
|
+
|
|
359
|
+
expect(res.statusCode).toBe(200);
|
|
360
|
+
expect(res.body.records[0].info.summary).toEqual({
|
|
361
|
+
diffs: [{
|
|
362
|
+
file: 'src/large.ts',
|
|
363
|
+
status: 'modified',
|
|
364
|
+
additions: 9,
|
|
365
|
+
deletions: 3,
|
|
366
|
+
}],
|
|
367
|
+
diffCount: 1,
|
|
368
|
+
hasDiffs: true,
|
|
369
|
+
});
|
|
370
|
+
expect(JSON.stringify(res.body)).not.toContain(patch);
|
|
371
|
+
expect(JSON.stringify(res.body)).not.toContain('"before"');
|
|
372
|
+
expect(JSON.stringify(res.body)).not.toContain('"after"');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('keeps L1 turn-page serialization under 256KiB for a 463-file / ~14MB patch summary', async () => {
|
|
376
|
+
const patchBody = 'P'.repeat(Math.floor((14 * 1024 * 1024) / 463));
|
|
377
|
+
const diffs = Array.from({ length: 463 }, (_, i) => ({
|
|
378
|
+
file: `src/generated/file-${i}.ts`,
|
|
379
|
+
status: 'modified',
|
|
380
|
+
additions: i % 7,
|
|
381
|
+
deletions: i % 3,
|
|
382
|
+
patch: `${patchBody}-${i}`,
|
|
383
|
+
before: `before-${i}`,
|
|
384
|
+
after: `after-${i}`,
|
|
385
|
+
from: `from-${i}`,
|
|
386
|
+
to: `to-${i}`,
|
|
387
|
+
}));
|
|
388
|
+
const loadPage = vi.fn(async () => ({
|
|
389
|
+
ok: true,
|
|
390
|
+
records: [{
|
|
391
|
+
info: {
|
|
392
|
+
id: 'msg_huge',
|
|
393
|
+
role: 'user',
|
|
394
|
+
summary: { title: 'huge', diffs },
|
|
395
|
+
},
|
|
396
|
+
parts: [{ id: 'prt_1', type: 'text', text: 'ok' }],
|
|
397
|
+
}],
|
|
398
|
+
turnCount: 1,
|
|
399
|
+
cursor: null,
|
|
400
|
+
complete: true,
|
|
401
|
+
}));
|
|
402
|
+
const { app, route } = registry();
|
|
403
|
+
registerSessionTurnPageRoutes(app, { sessionTurnPageService: { loadPage } });
|
|
404
|
+
const res = response();
|
|
405
|
+
|
|
406
|
+
await route('GET', ROUTE)({
|
|
407
|
+
params: { sessionID: 'ses_huge' },
|
|
408
|
+
query: { turns: '1' },
|
|
409
|
+
headers: {},
|
|
410
|
+
}, res);
|
|
411
|
+
|
|
412
|
+
expect(res.statusCode).toBe(200);
|
|
413
|
+
const serialized = JSON.stringify(res.body);
|
|
414
|
+
const summary = res.body.records[0].info.summary;
|
|
415
|
+
expect(summary.diffCount).toBe(463);
|
|
416
|
+
expect(summary.hasDiffs).toBe(true);
|
|
417
|
+
expect(summary.diffs).toHaveLength(463);
|
|
418
|
+
expect(summary.diffs[0]).toEqual({
|
|
419
|
+
file: 'src/generated/file-0.ts',
|
|
420
|
+
status: 'modified',
|
|
421
|
+
additions: 0,
|
|
422
|
+
deletions: 0,
|
|
423
|
+
});
|
|
424
|
+
expect(summary.diffs[462]).toEqual({
|
|
425
|
+
file: 'src/generated/file-462.ts',
|
|
426
|
+
status: 'modified',
|
|
427
|
+
additions: 462 % 7,
|
|
428
|
+
deletions: 462 % 3,
|
|
429
|
+
});
|
|
430
|
+
expect(serialized).not.toContain(patchBody.slice(0, 64));
|
|
431
|
+
expect(serialized).not.toContain('"patch"');
|
|
432
|
+
expect(serialized).not.toContain('"before"');
|
|
433
|
+
expect(serialized).not.toContain('"after"');
|
|
434
|
+
expect(serialized).not.toContain('"from"');
|
|
435
|
+
expect(serialized).not.toContain('"to"');
|
|
436
|
+
expect(Buffer.byteLength(serialized, 'utf8')).toBeLessThan(256 * 1024);
|
|
437
|
+
});
|
|
438
|
+
|
|
323
439
|
it('projects file parts on first packet and prepend the same way', async () => {
|
|
324
440
|
const png = Buffer.concat([
|
|
325
441
|
Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
|
|
@@ -479,3 +595,274 @@ describe('registerSessionTurnPageRoutes', () => {
|
|
|
479
595
|
await pending;
|
|
480
596
|
});
|
|
481
597
|
});
|
|
598
|
+
|
|
599
|
+
const EXACT_MESSAGE_ROUTE = '/api/session/:sessionID/message/:messageID';
|
|
600
|
+
const CHANGES_ROUTE = '/api/openchamber/sessions/:sessionID/changes';
|
|
601
|
+
|
|
602
|
+
describe('registerSessionTurnPageRoutes — exact message GET', () => {
|
|
603
|
+
it('registers GET /api/session/:sessionID/message/:messageID', () => {
|
|
604
|
+
const { app, route } = registry();
|
|
605
|
+
registerSessionTurnPageRoutes(app, {
|
|
606
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
607
|
+
fetchExactMessage: vi.fn(),
|
|
608
|
+
});
|
|
609
|
+
expect(route('GET', EXACT_MESSAGE_ROUTE)).toEqual(expect.any(Function));
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('returns original message shape with L1 summary projection only', async () => {
|
|
613
|
+
const patch = '@@ huge @@\n' + 'x'.repeat(2000);
|
|
614
|
+
const fetchExactMessage = vi.fn(async () => ({
|
|
615
|
+
info: {
|
|
616
|
+
id: 'msg_1',
|
|
617
|
+
role: 'user',
|
|
618
|
+
summary: {
|
|
619
|
+
title: 'turn',
|
|
620
|
+
diffs: [{
|
|
621
|
+
file: 'a.ts',
|
|
622
|
+
status: 'modified',
|
|
623
|
+
additions: 2,
|
|
624
|
+
deletions: 1,
|
|
625
|
+
patch,
|
|
626
|
+
}],
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
parts: [{ id: 'prt_1', type: 'text', text: 'hello' }],
|
|
630
|
+
}));
|
|
631
|
+
const { app, route } = registry();
|
|
632
|
+
registerSessionTurnPageRoutes(app, {
|
|
633
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
634
|
+
fetchExactMessage,
|
|
635
|
+
});
|
|
636
|
+
const res = response();
|
|
637
|
+
|
|
638
|
+
await route('GET', EXACT_MESSAGE_ROUTE)({
|
|
639
|
+
params: { sessionID: 'ses_1', messageID: 'msg_1' },
|
|
640
|
+
query: { directory: '/repo' },
|
|
641
|
+
headers: {},
|
|
642
|
+
}, res);
|
|
643
|
+
|
|
644
|
+
expect(res.statusCode).toBe(200);
|
|
645
|
+
expect(fetchExactMessage).toHaveBeenCalledWith(expect.objectContaining({
|
|
646
|
+
sessionID: 'ses_1',
|
|
647
|
+
messageID: 'msg_1',
|
|
648
|
+
directory: '/repo',
|
|
649
|
+
}));
|
|
650
|
+
expect(res.body).toEqual({
|
|
651
|
+
info: {
|
|
652
|
+
id: 'msg_1',
|
|
653
|
+
role: 'user',
|
|
654
|
+
summary: {
|
|
655
|
+
title: 'turn',
|
|
656
|
+
diffs: [{
|
|
657
|
+
file: 'a.ts',
|
|
658
|
+
status: 'modified',
|
|
659
|
+
additions: 2,
|
|
660
|
+
deletions: 1,
|
|
661
|
+
}],
|
|
662
|
+
diffCount: 1,
|
|
663
|
+
hasDiffs: true,
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
parts: [{ id: 'prt_1', type: 'text', text: 'hello' }],
|
|
667
|
+
});
|
|
668
|
+
expect(JSON.stringify(res.body)).not.toContain(patch);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it('rejects oversize messageID / sessionID with 400', async () => {
|
|
672
|
+
const fetchExactMessage = vi.fn();
|
|
673
|
+
const { app, route } = registry();
|
|
674
|
+
registerSessionTurnPageRoutes(app, {
|
|
675
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
676
|
+
fetchExactMessage,
|
|
677
|
+
});
|
|
678
|
+
const handler = route('GET', EXACT_MESSAGE_ROUTE);
|
|
679
|
+
|
|
680
|
+
const oversize = 'm'.repeat(513);
|
|
681
|
+
const res = response();
|
|
682
|
+
await handler({
|
|
683
|
+
params: { sessionID: 'ses_1', messageID: oversize },
|
|
684
|
+
query: {},
|
|
685
|
+
headers: {},
|
|
686
|
+
}, res);
|
|
687
|
+
expect(res.statusCode).toBe(400);
|
|
688
|
+
expect(res.body.code).toBe('invalid_message');
|
|
689
|
+
expect(fetchExactMessage).not.toHaveBeenCalled();
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it('maps not_found to HTTP 404', async () => {
|
|
693
|
+
const fetchExactMessage = vi.fn(async () => {
|
|
694
|
+
const error = new Error('not_found');
|
|
695
|
+
error.code = 'not_found';
|
|
696
|
+
throw error;
|
|
697
|
+
});
|
|
698
|
+
const { app, route } = registry();
|
|
699
|
+
registerSessionTurnPageRoutes(app, {
|
|
700
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
701
|
+
fetchExactMessage,
|
|
702
|
+
});
|
|
703
|
+
const res = response();
|
|
704
|
+
await route('GET', EXACT_MESSAGE_ROUTE)({
|
|
705
|
+
params: { sessionID: 'ses_1', messageID: 'msg_missing' },
|
|
706
|
+
query: {},
|
|
707
|
+
headers: {},
|
|
708
|
+
}, res);
|
|
709
|
+
expect(res.statusCode).toBe(404);
|
|
710
|
+
expect(res.body.code).toBe('not_found');
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
describe('registerSessionTurnPageRoutes — changes API', () => {
|
|
715
|
+
it('registers GET /api/openchamber/sessions/:sessionID/changes', () => {
|
|
716
|
+
const { app, route } = registry();
|
|
717
|
+
registerSessionTurnPageRoutes(app, {
|
|
718
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
719
|
+
sessionChangesService: { loadChanges: vi.fn() },
|
|
720
|
+
});
|
|
721
|
+
expect(route('GET', CHANGES_ROUTE)).toEqual(expect.any(Function));
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
it('returns L2 file list without patch bodies when file is omitted', async () => {
|
|
725
|
+
const loadChanges = vi.fn(async () => ({
|
|
726
|
+
ok: true,
|
|
727
|
+
body: {
|
|
728
|
+
files: [{
|
|
729
|
+
file: 'src/a.ts',
|
|
730
|
+
status: 'modified',
|
|
731
|
+
additions: 3,
|
|
732
|
+
deletions: 1,
|
|
733
|
+
}],
|
|
734
|
+
},
|
|
735
|
+
}));
|
|
736
|
+
const { app, route } = registry();
|
|
737
|
+
registerSessionTurnPageRoutes(app, {
|
|
738
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
739
|
+
sessionChangesService: { loadChanges },
|
|
740
|
+
});
|
|
741
|
+
const res = response();
|
|
742
|
+
|
|
743
|
+
await route('GET', CHANGES_ROUTE)({
|
|
744
|
+
params: { sessionID: 'ses_1' },
|
|
745
|
+
query: { messageID: 'msg_1', directory: '/repo' },
|
|
746
|
+
headers: {},
|
|
747
|
+
}, res);
|
|
748
|
+
|
|
749
|
+
expect(res.statusCode).toBe(200);
|
|
750
|
+
expect(loadChanges).toHaveBeenCalledWith(expect.objectContaining({
|
|
751
|
+
sessionID: 'ses_1',
|
|
752
|
+
messageID: 'msg_1',
|
|
753
|
+
directory: '/repo',
|
|
754
|
+
file: undefined,
|
|
755
|
+
}));
|
|
756
|
+
expect(res.body).toEqual({
|
|
757
|
+
files: [{
|
|
758
|
+
file: 'src/a.ts',
|
|
759
|
+
status: 'modified',
|
|
760
|
+
additions: 3,
|
|
761
|
+
deletions: 1,
|
|
762
|
+
}],
|
|
763
|
+
});
|
|
764
|
+
expect(JSON.stringify(res.body)).not.toContain('patch');
|
|
765
|
+
expect(res.body.info).toBeUndefined();
|
|
766
|
+
expect(res.body.parts).toBeUndefined();
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it('returns L3 single-file diff when file is provided', async () => {
|
|
770
|
+
const loadChanges = vi.fn(async () => ({
|
|
771
|
+
ok: true,
|
|
772
|
+
body: {
|
|
773
|
+
diff: {
|
|
774
|
+
file: 'src/a.ts',
|
|
775
|
+
status: 'modified',
|
|
776
|
+
additions: 3,
|
|
777
|
+
deletions: 1,
|
|
778
|
+
patch: '@@ -1 +1 @@\n+line',
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
}));
|
|
782
|
+
const { app, route } = registry();
|
|
783
|
+
registerSessionTurnPageRoutes(app, {
|
|
784
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
785
|
+
sessionChangesService: { loadChanges },
|
|
786
|
+
});
|
|
787
|
+
const res = response();
|
|
788
|
+
|
|
789
|
+
await route('GET', CHANGES_ROUTE)({
|
|
790
|
+
params: { sessionID: 'ses_1' },
|
|
791
|
+
query: { messageID: 'msg_1', file: 'src/a.ts' },
|
|
792
|
+
headers: {},
|
|
793
|
+
}, res);
|
|
794
|
+
|
|
795
|
+
expect(res.statusCode).toBe(200);
|
|
796
|
+
expect(loadChanges).toHaveBeenCalledWith(expect.objectContaining({
|
|
797
|
+
file: 'src/a.ts',
|
|
798
|
+
}));
|
|
799
|
+
expect(res.body.diff.file).toBe('src/a.ts');
|
|
800
|
+
expect(res.body.diff.patch).toContain('+line');
|
|
801
|
+
expect(res.body.files).toBeUndefined();
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it('rejects missing messageID and control-character file with 400', async () => {
|
|
805
|
+
const loadChanges = vi.fn();
|
|
806
|
+
const { app, route } = registry();
|
|
807
|
+
registerSessionTurnPageRoutes(app, {
|
|
808
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
809
|
+
sessionChangesService: { loadChanges },
|
|
810
|
+
});
|
|
811
|
+
const handler = route('GET', CHANGES_ROUTE);
|
|
812
|
+
|
|
813
|
+
const missing = response();
|
|
814
|
+
await handler({
|
|
815
|
+
params: { sessionID: 'ses_1' },
|
|
816
|
+
query: {},
|
|
817
|
+
headers: {},
|
|
818
|
+
}, missing);
|
|
819
|
+
expect(missing.statusCode).toBe(400);
|
|
820
|
+
expect(missing.body.code).toBe('invalid_message');
|
|
821
|
+
|
|
822
|
+
const badFile = response();
|
|
823
|
+
await handler({
|
|
824
|
+
params: { sessionID: 'ses_1' },
|
|
825
|
+
query: { messageID: 'msg_1', file: 'src/\0evil.ts' },
|
|
826
|
+
headers: {},
|
|
827
|
+
}, badFile);
|
|
828
|
+
expect(badFile.statusCode).toBe(400);
|
|
829
|
+
expect(badFile.body.code).toBe('invalid_file');
|
|
830
|
+
expect(loadChanges).not.toHaveBeenCalled();
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it('maps change_not_found to HTTP 404 with stable code', async () => {
|
|
834
|
+
const loadChanges = vi.fn(async () => ({ ok: false, error: 'change_not_found' }));
|
|
835
|
+
const { app, route } = registry();
|
|
836
|
+
registerSessionTurnPageRoutes(app, {
|
|
837
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
838
|
+
sessionChangesService: { loadChanges },
|
|
839
|
+
});
|
|
840
|
+
const res = response();
|
|
841
|
+
await route('GET', CHANGES_ROUTE)({
|
|
842
|
+
params: { sessionID: 'ses_1' },
|
|
843
|
+
query: { messageID: 'msg_1', file: 'missing.ts' },
|
|
844
|
+
headers: {},
|
|
845
|
+
}, res);
|
|
846
|
+
expect(res.statusCode).toBe(404);
|
|
847
|
+
expect(res.body).toEqual({
|
|
848
|
+
error: 'change file not found',
|
|
849
|
+
code: 'change_not_found',
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('maps aborted to HTTP 499', async () => {
|
|
854
|
+
const loadChanges = vi.fn(async () => ({ ok: false, error: 'aborted' }));
|
|
855
|
+
const { app, route } = registry();
|
|
856
|
+
registerSessionTurnPageRoutes(app, {
|
|
857
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
858
|
+
sessionChangesService: { loadChanges },
|
|
859
|
+
});
|
|
860
|
+
const res = response();
|
|
861
|
+
await route('GET', CHANGES_ROUTE)({
|
|
862
|
+
params: { sessionID: 'ses_1' },
|
|
863
|
+
query: { messageID: 'msg_1' },
|
|
864
|
+
headers: {},
|
|
865
|
+
}, res);
|
|
866
|
+
expect(res.statusCode).toBe(499);
|
|
867
|
+
});
|
|
868
|
+
});
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* (true only when nothing older was trimmed from the scanned window).
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { projectMessageSummaryDiffCounts } from '../event-stream/diff-summary.js';
|
|
20
|
+
|
|
19
21
|
const ASSISTANT_SESSION_DIVIDER_PREFIX = 'oc_asst_session_divider:';
|
|
20
22
|
|
|
21
23
|
const DEFAULT_MAX_SCAN_PAGES = 50;
|
|
@@ -457,22 +459,44 @@ const isUserRecord = (record) => {
|
|
|
457
459
|
};
|
|
458
460
|
|
|
459
461
|
/**
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
+
* L1: project message `summary.diffs` to a slim file list
|
|
463
|
+
* `{ file, status?, additions, deletions }` plus additive `diffCount` /
|
|
464
|
+
* `hasDiffs`. Never retains patch/before/after/from/to. Preserves every other
|
|
465
|
+
* message / summary field.
|
|
466
|
+
*/
|
|
467
|
+
export const projectMessageDiffSummaries = (records) => {
|
|
468
|
+
if (!Array.isArray(records) || records.length === 0) return records;
|
|
469
|
+
|
|
470
|
+
let changedAny = false;
|
|
471
|
+
const projected = records.map((record) => {
|
|
472
|
+
if (!record || typeof record !== 'object') return record;
|
|
473
|
+
const info = projectMessageSummaryDiffCounts(record.info);
|
|
474
|
+
if (info === record.info) return record;
|
|
475
|
+
changedAny = true;
|
|
476
|
+
return { ...record, info };
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
return changedAny ? projected : records;
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Summarize message diff snapshots and tool/reasoning parts so a first packet
|
|
484
|
+
* does not have to carry bodies from a long tool-heavy turn.
|
|
462
485
|
*
|
|
463
486
|
* Runs after `selectTurnRecords`, so `turnCount`, `complete`, and cursor
|
|
464
|
-
* encoding are all derived from unprojected records and cannot shift. User
|
|
465
|
-
*
|
|
487
|
+
* encoding are all derived from unprojected records and cannot shift. User and
|
|
488
|
+
* assistant text pass through untouched, and records that gain nothing from
|
|
466
489
|
* projection keep their original object reference.
|
|
467
490
|
*
|
|
468
491
|
* @param {unknown[]} records
|
|
469
492
|
* @returns {unknown[]}
|
|
470
493
|
*/
|
|
471
494
|
export const projectSlimParts = (records) => {
|
|
472
|
-
|
|
495
|
+
const summaryProjected = projectMessageDiffSummaries(records);
|
|
496
|
+
if (!Array.isArray(summaryProjected) || summaryProjected.length === 0) return summaryProjected;
|
|
473
497
|
|
|
474
498
|
let changedAny = false;
|
|
475
|
-
const projected =
|
|
499
|
+
const projected = summaryProjected.map((record) => {
|
|
476
500
|
if (!record || typeof record !== 'object') return record;
|
|
477
501
|
const userRow = isUserRecord(record);
|
|
478
502
|
|
|
@@ -503,7 +527,7 @@ export const projectSlimParts = (records) => {
|
|
|
503
527
|
return { ...record, parts: nextParts };
|
|
504
528
|
});
|
|
505
529
|
|
|
506
|
-
return changedAny ? projected :
|
|
530
|
+
return changedAny ? projected : summaryProjected;
|
|
507
531
|
};
|
|
508
532
|
|
|
509
533
|
const countAuthoredBoundaries = (records) =>
|
|
@@ -172,6 +172,46 @@ describe('projectSlimParts', () => {
|
|
|
172
172
|
},
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
it('projects message summary.diffs to L1 slim list + markers even when parts are empty', () => {
|
|
176
|
+
const patch = 'diff-body-'.repeat(20_000);
|
|
177
|
+
const [projected] = projectSlimParts([{
|
|
178
|
+
info: {
|
|
179
|
+
id: 'msg_diff',
|
|
180
|
+
role: 'user',
|
|
181
|
+
summary: {
|
|
182
|
+
title: 'kept',
|
|
183
|
+
diffs: [{
|
|
184
|
+
file: 'src/large.ts',
|
|
185
|
+
status: 'modified',
|
|
186
|
+
additions: 12,
|
|
187
|
+
deletions: 4,
|
|
188
|
+
patch,
|
|
189
|
+
before: 'old-body',
|
|
190
|
+
after: 'new-body',
|
|
191
|
+
from: 'from-blob',
|
|
192
|
+
to: 'to-blob',
|
|
193
|
+
}],
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
parts: [],
|
|
197
|
+
}]);
|
|
198
|
+
|
|
199
|
+
expect(projected.info.summary).toEqual({
|
|
200
|
+
title: 'kept',
|
|
201
|
+
diffs: [{
|
|
202
|
+
file: 'src/large.ts',
|
|
203
|
+
status: 'modified',
|
|
204
|
+
additions: 12,
|
|
205
|
+
deletions: 4,
|
|
206
|
+
}],
|
|
207
|
+
diffCount: 1,
|
|
208
|
+
hasDiffs: true,
|
|
209
|
+
});
|
|
210
|
+
expect(JSON.stringify(projected)).not.toContain(patch);
|
|
211
|
+
expect(JSON.stringify(projected)).not.toContain('old-body');
|
|
212
|
+
expect(JSON.stringify(projected)).not.toContain('from-blob');
|
|
213
|
+
});
|
|
214
|
+
|
|
175
215
|
it('drops tool output and metadata but keeps identity and status', () => {
|
|
176
216
|
const [record] = projectSlimParts([
|
|
177
217
|
{ info: { id: 'msg_a1', role: 'assistant' }, parts: [toolPart('prt_1', 'x'.repeat(5000))] },
|