@glwhappen/web-code 1.32.6 → 1.32.9

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 (30) hide show
  1. package/dist/assets/{index-BceOOjmP.js → index-D_7CSvqO.js} +239 -237
  2. package/dist/assets/index-DdxLnCfK.css +32 -0
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +4 -4
  5. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
  6. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +7 -1
  7. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  8. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +7 -1
  9. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  10. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +7 -1
  11. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  12. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +6 -0
  13. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -1
  14. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +49 -39
  15. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  16. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +5 -3
  17. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  18. package/dist-server/server/modules/websocket/services/websocket-writer.service.js +6 -0
  19. package/dist-server/server/modules/websocket/services/websocket-writer.service.js.map +1 -1
  20. package/package.json +1 -1
  21. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +4 -4
  22. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +9 -1
  23. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -1
  24. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +9 -2
  25. package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +8 -0
  26. package/server/modules/providers/services/sessions-watcher.service.ts +60 -40
  27. package/server/modules/websocket/services/chat-websocket.service.ts +5 -3
  28. package/server/modules/websocket/services/websocket-writer.service.ts +7 -0
  29. package/server/shared/types.ts +1 -0
  30. package/dist/assets/index-hQY0reSj.css +0 -32
@@ -86,6 +86,10 @@ export class GeminiSessionSynchronizer implements IProviderSessionSynchronizer {
86
86
  continue;
87
87
  }
88
88
 
89
+ if (!projectsDb.getProjectPath(ownerUserId, parsed.projectPath)) {
90
+ continue;
91
+ }
92
+
89
93
  const timestamps = await readFileTimestamps(filePath);
