@jacexh/claude-web-console 0.12.2 → 0.12.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacexh/claude-web-console",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "description": "Web-based console for Claude Code — manage sessions, switch models, preview artifacts",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { waitForSessionId } from '../session-id-resolver'
3
+
4
+ describe('waitForSessionId', () => {
5
+ it('resolves when session.sessionId becomes available', async () => {
6
+ let ready = false
7
+ const session = {
8
+ get sessionId() {
9
+ if (!ready) throw new Error('not ready')
10
+ return 'real-session-id'
11
+ },
12
+ }
13
+
14
+ // Simulate SDK resolving sessionId after 100ms
15
+ setTimeout(() => { ready = true }, 100)
16
+
17
+ const result = await waitForSessionId(session as any, 5000)
18
+ expect(result).toBe('real-session-id')
19
+ })
20
+
21
+ it('resolves immediately when sessionId is already available', async () => {
22
+ const session = { sessionId: 'already-ready' }
23
+ const result = await waitForSessionId(session as any, 5000)
24
+ expect(result).toBe('already-ready')
25
+ })
26
+
27
+ it('rejects on timeout if sessionId never becomes available', async () => {
28
+ const session = {
29
+ get sessionId(): string { throw new Error('not ready') },
30
+ }
31
+
32
+ await expect(waitForSessionId(session as any, 200)).rejects.toThrow('Session init timed out')
33
+ })
34
+
35
+ it('ignores pending- prefixed session IDs', async () => {
36
+ let callCount = 0
37
+ const session = {
38
+ get sessionId() {
39
+ callCount++
40
+ if (callCount < 3) return 'pending-123'
41
+ return 'real-id'
42
+ },
43
+ }
44
+
45
+ const result = await waitForSessionId(session as any, 5000)
46
+ expect(result).toBe('real-id')
47
+ })
48
+ })
@@ -0,0 +1,39 @@
1
+ import type { SDKSession } from '@anthropic-ai/claude-agent-sdk'
2
+
3
+ /**
4
+ * Polls session.sessionId until the SDK resolves the real ID.
5
+ * The SDK sets sessionId asynchronously after process init.
6
+ */
7
+ export function waitForSessionId(session: SDKSession, timeoutMs: number): Promise<string> {
8
+ return new Promise((resolve, reject) => {
9
+ const timeout = setTimeout(() => {
10
+ clearInterval(poll)
11
+ reject(new Error('Session init timed out'))
12
+ }, timeoutMs)
13
+
14
+ const poll = setInterval(() => {
15
+ try {
16
+ const id = session.sessionId
17
+ if (id && !id.startsWith('pending-')) {
18
+ clearInterval(poll)
19
+ clearTimeout(timeout)
20
+ resolve(id)
21
+ }
22
+ } catch {
23
+ // sessionId not ready yet, keep polling
24
+ }
25
+ }, 50)
26
+
27
+ // Check immediately (no need to wait 50ms if already ready)
28
+ try {
29
+ const id = session.sessionId
30
+ if (id && !id.startsWith('pending-')) {
31
+ clearInterval(poll)
32
+ clearTimeout(timeout)
33
+ resolve(id)
34
+ }
35
+ } catch {
36
+ // not ready, poll will handle it
37
+ }
38
+ })
39
+ }
@@ -19,6 +19,7 @@ import type { FastifyBaseLogger } from 'fastify'
19
19
  import type { SessionInfo, EffortLevel } from './types.js'
20
20
  import { shouldBroadcastTurnStarted, shouldResetToIdleOnStreamEnd, isTurnMessage, type TurnState } from './turn-lifecycle.js'
21
21
  import { SessionStatusTracker } from './session-status.js'
22
+ import { waitForSessionId } from './session-id-resolver.js'
22
23
 
23
24
  type PermissionResolver = {
24
25
  resolve: (approved: boolean, reason?: string, updatedPermissions?: import('@anthropic-ai/claude-agent-sdk').PermissionUpdate[]) => void
@@ -158,7 +159,7 @@ export class SessionManager {
158
159
  private streamingSessionIds = new Set<string>()
159
160
  private sessionListeners = new Map<string, Set<SessionListener>>()
160
161
  private idleTimers = new Map<string, ReturnType<typeof setTimeout>>()
161
- private pendingRemaps = new Map<string, { sessionIdRef: { current: string }; resolve?: (sessionId: string) => void }>()
162
+ private pendingRemaps = new Map<string, { sessionIdRef: { current: string } }>()
162
163
  // Track cwd for each session so we can resume in the correct project
163
164
  private sessionCwds = new Map<string, string>()
164
165
  /** User-supplied creation options that must survive resume cycles */
@@ -384,20 +385,13 @@ export class SessionManager {
384
385
  })
385
386
 
386
387
  // Store tempId in pendingRemaps for remap inside consumeStream
387
- const sessionIdReady = new Promise<string>((resolve) => {
388
- this.pendingRemaps.set(tempId, { sessionIdRef, resolve })
389
- })
388
+ this.pendingRemaps.set(tempId, { sessionIdRef })
390
389
 
391
390
  // For new sessions, start stream immediately — SDK may emit init messages
392
391
  this.startStreamConsumer(tempId, session)
393
392
 
394
- // Wait for real sessionId (resolved when consumeStream processes first SDK message)
395
- const timeoutMs = 10_000
396
- const sessionId = await Promise.race([
397
- sessionIdReady,
398
- new Promise<string>((_, reject) => setTimeout(() => reject(new Error('Session init timed out')), timeoutMs)),
399
- ])
400
-
393
+ // Poll session.sessionId directly don't depend on consumeStream remap timing
394
+ const sessionId = await waitForSessionId(session, 30_000)
401
395
  return sessionId
402
396
  }
403
397
 
@@ -537,8 +531,6 @@ export class SessionManager {
537
531
  if (prevStatus) this.sessionStatus.set(sessionId, prevStatus)
538
532
  const q = this.activeQueries.get(tempId)
539
533
  if (q) { this.activeQueries.delete(tempId); this.activeQueries.set(sessionId, q) }
540
- // Resolve the sessionId promise before cleaning up the remap entry
541
- if (remap.resolve) remap.resolve(sessionId)
542
534
  this.pendingRemaps.delete(tempId)
543
535
  // Update our tracking variable
544
536
  currentSessionId = sessionId