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
-272
View File
@@ -1,272 +0,0 @@
import { create } from "zustand";
import { devtools, persist, createJSONStorage } from "zustand/middleware";
import type { AttachedFile } from "./types/sessionTypes";
import { getSafeStorage } from "./utils/safeStorage";
import { runtimeFetch } from "@/lib/runtime-fetch";
interface FileState {
attachedFiles: AttachedFile[];
}
interface FileActions {
addAttachedFile: (file: File) => Promise<void>;
addServerFile: (path: string, name: string, content?: string) => Promise<void>;
removeAttachedFile: (id: string) => void;
clearAttachedFiles: () => void;
}
type FileStore = FileState & FileActions;
const MAX_ATTACHMENT_SIZE = 50 * 1024 * 1024;
const guessMimeTypeFromName = (filename: string): string => {
const name = (filename || "").toLowerCase();
const ext = name.includes(".") ? name.split(".").pop() || "" : "";
switch (ext) {
case "png":
return "image/png";
case "jpg":
case "jpeg":
return "image/jpeg";
case "gif":
return "image/gif";
case "webp":
return "image/webp";
case "svg":
return "image/svg+xml";
case "bmp":
return "image/bmp";
case "ico":
return "image/x-icon";
case "pdf":
return "application/pdf";
default:
return "application/octet-stream";
}
};
const guessMimeType = (file: File): string => {
if (file.type && file.type.trim().length > 0) {
return file.type;
}
const name = (file.name || "").toLowerCase();
const ext = name.includes(".") ? name.split(".").pop() || "" : "";
const noExtNames = new Set([
"license",
"readme",
"changelog",
"notice",
"authors",
"copying",
]);
if (noExtNames.has(name)) return "text/plain";
switch (ext) {
case "md":
case "markdown":
return "text/markdown";
case "txt":
return "text/plain";
case "json":
return "application/json";
case "yaml":
case "yml":
return "application/x-yaml";
case "ts":
case "tsx":
case "js":
case "jsx":
case "mjs":
case "cjs":
case "py":
case "rb":
case "sh":
case "bash":
case "zsh":
return "text/plain";
default:
return "application/octet-stream";
}
};
const normalizeServerPath = (inputPath: string): string => inputPath.replace(/\\/g, "/").trim();
const toFileUrl = (inputPath: string): string => {
const normalized = normalizeServerPath(inputPath);
if (normalized.startsWith("file://")) {
return normalized;
}
const withLeadingSlash = normalized.startsWith("/") ? normalized : `/${normalized}`;
return `file://${encodeURI(withLeadingSlash)}`;
};
const readRawFileAsDataUrl = async (absolutePath: string): Promise<string> => {
const response = await runtimeFetch("/api/fs/raw", { query: { path: absolutePath } });
if (!response.ok) {
throw new Error(`Failed to read raw file: ${response.status}`);
}
const blob = await response.blob();
return await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
};
export const useFileStore = create<FileStore>()(
devtools(
persist(
(set, get) => ({
attachedFiles: [],
addAttachedFile: async (file: File) => {
const { attachedFiles } = get();
const isDuplicate = attachedFiles.some((f) => f.filename === file.name && f.size === file.size);
if (isDuplicate) {
console.log(`File "${file.name}" is already attached`);
return;
}
const maxSize = MAX_ATTACHMENT_SIZE;
if (file.size > maxSize) {
throw new Error(`File "${file.name}" is too large. Maximum size is 50MB.`);
}
const allowedTypes = [
"text/",
"application/json",
"application/xml",
"application/pdf",
"image/",
"video/",
"audio/",
"application/javascript",
"application/typescript",
"application/x-python",
"application/x-ruby",
"application/x-sh",
"application/yaml",
"application/octet-stream",
];
const mimeType = guessMimeType(file);
const isAllowed = allowedTypes.some((type) => mimeType.startsWith(type) || mimeType === type || mimeType === "");
if (!isAllowed && mimeType !== "") {
console.warn(`File type "${mimeType}" might not be supported`);
}
const reader = new FileReader();
const rawDataUrl = await new Promise<string>((resolve, reject) => {
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
const dataUrl = rawDataUrl.startsWith("data:")
? rawDataUrl.replace(/^data:[^;]*/, `data:${mimeType}`)
: rawDataUrl;
const extractFilename = (fullPath: string) => {
const parts = fullPath.replace(/\\/g, "/").split("/");
return parts[parts.length - 1] || fullPath;
};
const attachedFile: AttachedFile = {
id: `file-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
file,
dataUrl,
mimeType,
filename: extractFilename(file.name),
size: file.size,
source: "local",
};
set((state) => ({
attachedFiles: [...state.attachedFiles, attachedFile],
}));
},
addServerFile: async (path: string, name: string, content?: string) => {
const normalizedPath = normalizeServerPath(path);
const { attachedFiles } = get();
const isDuplicate = attachedFiles.some((f) => normalizeServerPath(f.serverPath || "") === normalizedPath && f.source === "server");
if (isDuplicate) {
console.log(`Server file "${name}" is already attached`);
return;
}
const inferredMime = guessMimeTypeFromName(name);
const safeMimeType = inferredMime && inferredMime.trim().length > 0 ? inferredMime : "application/octet-stream";
const shouldInlineBinary = safeMimeType !== "text/plain" && safeMimeType !== "application/x-directory";
let dataUrl = toFileUrl(normalizedPath);
if (shouldInlineBinary) {
try {
dataUrl = await readRawFileAsDataUrl(normalizedPath);
} catch (error) {
console.warn("Failed to inline binary server file, falling back to file://", error);
}
}
const sizeBytes = typeof content === "string"
? new TextEncoder().encode(content).length
: 0;
const file = new File([], name, { type: safeMimeType });
const attachedFile: AttachedFile = {
id: `server-file-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
file,
dataUrl,
mimeType: safeMimeType,
filename: name,
size: sizeBytes,
source: "server",
serverPath: normalizedPath,
};
set((state) => ({
attachedFiles: [...state.attachedFiles, attachedFile],
}));
},
removeAttachedFile: (id: string) => {
set((state) => ({
attachedFiles: state.attachedFiles.filter((f) => f.id !== id),
}));
},
clearAttachedFiles: () => {
set({ attachedFiles: [] });
},
}),
{
name: "file-store",
storage: createJSONStorage(() => getSafeStorage()),
version: 3,
migrate: (persistedState) => {
const state = persistedState as { attachedFiles?: AttachedFile[] } | undefined;
return { attachedFiles: Array.isArray(state?.attachedFiles) ? state.attachedFiles : [] };
},
// Keep unsent draft attachments across restarts.
partialize: (state) => ({
attachedFiles: state.attachedFiles,
}),
}
),
{
name: "file-store",
}
)
);
-23
View File
@@ -73,29 +73,6 @@ const unwrapSessionList = (
return result.data as GlobalSessionRecord[];
};
export const readNextCursor = (response: unknown): number | null => {
return toNumber(readResponseHeader(response, "x-next-cursor"));
};
export const isMissingGlobalSessionsEndpointError = (error: unknown): boolean => {
if (!error || typeof error !== "object") {
return false;
}
const value = error as {
status?: number;
response?: { status?: number };
cause?: { status?: number; response?: { status?: number } };
};
const status = value.status
?? value.response?.status
?? value.cause?.status
?? value.cause?.response?.status;
return status === 404;
};
export async function listGlobalSessionPages(
apiClient: OpencodeClient,
options: {
+4 -249
View File
@@ -1,7 +1,3 @@
import type { Session, Message, Part } from "@opencode-ai/sdk/v2";
import type { PermissionRequest, PermissionResponse } from "@/types/permission";
import type { QuestionRequest } from "@/types/question";
export type SessionWorktreeAttachment = {
worktreeRoot: string | null;
cwd: string | null;
@@ -31,31 +27,6 @@ export type EditPermissionMode = 'allow' | 'ask' | 'deny' | 'full';
export type MessageStreamPhase = 'streaming' | 'cooldown' | 'completed';
export interface MessageStreamLifecycle {
phase: MessageStreamPhase;
startedAt: number;
lastUpdateAt: number;
completedAt?: number;
}
export interface SessionMemoryState {
viewportAnchor: number;
isStreaming: boolean;
streamStartTime?: number;
lastAccessedAt: number;
backgroundMessageCount: number;
isZombie?: boolean;
totalAvailableMessages?: number;
loadedTurnCount?: number;
hasMoreAbove?: boolean;
hasMoreTurnsAbove?: boolean;
historyLoading?: boolean;
historyComplete?: boolean;
historyLimit?: number;
streamingCooldownUntil?: number;
lastUserMessageAt?: number; // Timestamp when user last sent a message
}
export interface SessionHistoryMeta {
limit: number;
complete: boolean;
@@ -75,18 +46,14 @@ export interface SessionContextUsage {
// Default message limit (can be overridden via settings).
// Single value controls: fetch from server, active session ceiling, Load More chunk.
// Background trim is derived automatically as Math.round(limit * 0.6).
export const DEFAULT_MESSAGE_LIMIT = 200;
/** Timeout after which a session stuck in 'busy' or 'retry' with no SSE events is force-reset to idle. */
export const STUCK_SESSION_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
export const MEMORY_CONSTANTS = {
const DEFAULT_MESSAGE_LIMIT = 200;
const MEMORY_CONSTANTS = {
MAX_SESSIONS: 3,
ZOMBIE_TIMEOUT: 10 * 60 * 1000,
} as const;
/** OpenCode parity: fixed page/window size for message history. */
export const getMessageLimit = (): number => {
const getMessageLimit = (): number => {
return DEFAULT_MESSAGE_LIMIT;
};
@@ -95,7 +62,7 @@ export const getBackgroundTrimLimit = (): number =>
Math.round(getMessageLimit() * 0.6);
// --- Backward-compat shims (avoid mass refactor of non-critical callers) ---
export const DEFAULT_MEMORY_LIMITS = {
const DEFAULT_MEMORY_LIMITS = {
MAX_SESSIONS: MEMORY_CONSTANTS.MAX_SESSIONS,
VIEWPORT_MESSAGES: Math.round(DEFAULT_MESSAGE_LIMIT * 0.6),
HISTORICAL_MESSAGES: DEFAULT_MESSAGE_LIMIT,
@@ -116,216 +83,4 @@ export const getMemoryLimits = () => {
};
};
export const getActiveSessionWindow = () => getMessageLimit();
export const DEFAULT_ACTIVE_SESSION_WINDOW = DEFAULT_MESSAGE_LIMIT;
export const MEMORY_LIMITS = DEFAULT_MEMORY_LIMITS;
export const ACTIVE_SESSION_WINDOW = DEFAULT_ACTIVE_SESSION_WINDOW;
/** Synthetic context parts to attach when sending initial message */
export interface SyntheticContextPart {
text: string;
synthetic: true;
}
export type NewSessionDraftState = {
open: boolean;
selectedProjectId?: string | null;
directoryOverride: string | null;
pendingWorktreeRequestId?: string | null;
bootstrapPendingDirectory?: string | null;
preserveDirectoryOverride?: boolean;
parentID: string | null;
title?: string;
initialPrompt?: string;
/** Synthetic context parts to include with the initial message */
syntheticParts?: SyntheticContextPart[];
targetFolderId?: string;
};
// Voice state types
export type VoiceStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
export type VoiceMode = 'idle' | 'speaking' | 'listening';
export interface VoiceState {
status: VoiceStatus;
mode: VoiceMode;
}
export interface SessionStore {
sessions: Session[];
archivedSessions: Session[];
sessionsByDirectory: Map<string, Session[]>;
currentSessionId: string | null;
lastLoadedDirectory: string | null;
messages: Map<string, { info: Message; parts: Part[] }[]>;
sessionMemoryState: Map<string, SessionMemoryState>;
sessionHistoryMeta: Map<string, SessionHistoryMeta>;
messageStreamStates: Map<string, MessageStreamLifecycle>;
sessionCompactionUntil: Map<string, number>;
permissions: Map<string, PermissionRequest[]>;
questions: Map<string, QuestionRequest[]>;
sessionAbortFlags: Map<string, { timestamp: number; acknowledged: boolean }>;
attachedFiles: AttachedFile[];
abortPromptSessionId: string | null;
abortPromptExpiresAt: number | null;
isLoading: boolean;
error: string | null;
streamingMessageIds: Map<string, string | null>;
abortControllers: Map<string, AbortController>;
lastUsedProvider: { providerID: string; modelID: string } | null;
isSyncing: boolean;
sessionModelSelections: Map<string, { providerId: string; modelId: string }>;
sessionAgentSelections: Map<string, string>;
sessionAgentModelSelections: Map<string, Map<string, { providerId: string; modelId: string }>>;
webUICreatedSessions: Set<string>;
worktreeMetadata: Map<string, import('@/types/worktree').WorktreeMetadata>;
availableWorktrees: import('@/types/worktree').WorktreeMetadata[];
availableWorktreesByProject: Map<string, import('@/types/worktree').WorktreeMetadata[]>;
currentAgentContext: Map<string, string>;
sessionContextUsage: Map<string, SessionContextUsage>;
sessionAgentEditModes: Map<string, Map<string, EditPermissionMode>>;
// Server-owned session status (mirrors OpenCode SessionStatus: busy|retry|idle).
// Use as the single source of truth for "assistant working" UI.
// confirmedAt: timestamp when idle was confirmed locally (prevents race with server polling)
sessionStatus?: Map<
string,
{ type: 'idle' | 'busy' | 'retry'; attempt?: number; message?: string; next?: number; confirmedAt?: number }
>;
// sessionAttentionStates removed — replaced by notification-store
userSummaryTitles: Map<string, { title: string; createdAt: number | null }>;
pendingInputText: string | null;
pendingInputMode: 'replace' | 'append' | 'append-inline';
/** Synthetic context parts to include with the next message sent */
pendingSyntheticParts: SyntheticContextPart[] | null;
newSessionDraft: NewSessionDraftState;
// Voice state
voiceStatus: VoiceStatus;
voiceMode: VoiceMode;
// Voice actions
setVoiceStatus: (status: VoiceStatus) => void;
setVoiceMode: (mode: VoiceMode) => void;
getSessionAgentEditMode: (sessionId: string, agentName: string | undefined, defaultMode?: EditPermissionMode) => EditPermissionMode;
toggleSessionAgentEditMode: (sessionId: string, agentName: string | undefined, defaultMode?: EditPermissionMode) => void;
setSessionAgentEditMode: (sessionId: string, agentName: string | undefined, mode: EditPermissionMode, defaultMode?: EditPermissionMode) => void;
loadSessions: () => Promise<void>;
openNewSessionDraft: (options?: { projectId?: string | null; directoryOverride?: string | null; pendingWorktreeRequestId?: string | null; bootstrapPendingDirectory?: string | null; preserveDirectoryOverride?: boolean; parentID?: string | null; title?: string; initialPrompt?: string; syntheticParts?: SyntheticContextPart[]; targetFolderId?: string }) => void;
overrideNewSessionDraftTarget: (options: { projectId?: string | null; directoryOverride?: string | null; pendingWorktreeRequestId?: string | null; bootstrapPendingDirectory?: string | null; preserveDirectoryOverride?: boolean; title?: string; initialPrompt?: string }) => void;
setNewSessionDraftTarget: (target: { projectId?: string | null; directoryOverride?: string | null }, options?: { force?: boolean }) => void;
setPendingDraftWorktreeRequest: (requestId: string | null) => void;
resolvePendingDraftWorktreeTarget: (requestId: string, directory: string | null, options?: { projectId?: string | null; bootstrapPendingDirectory?: string | null; preserveDirectoryOverride?: boolean }) => void;
setDraftBootstrapPendingDirectory: (directory: string | null) => void;
setDraftPreserveDirectoryOverride: (value: boolean) => void;
closeNewSessionDraft: () => void;
createSession: (title?: string, directoryOverride?: string | null, parentID?: string | null) => Promise<Session | null>;
createSessionFromAssistantMessage: (sourceMessageId: string, execution: { providerID: string; modelID: string; variant: string; agent: string; instructions: string; createWorktree?: boolean }) => Promise<void>;
deleteSession: (id: string, options?: { archiveWorktree?: boolean; deleteRemoteBranch?: boolean; deleteLocalBranch?: boolean; remoteName?: string }) => Promise<boolean>;
deleteSessions: (ids: string[], options?: { archiveWorktree?: boolean; deleteRemoteBranch?: boolean; deleteLocalBranch?: boolean; remoteName?: string; silent?: boolean }) => Promise<{ deletedIds: string[]; failedIds: string[] }>;
archiveSession: (id: string) => Promise<boolean>;
archiveSessions: (ids: string[], options?: { silent?: boolean }) => Promise<{ archivedIds: string[]; failedIds: string[] }>;
updateSessionTitle: (id: string, title: string) => Promise<void>;
shareSession: (id: string) => Promise<Session | null>;
unshareSession: (id: string) => Promise<Session | null>;
setCurrentSession: (id: string | null) => void;
loadMessages: (sessionId: string, limit?: number) => Promise<void>;
sendMessage: (content: string, providerID: string, modelID: string, agent?: string, attachments?: AttachedFile[], agentMentionName?: string, additionalParts?: Array<{ text: string; attachments?: AttachedFile[]; synthetic?: boolean }>, variant?: string, inputMode?: 'normal' | 'shell', options?: { sessionId?: string }) => Promise<void>;
abortCurrentOperation: (sessionIdOverride?: string) => Promise<void>;
acknowledgeSessionAbort: (sessionId: string) => void;
armAbortPrompt: (durationMs?: number) => number | null;
clearAbortPrompt: () => void;
addStreamingPart: (sessionId: string, messageId: string, part: Part, role?: string) => void;
applyPartDelta: (sessionId: string, messageId: string, partId: string, field: string, delta: string, role?: string) => void;
completeStreamingMessage: (sessionId: string, messageId: string) => void;
markMessageStreamSettled: (messageId: string) => void;
updateMessageInfo: (sessionId: string, messageId: string, messageInfo: Message) => void;
updateSessionCompaction: (sessionId: string, compactingTimestamp?: number | null) => void;
addPermission: (permission: PermissionRequest) => void;
respondToPermission: (sessionId: string, requestId: string, response: PermissionResponse) => Promise<void>;
dismissPermission: (sessionId: string, requestId: string) => void;
addQuestion: (question: QuestionRequest) => void;
dismissQuestion: (sessionId: string, requestId: string) => void;
respondToQuestion: (sessionId: string, requestId: string, answers: string[] | string[][]) => Promise<void>;
rejectQuestion: (sessionId: string, requestId: string) => Promise<void>;
clearError: () => void;
getSessionsByDirectory: (directory: string) => Session[];
getDirectoryForSession: (sessionId: string) => string | null;
getLastUserChoice: (sessionId: string) => { agent?: string; providerID?: string; modelID?: string; variant?: string } | null;
getCurrentAgent: (sessionId: string) => string | undefined;
syncMessages: (
sessionId: string,
messages: { info: Message; parts: Part[] }[],
options?: { replace?: boolean }
) => void;
applySessionMetadata: (sessionId: string, metadata: Partial<Session>) => void;
setSessionDirectory: (sessionId: string, directory: string | null) => void;
addAttachedFile: (file: File) => Promise<void>;
addServerFile: (path: string, name: string, content?: string) => Promise<void>;
removeAttachedFile: (id: string) => void;
clearAttachedFiles: () => void;
updateViewportAnchor: (sessionId: string, anchor: number) => void;
loadMoreMessages: (sessionId: string, direction: "up" | "down") => Promise<void>;
saveSessionModelSelection: (sessionId: string, providerId: string, modelId: string) => void;
getSessionModelSelection: (sessionId: string) => { providerId: string; modelId: string } | null;
saveSessionAgentSelection: (sessionId: string, agentName: string) => void;
getSessionAgentSelection: (sessionId: string) => string | null;
saveAgentModelForSession: (sessionId: string, agentName: string, providerId: string, modelId: string) => void;
getAgentModelForSession: (sessionId: string, agentName: string) => { providerId: string; modelId: string } | null;
saveAgentModelVariantForSession: (sessionId: string, agentName: string, providerId: string, modelId: string, variant: string | undefined) => void;
getAgentModelVariantForSession: (sessionId: string, agentName: string, providerId: string, modelId: string) => string | undefined;
isOpenChamberCreatedSession: (sessionId: string) => boolean;
markSessionAsOpenChamberCreated: (sessionId: string) => void;
initializeNewOpenChamberSession: (sessionId: string, agents: Array<{ name: string; [key: string]: unknown }>) => void;
setWorktreeMetadata: (sessionId: string, metadata: import('@/types/worktree').WorktreeMetadata | null) => void;
getWorktreeMetadata: (sessionId: string) => import('@/types/worktree').WorktreeMetadata | undefined;
getContextUsage: (contextLimit: number, outputLimit: number) => SessionContextUsage | null;
updateSessionContextUsage: (sessionId: string, contextLimit: number, outputLimit: number) => void;
initializeSessionContextUsage: (sessionId: string, contextLimit: number, outputLimit: number) => void;
debugSessionMessages: (sessionId: string) => Promise<void>;
pollForTokenUpdates: (sessionId: string, messageId: string, maxAttempts?: number) => void;
updateSession: (session: Session) => void;
removeSessionFromStore: (sessionId: string) => void;
revertToMessage: (sessionId: string, messageId: string) => Promise<void>;
handleSlashUndo: (sessionId: string) => Promise<void>;
handleSlashRedo: (sessionId: string) => Promise<void>;
forkFromMessage: (sessionId: string, messageId: string) => Promise<void>;
setPendingInputText: (text: string | null, mode?: 'replace' | 'append' | 'append-inline') => void;
consumePendingInputText: () => { text: string; mode: 'replace' | 'append' | 'append-inline' } | null;
setPendingSyntheticParts: (parts: SyntheticContextPart[] | null) => void;
consumePendingSyntheticParts: () => SyntheticContextPart[] | null;
}
@@ -41,7 +41,7 @@ export interface AgentGroup {
sessionCount: number;
}
export interface DeleteAgentGroupResult {
interface DeleteAgentGroupResult {
failedIds: string[];
failedWorktreePaths: string[];
}
@@ -50,7 +50,7 @@ export interface DeleteAgentGroupResult {
// parseSessionTitle
// ---------------------------------------------------------------------------
export function parseSessionTitle(title: string | undefined): {
function parseSessionTitle(title: string | undefined): {
groupSlug: string;
provider: string;
model: string;
@@ -516,39 +516,6 @@ async function performFullConfigRefresh(options: { message?: string; delayMs?: n
}
}
export async function reloadOpenCodeConfiguration(options?: { message?: string; delayMs?: number }) {
startConfigUpdate(options?.message || "Reloading OpenCode configuration…");
try {
const response = await runtimeFetch('/api/config/reload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
const payload = await response.json().catch(() => null);
if (!response.ok) {
const message = payload?.error || 'Failed to reload configuration';
throw new Error(message);
}
if (payload?.requiresReload) {
await performFullConfigRefresh({
message: payload.message,
delayMs: payload.reloadDelayMs,
});
} else {
await performFullConfigRefresh(options);
}
} catch (error) {
console.error('[reloadOpenCodeConfiguration] Failed:', error);
updateConfigUpdateMessage('Failed to reload configuration. Please try again.');
await sleep(2000);
finishConfigUpdate();
throw error;
}
}
let unsubscribeCommandsConfigChanges: (() => void) | null = null;
if (!unsubscribeCommandsConfigChanges) {
@@ -632,15 +632,6 @@ export const useGitHubPrStatusStore = create<GitHubPrStatusStore>()(
},
),
);
export const usePrStatusForDirectoryBranch = (directory: string | null, branch: string | null) => {
return useGitHubPrStatusStore((state) => {
if (!directory || !branch) return null;
const key = getGitHubPrStatusKey(directory, branch);
return state.entries[key] ?? null;
});
};
export type PrVisualSummary = {
number: number;
visualState: string;
-34
View File
@@ -1048,13 +1048,6 @@ export const useIsGitRepo = (directory: string | null) => {
});
};
export const useGitFileCount = (directory: string | null) => {
return useGitStore((state) => {
if (!directory) return 0;
return state.directories.get(directory)?.status?.files?.length ?? 0;
});
};
export const useGitBranchLabel = (directory: string | null) => {
return useGitStore((state) => {
if (!directory) return null;
@@ -1083,26 +1076,6 @@ export const useGitAllBranches = () => {
});
};
export const useGitBranchMap = (directories: string[]) => {
const cacheRef = React.useRef<Map<string, string | null>>(new Map());
return useGitStore((state) => {
const prev = cacheRef.current;
let same = prev.size === directories.length;
if (same) {
for (const dir of directories) {
if (prev.get(dir) !== (state.directories.get(dir)?.status?.current ?? null)) { same = false; break; }
}
}
if (same) return prev;
const result = new Map<string, string | null>();
for (const dir of directories) {
result.set(dir, state.directories.get(dir)?.status?.current ?? null);
}
cacheRef.current = result;
return result;
});
};
export const useGitRepoStatusMap = (directories: string[]) => {
const cacheRef = React.useRef<Map<string, { isGitRepo: boolean | null; branch: string | null }>>(new Map());
return useGitStore((state) => {
@@ -1146,10 +1119,3 @@ export const useGitLoadingBranches = (directory: string | null) => {
return state.directories.get(directory)?.isLoadingBranches ?? false;
});
};
export const useGitLoadingIdentity = (directory: string | null) => {
return useGitStore((state) => {
if (!directory) return false;
return state.directories.get(directory)?.isLoadingIdentity ?? false;
});
};
@@ -224,5 +224,3 @@ export const useInlineCommentDraftStore = create<InlineCommentDraftStore>()(
{ name: 'inline-comment-draft-store' }
)
);
export default useInlineCommentDraftStore;
+5 -5
View File
@@ -39,21 +39,21 @@ const getConfigDirectory = (): string | null => {
// ============== TYPES ==============
export interface McpLocalConfig {
interface McpLocalConfig {
type: 'local';
command: string[];
environment?: Record<string, string>;
enabled: boolean;
}
export interface McpOAuthConfig {
interface McpOAuthConfig {
clientId?: string;
clientSecret?: string;
scope?: string;
redirectUri?: string;
}
export interface McpRemoteConfig {
interface McpRemoteConfig {
type: 'remote';
url: string;
environment?: Record<string, string>;
@@ -64,7 +64,7 @@ export interface McpRemoteConfig {
}
export type McpServerConfig = (McpLocalConfig | McpRemoteConfig) & { name: string };
export type McpServerWithScope = McpServerConfig & { scope?: McpScope | null };
type McpServerWithScope = McpServerConfig & { scope?: McpScope | null };
export interface McpDraft {
name: string;
@@ -90,7 +90,7 @@ export const envRecordToArray = (env?: Record<string, string>): Array<{ key: str
return Object.entries(env).map(([key, value]) => ({ key, value }));
};
export const envArrayToRecord = (arr: Array<{ key: string; value: string }>): Record<string, string> | undefined => {
const envArrayToRecord = (arr: Array<{ key: string; value: string }>): Record<string, string> | undefined => {
const filtered = arr.filter((e) => e.key.trim());
if (filtered.length === 0) return undefined;
return Object.fromEntries(filtered.map((e) => [e.key.trim(), e.value]));
+2 -2
View File
@@ -5,11 +5,11 @@ import { opencodeClient } from '@/lib/opencode/client';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
export type McpStatusMap = Record<string, McpStatus>;
export type McpRuntimeDiagnostic = {
type McpRuntimeDiagnostic = {
status: 'failed';
error: string;
};
export type McpRuntimeDiagnosticMap = Record<string, McpRuntimeDiagnostic>;
type McpRuntimeDiagnosticMap = Record<string, McpRuntimeDiagnostic>;
const EMPTY_STATUS: McpStatusMap = {};
const EMPTY_DIAGNOSTICS: McpRuntimeDiagnosticMap = {};
+2 -2
View File
@@ -11,7 +11,7 @@ import { opencodeClient } from '@/lib/opencode/client';
import { runtimeFetch } from '@/lib/runtime-fetch';
export type PluginScope = 'user' | 'project';
export type PluginParsedKind = 'npm' | 'path';
type PluginParsedKind = 'npm' | 'path';
export interface PluginEntry {
id: string;
@@ -123,7 +123,7 @@ const getConfigDirectory = (): string | null => {
};
const CLIENT_RELOAD_DELAY_MS = 800;
export const PLUGINS_LOAD_CACHE_TTL_MS = 5000;
const PLUGINS_LOAD_CACHE_TTL_MS = 5000;
const DEFAULT_PLUGINS_CACHE_KEY = '__default__';
const pluginsLastLoadedAt = new Map<string, number>();
const pluginsLoadInFlight = new Map<string, Promise<boolean>>();
@@ -1,7 +1,7 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export type SessionDisplayMode = 'default' | 'minimal';
type SessionDisplayMode = 'default' | 'minimal';
type SessionDisplayStore = {
displayMode: SessionDisplayMode;
+2 -2
View File
@@ -40,7 +40,7 @@ export interface SupportingFile {
fullPath: string;
}
export interface SkillSources {
interface SkillSources {
md: {
exists: boolean;
path: string | null;
@@ -123,7 +123,7 @@ export interface SkillDraft {
pendingFiles?: PendingFile[];
}
export interface SkillDetail {
interface SkillDetail {
name: string;
sources: SkillSources;
scope?: SkillScope | null;
+1 -1
View File
@@ -8,7 +8,7 @@ import { useDirectoryStore } from '@/stores/useDirectoryStore';
export type SnippetScope = 'global' | 'project';
export interface SnippetDraft {
interface SnippetDraft {
name: string;
scope: SnippetScope;
content?: string;
+1 -1
View File
@@ -13,7 +13,7 @@ import {
} from '@/lib/desktop';
import { runtimeFetch } from '@/lib/runtime-fetch';
export type UpdateState = {
type UpdateState = {
checking: boolean;
available: boolean;
downloading: boolean;
@@ -1,46 +0,0 @@
import type { Message, Part } from '@opencode-ai/sdk/v2';
export const resolveClientRole = (info: Pick<Message, 'role'> & { clientRole?: string | null }): string => {
const role = info.clientRole ?? info.role;
return typeof role === 'string' ? role : '';
};
export const normalizeMessageInfoForProjection = <T extends Message>(info: T): T => {
const clientRole = resolveClientRole(info);
const shouldMarkUser = clientRole === 'user';
return {
...info,
clientRole,
...(shouldMarkUser ? { userMessageMarker: true } : {}),
} as T;
};
export interface ChatMessageRecord {
info: Message;
parts: Part[];
}
export const normalizeMessageRecordsForProjection = (messages: ChatMessageRecord[]): ChatMessageRecord[] => {
return messages.map((message) => ({
...message,
info: normalizeMessageInfoForProjection(message.info),
parts: Array.isArray(message.parts) ? message.parts : [],
}));
};
export const filterMessagesByRevertPoint = <T extends { info: { id: string } }>(
messages: T[],
revertMessageId: string | null,
): T[] => {
if (!revertMessageId) {
return messages;
}
const revertIndex = messages.findIndex((message) => message.info.id === revertMessageId);
if (revertIndex < 0) {
return messages;
}
return messages.slice(0, revertIndex);
};
@@ -1,286 +0,0 @@
import type { Part } from "@opencode-ai/sdk/v2";
import { isFinalToolStatus } from "@/lib/toolStatus";
const isRecord = (value: unknown): value is Record<string, unknown> => {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
};
const readTimestamp = (value: unknown): number | undefined => {
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
};
const mergeTimeRange = (existing: unknown, incoming: unknown): Record<string, number> | undefined => {
const existingTime = isRecord(existing) ? existing : undefined;
const incomingTime = isRecord(incoming) ? incoming : undefined;
const startCandidates = [
readTimestamp(existingTime?.start),
readTimestamp(incomingTime?.start),
].filter((value): value is number => typeof value === 'number');
const endCandidates = [
readTimestamp(existingTime?.end),
readTimestamp(incomingTime?.end),
].filter((value): value is number => typeof value === 'number');
if (startCandidates.length === 0 && endCandidates.length === 0) {
return undefined;
}
const merged: Record<string, number> = {};
if (startCandidates.length > 0) {
merged.start = Math.min(...startCandidates);
}
if (endCandidates.length > 0) {
merged.end = Math.max(...endCandidates);
}
return merged;
};
const mergeToolState = (existing: unknown, incoming: unknown): Record<string, unknown> | undefined => {
const existingState = isRecord(existing) ? existing : undefined;
const incomingState = isRecord(incoming) ? incoming : undefined;
if (!existingState && !incomingState) {
return undefined;
}
// Never downgrade a terminal status. Once a tool reaches completed/error/etc,
// a late-arriving "running" SSE must not overwrite it.
const existingStatus = existingState?.status;
const incomingStatus = incomingState?.status;
const existingIsTerminal = typeof existingStatus === 'string' && isFinalToolStatus(existingStatus);
const incomingIsTerminal = typeof incomingStatus === 'string' && isFinalToolStatus(incomingStatus);
const merged: Record<string, unknown> = {
...(existingState ?? {}),
...(incomingState ?? {}),
};
// If existing was terminal but incoming is not, keep the terminal status.
if (existingIsTerminal && !incomingIsTerminal && typeof incomingStatus === 'string') {
merged.status = existingStatus;
}
const mergedTime = mergeTimeRange(existingState?.time, incomingState?.time);
if (mergedTime) {
merged.time = mergedTime;
}
if (isRecord(existingState?.metadata) || isRecord(incomingState?.metadata)) {
merged.metadata = {
...(isRecord(existingState?.metadata) ? existingState.metadata : {}),
...(isRecord(incomingState?.metadata) ? incomingState.metadata : {}),
};
}
if (isRecord(existingState?.input) || isRecord(incomingState?.input)) {
merged.input = {
...(isRecord(existingState?.input) ? existingState.input : {}),
...(isRecord(incomingState?.input) ? incomingState.input : {}),
};
}
return merged;
};
const extractTextFromDelta = (delta: unknown): string => {
if (!delta) return '';
if (typeof delta === 'string') return delta;
if (Array.isArray(delta)) {
return delta.map((item) => extractTextFromDelta(item)).join('');
}
if (typeof delta === 'object') {
if (typeof (delta as { text?: unknown }).text === 'string') {
return (delta as { text: string }).text;
}
if (Array.isArray((delta as { content?: unknown[] }).content)) {
return (delta as { content: unknown[] }).content.map((item: unknown) => extractTextFromDelta(item)).join('');
}
}
return '';
};
export const extractTextFromPart = (part: unknown): string => {
if (!part) return '';
const typedPart = part as { text?: string | unknown[]; content?: string | unknown[]; value?: string | unknown[]; delta?: unknown };
const toText = (value: unknown): string => {
if (typeof value === 'string') {
return value;
}
if (Array.isArray(value)) {
return value
.map((item: unknown) => {
if (typeof item === 'string') return item;
if (item && typeof item === 'object') {
const typedItem = item as { text?: unknown; content?: unknown; value?: unknown; delta?: unknown };
return toText(typedItem.text) || toText(typedItem.content) || toText(typedItem.value) || extractTextFromDelta(typedItem.delta);
}
return '';
})
.join('');
}
return '';
};
const candidates = [
toText(typedPart.text),
toText(typedPart.content),
toText(typedPart.value),
extractTextFromDelta(typedPart.delta),
];
let best = '';
for (const candidate of candidates) {
if (candidate.length > best.length) {
best = candidate;
}
}
return best;
};
export const normalizeStreamingPart = (incoming: Part, existing?: Part): Part => {
const normalized: { type?: string; text?: string; content?: string; value?: string; delta?: unknown; [key: string]: unknown } = {
...(existing as Record<string, unknown> | undefined),
...incoming,
} as { type?: string; text?: string; content?: string; value?: string; delta?: unknown; [key: string]: unknown };
const existingType = typeof (existing as { type?: unknown } | undefined)?.type === 'string'
? (existing as { type: string }).type
: undefined;
normalized.type = normalized.type || existingType || 'text';
const isStreamingTextLikePart = normalized.type === 'text' || normalized.type === 'reasoning';
if (isStreamingTextLikePart) {
const existingRecord = (existing ?? {}) as { text?: unknown; content?: unknown; value?: unknown };
const existingText = extractTextFromPart(existing);
const directText = extractTextFromPart(incoming);
const deltaText = extractTextFromDelta((incoming as { delta?: unknown }).delta);
let mergedText = '';
const incomingField =
typeof normalized.text === 'string'
? 'text'
: typeof normalized.content === 'string'
? 'content'
: typeof normalized.value === 'string'
? 'value'
: null;
const targetField = incomingField ?? (
typeof existingRecord.text === 'string'
? 'text'
: typeof existingRecord.content === 'string'
? 'content'
: typeof existingRecord.value === 'string'
? 'value'
: 'text'
);
if (deltaText) {
mergedText = existingText ? `${existingText}${deltaText}` : deltaText;
} else if (directText) {
mergedText = directText;
} else {
mergedText = existingText;
}
normalized[targetField] = mergedText;
if (targetField !== 'text') {
normalized.text = mergedText;
}
delete normalized.delta;
}
const mergedTime = mergeTimeRange(
(existing as { time?: unknown } | undefined)?.time,
(incoming as { time?: unknown } | undefined)?.time,
);
if (mergedTime) {
normalized.time = mergedTime;
}
if (normalized.type === 'tool') {
const mergedState = mergeToolState(
(existing as { state?: unknown } | undefined)?.state,
(incoming as { state?: unknown } | undefined)?.state,
);
if (mergedState) {
normalized.state = mergedState;
}
}
return normalized as Part;
};
const deepEqualRecord = (left: Record<string, unknown>, right: Record<string, unknown>): boolean => {
const keys = new Set<string>([
...Object.keys(left),
...Object.keys(right),
]);
for (const key of keys) {
const leftValue = left[key];
const rightValue = right[key];
if (Array.isArray(leftValue) || Array.isArray(rightValue)) {
if (!Array.isArray(leftValue) || !Array.isArray(rightValue) || leftValue.length !== rightValue.length) {
return false;
}
for (let index = 0; index < leftValue.length; index += 1) {
if (!deepEqualUnknown(leftValue[index], rightValue[index])) {
return false;
}
}
continue;
}
if (!deepEqualUnknown(leftValue, rightValue)) {
return false;
}
}
return true;
};
const deepEqualUnknown = (left: unknown, right: unknown): boolean => {
if (left === right) {
return true;
}
if (!left || !right) {
return false;
}
if (typeof left !== typeof right) {
return false;
}
if (typeof left === 'object' && typeof right === 'object') {
if (Array.isArray(left) || Array.isArray(right)) {
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
return false;
}
for (let index = 0; index < left.length; index += 1) {
if (!deepEqualUnknown(left[index], right[index])) {
return false;
}
}
return true;
}
return deepEqualRecord(left as Record<string, unknown>, right as Record<string, unknown>);
}
return false;
};
export const arePartsEquivalent = (left: Part | undefined, right: Part | undefined): boolean => {
if (!left || !right) {
return left === right;
}
return deepEqualUnknown(left, right);
};
@@ -1,20 +1,5 @@
import type { EditPermissionMode } from "../types/sessionTypes";
const EDIT_PERMISSION_TOOL_NAMES = new Set([
'edit',
'multiedit',
'str_replace',
'str_replace_based_edit_tool',
'write',
]);
export const isEditPermissionType = (type?: string | null): boolean => {
if (!type) {
return false;
}
return EDIT_PERMISSION_TOOL_NAMES.has(type.toLowerCase());
};
type PermissionAction = 'allow' | 'deny' | 'ask';
type PermissionRule = {
+2 -11
View File
@@ -7,15 +7,6 @@ export const streamDebugEnabled = (): boolean => {
}
};
export const sessionStatusDebugEnabled = (): boolean => {
if (typeof window === 'undefined') return false;
try {
return window.localStorage.getItem('openchamber_session_status_debug') === '1';
} catch {
return false;
}
};
const STREAM_PERF_STORAGE_KEY = 'openchamber_stream_perf';
type PerfCounter = {
@@ -31,7 +22,7 @@ type StreamPerfState = {
lastUpdatedAt: number;
};
export type StreamPerfEntry = {
type StreamPerfEntry = {
metric: string;
count: number;
avg: number;
@@ -61,7 +52,7 @@ declare global {
}
}
export const streamPerfEnabled = (): boolean => {
const streamPerfEnabled = (): boolean => {
if (typeof window === 'undefined') return false;
try {
return window.localStorage.getItem(STREAM_PERF_STORAGE_KEY) === '1';
@@ -1,55 +0,0 @@
import type { MessageStreamLifecycle } from "../types/sessionTypes";
export type { MessageStreamLifecycle };
export const touchStreamingLifecycle = (
source: Map<string, MessageStreamLifecycle>,
messageId: string
): Map<string, MessageStreamLifecycle> => {
const now = Date.now();
const existing = source.get(messageId);
const next = new Map(source);
next.set(messageId, {
phase: 'streaming',
startedAt: existing?.startedAt ?? now,
lastUpdateAt: now,
});
return next;
};
export const removeLifecycleEntries = (
source: Map<string, MessageStreamLifecycle>,
ids: Iterable<string>
): Map<string, MessageStreamLifecycle> => {
const idsArray = Array.from(ids);
const shouldClone = idsArray.some((id) => source.has(id));
if (!shouldClone) {
return source;
}
const next = new Map(source);
idsArray.forEach((id) => {
next.delete(id);
});
return next;
};
const lifecycleCompletionTimers = new Map<string, ReturnType<typeof setTimeout>>();
export const clearLifecycleCompletionTimer = (messageId: string) => {
const timer = lifecycleCompletionTimers.get(messageId);
if (timer) {
clearTimeout(timer);
lifecycleCompletionTimers.delete(messageId);
}
};
export const clearLifecycleTimersForIds = (ids: Iterable<string>) => {
for (const id of ids) {
clearLifecycleCompletionTimer(id);
}
};