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:
committed by
GitHub
co-authored by
Serhii Dziupin
Bohdan Triapitsyn
parent
4a37b9a005
commit
00821700de
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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[] = []
|
||||
|
||||
@@ -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] ?? []
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 }),
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user