@openchambery/web 1.18.1 → 1.18.2-beta.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/{AgentAvatar-B2kZjgaf.js → AgentAvatar-BzRb9sP6.js} +1 -1
- package/dist/assets/{AssistantView-f5eK9QED.js → AssistantView-Bcx9ER-E.js} +1 -1
- package/dist/assets/{ChatContainer-BvJoxcjJ.js → ChatContainer-DaBnpayh.js} +38 -38
- package/dist/assets/{DiagramView-Vu2JnmC-.js → DiagramView-DaZSw3PM.js} +1 -1
- package/dist/assets/{FadeInOnReveal-aeWlaqVB.js → FadeInOnReveal-oeyB_nIi.js} +1 -1
- package/dist/assets/{Icon-AXqa-fYB.js → Icon-DHaR0peD.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-CSxVZ_eA.js → MarkdownRendererImpl-CoMjP1t6.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-3NIRdxpt.js → MobileDetailNavigation-Bsi54FsV.js} +1 -1
- package/dist/assets/{MobileShareBridge-B7jqfJ-S.js → MobileShareBridge-BQd5tCJN.js} +1 -1
- package/dist/assets/MobileSurface-C0bqzAcb.js +2 -0
- package/dist/assets/{MultiRunWindow-DkYTqcPq.js → MultiRunWindow-CJLa67M0.js} +1 -1
- package/dist/assets/{OnboardingScreen-CCfdEtW6.js → OnboardingScreen-BZHN7C2r.js} +1 -1
- package/dist/assets/{OverlayScrollbar-DeL1aO59.js → OverlayScrollbar-BLbgO_QF.js} +1 -1
- package/dist/assets/SettingsView-pHa9Y38q.js +25 -0
- package/dist/assets/{SettingsWindow--nLXOo-L.js → SettingsWindow-q6zT03-N.js} +1 -1
- package/dist/assets/{TerminalView-B80SmYHn.js → TerminalView-BFb17E29.js} +1 -1
- package/dist/assets/{ToolOutputDialog-CtbnU35x.js → ToolOutputDialog-sVkggrhW.js} +1 -1
- package/dist/assets/appThemeRegistry-Bh6gmBoH.js +2 -0
- package/dist/assets/{button-CDvMVaiO.js → button-DCUMIXDC.js} +1 -1
- package/dist/assets/{clipboard-Jdqe05db.js → clipboard-DF5tFLDb.js} +1 -1
- package/dist/assets/{collapsible-C2aamDvD.js → collapsible-DNg5bLKV.js} +1 -1
- package/dist/assets/{context-menu-D16ocNrk.js → context-menu-VD4qvZ6C.js} +1 -1
- package/dist/assets/{desktopHosts-CBoBpJ1Z.js → desktopHosts-DSgGxDpI.js} +1 -1
- package/dist/assets/{desktopRelayRestore-l2R9muux.js → desktopRelayRestore-DVIx7jsz.js} +1 -1
- package/dist/assets/{diagram-CEon0BQs.js → diagram-cjvFqVa8.js} +1 -1
- package/dist/assets/{dialog-BzZ3cKxG.js → dialog-DaSIYgrj.js} +1 -1
- package/dist/assets/{es-C25PRSjo.js → es-DCoGZljP.js} +1 -1
- package/dist/assets/{fileQueries-CE_DaZWQ.js → fileQueries-DXvhIL_U.js} +1 -1
- package/dist/assets/{fr-Cv8ZWQdW.js → fr-dkEoZpPz.js} +2 -2
- package/dist/assets/{gitApi-B1CIqazO.js → gitApi-cPhxx3NZ.js} +1 -1
- package/dist/assets/gitApiHttp-SLrnr8a5.js +2 -0
- package/dist/assets/{input-k9DGGyZ6.js → input-QS3IWgY5.js} +1 -1
- package/dist/assets/{insertionBoundaries-B9cwRNac.js → insertionBoundaries-XUyczmFs.js} +1 -1
- package/dist/assets/{ja-Ue9m9eSe.js → ja-bQwSsvRr.js} +1 -1
- package/dist/assets/{ko-DWBUCCV6.js → ko-Kq5C3hsv.js} +1 -1
- package/dist/assets/{main-BSVnX01h.js → main-D4CTy7xa.js} +2 -2
- package/dist/assets/{main-DzZumJ48.js → main-Mkx9wBnO.js} +23 -23
- package/dist/assets/{markdown-worker-DkeWiER6.js → markdown-worker-D-OR2e_G.js} +1 -1
- package/dist/assets/{markdownTheme-DNT93i5L.js → markdownTheme-y_Rf6mLa.js} +1 -1
- package/dist/assets/{miniChat-CrEtOV4u.js → miniChat-Bq6uelzq.js} +2 -2
- package/dist/assets/mobile-C1L9OCdh.js +2 -0
- package/dist/assets/{multirun-G-ZwKVUq.js → multirun-9OIhLOWP.js} +1 -1
- package/dist/assets/{persistence-DPNMIbF5.js → persistence-oSFWyKgV.js} +1 -1
- package/dist/assets/{pl-DaBaPCyE.js → pl-CYpMAgVn.js} +2 -2
- package/dist/assets/projectMeta-BXPWMXjv.js +3 -0
- package/dist/assets/{pt-BR-C2cJZ43o.js → pt-BR-MSPTLfTm.js} +1 -1
- package/dist/assets/{radio-BtilB34p.js → radio-CqpSp0pL.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-D1HWiWMn.js → renderElectronMiniChatApp-B8cSB7U4.js} +2 -2
- package/dist/assets/renderMobileApp-DmBjzy65.js +23 -0
- package/dist/assets/{runtimeConfig-BtWi74Pm.js → runtimeConfig-n4QAk1lU.js} +1 -1
- package/dist/assets/runtimeEndpointReset-Bw-RBy5a.js +145 -0
- package/dist/assets/{sortable-tabs-strip-BpvOof2G.js → sortable-tabs-strip-B4wI7VAP.js} +1 -1
- package/dist/assets/src-BhvNwAr-.css +2 -0
- package/dist/assets/{store-SqV3zfE8.js → store-OURVnLSN.js} +2 -2
- package/dist/assets/{terminalApi-tB3qLcfa.js → terminalApi-_NnfkxgP.js} +1 -1
- package/dist/assets/{tooltip-DMNR5rN0.js → tooltip-xEnco9xY.js} +1 -1
- package/dist/assets/{uk-DJz_P8OZ.js → uk-cSkRAYFY.js} +1 -1
- package/dist/assets/{url-7MqoLs9X.js → url-DIZgk80M.js} +1 -1
- package/dist/assets/{useAppFontEffects-BBxqFIta.js → useAppFontEffects-BHoHqFhx.js} +3 -3
- package/dist/assets/{useDesktopWindowControlsLayout-D8fjPyqI.js → useDesktopWindowControlsLayout-DwmEdp3O.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-C5OxWqUH.js → useEffectiveDirectory-Bjo9xE9q.js} +1 -1
- package/dist/assets/{useFontPreferences-CIurCeNv.js → useFontPreferences-B-QHyR3C.js} +1 -1
- package/dist/assets/useMobileNavigationStore-SCw9NohC.js +476 -0
- package/dist/assets/useSessionAutoCleanup-KwCKRF_d.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-CJozblYF.js → useWorkerHighlightedLines-Cm3UQxhi.js} +1 -1
- package/dist/assets/{utils-Co19EJEL.js → utils-Dxsi3J3l.js} +1 -1
- package/dist/assets/{zh-CN-DLAKDOM9.js → zh-CN-BKqeT_uz.js} +1 -1
- package/dist/assets/{zh-TW-s3Jjx1_n.js → zh-TW-BaNjJ8Y4.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 +6 -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 +35 -16
- package/server/lib/event-stream/diff-summary.test.js +46 -6
- package/server/lib/event-stream/protocol.test.js +7 -7
- package/server/lib/git/DOCUMENTATION.md +2 -1
- package/server/lib/git/service.js +24 -5
- package/server/lib/git/service.test.js +51 -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-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 +48 -0
- package/server/lib/session-turn-pages/reconcile.service.js +3 -1
- package/server/lib/session-turn-pages/reconcile.service.test.js +40 -0
- package/server/lib/session-turn-pages/routes.js +273 -12
- package/server/lib/session-turn-pages/routes.test.js +359 -0
- package/server/lib/session-turn-pages/service.js +29 -7
- package/server/lib/session-turn-pages/service.test.js +30 -0
- package/dist/assets/MobileSurface-BL3hired.js +0 -2
- package/dist/assets/SettingsView-CyrIDyDl.js +0 -25
- package/dist/assets/appThemeRegistry-D2ZUcQmr.js +0 -2
- package/dist/assets/gitApiHttp-D8j1qRH4.js +0 -2
- package/dist/assets/mobile-DMjO-f5u.js +0 -2
- package/dist/assets/projectMeta-owiDYjF9.js +0 -3
- package/dist/assets/renderMobileApp-Cqxwb3Ub.js +0 -23
- package/dist/assets/runtimeEndpointReset-C0oDuy2R.js +0 -145
- package/dist/assets/src-oHrYIqHF.css +0 -2
- package/dist/assets/useMobileNavigationStore-U3b_hG4m.js +0 -475
- package/dist/assets/useSessionAutoCleanup-CDYDm76L.js +0 -1
|
@@ -320,6 +320,99 @@ describe('registerSessionTurnPageRoutes', () => {
|
|
|
320
320
|
expect(part.state.output).toBeUndefined();
|
|
321
321
|
});
|
|
322
322
|
|
|
323
|
+
it('projects message summary.diffs to L1 count/marker 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
|
+
}],
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
parts: [],
|
|
342
|
+
}],
|
|
343
|
+
turnCount: 1,
|
|
344
|
+
cursor: null,
|
|
345
|
+
complete: true,
|
|
346
|
+
}));
|
|
347
|
+
const { app, route } = registry();
|
|
348
|
+
registerSessionTurnPageRoutes(app, { sessionTurnPageService: { loadPage } });
|
|
349
|
+
const res = response();
|
|
350
|
+
|
|
351
|
+
await route('GET', ROUTE)({
|
|
352
|
+
params: { sessionID: 'ses_1' },
|
|
353
|
+
query: { turns: '3' },
|
|
354
|
+
headers: {},
|
|
355
|
+
}, res);
|
|
356
|
+
|
|
357
|
+
expect(res.statusCode).toBe(200);
|
|
358
|
+
expect(res.body.records[0].info.summary).toEqual({
|
|
359
|
+
diffCount: 1,
|
|
360
|
+
hasDiffs: true,
|
|
361
|
+
});
|
|
362
|
+
expect(res.body.records[0].info.summary.diffs).toBeUndefined();
|
|
363
|
+
expect(JSON.stringify(res.body)).not.toContain(patch);
|
|
364
|
+
expect(JSON.stringify(res.body)).not.toContain('src/large.ts');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('keeps L1 turn-page serialization under 64KiB for a 463-file / ~14MB patch summary', async () => {
|
|
368
|
+
const patchBody = 'P'.repeat(Math.floor((14 * 1024 * 1024) / 463));
|
|
369
|
+
const diffs = Array.from({ length: 463 }, (_, i) => ({
|
|
370
|
+
file: `src/generated/file-${i}.ts`,
|
|
371
|
+
status: 'modified',
|
|
372
|
+
additions: i % 7,
|
|
373
|
+
deletions: i % 3,
|
|
374
|
+
patch: `${patchBody}-${i}`,
|
|
375
|
+
before: `before-${i}`,
|
|
376
|
+
after: `after-${i}`,
|
|
377
|
+
}));
|
|
378
|
+
const loadPage = vi.fn(async () => ({
|
|
379
|
+
ok: true,
|
|
380
|
+
records: [{
|
|
381
|
+
info: {
|
|
382
|
+
id: 'msg_huge',
|
|
383
|
+
role: 'user',
|
|
384
|
+
summary: { title: 'huge', diffs },
|
|
385
|
+
},
|
|
386
|
+
parts: [{ id: 'prt_1', type: 'text', text: 'ok' }],
|
|
387
|
+
}],
|
|
388
|
+
turnCount: 1,
|
|
389
|
+
cursor: null,
|
|
390
|
+
complete: true,
|
|
391
|
+
}));
|
|
392
|
+
const { app, route } = registry();
|
|
393
|
+
registerSessionTurnPageRoutes(app, { sessionTurnPageService: { loadPage } });
|
|
394
|
+
const res = response();
|
|
395
|
+
|
|
396
|
+
await route('GET', ROUTE)({
|
|
397
|
+
params: { sessionID: 'ses_huge' },
|
|
398
|
+
query: { turns: '1' },
|
|
399
|
+
headers: {},
|
|
400
|
+
}, res);
|
|
401
|
+
|
|
402
|
+
expect(res.statusCode).toBe(200);
|
|
403
|
+
const serialized = JSON.stringify(res.body);
|
|
404
|
+
expect(res.body.records[0].info.summary).toEqual({
|
|
405
|
+
title: 'huge',
|
|
406
|
+
diffCount: 463,
|
|
407
|
+
hasDiffs: true,
|
|
408
|
+
});
|
|
409
|
+
expect(res.body.records[0].info.summary.diffs).toBeUndefined();
|
|
410
|
+
expect(serialized).not.toContain('"diffs"');
|
|
411
|
+
expect(serialized).not.toContain(patchBody.slice(0, 64));
|
|
412
|
+
expect(serialized).not.toContain('src/generated/file-0.ts');
|
|
413
|
+
expect(Buffer.byteLength(serialized, 'utf8')).toBeLessThan(64 * 1024);
|
|
414
|
+
});
|
|
415
|
+
|
|
323
416
|
it('projects file parts on first packet and prepend the same way', async () => {
|
|
324
417
|
const png = Buffer.concat([
|
|
325
418
|
Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
|
|
@@ -479,3 +572,269 @@ describe('registerSessionTurnPageRoutes', () => {
|
|
|
479
572
|
await pending;
|
|
480
573
|
});
|
|
481
574
|
});
|
|
575
|
+
|
|
576
|
+
const EXACT_MESSAGE_ROUTE = '/api/session/:sessionID/message/:messageID';
|
|
577
|
+
const CHANGES_ROUTE = '/api/openchamber/sessions/:sessionID/changes';
|
|
578
|
+
|
|
579
|
+
describe('registerSessionTurnPageRoutes — exact message GET', () => {
|
|
580
|
+
it('registers GET /api/session/:sessionID/message/:messageID', () => {
|
|
581
|
+
const { app, route } = registry();
|
|
582
|
+
registerSessionTurnPageRoutes(app, {
|
|
583
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
584
|
+
fetchExactMessage: vi.fn(),
|
|
585
|
+
});
|
|
586
|
+
expect(route('GET', EXACT_MESSAGE_ROUTE)).toEqual(expect.any(Function));
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('returns original message shape with L1 summary projection only', async () => {
|
|
590
|
+
const patch = '@@ huge @@\n' + 'x'.repeat(2000);
|
|
591
|
+
const fetchExactMessage = vi.fn(async () => ({
|
|
592
|
+
info: {
|
|
593
|
+
id: 'msg_1',
|
|
594
|
+
role: 'user',
|
|
595
|
+
summary: {
|
|
596
|
+
title: 'turn',
|
|
597
|
+
diffs: [{
|
|
598
|
+
file: 'a.ts',
|
|
599
|
+
status: 'modified',
|
|
600
|
+
additions: 2,
|
|
601
|
+
deletions: 1,
|
|
602
|
+
patch,
|
|
603
|
+
}],
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
parts: [{ id: 'prt_1', type: 'text', text: 'hello' }],
|
|
607
|
+
}));
|
|
608
|
+
const { app, route } = registry();
|
|
609
|
+
registerSessionTurnPageRoutes(app, {
|
|
610
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
611
|
+
fetchExactMessage,
|
|
612
|
+
});
|
|
613
|
+
const res = response();
|
|
614
|
+
|
|
615
|
+
await route('GET', EXACT_MESSAGE_ROUTE)({
|
|
616
|
+
params: { sessionID: 'ses_1', messageID: 'msg_1' },
|
|
617
|
+
query: { directory: '/repo' },
|
|
618
|
+
headers: {},
|
|
619
|
+
}, res);
|
|
620
|
+
|
|
621
|
+
expect(res.statusCode).toBe(200);
|
|
622
|
+
expect(fetchExactMessage).toHaveBeenCalledWith(expect.objectContaining({
|
|
623
|
+
sessionID: 'ses_1',
|
|
624
|
+
messageID: 'msg_1',
|
|
625
|
+
directory: '/repo',
|
|
626
|
+
}));
|
|
627
|
+
expect(res.body).toEqual({
|
|
628
|
+
info: {
|
|
629
|
+
id: 'msg_1',
|
|
630
|
+
role: 'user',
|
|
631
|
+
summary: {
|
|
632
|
+
title: 'turn',
|
|
633
|
+
diffCount: 1,
|
|
634
|
+
hasDiffs: true,
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
parts: [{ id: 'prt_1', type: 'text', text: 'hello' }],
|
|
638
|
+
});
|
|
639
|
+
expect(JSON.stringify(res.body)).not.toContain(patch);
|
|
640
|
+
expect(JSON.stringify(res.body)).not.toContain('a.ts');
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it('rejects oversize messageID / sessionID with 400', async () => {
|
|
644
|
+
const fetchExactMessage = vi.fn();
|
|
645
|
+
const { app, route } = registry();
|
|
646
|
+
registerSessionTurnPageRoutes(app, {
|
|
647
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
648
|
+
fetchExactMessage,
|
|
649
|
+
});
|
|
650
|
+
const handler = route('GET', EXACT_MESSAGE_ROUTE);
|
|
651
|
+
|
|
652
|
+
const oversize = 'm'.repeat(513);
|
|
653
|
+
const res = response();
|
|
654
|
+
await handler({
|
|
655
|
+
params: { sessionID: 'ses_1', messageID: oversize },
|
|
656
|
+
query: {},
|
|
657
|
+
headers: {},
|
|
658
|
+
}, res);
|
|
659
|
+
expect(res.statusCode).toBe(400);
|
|
660
|
+
expect(res.body.code).toBe('invalid_message');
|
|
661
|
+
expect(fetchExactMessage).not.toHaveBeenCalled();
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('maps not_found to HTTP 404', async () => {
|
|
665
|
+
const fetchExactMessage = vi.fn(async () => {
|
|
666
|
+
const error = new Error('not_found');
|
|
667
|
+
error.code = 'not_found';
|
|
668
|
+
throw error;
|
|
669
|
+
});
|
|
670
|
+
const { app, route } = registry();
|
|
671
|
+
registerSessionTurnPageRoutes(app, {
|
|
672
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
673
|
+
fetchExactMessage,
|
|
674
|
+
});
|
|
675
|
+
const res = response();
|
|
676
|
+
await route('GET', EXACT_MESSAGE_ROUTE)({
|
|
677
|
+
params: { sessionID: 'ses_1', messageID: 'msg_missing' },
|
|
678
|
+
query: {},
|
|
679
|
+
headers: {},
|
|
680
|
+
}, res);
|
|
681
|
+
expect(res.statusCode).toBe(404);
|
|
682
|
+
expect(res.body.code).toBe('not_found');
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
describe('registerSessionTurnPageRoutes — changes API', () => {
|
|
687
|
+
it('registers GET /api/openchamber/sessions/:sessionID/changes', () => {
|
|
688
|
+
const { app, route } = registry();
|
|
689
|
+
registerSessionTurnPageRoutes(app, {
|
|
690
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
691
|
+
sessionChangesService: { loadChanges: vi.fn() },
|
|
692
|
+
});
|
|
693
|
+
expect(route('GET', CHANGES_ROUTE)).toEqual(expect.any(Function));
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('returns L2 file list without patch bodies when file is omitted', async () => {
|
|
697
|
+
const loadChanges = vi.fn(async () => ({
|
|
698
|
+
ok: true,
|
|
699
|
+
body: {
|
|
700
|
+
files: [{
|
|
701
|
+
file: 'src/a.ts',
|
|
702
|
+
status: 'modified',
|
|
703
|
+
additions: 3,
|
|
704
|
+
deletions: 1,
|
|
705
|
+
}],
|
|
706
|
+
},
|
|
707
|
+
}));
|
|
708
|
+
const { app, route } = registry();
|
|
709
|
+
registerSessionTurnPageRoutes(app, {
|
|
710
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
711
|
+
sessionChangesService: { loadChanges },
|
|
712
|
+
});
|
|
713
|
+
const res = response();
|
|
714
|
+
|
|
715
|
+
await route('GET', CHANGES_ROUTE)({
|
|
716
|
+
params: { sessionID: 'ses_1' },
|
|
717
|
+
query: { messageID: 'msg_1', directory: '/repo' },
|
|
718
|
+
headers: {},
|
|
719
|
+
}, res);
|
|
720
|
+
|
|
721
|
+
expect(res.statusCode).toBe(200);
|
|
722
|
+
expect(loadChanges).toHaveBeenCalledWith(expect.objectContaining({
|
|
723
|
+
sessionID: 'ses_1',
|
|
724
|
+
messageID: 'msg_1',
|
|
725
|
+
directory: '/repo',
|
|
726
|
+
file: undefined,
|
|
727
|
+
}));
|
|
728
|
+
expect(res.body).toEqual({
|
|
729
|
+
files: [{
|
|
730
|
+
file: 'src/a.ts',
|
|
731
|
+
status: 'modified',
|
|
732
|
+
additions: 3,
|
|
733
|
+
deletions: 1,
|
|
734
|
+
}],
|
|
735
|
+
});
|
|
736
|
+
expect(JSON.stringify(res.body)).not.toContain('patch');
|
|
737
|
+
expect(res.body.info).toBeUndefined();
|
|
738
|
+
expect(res.body.parts).toBeUndefined();
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it('returns L3 single-file diff when file is provided', async () => {
|
|
742
|
+
const loadChanges = vi.fn(async () => ({
|
|
743
|
+
ok: true,
|
|
744
|
+
body: {
|
|
745
|
+
diff: {
|
|
746
|
+
file: 'src/a.ts',
|
|
747
|
+
status: 'modified',
|
|
748
|
+
additions: 3,
|
|
749
|
+
deletions: 1,
|
|
750
|
+
patch: '@@ -1 +1 @@\n+line',
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
}));
|
|
754
|
+
const { app, route } = registry();
|
|
755
|
+
registerSessionTurnPageRoutes(app, {
|
|
756
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
757
|
+
sessionChangesService: { loadChanges },
|
|
758
|
+
});
|
|
759
|
+
const res = response();
|
|
760
|
+
|
|
761
|
+
await route('GET', CHANGES_ROUTE)({
|
|
762
|
+
params: { sessionID: 'ses_1' },
|
|
763
|
+
query: { messageID: 'msg_1', file: 'src/a.ts' },
|
|
764
|
+
headers: {},
|
|
765
|
+
}, res);
|
|
766
|
+
|
|
767
|
+
expect(res.statusCode).toBe(200);
|
|
768
|
+
expect(loadChanges).toHaveBeenCalledWith(expect.objectContaining({
|
|
769
|
+
file: 'src/a.ts',
|
|
770
|
+
}));
|
|
771
|
+
expect(res.body.diff.file).toBe('src/a.ts');
|
|
772
|
+
expect(res.body.diff.patch).toContain('+line');
|
|
773
|
+
expect(res.body.files).toBeUndefined();
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it('rejects missing messageID and control-character file with 400', async () => {
|
|
777
|
+
const loadChanges = vi.fn();
|
|
778
|
+
const { app, route } = registry();
|
|
779
|
+
registerSessionTurnPageRoutes(app, {
|
|
780
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
781
|
+
sessionChangesService: { loadChanges },
|
|
782
|
+
});
|
|
783
|
+
const handler = route('GET', CHANGES_ROUTE);
|
|
784
|
+
|
|
785
|
+
const missing = response();
|
|
786
|
+
await handler({
|
|
787
|
+
params: { sessionID: 'ses_1' },
|
|
788
|
+
query: {},
|
|
789
|
+
headers: {},
|
|
790
|
+
}, missing);
|
|
791
|
+
expect(missing.statusCode).toBe(400);
|
|
792
|
+
expect(missing.body.code).toBe('invalid_message');
|
|
793
|
+
|
|
794
|
+
const badFile = response();
|
|
795
|
+
await handler({
|
|
796
|
+
params: { sessionID: 'ses_1' },
|
|
797
|
+
query: { messageID: 'msg_1', file: 'src/\0evil.ts' },
|
|
798
|
+
headers: {},
|
|
799
|
+
}, badFile);
|
|
800
|
+
expect(badFile.statusCode).toBe(400);
|
|
801
|
+
expect(badFile.body.code).toBe('invalid_file');
|
|
802
|
+
expect(loadChanges).not.toHaveBeenCalled();
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it('maps change_not_found to HTTP 404 with stable code', async () => {
|
|
806
|
+
const loadChanges = vi.fn(async () => ({ ok: false, error: 'change_not_found' }));
|
|
807
|
+
const { app, route } = registry();
|
|
808
|
+
registerSessionTurnPageRoutes(app, {
|
|
809
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
810
|
+
sessionChangesService: { loadChanges },
|
|
811
|
+
});
|
|
812
|
+
const res = response();
|
|
813
|
+
await route('GET', CHANGES_ROUTE)({
|
|
814
|
+
params: { sessionID: 'ses_1' },
|
|
815
|
+
query: { messageID: 'msg_1', file: 'missing.ts' },
|
|
816
|
+
headers: {},
|
|
817
|
+
}, res);
|
|
818
|
+
expect(res.statusCode).toBe(404);
|
|
819
|
+
expect(res.body).toEqual({
|
|
820
|
+
error: 'change file not found',
|
|
821
|
+
code: 'change_not_found',
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it('maps aborted to HTTP 499', async () => {
|
|
826
|
+
const loadChanges = vi.fn(async () => ({ ok: false, error: 'aborted' }));
|
|
827
|
+
const { app, route } = registry();
|
|
828
|
+
registerSessionTurnPageRoutes(app, {
|
|
829
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
830
|
+
sessionChangesService: { loadChanges },
|
|
831
|
+
});
|
|
832
|
+
const res = response();
|
|
833
|
+
await route('GET', CHANGES_ROUTE)({
|
|
834
|
+
params: { sessionID: 'ses_1' },
|
|
835
|
+
query: { messageID: 'msg_1' },
|
|
836
|
+
headers: {},
|
|
837
|
+
}, res);
|
|
838
|
+
expect(res.statusCode).toBe(499);
|
|
839
|
+
});
|
|
840
|
+
});
|
|
@@ -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,42 @@ const isUserRecord = (record) => {
|
|
|
457
459
|
};
|
|
458
460
|
|
|
459
461
|
/**
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
+
* L1: project message `summary.diffs` arrays to `{ diffCount, hasDiffs }` and
|
|
463
|
+
* remove the file array. Preserves every other message / summary field.
|
|
464
|
+
*/
|
|
465
|
+
export const projectMessageDiffSummaries = (records) => {
|
|
466
|
+
if (!Array.isArray(records) || records.length === 0) return records;
|
|
467
|
+
|
|
468
|
+
let changedAny = false;
|
|
469
|
+
const projected = records.map((record) => {
|
|
470
|
+
if (!record || typeof record !== 'object') return record;
|
|
471
|
+
const info = projectMessageSummaryDiffCounts(record.info);
|
|
472
|
+
if (info === record.info) return record;
|
|
473
|
+
changedAny = true;
|
|
474
|
+
return { ...record, info };
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
return changedAny ? projected : records;
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Summarize message diff snapshots and tool/reasoning parts so a first packet
|
|
482
|
+
* does not have to carry bodies from a long tool-heavy turn.
|
|
462
483
|
*
|
|
463
484
|
* Runs after `selectTurnRecords`, so `turnCount`, `complete`, and cursor
|
|
464
|
-
* encoding are all derived from unprojected records and cannot shift. User
|
|
465
|
-
*
|
|
485
|
+
* encoding are all derived from unprojected records and cannot shift. User and
|
|
486
|
+
* assistant text pass through untouched, and records that gain nothing from
|
|
466
487
|
* projection keep their original object reference.
|
|
467
488
|
*
|
|
468
489
|
* @param {unknown[]} records
|
|
469
490
|
* @returns {unknown[]}
|
|
470
491
|
*/
|
|
471
492
|
export const projectSlimParts = (records) => {
|
|
472
|
-
|
|
493
|
+
const summaryProjected = projectMessageDiffSummaries(records);
|
|
494
|
+
if (!Array.isArray(summaryProjected) || summaryProjected.length === 0) return summaryProjected;
|
|
473
495
|
|
|
474
496
|
let changedAny = false;
|
|
475
|
-
const projected =
|
|
497
|
+
const projected = summaryProjected.map((record) => {
|
|
476
498
|
if (!record || typeof record !== 'object') return record;
|
|
477
499
|
const userRow = isUserRecord(record);
|
|
478
500
|
|
|
@@ -503,7 +525,7 @@ export const projectSlimParts = (records) => {
|
|
|
503
525
|
return { ...record, parts: nextParts };
|
|
504
526
|
});
|
|
505
527
|
|
|
506
|
-
return changedAny ? projected :
|
|
528
|
+
return changedAny ? projected : summaryProjected;
|
|
507
529
|
};
|
|
508
530
|
|
|
509
531
|
const countAuthoredBoundaries = (records) =>
|
|
@@ -172,6 +172,36 @@ describe('projectSlimParts', () => {
|
|
|
172
172
|
},
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
it('projects message summary.diffs to L1 count/marker 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
|
+
}],
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
parts: [],
|
|
193
|
+
}]);
|
|
194
|
+
|
|
195
|
+
expect(projected.info.summary).toEqual({
|
|
196
|
+
title: 'kept',
|
|
197
|
+
diffCount: 1,
|
|
198
|
+
hasDiffs: true,
|
|
199
|
+
});
|
|
200
|
+
expect(projected.info.summary.diffs).toBeUndefined();
|
|
201
|
+
expect(JSON.stringify(projected)).not.toContain(patch);
|
|
202
|
+
expect(JSON.stringify(projected)).not.toContain('src/large.ts');
|
|
203
|
+
});
|
|
204
|
+
|
|
175
205
|
it('drops tool output and metadata but keeps identity and status', () => {
|
|
176
206
|
const [record] = projectSlimParts([
|
|
177
207
|
{ info: { id: 'msg_a1', role: 'assistant' }, parts: [toolPart('prt_1', 'x'.repeat(5000))] },
|