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>
This commit is contained in:
Serhii Dziupin
2026-06-26 19:27:53 +03:00
committed by GitHub
co-authored by Serhii Dziupin Bohdan Triapitsyn
parent 4a37b9a005
commit 00821700de
324 changed files with 444 additions and 14876 deletions
-20
View File
@@ -23,24 +23,4 @@ export namespace Binary {
return { found: false, index: left }
}
export function insert<T>(array: T[], item: T, compare: (item: T) => string): T[] {
const id = compare(item)
let left = 0
let right = array.length
while (left < right) {
const mid = Math.floor((left + right) / 2)
const midId = compare(array[mid])
if (midId < id) {
left = mid + 1
} else {
right = mid
}
}
array.splice(left, 0, item)
return array
}
}
-17
View File
@@ -63,11 +63,6 @@ export function evictContentLru(keep: Set<string> | undefined, evict: (path: str
}
}
export function resetContentLru() {
lru.clear()
total = 0
}
export function setContentBytes(path: string, bytes: number) {
setBytes(path, bytes)
}
@@ -79,15 +74,3 @@ export function removeContentBytes(path: string) {
export function touchContent(path: string, bytes?: number) {
touch(path, bytes)
}
export function getContentBytesTotal(): number {
return total
}
export function getContentEntryCount(): number {
return lru.size
}
export function hasContent(path: string): boolean {
return lru.has(path)
}
+1 -7
View File
@@ -14,7 +14,7 @@ const FLAG_KEY = "openchamber:sync:debug"
let _enabled: boolean | undefined
export function isSyncDebugEnabled(): boolean {
function isSyncDebugEnabled(): boolean {
if (_enabled !== undefined) return _enabled
try {
_enabled = typeof localStorage !== "undefined" && localStorage.getItem(FLAG_KEY) === "1"
@@ -23,12 +23,6 @@ export function isSyncDebugEnabled(): boolean {
}
return _enabled
}
/** Force-refresh the flag (call after user toggles localStorage). */
export function refreshSyncDebugFlag(): void {
_enabled = undefined
}
type SyncDebugCategory = "pipeline" | "reducer" | "dispatch"
function log(cat: SyncDebugCategory, ...args: unknown[]): void {
-7
View File
@@ -18,13 +18,6 @@ import { getRuntimeUrlResolver } from "@/lib/runtime-url"
import { clearRuntimeUrlAuthToken, refreshRuntimeUrlAuthToken } from "@/lib/runtime-auth"
import { syncDebug } from "./debug"
export type QueuedEvent = {
directory: string
payload: Event
}
export type FlushHandler = (events: QueuedEvent[]) => void
const FLUSH_FRAME_MS = 33
const BACKPRESSURE_FLUSH_FRAME_MS = 200
const BACKPRESSURE_MODE_MS = 10_000
@@ -16,12 +16,3 @@ export const useGlobalSyncStore = create<GlobalSyncStore>()((set) => ({
reset: () => set(INITIAL_GLOBAL_STATE),
},
}))
// Fine-grained selectors — use these in components for minimal re-renders
export const selectReady = (s: GlobalSyncStore) => s.ready
export const selectProjects = (s: GlobalSyncStore) => s.projects
export const selectProviders = (s: GlobalSyncStore) => s.providers
export const selectConfig = (s: GlobalSyncStore) => s.config
export const selectPath = (s: GlobalSyncStore) => s.path
export const selectReload = (s: GlobalSyncStore) => s.reload
export const selectSessionTodo = (s: GlobalSyncStore) => s.sessionTodo
-158
View File
@@ -1,158 +0,0 @@
// Core utilities
export { Binary } from "./binary"
export { retry, type RetryOptions } from "./retry"
// Types
export type { State, GlobalState, ProjectMeta, DirState, EvictPlan, DisposeCheck, ChildOptions } from "./types"
export {
INITIAL_STATE,
INITIAL_GLOBAL_STATE,
MAX_DIR_STORES,
DIR_IDLE_TTL_MS,
SESSION_CACHE_LIMIT,
SESSION_RECENT_LIMIT,
SESSION_RECENT_WINDOW,
} from "./types"
// Eviction
export { pickDirectoriesToEvict, canDisposeDirectory } from "./eviction"
// Session cache
export { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
// Optimistic
export {
applyOptimisticAdd,
applyOptimisticRemove,
mergeOptimisticPage,
mergeMessages,
type OptimisticItem,
type OptimisticStore,
type OptimisticAddInput,
type OptimisticRemoveInput,
type MessagePage,
} from "./optimistic"
// Event reducer
export {
reduceGlobalEvent,
applyGlobalProject,
applyDirectoryEvent,
type GlobalEventResult,
} from "./event-reducer"
// Event pipeline
export { createEventPipeline, type QueuedEvent, type FlushHandler } from "./event-pipeline"
// Stores
export { useGlobalSyncStore, type GlobalSyncStore } from "./global-sync-store"
export { ChildStoreManager, type DirectoryStore } from "./child-store"
// Bootstrap
export { bootstrapGlobal, bootstrapDirectory } from "./bootstrap"
// React integration
export {
SyncProvider,
useGlobalSync,
useGlobalSyncSelector,
useDirectoryStore,
useDirectorySync,
useSessionMessages,
useSessionMessageCount,
useSessionMessagesResolved,
useSessionParts,
useSessionStatus,
useSessionPermissions,
useSessionQuestions,
useSessions,
useSyncSDK,
useSyncDirectory,
useChildStoreManager,
useSessionMessageRecords,
useEnsureSessionMessages,
useSessionTextMessages,
useUserMessageHistory,
buildSessionMessageRecordsSnapshot,
} from "./sync-context"
// Sync operations
export { useSync } from "./use-sync"
// Prompt submission
export { usePromptSubmit, type SubmitInput } from "./submit"
// Streaming lifecycle
export {
useStreamingStore,
updateStreamingState,
selectStreamingMessageId,
selectMessageStreamState,
selectIsStreaming,
type StreamPhase,
type MessageStreamState,
type StreamingStore,
} from "./streaming"
// Session UI state
export {
useSessionUIStore,
type SessionUIState,
type AttachedFile,
type NewSessionDraftState,
} from "./session-ui-store"
// Input store (pending input, synthetic parts, attached files)
export { useInputStore, type SyntheticContextPart } from "./input-store"
// Viewport store (per-session scroll anchors, memory state)
export {
useViewportStore,
type SessionMemoryState,
type ViewportState,
} from "./viewport-store"
// Sync refs (imperative access from non-React code)
export {
setSyncRefs,
getSyncSDK,
getSyncChildStores,
getSyncDirectory,
getDirectoryState,
getSyncSessions,
getSyncMessages,
getSyncParts,
getSyncSessionStatus,
getSyncPermissions,
getSyncQuestions,
} from "./sync-refs"
// Persisted metadata caches
export {
readDirCache,
persistVcs,
persistProjectMeta,
persistIcon,
clearDirCache,
type PersistedDirCache,
} from "./persist-cache"
// Session actions
export {
setActionRefs,
createSession,
deleteSession,
archiveSession,
updateSessionTitle,
shareSession,
unshareSession,
optimisticSend,
abortCurrentOperation,
respondToPermission,
dismissPermission,
respondToQuestion,
rejectQuestion,
revertToMessage,
forkFromMessage,
} from "./session-actions"
@@ -161,10 +161,3 @@ export function useSessionUnseenCount(sessionId: string): number {
return useNotificationStore((s) => s.index.session.unseenCount[sessionId] ?? 0)
}
export function useSessionHasError(sessionId: string): boolean {
return useNotificationStore((s) => s.index.session.unseenHasError[sessionId] ?? false)
}
export function useProjectUnseenCount(directory: string): number {
return useNotificationStore((s) => s.index.project.unseenCount[directory] ?? 0)
}
-40
View File
@@ -7,27 +7,11 @@ function sortParts(parts: Part[]) {
return parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id))
}
export type OptimisticStore = {
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
}
export type OptimisticItem = {
message: Message
parts: Part[]
}
export type OptimisticAddInput = {
sessionID: string
message: Message
parts: Part[]
}
export type OptimisticRemoveInput = {
sessionID: string
messageID: string
}
export type MessagePage = {
session: Message[]
part: { id: string; part: Part[] }[]
@@ -86,30 +70,6 @@ export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[])
}
}
/** Apply optimistic add to a mutable draft (for immer/produce) */
export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) {
const messages = draft.message[input.sessionID]
if (messages) {
const result = Binary.search(messages, input.message.id, (m) => m.id)
if (!result.found) {
messages.splice(result.index, 0, input.message)
}
} else {
draft.message[input.sessionID] = [input.message]
}
draft.part[input.message.id] = sortParts(input.parts)
}
/** Apply optimistic remove to a mutable draft (for immer/produce) */
export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticRemoveInput) {
const messages = draft.message[input.sessionID]
if (messages) {
const result = Binary.search(messages, input.messageID, (m) => m.id)
if (result.found) messages.splice(result.index, 1)
}
delete draft.part[input.messageID]
}
/** Merge two sorted message arrays by id, deduplicating.
* Preserves references from `a` for items that already exist — avoids
* unnecessary React re-renders when prepending older history. */
-19
View File
@@ -65,20 +65,6 @@ function writeCache<T>(directory: string, key: CacheKey, value: T | undefined):
}
}
function clearCache(directory: string): void {
try {
const prefix = storagePrefix(directory)
const keys: string[] = []
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i)
if (k?.startsWith(prefix)) keys.push(k)
}
for (const k of keys) localStorage.removeItem(k)
} catch {
// ignore
}
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
@@ -130,8 +116,3 @@ export function persistProjectMeta(directory: string, meta: ProjectMeta | undefi
export function persistIcon(directory: string, icon: string | undefined): void {
writeCache(directory, "icon", icon)
}
/** Clear all cached metadata for a directory */
export function clearDirCache(directory: string): void {
clearCache(directory)
}
+1 -1
View File
@@ -9,7 +9,7 @@ type ReconnectMaterializationState = {
part?: Record<string, Part[]>
}
export type ViewedSessionMaterializationTarget = {
type ViewedSessionMaterializationTarget = {
directory: string
sessionId: string
}
@@ -69,34 +69,6 @@ export function subscribeSessionPrefetch(directory: string, sessionID: string, c
}
}
export function getSessionPrefetchPromise(directory: string, sessionID: string) {
return inflight.get(compositeKey(directory, sessionID))
}
export function isSessionPrefetchCurrent(directory: string, sessionID: string, value: number) {
return version(compositeKey(directory, sessionID)) === value
}
/** Run a prefetch task with inflight dedup + version tracking. */
export function runSessionPrefetch(input: {
directory: string
sessionID: string
task: (value: number) => Promise<Meta | undefined>
}) {
const id = compositeKey(input.directory, input.sessionID)
const pending = inflight.get(id)
if (pending) return pending
const value = version(id)
const promise = input.task(value).finally(() => {
if (inflight.get(id) === promise) inflight.delete(id)
})
inflight.set(id, promise)
return promise
}
export function setSessionPrefetch(input: {
directory: string
sessionID: string
@@ -126,16 +98,3 @@ export function clearSessionPrefetch(directory: string, sessionIDs: Iterable<str
notify(id)
}
}
/** Invalidate all cache entries for a directory. */
export function clearSessionPrefetchDirectory(directory: string) {
const prefix = `${directory}\n`
const keys = new Set([...cache.keys(), ...inflight.keys()])
for (const id of keys) {
if (!id.startsWith(prefix)) continue
rev.set(id, version(id) + 1)
cache.delete(id)
inflight.delete(id)
notify(id)
}
}
@@ -1,21 +1,14 @@
import type { WorktreeMetadata } from '@/types/worktree';
import type { SessionWorktreeAttachment } from '@/stores/types/sessionTypes';
export type ResolveSessionWorktreeStateInput = {
type ResolveSessionWorktreeStateInput = {
sessionDirectory: string | null;
metadata: WorktreeMetadata | null;
cwdExists?: boolean;
runtimeResolution?: SessionWorktreeAttachment | null;
};
export type WorktreeDirectoryValidation = {
valid: boolean;
insideWorktreeRoot: boolean;
resolvedWorktreeRoot: string | null;
resolvedCwd: string | null;
};
export type WorktreeCanonicalizationResult = {
type WorktreeCanonicalizationResult = {
worktreeRoot: string | null;
cwd: string | null;
branch: string | null;
@@ -26,7 +19,7 @@ export type WorktreeCanonicalizationResult = {
attentionReason?: 'merge' | 'rebase' | 'cherry-pick' | 'revert' | 'bisect' | null;
};
export type SessionWorktreeCanonicalizationOptions = {
type SessionWorktreeCanonicalizationOptions = {
existingAttachment?: SessionWorktreeAttachment | null;
fallbackDirectory?: string | null;
worktreeSource?: SessionWorktreeAttachment['worktreeSource'];
@@ -39,7 +32,7 @@ const normalizePath = (value: string): string => {
return replaced.replace(/\/+$/, '') || replaced;
};
export function isWithinWorktreeRoot(candidate: string | null, worktreeRoot: string | null): boolean {
function isWithinWorktreeRoot(candidate: string | null, worktreeRoot: string | null): boolean {
if (!candidate || !worktreeRoot) return false;
const c = normalizePath(candidate);
const r = normalizePath(worktreeRoot);
+2 -12
View File
@@ -10,9 +10,9 @@ import { create } from "zustand"
import type { Message, SessionStatus } from "@opencode-ai/sdk/v2/client"
import type { State } from "./types"
export type StreamPhase = "streaming" | "cooldown" | "completed"
type StreamPhase = "streaming" | "cooldown" | "completed"
export type MessageStreamState = {
type MessageStreamState = {
phase: StreamPhase
startedAt: number
lastUpdateAt: number
@@ -140,13 +140,3 @@ export function updateStreamingState(state: State) {
})
}
}
// Selectors
export const selectStreamingMessageId = (sessionID: string) =>
(state: StreamingStore) => state.streamingMessageIds.get(sessionID) ?? null
export const selectMessageStreamState = (messageID: string) =>
(state: StreamingStore) => state.messageStreamStates.get(messageID) ?? null
export const selectIsStreaming = (sessionID: string) =>
(state: StreamingStore) => state.streamingMessageIds.get(sessionID) != null
-133
View File
@@ -1,133 +0,0 @@
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
import { useCallback } from "react"
import { opencodeClient } from "@/lib/opencode/client"
import { useDirectoryStore, useSyncDirectory } from "./sync-context"
import { useSync } from "./use-sync"
// ---------------------------------------------------------------------------
// Ascending ID generator — monotonic timestamp + sequence counter
// ---------------------------------------------------------------------------
let counter = 0
function ascending(prefix: string): string {
const now = Date.now()
const seq = (counter++ % 1000).toString().padStart(3, "0")
return `${prefix}_${now}${seq}`
}
// ---------------------------------------------------------------------------
// Prompt submission with optimistic updates
// Prompt submission with optimistic message insertion
// ---------------------------------------------------------------------------
export type SubmitInput = {
sessionID: string
text: string
parts?: Part[]
agent: string
model: { providerID: string; modelID: string }
variant?: string
command?: { name: string; arguments: string }
images?: Array<{ id?: string; type: "file"; mime: string; url: string; filename: string }>
}
export function usePromptSubmit() {
const store = useDirectoryStore()
const directory = useSyncDirectory()
const sync = useSync()
const submit = useCallback(
async (input: SubmitInput) => {
const messageID = ascending("message")
// Build optimistic user message
const message: Message = {
id: messageID,
sessionID: input.sessionID,
role: "user",
time: { created: Date.now() },
agent: input.agent,
model: input.model,
variant: input.variant,
} as Message
// Build optimistic parts
const textPart: Part = {
id: ascending("part"),
sessionID: input.sessionID,
messageID,
type: "text",
text: input.text,
} as Part
const optimisticParts: Part[] = [textPart, ...(input.parts ?? [])]
// Set busy status optimistically
store.setState((prev) => ({
...prev,
session_status: {
...prev.session_status,
[input.sessionID]: { type: "busy" },
},
}))
// Add optimistic message immediately
sync.optimistic.add({
sessionID: input.sessionID,
message,
parts: optimisticParts,
})
try {
if (input.command) {
// Slash command
await opencodeClient.sendCommand({
id: input.sessionID,
command: input.command?.name ?? "",
arguments: input.command?.arguments ?? "",
agent: input.agent,
providerID: input.model.providerID,
modelID: input.model.modelID,
variant: input.variant,
files: input.images,
messageId: messageID,
directory,
}).then(() => undefined)
} else {
// Regular prompt
await opencodeClient.sendMessage({
id: input.sessionID,
agent: input.agent,
providerID: input.model.providerID,
modelID: input.model.modelID,
messageId: messageID,
text: input.text,
files: input.images,
variant: input.variant,
directory,
}).then(() => undefined)
}
return true
} catch (error) {
// Revert optimistic on failure
sync.optimistic.remove({
sessionID: input.sessionID,
messageID,
})
// Reset status
store.setState((prev) => ({
...prev,
session_status: {
...prev.session_status,
[input.sessionID]: { type: "idle" },
},
}))
throw error
}
},
[directory, store, sync],
)
return submit
}
+2 -210
View File
@@ -9,7 +9,7 @@ import { createEventPipeline } from "./event-pipeline"
import { isVSCodeRuntime } from "@/lib/desktop"
import { isMobileSurfaceRuntime } from "@/lib/runtimeSurface"
import { reduceGlobalEvent, applyGlobalProject, applyDirectoryEvent } from "./event-reducer"
import { useGlobalSyncStore, type GlobalSyncStore } from "./global-sync-store"
import { useGlobalSyncStore } from "./global-sync-store"
import { ChildStoreManager, type DirectoryStore } from "./child-store"
import {
aggregateLiveSessions,
@@ -151,35 +151,6 @@ export function useAllSessionStatuses(): Record<string, SessionStatus> {
)
}
type LiveSessionStatusCounts = {
running: number
}
const EMPTY_LIVE_SESSION_STATUS_COUNTS: LiveSessionStatusCounts = { running: 0 }
const isRunningSessionStatus = (status: SessionStatus | undefined): boolean => (
status?.type === "busy" || status?.type === "retry"
)
const areLiveSessionStatusCountsEquivalent = (left: LiveSessionStatusCounts, right: LiveSessionStatusCounts): boolean => (
left.running === right.running
)
export function useLiveSessionStatusCounts(): LiveSessionStatusCounts {
return useLiveSyncSelector(
useCallback((states) => {
let running = 0
for (const state of states) {
for (const status of Object.values(state.session_status ?? {})) {
if (isRunningSessionStatus(status)) running += 1
}
}
return running === 0 ? EMPTY_LIVE_SESSION_STATUS_COUNTS : { running }
}, []),
areLiveSessionStatusCountsEquivalent,
)
}
export function useAllLiveSessions(): Session[] {
return useLiveSyncSelector(
useCallback((states) => aggregateLiveSessions(states), []),
@@ -2047,16 +2018,6 @@ export function SyncProvider(props: {
// Hooks
// ---------------------------------------------------------------------------
/** Access the global sync store */
export function useGlobalSync() {
return useGlobalSyncStore()
}
/** Access the global sync store with a selector */
export function useGlobalSyncSelector<T>(selector: (state: GlobalSyncStore) => T): T {
return useGlobalSyncStore(selector)
}
/**
* Get the child store for a directory (defaults to current).
*
@@ -2081,18 +2042,7 @@ export function useDirectorySync<T>(selector: (state: State) => T, directory?: s
return useStore(store, selector)
}
/** Get the revert messageID for a session (if reverted) */
export function useSessionRevertMessageID(sessionID: string, directory?: string): string | undefined {
return useDirectorySync(
useCallback((state: State) => {
const session = state.session.find((s) => s.id === sessionID)
return (session as { revert?: { messageID?: string } } | undefined)?.revert?.messageID
}, [sessionID]),
directory,
)
}
/** Get session messages for a specific session */
/** Get session messages for a specific session */
export function useSessionMessages(sessionID: string, directory?: string) {
const store = useDirectoryStore(directory)
const getSnapshot = useCallback(() => {
@@ -2106,19 +2056,6 @@ export function useSessionMessages(sessionID: string, directory?: string) {
return React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
}
/**
* Get visible session messages — filters out reverted messages.
* Filters out reverted messages (id >= session.revert.messageID).
*/
export function useVisibleSessionMessages(sessionID: string, directory?: string) {
const messages = useSessionMessages(sessionID, directory)
const revertMessageID = useSessionRevertMessageID(sessionID, directory)
return useMemo(() => {
if (!revertMessageID) return messages
return messages.filter((m) => m.id < revertMessageID)
}, [messages, revertMessageID])
}
/** Check whether the message list for a session has been loaded into sync state. */
export function useSessionMessagesResolved(sessionID: string, directory?: string): boolean {
return useDirectorySync(
@@ -2253,116 +2190,6 @@ export function useParentSession(sessionID: string | null, directory?: string):
)
}
const getSidebarSessionSignature = (session: Session, stableUpdatedAt: number): string => {
const directory = (session as Session & { directory?: string | null }).directory ?? ''
const parentID = (session as Session & { parentID?: string | null }).parentID ?? ''
const projectWorktree = (session as Session & { project?: { worktree?: string | null } | null }).project?.worktree ?? ''
const shared = session.share?.url ?? ''
return [
session.id,
session.title ?? '',
session.time?.created ?? 0,
session.time?.archived ? 1 : 0,
directory,
parentID,
projectWorktree,
shared,
stableUpdatedAt,
].join('|')
}
/** Get sessions stabilized for sidebar tree rendering */
export function useSidebarSessions(directory?: string): Session[] {
const store = useDirectoryStore(directory)
const cacheRef = React.useRef<{
source: Session[]
streamingSignature: string
array: Session[]
signatures: Map<string, string>
sessionsById: Map<string, Session>
stableUpdatedAtById: Map<string, number>
streamingById: Map<string, boolean>
} | null>(null)
const getSnapshot = React.useCallback(() => {
const state = store.getState()
const source = state.session
const cached = cacheRef.current
const streamingSignature = source
.map((session) => {
const statusType = state.session_status?.[session.id]?.type
const isStreaming = statusType === 'busy' || statusType === 'retry'
return `${session.id}:${isStreaming ? 1 : 0}`
})
.join('|')
if (cached && cached.source === source && cached.streamingSignature === streamingSignature) {
return cached.array
}
const signatures = new Map<string, string>()
const sessionsById = new Map<string, Session>()
const stableUpdatedAtById = new Map<string, number>()
const streamingById = new Map<string, boolean>()
let changed = !cached || cached.array.length !== source.length
const array = source.map((session) => {
const rawUpdatedAt = Number(session.time?.updated ?? session.time?.created ?? 0)
const statusType = state.session_status?.[session.id]?.type
const isStreaming = statusType === 'busy' || statusType === 'retry'
const cachedUpdatedAt = cached?.stableUpdatedAtById.get(session.id) ?? rawUpdatedAt
const wasStreaming = cached?.streamingById.get(session.id) ?? false
const stableUpdatedAt = isStreaming
? (wasStreaming ? cachedUpdatedAt : Math.max(rawUpdatedAt, cachedUpdatedAt, Date.now()))
: Math.max(rawUpdatedAt, cachedUpdatedAt)
const signature = getSidebarSessionSignature(session, stableUpdatedAt)
signatures.set(session.id, signature)
stableUpdatedAtById.set(session.id, stableUpdatedAt)
streamingById.set(session.id, isStreaming)
const cachedSession = cached?.sessionsById.get(session.id)
if (
cachedSession
&& cached?.signatures.get(session.id) === signature
) {
sessionsById.set(session.id, cachedSession)
return cachedSession
}
changed = true
const nextSession = stableUpdatedAt === rawUpdatedAt
? session
: {
...session,
time: {
...session.time,
updated: stableUpdatedAt,
},
}
sessionsById.set(session.id, nextSession)
return nextSession
})
if (!changed && cached) {
cacheRef.current = {
source,
streamingSignature,
array: cached.array,
signatures,
sessionsById: cached.sessionsById,
stableUpdatedAtById,
streamingById,
}
return cached.array
}
cacheRef.current = { source, streamingSignature, array, signatures, sessionsById, stableUpdatedAtById, streamingById }
return array
}, [store])
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot)
}
/** Get one session by id for a directory */
export function useSession(sessionID?: string | null, directory?: string) {
const { childStores } = useSyncSystem()
@@ -2805,41 +2632,6 @@ export function useEnsureSessionMessages(sessionID: string, directory?: string)
})()
}, [sessionID, store, resolvedDirectory])
}
/**
* Determines if a session is actively working.
* Checks session_status and only falls back to incomplete assistant messages
* when authoritative status is missing.
* Returns false when permissions are pending (permission indicator takes priority).
*/
export function useIsSessionWorking(sessionID: string, directory?: string): boolean {
const status = useSessionStatus(sessionID, directory)
const permissions = useSessionPermissions(sessionID, directory)
const messages = useSessionMessages(sessionID, directory)
return useMemo(() => {
// Permissions pending → not "working" (show permission indicator instead)
if (permissions.length > 0) return false
// Check session_status
const hasAuthoritativeStatus = status !== undefined
const statusWorking = hasAuthoritativeStatus && status.type !== "idle"
// Check for incomplete assistant message (fallback if status event delayed)
let hasPendingAssistant = false
for (let i = messages.length - 1; i >= 0; i--) {
const m = messages[i]
if (m.role === "assistant" && typeof (m as { time?: { completed?: number } }).time?.completed !== "number") {
hasPendingAssistant = true
break
}
}
if (hasAuthoritativeStatus) return statusWorking
return hasPendingAssistant
}, [status, permissions, messages])
}
const EMPTY_MESSAGES: Message[] = []
const EMPTY_PARTS: Part[] = []
const EMPTY_PERMISSION_REQUESTS: PermissionRequest[] = []
+1 -22
View File
@@ -10,19 +10,17 @@ import type { ChildStoreManager } from "./child-store"
import { getSessionMaterializationStatus } from "./materialization"
import type { State } from "./types"
let _sdk: OpencodeClient | null = null
let _childStores: ChildStoreManager | null = null
let _directory: string = ""
let _registerSessionDirectory: ((sessionID: string, directory: string) => void) | null = null
const configListeners = new Set<(directory: string, config: Config) => void>()
export function setSyncRefs(
sdk: OpencodeClient,
_sdk: OpencodeClient,
childStores: ChildStoreManager,
directory: string,
registerSessionDirectory?: (sessionID: string, directory: string) => void,
) {
_sdk = sdk
_childStores = childStores
_directory = directory
if (registerSessionDirectory) {
@@ -37,20 +35,11 @@ export function registerSessionDirectory(sessionID: string, directory: string) {
_registerSessionDirectory?.(sessionID, directory)
}
export function getSyncSDK(): OpencodeClient {
if (!_sdk) throw new Error("SDK not initialized — is SyncProvider mounted?")
return _sdk
}
export function getSyncChildStores(): ChildStoreManager {
if (!_childStores) throw new Error("ChildStoreManager not initialized — is SyncProvider mounted?")
return _childStores
}
export function getSyncDirectory(): string {
return _directory
}
/** Read current directory's child store state. Returns undefined if not bootstrapped. */
export function getDirectoryState(directory?: string): State | undefined {
const stores = _childStores
@@ -121,13 +110,3 @@ export function getSyncParts(messageId: string, directory?: string) {
export function getSyncSessionStatus(sessionId: string, directory?: string) {
return getDirectoryState(directory)?.session_status[sessionId]
}
/** Read permissions for a session from current directory's child store */
export function getSyncPermissions(sessionId: string, directory?: string) {
return getDirectoryState(directory)?.permission[sessionId] ?? []
}
/** Read questions for a session from current directory's child store */
export function getSyncQuestions(sessionId: string, directory?: string) {
return getDirectoryState(directory)?.question[sessionId] ?? []
}
+1 -7
View File
@@ -77,7 +77,7 @@ export type GlobalState = {
sessionTodo: Record<string, Todo[]>
}
export type InitError = {
type InitError = {
type: "init"
message: string
}
@@ -105,14 +105,8 @@ export type DisposeCheck = {
hasPendingBlockingRequests: boolean
}
export type ChildOptions = {
bootstrap?: boolean
}
export const MAX_DIR_STORES = 30
export const DIR_IDLE_TTL_MS = 20 * 60 * 1000
export const SESSION_RECENT_WINDOW = 4 * 60 * 60 * 1000
export const SESSION_RECENT_LIMIT = 50
export const SESSION_CACHE_LIMIT = 40
export const INITIAL_STATE: State = {
-16
View File
@@ -3,21 +3,5 @@
* Extracted from session-ui-store for subscription isolation.
*/
import { create } from "zustand"
export type VoiceStatus = "disconnected" | "connecting" | "connected" | "error"
export type VoiceMode = "idle" | "speaking" | "listening"
export type VoiceState = {
voiceStatus: VoiceStatus
voiceMode: VoiceMode
setVoiceStatus: (status: VoiceStatus) => void
setVoiceMode: (mode: VoiceMode) => void
}
export const useVoiceStore = create<VoiceState>()((set) => ({
voiceStatus: "disconnected",
voiceMode: "idle",
setVoiceStatus: (status) => set({ voiceStatus: status }),
setVoiceMode: (mode) => set({ voiceMode: mode }),
}))