Files
openchamber/packages/ui/src/sync/streaming.ts
T
00821700de chore: remove dead code (59 unused files + ~125 unused exports) (#1835)
* chore: remove dead/unreferenced files across ui, vscode

Remove 59 unused source files (components, hooks, lib utils, stores,
barrels, and orphaned vscode github modules) that are not imported by
any entry-reachable code. Also drop a stale test mock for the removed
execCommands module.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove unused exported symbols (types, functions, consts, hooks)

Remove exported symbols whose identifier is referenced nowhere in the
repository (verified via repo-wide search), across ui types/contracts,
lib utilities, sync layer, stores, and components. Also drop the few
imports/private helpers orphaned by these removals.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove more unused exports (desktop, shortcuts, worktree, vscode)

Continue removing repo-wide unreferenced exported functions, consts and
types across lib/desktop, shortcuts, worktreeSessionCreator, sync, and
vscode gitService, with cascading orphaned helpers/imports cleaned up.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* chore: add dead-code cleanup tooling

* refactor: checkpoint dead-code cleanup

* refactor: remove dead-code suppressions

---------

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-26 19:27:53 +03:00

143 lines
4.3 KiB
TypeScript

/**
* Streaming lifecycle tracking.
*
* Derives streaming state from the sync child store's session_status and
* message/part updates. Components read this to know which messages are
* currently streaming and their lifecycle phase.
*/
import { create } from "zustand"
import type { Message, SessionStatus } from "@opencode-ai/sdk/v2/client"
import type { State } from "./types"
type StreamPhase = "streaming" | "cooldown" | "completed"
type MessageStreamState = {
phase: StreamPhase
startedAt: number
lastUpdateAt: number
completedAt?: number
}
export type StreamingStore = {
/** Currently streaming message per session */
streamingMessageIds: Map<string, string | null>
/** Lifecycle phase per message */
messageStreamStates: Map<string, MessageStreamState>
}
export const useStreamingStore = create<StreamingStore>()(() => ({
streamingMessageIds: new Map(),
messageStreamStates: new Map(),
}))
export function resetStreamingState() {
useStreamingStore.setState({
streamingMessageIds: new Map(),
messageStreamStates: new Map(),
})
}
/**
* Called from the SyncBridge/flush handler when child store state changes.
* Derives streaming state from session_status + messages.
*/
/** Only update lastUpdateAt every this many ms to avoid 60Hz store churn */
const STREAMING_HEARTBEAT_MS = 1000
export function updateStreamingState(state: State) {
const now = Date.now()
const currentStore = useStreamingStore.getState()
const currentStreamingIds = currentStore.streamingMessageIds
const currentStreamStates = currentStore.messageStreamStates
const nextStreamingIds = new Map<string, string | null>()
const nextStreamStates = new Map(currentStreamStates)
let changed = false
// Fast path: only scan sessions that are actually busy.
// Idle sessions are handled by checking against currentStreamingIds below.
const busySessionIds = new Set<string>()
for (const [sessionID, status] of Object.entries(state.session_status ?? {})) {
if ((status as SessionStatus).type === "busy") {
busySessionIds.add(sessionID)
}
}
const completeStreamingMessage = (sessionID: string, msgId: string) => {
nextStreamingIds.set(sessionID, null)
const existing = nextStreamStates.get(msgId)
if (existing && existing.phase === "streaming") {
nextStreamStates.set(msgId, {
...existing,
phase: "completed",
completedAt: now,
})
}
changed = true
}
for (const sessionID of busySessionIds) {
const messages = state.message[sessionID]
if (!messages || messages.length === 0) continue
// Only the trailing assistant turn can be streaming. If a new user turn is
// last, the next assistant message has not arrived yet.
let streamingMsg: Message | null = null
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === "user") {
break
}
if (messages[i].role === "assistant") {
streamingMsg = messages[i]
break
}
}
if (!streamingMsg) {
const prevId = currentStreamingIds.get(sessionID)
if (prevId) {
completeStreamingMessage(sessionID, prevId)
}
continue
}
const prevId = currentStreamingIds.get(sessionID)
if (prevId !== streamingMsg.id) changed = true
nextStreamingIds.set(sessionID, streamingMsg.id)
const existing = nextStreamStates.get(streamingMsg.id)
if (!existing || existing.phase !== "streaming") {
nextStreamStates.set(streamingMsg.id, {
phase: "streaming",
startedAt: existing?.startedAt ?? now,
lastUpdateAt: now,
})
changed = true
} else if (now - existing.lastUpdateAt >= STREAMING_HEARTBEAT_MS) {
// Throttle lastUpdateAt writes to ~1Hz instead of 60Hz
nextStreamStates.set(streamingMsg.id, {
...existing,
lastUpdateAt: now,
})
changed = true
}
}
// Mark completed any previously streaming sessions that are now idle or gone
for (const [sessionID, msgId] of currentStreamingIds) {
if (!msgId) continue
const isStillBusy = busySessionIds.has(sessionID)
if (isStillBusy) continue
completeStreamingMessage(sessionID, msgId)
}
if (changed) {
useStreamingStore.setState({
streamingMessageIds: nextStreamingIds,
messageStreamStates: nextStreamStates,
})
}
}