@openchambery/web 1.19.7-beta.25 → 1.19.7-beta.26

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.
Files changed (97) hide show
  1. package/dist/assets/{AgentAvatar-DI2zs2WH.js → AgentAvatar-CX0a1rKE.js} +1 -1
  2. package/dist/assets/AssistantView-BYHD7Fcy.js +2 -0
  3. package/dist/assets/{DiagramView-DRIEYUNV.js → DiagramView-BUucK4Mn.js} +1 -1
  4. package/dist/assets/{FadeInOnReveal-BWJcUp5P.js → FadeInOnReveal-CK4asqVR.js} +1 -1
  5. package/dist/assets/{Icon-4My8YZwt.js → Icon-BYM02QyU.js} +1 -1
  6. package/dist/assets/{MarkdownLoadingSkeleton-Vezjvk9B.js → MarkdownLoadingSkeleton-DWs4Jg7b.js} +1 -1
  7. package/dist/assets/{MarkdownRenderer-SLKCbxwI.js → MarkdownRenderer-uiTtRexW.js} +2 -2
  8. package/dist/assets/{MarkdownRendererImpl-D9kcSswH.js → MarkdownRendererImpl-DszSA6px.js} +1 -1
  9. package/dist/assets/MarkstreamRendererImpl-DkjZ7QYA.js +1 -0
  10. package/dist/assets/{MobileDetailNavigation-C5lZVW1v.js → MobileDetailNavigation-BElZdvTL.js} +1 -1
  11. package/dist/assets/MobileShareBridge-DiXOck30.js +4 -0
  12. package/dist/assets/{MobileSurface-CtMp7rI8.js → MobileSurface-BCIZyDOc.js} +1 -1
  13. package/dist/assets/{MultiRunWindow-6-E944pd.js → MultiRunWindow-XXWgGd6d.js} +1 -1
  14. package/dist/assets/{OnboardingScreen-BNEtPmvw.js → OnboardingScreen-C3-wzeSX.js} +1 -1
  15. package/dist/assets/{OverlayScrollbar-BZB21Yp_.js → OverlayScrollbar-KUpvYHdv.js} +1 -1
  16. package/dist/assets/{SettingsView-Cy_D8K8y.js → SettingsView-CdYIJh0f.js} +5 -5
  17. package/dist/assets/{SettingsWindow-BA9QjT8d.js → SettingsWindow-CLr0dXVg.js} +1 -1
  18. package/dist/assets/{TerminalView-izdQm4WZ.js → TerminalView-BdtasXsi.js} +1 -1
  19. package/dist/assets/{ToolOutputDialog-CXbfvZ3M.js → ToolOutputDialog-D22RYqMI.js} +1 -1
  20. package/dist/assets/{appThemeRegistry-Eq3WEH-h.js → appThemeRegistry-D1wTvHoc.js} +1 -1
  21. package/dist/assets/{button-Or0daT9o.js → button-C0cth9tl.js} +1 -1
  22. package/dist/assets/{clipboard-BXxLshVq.js → clipboard-DJ4nQEuY.js} +1 -1
  23. package/dist/assets/{collapsible-C-kzSGkq.js → collapsible-DFT9cE80.js} +1 -1
  24. package/dist/assets/{context-menu-Dpjla5p4.js → context-menu-CAp74NfV.js} +1 -1
  25. package/dist/assets/desktopHosts-CWxex1c3.js +1 -0
  26. package/dist/assets/{desktopRelayRestore-BaKjk1kQ.js → desktopRelayRestore-9EbBYQ9s.js} +1 -1
  27. package/dist/assets/{diagram-D0mT3mo1.js → diagram-BryzMuta.js} +1 -1
  28. package/dist/assets/{dialog-BkhHBTd4.js → dialog-CgbV5ljg.js} +1 -1
  29. package/dist/assets/{fileQueries-DFY6Kiy1.js → fileQueries-BY-9OWzp.js} +1 -1
  30. package/dist/assets/{fileReferenceActions-Di_DoDex.js → fileReferenceActions-BJJtX_S5.js} +1 -1
  31. package/dist/assets/{gitApi-SIviXHpZ.js → gitApi-RhwRTt2X.js} +1 -1
  32. package/dist/assets/{input-BUsGnzwe.js → input-DFLpqjGR.js} +1 -1
  33. package/dist/assets/{insertionBoundaries-q72MeGLB.js → insertionBoundaries-D-UUqego.js} +1 -1
  34. package/dist/assets/{main-Ct6J19qT.js → main-BRB8Kly8.js} +2 -2
  35. package/dist/assets/{main-VXSSCHDb.js → main-BaSeqhTm.js} +11 -11
  36. package/dist/assets/{markdownTheme-CVS01wHf.js → markdownTheme-CzZm3riB.js} +1 -1
  37. package/dist/assets/{miniChat-D_SEWy1a.js → miniChat-CDZ48aEf.js} +2 -2
  38. package/dist/assets/{mobile-Cx032aiN.js → mobile-B2Hpp_5U.js} +2 -2
  39. package/dist/assets/{multirun-BKGQQeek.js → multirun-CfvHH_3s.js} +1 -1
  40. package/dist/assets/{outsideFileGrants-BB4GPvCq.js → outsideFileGrants-DnplRxr-.js} +1 -1
  41. package/dist/assets/persistence-DdUac14I.js +3 -0
  42. package/dist/assets/platform-B4KKLnZ5.js +2 -0
  43. package/dist/assets/{projectMeta-yRCsEkJK.js → projectMeta-vCCip4-G.js} +1 -1
  44. package/dist/assets/{renderElectronMiniChatApp-BnDRaSFx.js → renderElectronMiniChatApp-D9RarEOS.js} +2 -2
  45. package/dist/assets/{renderMobileApp-BAGhZKMW.js → renderMobileApp-DOiQLCcY.js} +7 -7
  46. package/dist/assets/{runtimeConfig-D5H05aTj.js → runtimeConfig-CkWKMmAR.js} +1 -1
  47. package/dist/assets/{runtimeEndpointReset-BwSh51Vc.js → runtimeEndpointReset-aW8qogl0.js} +1 -1
  48. package/dist/assets/{sessionChangeSummary-CX2MJIkc.js → sessionChangeSummary-DOq8EN_H.js} +1 -1
  49. package/dist/assets/{sessionLookup-CSkeoKrV.js → sessionLookup-BNO4Ta46.js} +1 -1
  50. package/dist/assets/{sortable-tabs-strip-C5gGnikj.js → sortable-tabs-strip-C8_9kHqu.js} +1 -1
  51. package/dist/assets/{terminalApi-CIIs9vFx.js → terminalApi-947l7Ra3.js} +1 -1
  52. package/dist/assets/{textarea-CIO34TZz.js → textarea-Cgo_nWUU.js} +1 -1
  53. package/dist/assets/{tooltip-BSrJYYYc.js → tooltip-BifLJ8o0.js} +1 -1
  54. package/dist/assets/{url-CE_gAfNa.js → url-Pk6tQ1EZ.js} +1 -1
  55. package/dist/assets/{useAppFontEffects-Dh_NwW5O.js → useAppFontEffects-BqBeuVr8.js} +6 -6
  56. package/dist/assets/{useAssistantUIStore-BHacrxe_.js → useAssistantUIStore-me6s63b2.js} +1 -1
  57. package/dist/assets/{useDesktopWindowControlsLayout-CV96SEmA.js → useDesktopWindowControlsLayout-CnX7JJa1.js} +1 -1
  58. package/dist/assets/{useEffectiveDirectory-XSKpr7Fy.js → useEffectiveDirectory-F2fNcQ22.js} +1 -1
  59. package/dist/assets/{useFontPreferences-BxOHKuVw.js → useFontPreferences-BCOBlaAd.js} +1 -1
  60. package/dist/assets/useMobileNavigationStore-CBfSvE2P.js +413 -0
  61. package/dist/assets/{useSessionAutoCleanup-Da5ILm3s.js → useSessionAutoCleanup-BVJKWCtK.js} +1 -1
  62. package/dist/assets/useUIStore-CuaA8-Hp.js +1 -0
  63. package/dist/assets/{useWorkerHighlightedLines-dC60TMnO.js → useWorkerHighlightedLines-WYqpkPyP.js} +1 -1
  64. package/dist/assets/{utils-B0mQXN9L.js → utils-BiK3tBGM.js} +1 -1
  65. package/dist/index.html +8 -8
  66. package/dist/mini-chat.html +8 -8
  67. package/dist/mobile.html +8 -8
  68. package/package.json +1 -1
  69. package/server/lib/assistants/contact-tools.test.js +6 -6
  70. package/server/lib/assistants/harness.test.js +3 -3
  71. package/server/lib/assistants/service.test.js +1 -1
  72. package/server/lib/event-stream/DOCUMENTATION.md +12 -1
  73. package/server/lib/event-stream/directory-ws-bridge.js +11 -2
  74. package/server/lib/event-stream/global-ws-bridge.js +19 -3
  75. package/server/lib/event-stream/protocol.js +11 -1
  76. package/server/lib/event-stream/reasoning-projection.js +425 -0
  77. package/server/lib/event-stream/reasoning-projection.test.js +300 -0
  78. package/server/lib/event-stream/runtime.js +10 -0
  79. package/server/lib/event-stream/runtime.test.js +53 -0
  80. package/server/lib/opencode/DOCUMENTATION.md +2 -1
  81. package/server/lib/opencode/proxy.js +103 -9
  82. package/server/lib/opencode/proxy.test.js +177 -1
  83. package/server/lib/relay/reasoning-projection.e2e.test.ts +529 -0
  84. package/server/lib/session-turn-pages/DOCUMENTATION.md +2 -1
  85. package/server/lib/session-turn-pages/routes.js +22 -3
  86. package/server/lib/session-turn-pages/routes.test.js +43 -0
  87. package/server/lib/transcript-cache/DOCUMENTATION.md +2 -2
  88. package/server/lib/transcript-cache/routes.js +9 -2
  89. package/server/lib/transcript-cache/routes.test.js +32 -0
  90. package/dist/assets/AssistantView-BdZ8qKY7.js +0 -2
  91. package/dist/assets/MarkstreamRendererImpl-DnjoP9Et.js +0 -1
  92. package/dist/assets/MobileShareBridge-BeYYag96.js +0 -4
  93. package/dist/assets/desktopHosts-BzSE0sK7.js +0 -1
  94. package/dist/assets/persistence-CSZp1CNB.js +0 -3
  95. package/dist/assets/platform-CenlWqxu.js +0 -2
  96. package/dist/assets/useMobileNavigationStore-BrQwx0Jz.js +0 -412
  97. package/dist/assets/useUIStore-CqqO11ZI.js +0 -1
