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
@@ -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",
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user