Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
- Prefer composable primitives (signals, hooks, utilities) over deep inheritance or implicit global state.
- When adding platform integrations (SSE, IPC, SDK), isolate them in thin adapters that surface typed events/actions.

## PR Review Principles
- **Check for regressions first.** Before approving any change, verify the existing behavior still works — run the test suite, test manually on both mobile and desktop, and confirm no unintended side effects in related subsystems.
- **Look for better possible implementations.** Don't settle for the first working approach. Ask: is there a simpler way? Does the codebase already have a pattern for this? Would a different abstraction reduce future maintenance cost?
- **Be the PR gatekeeper.** Every line merged becomes technical debt someone else will read. If it's unclear, fragile, or lacks tests, push back. The reviewer's job is to protect the codebase, not to be nice.
- **Be ruthless about code quality.** Surface-level "LGTM" is negligence. Inspect: naming, error handling, edge cases, type safety, logging (is it useful or just noise?), performance (any unnecessary allocations or re-renders?), and whether the change respects existing architectural boundaries.
- **Test before responding to review comments.** Never reply "works for me" or "this fixes it" without deploying the exact commit and verifying the behavior. Untested responses waste reviewer time and erode trust.
- **UI and server must be built from the same version.** Version mismatches between UI and server cause subtle bugs (e.g., sessions disappearing). Always build both from the same commit before testing.

## Multi-Language Support (i18n)

The UI uses a small custom i18n layer (no ICU/messageformat). When building features, never hardcode user-visible strings.
Expand Down
8 changes: 8 additions & 0 deletions packages/ui/src/lib/server-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function logSse(message: string, context?: Record<string, unknown>) {
class ServerEvents {
private handlers = new Map<WorkspaceEventType | "*", Set<(event: WorkspaceEventPayload) => void>>()
private openHandlers = new Set<() => void>()
private disconnectHandlers = new Set<() => void>()
private connection: WorkspaceEventConnection | null = null
private connectGeneration = 0
private retryDelay = RETRY_BASE_DELAY
Expand Down Expand Up @@ -105,6 +106,8 @@ class ServerEvents {
this.connection = null
}

this.disconnectHandlers.forEach((handler) => handler())

logSse("Events stream disconnected, scheduling reconnect", { delayMs: this.retryDelay })
this.retryTimer = setTimeout(() => {
this.retryTimer = null
Expand Down Expand Up @@ -154,6 +157,11 @@ class ServerEvents {
return () => this.openHandlers.delete(handler)
}

onDisconnect(handler: () => void): () => void {
this.disconnectHandlers.add(handler)
return () => this.disconnectHandlers.delete(handler)
}

restart(reason = "manual restart"): void {
this.retryDelay = RETRY_BASE_DELAY
this.clearReconnectTimer()
Expand Down
26 changes: 26 additions & 0 deletions packages/ui/src/lib/sse-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ const [connectionStatus, setConnectionStatus] = createSignal<Map<string, Connect

class SSEManager {
constructor() {
log.info("sseManager initialized: listening for SSE disconnect and reconnect")

serverEvents.on("instance.eventStatus", (event) => {
const payload = event as InstanceStatusPayload
this.updateConnectionStatus(payload.instanceId, payload.status)
Expand All @@ -118,6 +120,30 @@ class SSEManager {
this.updateConnectionStatus(payload.instanceId, "connected")
this.handleEvent(payload.instanceId, payload.event as SSEEvent)
})

serverEvents.onDisconnect(() => {
log.info("SSE transport disconnected → setting all instances to 'connecting'")
setConnectionStatus((prev) => {
const next = new Map(prev)
for (const [id] of next) {
next.set(id, "connecting")
}
return next
})
})

serverEvents.onOpen(() => {
log.info("SSE transport reconnected → clearing 'connecting' status")
setConnectionStatus((prev) => {
const next = new Map(prev)
for (const [id, status] of next) {
if (status === "connecting") {
next.delete(id)
}
}
return next
})
})
}

seedStatus(instanceId: string, status: ConnectionStatus) {
Expand Down
Loading