@@ -22,9 +22,13 @@ export function createGlobalMessageStreamWsBridge({
22
22
  }) {
23
23
  const clients = new Set();
24
24
  const clientLastEventIds = new Map();
25
+ const clientReasoningFilters = new Map();
25
26
  const readyClients = new Set();
26
27
 
27
28
  const removeClient = (socket) => {
29
+ const filter = clientReasoningFilters.get(socket);
30
+ filter?.dispose?.();
31
+ clientReasoningFilters.delete(socket);
28
32
  clients.delete(socket);
29
33
  clientLastEventIds.delete(socket);
30
34
  readyClients.delete(socket);
@@ -32,10 +36,12 @@ export function createGlobalMessageStreamWsBridge({
32
36
  };
33
37
 
34
38
  const replayEvents = (socket, requestedLastEventId) => {
39
+ const reasoningFilter = clientReasoningFilters.get(socket);
35
40
  for (const entry of globalHub.replayAfter(requestedLastEventId)) {
36
41
  const sent = sendMessageStreamWsEvent(socket, entry.payload, {
37
42
  directory: entry.directory,
38
43
  eventId: entry.eventId,
44
+ reasoningFilter,
39
45
  });
40
46
  if (!sent) {
41
47
  removeClient(socket);
@@ -104,6 +110,7 @@ export function createGlobalMessageStreamWsBridge({
104
110
  const sent = sendMessageStreamWsEvent(socket, payload, {
105
111
  directory,
106
112
  eventId,
113
+ reasoningFilter: clientReasoningFilters.get(socket),
107
114
  });
108
115
  if (!sent) {
109
116
  removeClient(socket);
@@ -115,7 +122,10 @@ export function createGlobalMessageStreamWsBridge({
115
122
  if (!readyClients.has(socket)) {
116
123
  continue;
117
124
  }
118
- const sent = sendMessageStreamWsEvent(socket, syntheticPayload, { directory: 'global' });
125
+ const sent = sendMessageStreamWsEvent(socket, syntheticPayload, {
126
+ directory: 'global',
127
+ reasoningFilter: clientReasoningFilters.get(socket),
128
+ });
119
129
  if (!sent) {
120
130
  removeClient(socket);
121
131
  }
@@ -168,7 +178,7 @@ export function createGlobalMessageStreamWsBridge({
168
178
  }
169
179
  });
170
180
 
171
- const accept = (socket, { requestedLastEventId = '' } = {}) => {
181
+ const accept = (socket, { requestedLastEventId = '', reasoningFilter = null } = {}) => {
172
182
  const pingInterval = setInterval(() => {
173
183
  if (socket.readyState !== 1) {
174
184
  return;
@@ -185,7 +195,10 @@ export function createGlobalMessageStreamWsBridge({
185
195
  return;
186
196
  }
187
197
 
188
- sendMessageStreamWsEvent(socket, { type: 'openchamber:heartbeat', timestamp: Date.now() }, { directory: 'global' });
198
+ sendMessageStreamWsEvent(socket, { type: 'openchamber:heartbeat', timestamp: Date.now() }, {
199
+ directory: 'global',
200
+ reasoningFilter: clientReasoningFilters.get(socket),
201
+ });
189
202
  }, heartbeatIntervalMs);
190
203
 
191
204
  socket.on('close', () => {
@@ -201,6 +214,9 @@ export function createGlobalMessageStreamWsBridge({
201
214
 
202
215
  clients.add(socket);
203
216
  clientLastEventIds.set(socket, requestedLastEventId);
217
+ if (reasoningFilter) {
218
+ clientReasoningFilters.set(socket, reasoningFilter);
219
+ }
204
220
  globalHub.start();
205
221
  if (globalHub.isConnected()) {
206
222
  markReady(socket, requestedLastEventId);
@@ -126,9 +126,19 @@ export function sendMessageStreamWsFrame(socket, payload) {
126
126
  }
127
127
 
128
128
  export function sendMessageStreamWsEvent(socket, payload, options = {}) {
129
+ // Optional per-connection reasoning projector (includeReasoning=false).
130
+ // null from projectEvent means drop this frame entirely.
131
+ let candidate = payload;
132
+ if (options.reasoningFilter && typeof options.reasoningFilter.projectEvent === 'function') {
133
+ candidate = options.reasoningFilter.projectEvent(payload);
134
+ if (candidate == null) {
135
+ return true;
136
+ }
137
+ }
138
+
129
139
  // Summarize FileDiff bodies before fan-out so Relay/WS never carries full
130
140
  // patch frames. Identity is preserved for non-diff and already-summary events.
131
- const outbound = summarizeOutboundEventPayload(payload);
141
+ const outbound = summarizeOutboundEventPayload(candidate);
132
142
  return sendMessageStreamWsFrame(socket, {
133
143
  type: 'event',
134
144
  payload: outbound,
@@ -0,0 +1,425 @@
1
+ /**
2
+ * OpenChamber outbound reasoning projection.
3
+ *
4
+ * `includeReasoning=false` (strict string only) strips reasoning from Host→client
5
+ * HTTP/SSE/WS payloads. Missing/other values keep current behavior. OpenChamber
6
+ * projection only — never forward the param to OpenCode. Hub/upstream stay full.
7
+ *
8
+ * Shared by HTTP snapshots and stateful stream filters:
9
+ * drop type=reasoning parts, their deltas, session.next.reasoning.* events.
10
+ * Keep tokens.reasoning totals, tools, text, status, permissions/errors.
11
+ * Never mutate hub/replay objects.
12
+ */
13
+
14
+ const REASONING_DELTA_FIELDS = new Set([
15
+ 'reasoning',
16
+ 'reasoning_content',
17
+ 'reasoning_details',
18
+ ]);
19
+
20
+ /** Match UI opencode-event-normalizer: strip terminal `.N` version suffix for dispatch. */
21
+ const VERSIONED_TYPE = /^(.*)\.(\d+)$/;
22
+
23
+ const PART_TYPE_CACHE_LIMIT = 8192;
24
+
25
+ const isPlainObject = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
26
+
27
+ /** @param {string} type */
28
+ const baseEventType = (type) => {
29
+ const match = VERSIONED_TYPE.exec(type);
30
+ return match?.[1] ?? type;
31
+ };
32
+
33
+ /**
34
+ * Strict: only string `'false'` disables. Missing / other keep include.
35
+ * @param {unknown} value
36
+ * @returns {boolean}
37
+ */
38
+ export function shouldIncludeReasoning(value) {
39
+ if (Array.isArray(value) && value.length > 0) return value[0] !== 'false';
40
+ return value !== 'false';
41
+ }
42
+
43
+ /** @param {unknown} query */
44
+ export function readIncludeReasoningQuery(query) {
45
+ if (!query || typeof query !== 'object') return true;
46
+ return shouldIncludeReasoning(/** @type {Record<string, unknown>} */ (query).includeReasoning);
47
+ }
48
+
49
+ /** @param {string} rawUrl */
50
+ export function readIncludeReasoningFromUrl(rawUrl) {
51
+ if (typeof rawUrl !== 'string' || !rawUrl.includes('includeReasoning=')) return true;
52
+ try {
53
+ return shouldIncludeReasoning(new URL(rawUrl, 'http://openchamber.local').searchParams.get('includeReasoning'));
54
+ } catch {
55
+ return true;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Strip OpenChamber-only `includeReasoning` before OpenCode upstream.
61
+ * @param {string} requestUrl
62
+ */
63
+ export function stripIncludeReasoningParam(requestUrl) {
64
+ if (typeof requestUrl !== 'string' || !requestUrl.includes('includeReasoning=')) return requestUrl;
65
+ try {
66
+ const url = new URL(requestUrl, 'http://openchamber.local');
67
+ if (!url.searchParams.has('includeReasoning')) return requestUrl;
68
+ url.searchParams.delete('includeReasoning');
69
+ const search = url.searchParams.toString();
70
+ if (requestUrl.startsWith('http://') || requestUrl.startsWith('https://')) {
71
+ return `${url.origin}${url.pathname}${search ? `?${search}` : ''}${url.hash || ''}`;
72
+ }
73
+ return `${url.pathname}${search ? `?${search}` : ''}${url.hash || ''}`;
74
+ } catch {
75
+ return requestUrl;
76
+ }
77
+ }
78
+
79
+ /** @param {unknown} parts */
80
+ function projectParts(parts) {
81
+ if (!Array.isArray(parts) || parts.length === 0) return parts;
82
+ let changed = false;
83
+ const next = [];
84
+ for (const part of parts) {
85
+ if (isPlainObject(part) && part.type === 'reasoning') {
86
+ changed = true;
87
+ continue;
88
+ }
89
+ next.push(part);
90
+ }
91
+ return changed ? next : parts;
92
+ }
93
+
94
+ /**
95
+ * One message record: `{ info, parts }` (turn-page / exact / cache / session.messages).
96
+ * @param {unknown} record
97
+ */
98
+ function projectRecord(record) {
99
+ if (!isPlainObject(record) || !Array.isArray(record.parts)) return record;
100
+ const parts = projectParts(record.parts);
101
+ return parts === record.parts ? record : { ...record, parts };
102
+ }
103
+
104
+ /**
105
+ * Real Host payload shapes only:
106
+ * - bare array of records
107
+ * - `{ records: [...] }` (turn-page / reconcile / cache session)
108
+ * - `{ record }` (cache message)
109
+ * - `{ info, parts }` (exact message)
110
+ *
111
+ * @param {unknown} payload
112
+ */
113
+ export function projectMessagesPayloadWithoutReasoning(payload) {
114
+ if (Array.isArray(payload)) {
115
+ let changed = false;
116
+ const next = payload.map((entry) => {
117
+ const projected = projectRecord(entry);
118
+ if (projected !== entry) changed = true;
119
+ return projected;
120
+ });
121
+ return changed ? next : payload;
122
+ }
123
+
124
+ if (!isPlainObject(payload)) return payload;
125
+
126
+ let next = payload;
127
+ let changed = false;
128
+
129
+ if (Array.isArray(payload.records)) {
130
+ const records = projectMessagesPayloadWithoutReasoning(payload.records);
131
+ if (records !== payload.records) {
132
+ next = { ...next, records };
133
+ changed = true;
134
+ }
135
+ }
136
+
137
+ if (isPlainObject(payload.record)) {
138
+ const record = projectRecord(payload.record);
139
+ if (record !== payload.record) {
140
+ next = { ...next, record };
141
+ changed = true;
142
+ }
143
+ }
144
+
145
+ // Exact message / single record body: top-level parts.
146
+ if (Array.isArray(payload.parts)) {
147
+ const projected = projectRecord(next);
148
+ if (projected !== next) {
149
+ next = projected;
150
+ changed = true;
151
+ }
152
+ }
153
+
154
+ return changed ? next : payload;
155
+ }
156
+
157
+ /**
158
+ * @param {unknown} payload
159
+ * @param {boolean} includeReasoning
160
+ */
161
+ export function projectMessagesPayloadForReasoning(payload, includeReasoning) {
162
+ if (includeReasoning) return payload;
163
+ return projectMessagesPayloadWithoutReasoning(payload);
164
+ }
165
+
166
+ const eventBodyKeys = /** @type {const} */ (['properties', 'data']);
167
+
168
+ /** @param {unknown} payload */
169
+ function eventBodyOf(payload) {
170
+ if (!isPlainObject(payload)) return null;
171
+ for (const key of eventBodyKeys) {
172
+ const body = payload[key];
173
+ if (isPlainObject(body)) return body;
174
+ }
175
+ return null;
176
+ }
177
+
178
+ /**
179
+ * Stateful per-connection outbound filter. Classification map is bounded.
180
+ * @returns {{ projectEvent: (payload: unknown) => unknown | null, knownPartCount: () => number, dispose: () => void }}
181
+ */
182
+ export function createReasoningOutboundFilter() {
183
+ /** @type {Map<string, string>} */
184
+ const partTypes = new Map();
185
+
186
+ const partKey = (messageID, partID) => `${messageID}\0${partID}`;
187
+
188
+ const rememberPartType = (messageID, partID, type) => {
189
+ if (typeof messageID !== 'string' || !messageID) return;
190
+ if (typeof partID !== 'string' || !partID) return;
191
+ if (typeof type !== 'string' || !type) return;
192
+ const key = partKey(messageID, partID);
193
+ if (partTypes.size >= PART_TYPE_CACHE_LIMIT && !partTypes.has(key)) {
194
+ const oldest = partTypes.keys().next().value;
195
+ if (oldest !== undefined) partTypes.delete(oldest);
196
+ }
197
+ partTypes.set(key, type);
198
+ };
199
+
200
+ const forgetPart = (messageID, partID) => {
201
+ if (typeof messageID === 'string' && typeof partID === 'string') {
202
+ partTypes.delete(partKey(messageID, partID));
203
+ }
204
+ };
205
+
206
+ const forgetMessage = (messageID) => {
207
+ if (typeof messageID !== 'string' || !messageID) return;
208
+ const prefix = `${messageID}\0`;
209
+ for (const key of Array.from(partTypes.keys())) {
210
+ if (key.startsWith(prefix)) partTypes.delete(key);
211
+ }
212
+ };
213
+
214
+ /**
215
+ * @param {unknown} payload
216
+ * @returns {unknown | null} null = drop; otherwise original or shallow-cloned payload (type string preserved)
217
+ */
218
+ const projectEvent = (payload) => {
219
+ if (!isPlainObject(payload)) return payload;
220
+
221
+ const rawType = typeof payload.type === 'string' ? payload.type : '';
222
+ const type = baseEventType(rawType);
223
+
224
+ // session.next.reasoning.* (incl. versioned …reasoning.delta.1)
225
+ if (type.startsWith('session.next.reasoning.')) return null;
226
+
227
+ if (type === 'message.part.updated') {
228
+ const body = eventBodyOf(payload);
229
+ const part = body && isPlainObject(body.part) ? body.part : null;
230
+ if (part) {
231
+ const messageID =
232
+ (typeof part.messageID === 'string' && part.messageID)
233
+ || (typeof body.messageID === 'string' && body.messageID)
234
+ || '';
235
+ const partID = typeof part.id === 'string' ? part.id : '';
236
+ if (typeof part.type === 'string') rememberPartType(messageID, partID, part.type);
237
+ if (part.type === 'reasoning') return null;
238
+ }
239
+ return payload;
240
+ }
241
+
242
+ if (type === 'message.part.delta') {
243
+ const body = eventBodyOf(payload);
244
+ if (!body) return null;
245
+
246
+ const messageID = typeof body.messageID === 'string' ? body.messageID : '';
247
+ const partID = typeof body.partID === 'string' ? body.partID : '';
248
+ const field = typeof body.field === 'string' ? body.field : '';
249
+
250
+ if (REASONING_DELTA_FIELDS.has(field)) return null;
251
+
252
+ const learned = messageID && partID ? partTypes.get(partKey(messageID, partID)) : undefined;
253
+ if (learned === 'reasoning' || learned === undefined) return null;
254
+ return payload;
255
+ }
256
+
257
+ if (type === 'message.part.removed') {
258
+ const body = eventBodyOf(payload);
259
+ if (body) {
260
+ forgetPart(
261
+ typeof body.messageID === 'string' ? body.messageID : '',
262
+ typeof body.partID === 'string' ? body.partID : '',
263
+ );
264
+ }
265
+ return payload;
266
+ }
267
+
268
+ if (type === 'message.removed') {
269
+ const body = eventBodyOf(payload);
270
+ if (body) {
271
+ const messageID =
272
+ (typeof body.messageID === 'string' && body.messageID)
273
+ || (isPlainObject(body.info) && typeof body.info.id === 'string' && body.info.id)
274
+ || '';
275
+ forgetMessage(messageID);
276
+ }
277
+ return payload;
278
+ }
279
+
280
+ // message.updated / message.created may embed parts on properties or data.
281
+ if (type === 'message.updated' || type === 'message.created') {
282
+ let next = payload;
283
+ let changed = false;
284
+ for (const key of eventBodyKeys) {
285
+ const body = next[key];
286
+ if (!isPlainObject(body) || !Array.isArray(body.parts)) continue;
287
+ const parts = projectParts(body.parts);
288
+ if (parts === body.parts) continue;
289
+ next = { ...next, [key]: { ...body, parts } };
290
+ changed = true;
291
+ }
292
+ return changed ? next : payload;
293
+ }
294
+
295
+ return payload;
296
+ };
297
+
298
+ return {
299
+ projectEvent,
300
+ knownPartCount: () => partTypes.size,
301
+ dispose: () => {
302
+ partTypes.clear();
303
+ },
304
+ };
305
+ }
306
+
307
+ /**
308
+ * Project SSE data JSON; preserve GlobalEvent `{ directory, payload }` wrap.
309
+ * @param {unknown} dataJson
310
+ * @param {{ projectEvent: (payload: unknown) => unknown | null }} filter
311
+ */
312
+ function projectSseDataJson(dataJson, filter) {
313
+ if (!isPlainObject(dataJson)) {
314
+ return { action: 'keep', value: dataJson, changed: false };
315
+ }
316
+
317
+ if (isPlainObject(dataJson.payload) && typeof dataJson.payload.type === 'string') {
318
+ const projected = filter.projectEvent(dataJson.payload);
319
+ if (projected == null) return { action: 'drop' };
320
+ if (projected === dataJson.payload) return { action: 'keep', value: dataJson, changed: false };
321
+ return { action: 'keep', value: { ...dataJson, payload: projected }, changed: true };
322
+ }
323
+
324
+ const projected = filter.projectEvent(dataJson);
325
+ if (projected == null) return { action: 'drop' };
326
+ if (projected === dataJson) return { action: 'keep', value: dataJson, changed: false };
327
+ return { action: 'keep', value: projected, changed: true };
328
+ }
329
+
330
+ /**
331
+ * Filter one SSE block (no trailing separator). null = drop.
332
+ * @param {string} block
333
+ * @param {{ projectEvent: (payload: unknown) => unknown | null }} filter
334
+ * @returns {string | null}
335
+ */
336
+ export function filterSseBlock(block, filter) {
337
+ if (typeof block !== 'string' || block.trim().length === 0) return null;
338
+ if (!block.includes('data:')) return `${block}\n\n`;
339
+
340
+ const lines = block.split('\n');
341
+ const dataLines = [];
342
+ const otherLines = [];
343
+ for (const line of lines) {
344
+ if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^\s/, ''));
345
+ else otherLines.push(line);
346
+ }
347
+ if (dataLines.length === 0) return `${block}\n\n`;
348
+
349
+ const payloadText = dataLines.join('\n').trim();
350
+ if (!payloadText) return `${block}\n\n`;
351
+
352
+ let parsed;
353
+ try {
354
+ parsed = JSON.parse(payloadText);
355
+ } catch {
356
+ return `${block}\n\n`;
357
+ }
358
+
359
+ const result = projectSseDataJson(parsed, filter);
360
+ if (result.action === 'drop') return null;
361
+ if (!result.changed) return `${block}\n\n`;
362
+ return [...otherLines, `data: ${JSON.stringify(result.value)}`, '', ''].join('\n');
363
+ }
364
+
365
+ /**
366
+ * Incremental SSE block splitter. Holds a trailing CR so `\r` + `\n` across
367
+ * chunks never invents a false blank line (which would split one event in two).
368
+ */
369
+ export function createSseBlockSplitter() {
370
+ const decoder = new TextDecoder();
371
+ let buffer = '';
372
+ /** @type {string} trailing CR held until the next chunk clarifies `\r\n` vs lone `\r` */
373
+ let carryCr = '';
374
+
375
+ const appendNormalized = (text) => {
376
+ if (!text && !carryCr) return;
377
+ let input = carryCr + (text || '');
378
+ carryCr = '';
379
+ if (input.endsWith('\r')) {
380
+ carryCr = '\r';
381
+ input = input.slice(0, -1);
382
+ }
383
+ // Complete CRLF pairs first, then any remaining lone CR → LF.
384
+ buffer += input.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
385
+ };
386
+
387
+ const drainBlocks = () => {
388
+ const blocks = [];
389
+ while (true) {
390
+ const idx = buffer.indexOf('\n\n');
391
+ if (idx === -1) break;
392
+ const block = buffer.slice(0, idx);
393
+ buffer = buffer.slice(idx + 2);
394
+ if (block.trim().length > 0) blocks.push(block);
395
+ }
396
+ return blocks;
397
+ };
398
+
399
+ return {
400
+ /**
401
+ * @param {Uint8Array | string} chunk
402
+ * @returns {string[]}
403
+ */
404
+ push(chunk) {
405
+ const text = typeof chunk === 'string'
406
+ ? chunk
407
+ : decoder.decode(chunk, { stream: true });
408
+ if (!text) return [];
409
+ appendNormalized(text);
410
+ return drainBlocks();
411
+ },
412
+ /** @returns {string[]} */
413
+ finish() {
414
+ const tail = decoder.decode();
415
+ if (tail) appendNormalized(tail);
416
+ if (carryCr) {
417
+ buffer += '\n';
418
+ carryCr = '';
419
+ }
420
+ const remaining = buffer;
421
+ buffer = '';
422
+ return remaining.trim().length > 0 ? [remaining] : [];
423
+ },
424
+ };
425
+ }