90
94
  sessionsDb.createSession(
91
95
  ownerUserId,
@@ -122,6 +126,10 @@ export class GeminiSessionSynchronizer implements IProviderSessionSynchronizer {
122
126
  return null;
123
127
  }
124
128
 
129
+ if (!projectsDb.getProjectPath(ownerUserId, parsed.projectPath)) {
130
+ return null;
131
+ }
132
+
125
133
  const timestamps = await readFileTimestamps(filePath);
126
134
  return sessionsDb.createSession(
127
135
  ownerUserId,
@@ -8,22 +8,22 @@ import { sessionSynchronizerService } from '@/modules/providers/services/session
8
8
  import { WS_OPEN_STATE, connectedClients } from '@/modules/websocket/index.js';
9
9
  import type { LLMProvider } from '@/shared/types.js';
10
10
  import { getProjectsWithSessions } from '@/modules/projects/index.js';
11
- import { getDefaultOwnerUserId } from '@/shared/default-user.js';
11
+ import { userDb } from '@/modules/database/index.js';
12
12
  import { AppError } from '@/shared/utils.js';
13
13
 
14
14
  /**
15
- * Resolves the user id used to attribute background-watcher synchronizations.
15
+ * Resolves active app users that should receive background synchronizations.
16
16
  *
17
- * Returns `null` (with a console warning) when no user has registered yet so
17
+ * Returns an empty list (with a console warning) when no user has registered yet so
18
18
  * that the watcher can keep running silently until the first account exists.
19
19
  */
20
- function resolveWatcherOwnerUserId(): number | null {
20
+ function resolveWatcherOwnerUserIds(): number[] {
21
21
  try {
22
- return getDefaultOwnerUserId();
22
+ return userDb.listUsers().map((user) => Number(user.id)).filter((id) => Number.isFinite(id));
23
23
  } catch (error) {
24
24
  if (error instanceof AppError && error.code === 'DEFAULT_USER_NOT_AVAILABLE') {
25
25
  console.warn('Session watcher skipping synchronization: no registered user yet');
26
- return null;
26
+ return [];
27
27
  }
28
28
  throw error;
29
29
  }
@@ -161,33 +161,38 @@ async function flushPendingWatcherUpdate(): Promise<void> {
161
161
  watcherRefreshInFlight = true;
162
162
 
163
163
  try {
164
- const ownerUserId = resolveWatcherOwnerUserId();
165
- const updatedProjects = ownerUserId !== null
166
- ? await getProjectsWithSessions(ownerUserId, { skipSynchronization: true })
167
- : [];
168
164
  const changeTypes = Array.from(queuedUpdate.changeTypes);
169
165
  const watchProviders = Array.from(queuedUpdate.providers);
170
166
  const updatedSessionIds = Array.from(queuedUpdate.updatedSessionIds);
167
+ const targetUserIds = Array.from(
168
+ new Set(
169
+ Array.from(connectedClients)
170
+ .map((client) => Number(client.userId))
171
+ .filter((userId) => Number.isFinite(userId)),
172
+ ),
173
+ );
174
+
175
+ await Promise.all(targetUserIds.map(async (userId) => {
176
+ const updatedProjects = await getProjectsWithSessions(userId, { skipSynchronization: true });
177
+ const updateMessage = JSON.stringify({
178
+ type: 'projects_updated',
179
+ projects: updatedProjects,
180
+ timestamp: new Date().toISOString(),
181
+ changeType: changeTypes[0] ?? 'change',
182
+ updatedSessionId: updatedSessionIds[0] ?? undefined,
183
+ watchProvider: watchProviders[0] ?? undefined,
184
+ changeTypes,
185
+ updatedSessionIds,
186
+ watchProviders,
187
+ batched: true,
188
+ });
171
189
 
172
- // Backward-compatible fields stay populated with the first queued values.
173
- const updateMessage = JSON.stringify({
174
- type: 'projects_updated',
175
- projects: updatedProjects,
176
- timestamp: new Date().toISOString(),
177
- changeType: changeTypes[0] ?? 'change',
178
- updatedSessionId: updatedSessionIds[0] ?? undefined,
179
- watchProvider: watchProviders[0] ?? undefined,
180
- changeTypes,
181
- updatedSessionIds,
182
- watchProviders,
183
- batched: true,
184
- });
185
-
186
- connectedClients.forEach(client => {
187
- if (client.readyState === WS_OPEN_STATE) {
188
- client.send(updateMessage);
189
- }
190
- });
190
+ connectedClients.forEach((client) => {
191
+ if (client.readyState === WS_OPEN_STATE && Number(client.userId) === userId) {
192
+ client.send(updateMessage);
193
+ }
194
+ });
195
+ }));
191
196
  } catch (error) {
192
197
  const message = error instanceof Error ? error.message : String(error);
193
198
  console.error('Session watcher refresh failed while broadcasting projects_updated', { error: message });
@@ -214,21 +219,28 @@ async function onUpdate(
214
219
  }
215
220
 
216
221
  try {
217
- const ownerUserId = resolveWatcherOwnerUserId();
218
- if (ownerUserId === null) {
222
+ const ownerUserIds = resolveWatcherOwnerUserIds();
223
+ if (ownerUserIds.length === 0) {
219
224
  return;
220
225
  }
221
226
 
222
- const result = await sessionSynchronizerService.synchronizeProviderFile(ownerUserId, provider, filePath);
223
- if (!result.indexed) {
227
+ const results = await Promise.all(
228
+ ownerUserIds.map((ownerUserId) =>
229
+ sessionSynchronizerService.synchronizeProviderFile(ownerUserId, provider, filePath),
230
+ ),
231
+ );
232
+ const indexedResults = results.filter((result) => result.indexed);
233
+ if (indexedResults.length === 0) {
224
234
  return;
225
235
  }
226
236
 
227
237
  console.log(`Session synchronization triggered by ${eventType} event for provider "${provider}"`, {
228
238
  filePath,
229
- sessionId: result.sessionId,
239
+ sessionIds: indexedResults.map((result) => result.sessionId).filter(Boolean),
240
+ });
241
+ indexedResults.forEach((result) => {
242
+ queuePendingWatcherUpdate(eventType, provider, result.sessionId);
230
243
  });
231
- queuePendingWatcherUpdate(eventType, provider, result.sessionId);
232
244
  } catch (error) {
233
245
  const message = error instanceof Error ? error.message : String(error);
234
246
  console.error(`Session watcher sync failed for provider "${provider}"`, {
@@ -245,12 +257,20 @@ async function onUpdate(
245
257
  export async function initializeSessionsWatcher(): Promise<void> {
246
258
  console.log('Setting up session watchers');
247
259
 
248
- const initialOwnerUserId = resolveWatcherOwnerUserId();
249
- if (initialOwnerUserId !== null) {
250
- const initialSync = await sessionSynchronizerService.synchronizeSessions(initialOwnerUserId);
260
+ const initialOwnerUserIds = resolveWatcherOwnerUserIds();
261
+ if (initialOwnerUserIds.length > 0) {
262
+ const initialSyncResults = await Promise.all(
263
+ initialOwnerUserIds.map(async (ownerUserId) => ({
264
+ userId: ownerUserId,
265
+ result: await sessionSynchronizerService.synchronizeSessions(ownerUserId),
266
+ })),
267
+ );
251
268
  console.log('Initial session synchronization complete', {
252
- processedByProvider: initialSync.processedByProvider,
253
- failures: initialSync.failures,
269
+ users: initialSyncResults.map(({ userId, result }) => ({
270
+ userId,
271
+ processedByProvider: result.processedByProvider,
272
+ failures: result.failures,
273
+ })),
254
274
  });
255
275
  } else {
256
276
  console.log('Initial session synchronization skipped: no registered user yet');
@@ -127,9 +127,11 @@ export function handleChatConnection(
127
127
  dependencies: ChatWebSocketDependencies
128
128
  ): void {
129
129
  console.log('[INFO] Chat WebSocket connected');
130
- connectedClients.add(ws);
130
+ const clientConnection = ws as WebSocket & { userId?: string | number | null };
131
+ clientConnection.userId = readRequestUserId(request);
132
+ connectedClients.add(clientConnection);
131
133
 
132
- const writer = new WebSocketWriter(ws, readRequestUserId(request));
134
+ const writer = new WebSocketWriter(ws, clientConnection.userId ?? null);
133
135
 
134
136
  const sendAuthorizationError = (error: AppError, provider: LLMProvider): void => {
135
137
  writer.send({
@@ -338,6 +340,6 @@ export function handleChatConnection(
338
340
 
339
341
  ws.on('close', () => {
340
342
  console.log('[INFO] Chat client disconnected');
341
- connectedClients.delete(ws);
343
+ connectedClients.delete(clientConnection);
342
344
  });
343
345
  }
@@ -21,6 +21,13 @@ export class WebSocketWriter {
21
21
  send(data: unknown): void {
22
22
  if (this.ws.readyState === WS_OPEN_STATE) {
23
23
  this.ws.send(JSON.stringify(data));
24
+ } else {
25
+ const kind = (data as Record<string, unknown>)?.kind;
26
+ if (kind === 'complete' || kind === 'error') {
27
+ console.warn(
28
+ `[WebSocketWriter] Dropped "${kind}" event — socket not open (readyState=${this.ws.readyState}, session=${this.sessionId})`,
29
+ );
30
+ }
24
31
  }
25
32
  }
26
33
 
@@ -32,6 +32,7 @@ export type AnyRecord = Record<string, any>;
32
32
  export type RealtimeClientConnection = {
33
33
  readyState: number;
34
34
  send(data: string): void;
35
+ userId?: string | number | null;
35
36
  };
36
37
 
37
38
  /**