diff --git a/.agent-sessions/sessions.db b/.agent-sessions/sessions.db deleted file mode 100644 index b8c26e4..0000000 Binary files a/.agent-sessions/sessions.db and /dev/null differ diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..80c4e41 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +.git +.github +.gitignore +.agent-sessions +.env +.env.local +.env.* +node_modules +**/node_modules +packages/*/dist +*.tsbuildinfo +bundle.* +docs +tests +.DS_Store diff --git a/.gitignore b/.gitignore index cd7381b..4d7641e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,9 @@ bundle.* docs/* !docs/index.html .DS_Store # macOS +.agent-sessions/* +packages/client/dist/* +packages/server/dist/* +packages/agent-core/dist/* +sessions.db +packages/session-sqlite/dist/* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..72e2e97 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# 使用 Node 24 提供 node:sqlite 和 npm,Bun 负责按 bun.lock 安装依赖 +FROM node:24-bookworm-slim + +# coding agent 会在挂载的工作区执行命令,补齐常用工具 +RUN apt-get update \ + && apt-get install -y --no-install-recommends bash git ripgrep ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# 与 packageManager 保持一致,避免 bun.lock 被其他版本改写 +RUN npm install -g bun@1.3.11 + +WORKDIR /app + +# 先复制清单文件,依赖层可以复用 Docker 缓存 +COPY package.json bun.lock bunfig.toml tsconfig.json ./ +COPY packages/agent-core/package.json packages/agent-core/ +COPY packages/client/package.json packages/client/ +COPY packages/session-sqlite/package.json packages/session-sqlite/ + +RUN bun install --frozen-lockfile + +COPY source source +COPY packages/agent-core packages/agent-core +COPY packages/session-sqlite packages/session-sqlite + +# 默认把挂载的用户目录作为工作区,会话和工具执行都落在里面 +WORKDIR /workspace + +# 入口用 tsx 加载 TypeScript,tsconfig 固定指向 /app,避免受工作区影响 +CMD ["node", "/app/node_modules/tsx/dist/cli.mjs", "--tsconfig", "/app/tsconfig.json", "/app/source/app.tsx"] diff --git a/README.md b/README.md index 2d232fa..006fc7b 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ call-code 是一个本地运行的终端编程 Agent(CLI coding agent),基 - 本地记忆:短期记忆按任务保存,长期记忆按主题沉淀,仅在进程内使用,不写入本地 JSON。 - 上下文预算:运行时基于 token 估算对历史消息做裁剪,减少超出模型上下文的风险。 - 会话持久化:基于 Node 内置 `node:sqlite` 保存会话、条目、泳道、分支、记录、统计、事实和租约,默认写入 `.agent-sessions/sessions.db`。 -- 会话客户端:`packages/client` 提供 React + Vite 会话界面,静态展示已停止,后续用于实时对话展示。 +- 会话客户端:`packages/client` 提供 React + Vite 会话界面,通过 WebSocket 实时展示会话数据。 ## 架构 @@ -63,6 +63,7 @@ OpenAI-compatible LLM 模型层 ```text source/ app.tsx # Ink CLI 入口与交互界面 + web-server.ts # Web 会话面板服务入口 packages/ agent-core/ # 核心 agent、上下文、记忆、工具与协议实现 src/ @@ -80,8 +81,8 @@ packages/ utils/ # shell 与文本截断等工具 types/ # 领域类型 utils/ # JSON、日志工具 - web/ # 会话数据导出 - client/ # TypeScript + React 会话界面客户端 + web/ # 会话导出与 WebSocket 服务 + client/ # TypeScript + React 会话界面客户端(WebSocket 数据) session-sqlite/ # 基于 node:sqlite 的会话历史与运行状态存储 tests/ # 项目统一单元测试 vitest.config.ts # Vitest 测试配置 @@ -121,6 +122,7 @@ bun dev | `AGENT_DESKTOP_DIR` | 可选,覆盖桌面目录路径,便于测试或自定义工作环境。 | | `SESSION_DB_PATH` | 可选,SQLite 会话库文件路径,默认 `.agent-sessions/sessions.db`。 | | `CALL_CODE_WEB_DATA` | 可选,CLI 内 `/export` 的输出路径,默认 `packages/client/public/data.json`。 | +| `CALL_CODE_WEB_PORT` | 可选,Web 会话面板监听端口,默认 4173。 | ## CLI 命令与快捷键 @@ -169,8 +171,40 @@ bun run export:web # 构建会话客户端(本地预览用) bun run build:client + +# 启动 Web 会话面板(静态托管客户端 + WebSocket 数据服务) +bun run web:serve +``` + +## Web 会话面板 + +Web 会话面板由 `source/web-server.ts` 启动:它读取 `SESSION_DB_PATH` 指向的会话库,静态托管 `packages/client/dist`,并在 `/ws` 提供 `sessions.list` / `sessions.snapshot` 消息。客户端默认连接同源 `/ws`,并周期性刷新会话快照;也可以用 `?ws=` 覆盖服务地址,用 `?session=` 直达某个会话。 + +```bash +bun run build:client +bun run web:serve +``` + +然后打开 `http://127.0.0.1:4173`。开发模式下先启动 `web:serve`,再运行 `bun run dev:client`,Vite 会把 `/ws` 代理到会话服务。 + +## Docker 镜像 + +构建镜像: + +```bash +docker build -t call-code . ``` +以当前目录作为工作区运行,容器内工作目录固定为 `/workspace`,会话库会写入 `/workspace/.agent-sessions`: + +```bash +docker run --rm -it \ + -v "$PWD":/workspace \ + call-code +``` + +API 配置优先从挂载目录下的 `.env.local` 读取,也可以用 `-e OPENAI_API_KEY=... -e OPENAI_MODEL=...` 传入。 + ## 测试说明 测试文件统一放在项目根目录的 `tests/` 目录下,根目录的 `vitest.config.ts` 会统一收集并运行。 diff --git a/bun.lock b/bun.lock index 21444af..a79467b 100644 --- a/bun.lock +++ b/bun.lock @@ -32,8 +32,10 @@ "openai": "^6.34.0", "tesseract.js": "^7.0.0", "tiktoken": "^1.0.20", + "ws": "^8.21.3", }, "devDependencies": { + "@types/ws": "^8.18.1", "typescript": "^6.0.3", }, }, @@ -41,6 +43,7 @@ "name": "@call-code/client", "version": "0.1.0", "dependencies": { + "@call-code/server": "workspace:*", "react": "^19.2.8", "react-dom": "^19.2.8", }, @@ -54,6 +57,20 @@ "vite": "^8.2.0", }, }, + "packages/server": { + "name": "@call-code/server", + "version": "0.1.0", + "dependencies": { + "@call-code/agent-core": "workspace:*", + "@call-code/session-sqlite": "workspace:*", + "ws": "^8.21.3", + }, + "devDependencies": { + "@types/node": "^25.6.0", + "@types/ws": "^8.18.1", + "typescript": "^6.0.3", + }, + }, "packages/session-sqlite": { "name": "@call-code/session-sqlite", "version": "0.1.0", @@ -73,6 +90,8 @@ "@call-code/client": ["@call-code/client@workspace:packages/client"], + "@call-code/server": ["@call-code/server@workspace:packages/server"], + "@call-code/session-sqlite": ["@call-code/session-sqlite@workspace:packages/session-sqlite"], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], @@ -263,6 +282,8 @@ "@types/react-dom": ["@types/react-dom@19.2.4", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.5", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA=="], "@vitest/expect": ["@vitest/expect@3.2.7", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.7", "@vitest/utils": "3.2.7", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w=="], diff --git a/package.json b/package.json index 896b1ec..02def43 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,13 @@ "typecheck:client": "tsc -p packages/client/tsconfig.json --noEmit", "test": "vitest run --reporter verbose", "build:agent-core": "tsc -p packages/agent-core/tsconfig.json", + "build:session-sqlite": "tsc -p packages/session-sqlite/tsconfig.json", + "build:server": "bun run build:session-sqlite && bun run build:agent-core && bun run --cwd packages/server build", "export:web": "tsx tests/export-web.ts", "dev:client": "bun run --cwd packages/client dev", "build:client": "bun run --cwd packages/client build", - "preview:client": "bun run --cwd packages/client preview" + "preview:client": "bun run --cwd packages/client preview", + "web:serve": "tsx source/web-server.ts" }, "dependencies": { "dotenv": "^17.4.2", diff --git a/packages/agent-core/package.json b/packages/agent-core/package.json index f07f34a..a7e5922 100644 --- a/packages/agent-core/package.json +++ b/packages/agent-core/package.json @@ -13,9 +13,11 @@ "dotenv": "^17.4.2", "openai": "^6.34.0", "tesseract.js": "^7.0.0", - "tiktoken": "^1.0.20" + "tiktoken": "^1.0.20", + "ws": "^8.21.3" }, "devDependencies": { + "@types/ws": "^8.18.1", "typescript": "^6.0.3" } } diff --git a/packages/agent-core/src/harness/compaction/branch-summarization.ts b/packages/agent-core/src/harness/compaction/branch-summarization.ts index 74eaefb..1100aa1 100644 --- a/packages/agent-core/src/harness/compaction/branch-summarization.ts +++ b/packages/agent-core/src/harness/compaction/branch-summarization.ts @@ -1,6 +1,6 @@ -import type { ContextMessage } from '../context/context-types'; -import type { EntryLike, SessionStoreLike } from '../session/store-types'; -import type { SummarizeFn } from './compaction'; +import type { ContextMessage } from '../context/context-types.js'; +import type { EntryLike, SessionStoreLike } from '../session/store-types.js'; +import type { SummarizeFn } from './compaction.js'; import { computeFileLists, createFileOps, @@ -9,7 +9,7 @@ import { formatFileOperations, serializeConversation, type FileOperations, -} from './utils'; +} from './utils.js'; export interface BranchSummaryEntryPayload { kind: 'branch_summary'; diff --git a/packages/agent-core/src/harness/compaction/compaction.ts b/packages/agent-core/src/harness/compaction/compaction.ts index 54f71d2..83d7c78 100644 --- a/packages/agent-core/src/harness/compaction/compaction.ts +++ b/packages/agent-core/src/harness/compaction/compaction.ts @@ -1,5 +1,5 @@ -import type { ContextMessage } from '../context/context-types'; -import type { EntryLike, SessionStoreLike } from '../session/store-types'; +import type { ContextMessage } from '../context/context-types.js'; +import type { EntryLike, SessionStoreLike } from '../session/store-types.js'; import { computeFileLists, createFileOps, @@ -9,7 +9,7 @@ import { formatFileOperations, serializeConversation, type FileOperations, -} from './utils'; +} from './utils.js'; export interface CompactionSettings { enabled: boolean; diff --git a/packages/agent-core/src/harness/compaction/index.ts b/packages/agent-core/src/harness/compaction/index.ts index 52d476d..8b73e51 100644 --- a/packages/agent-core/src/harness/compaction/index.ts +++ b/packages/agent-core/src/harness/compaction/index.ts @@ -1,3 +1,3 @@ -export * from './branch-summarization'; -export * from './compaction'; -export * from './utils'; +export * from './branch-summarization.js'; +export * from './compaction.js'; +export * from './utils.js'; diff --git a/packages/agent-core/src/harness/compaction/utils.ts b/packages/agent-core/src/harness/compaction/utils.ts index 64b9948..68ce017 100644 --- a/packages/agent-core/src/harness/compaction/utils.ts +++ b/packages/agent-core/src/harness/compaction/utils.ts @@ -1,5 +1,5 @@ -import type { ContextMessage } from '../context/context-types'; -import type { EntryLike } from '../session/store-types'; +import type { ContextMessage } from '../context/context-types.js'; +import type { EntryLike } from '../session/store-types.js'; /** 摘要阶段累积的文件读写信息,后续追加到摘要里让模型保留文件上下文。 */ export interface FileOperations { diff --git a/packages/agent-core/src/harness/context/builder.ts b/packages/agent-core/src/harness/context/builder.ts index c3ac36f..b5f3f9e 100644 --- a/packages/agent-core/src/harness/context/builder.ts +++ b/packages/agent-core/src/harness/context/builder.ts @@ -1,7 +1,7 @@ -export { ContextBuilder } from './context-builder'; +export { ContextBuilder } from './context-builder.js'; export type { ContextMessage, MessageRole, RuntimeContext, -} from './context-types'; -export { buildRuntimeContext } from './runtime-context'; +} from './context-types.js'; +export { buildRuntimeContext } from './runtime-context.js'; diff --git a/packages/agent-core/src/harness/context/context-builder.ts b/packages/agent-core/src/harness/context/context-builder.ts index f1eb1e7..e4a672e 100644 --- a/packages/agent-core/src/harness/context/context-builder.ts +++ b/packages/agent-core/src/harness/context/context-builder.ts @@ -1,5 +1,5 @@ import { get_encoding, type Tiktoken } from 'tiktoken'; -import type { ContextMessage } from './context-types'; +import type { ContextMessage } from './context-types.js'; export class ContextBuilder { private readonly encoder: Tiktoken; diff --git a/packages/agent-core/src/harness/context/context-summarizer.ts b/packages/agent-core/src/harness/context/context-summarizer.ts index 11c9d84..16da1be 100644 --- a/packages/agent-core/src/harness/context/context-summarizer.ts +++ b/packages/agent-core/src/harness/context/context-summarizer.ts @@ -1,4 +1,4 @@ -import type { ContextMessage } from './context-types'; +import type { ContextMessage } from './context-types.js'; export interface HistorySummary { summary: string; diff --git a/packages/agent-core/src/harness/context/runtime-context.ts b/packages/agent-core/src/harness/context/runtime-context.ts index eb470a6..1730152 100644 --- a/packages/agent-core/src/harness/context/runtime-context.ts +++ b/packages/agent-core/src/harness/context/runtime-context.ts @@ -1,11 +1,11 @@ -import type { TaskState } from '@agent-core/harness/core/state'; -import { createTaskContext } from './task-context'; -import { summarizeHistory } from './context-summarizer'; -import { ContextBuilder } from './context-builder'; +import type { TaskState } from '../core/state.js'; +import { createTaskContext } from './task-context.js'; +import { summarizeHistory } from './context-summarizer.js'; +import { ContextBuilder } from './context-builder.js'; import type { ContextMessage, RuntimeContext, -} from './context-types'; +} from './context-types.js'; export interface BuildRuntimeContextInput { system: string; diff --git a/packages/agent-core/src/harness/context/task-context.ts b/packages/agent-core/src/harness/context/task-context.ts index 0d62d96..066fe35 100644 --- a/packages/agent-core/src/harness/context/task-context.ts +++ b/packages/agent-core/src/harness/context/task-context.ts @@ -1,4 +1,4 @@ -import type { TaskState } from '@agent-core/harness/core/state'; +import type { TaskState } from '../core/state.js'; export interface TaskContext { id: string; diff --git a/packages/agent-core/src/harness/core/agent.ts b/packages/agent-core/src/harness/core/agent.ts index 04fef07..19c67be 100644 --- a/packages/agent-core/src/harness/core/agent.ts +++ b/packages/agent-core/src/harness/core/agent.ts @@ -1,6 +1,6 @@ -import { runLoop } from '@agent-core/harness/runtime/run-loop'; -import type { StreamHandlers } from '@agent-core/harness/core/llm'; -import { createTaskState, type AgentMode } from '@agent-core/harness/core/state'; +import { runLoop } from '../runtime/run-loop.js'; +import type { StreamHandlers } from './llm.js'; +import { createTaskState, type AgentMode } from './state.js'; export interface AgentOptions { mode?: AgentMode; diff --git a/packages/agent-core/src/harness/memory/memory-retriever.ts b/packages/agent-core/src/harness/memory/memory-retriever.ts index 528764b..d2ad9d1 100644 --- a/packages/agent-core/src/harness/memory/memory-retriever.ts +++ b/packages/agent-core/src/harness/memory/memory-retriever.ts @@ -1,4 +1,4 @@ -import { memoryStore } from './memory-store'; +import { memoryStore } from './memory-store.js'; export interface RetrievedMemory { longFacts: string[]; diff --git a/packages/agent-core/src/harness/memory/memory-store.ts b/packages/agent-core/src/harness/memory/memory-store.ts index c1dfe49..68abb8b 100644 --- a/packages/agent-core/src/harness/memory/memory-store.ts +++ b/packages/agent-core/src/harness/memory/memory-store.ts @@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto'; import type { LongMemoryItem, MemorySnapshot, -} from './memory-schema'; +} from './memory-schema.js'; export class MemoryStore { private readonly longMemory: LongMemoryItem[] = []; diff --git a/packages/agent-core/src/harness/memory/memory-writer.ts b/packages/agent-core/src/harness/memory/memory-writer.ts index afebcfe..49d21be 100644 --- a/packages/agent-core/src/harness/memory/memory-writer.ts +++ b/packages/agent-core/src/harness/memory/memory-writer.ts @@ -1,6 +1,6 @@ -import type { TaskState } from '@core/state'; -import type { ContextMessage } from '../context/context-types'; -import { memoryStore } from './memory-store'; +import type { TaskState } from '../core/state.js'; +import type { ContextMessage } from '../context/context-types.js'; +import { memoryStore } from './memory-store.js'; const countStableMentions = (messages: ContextMessage[], text: string): number => messages.reduce((count, item) => { diff --git a/packages/agent-core/src/harness/prompt/modes.ts b/packages/agent-core/src/harness/prompt/modes.ts index 4605e0b..c7e956c 100644 --- a/packages/agent-core/src/harness/prompt/modes.ts +++ b/packages/agent-core/src/harness/prompt/modes.ts @@ -1,4 +1,4 @@ -import type { AgentMode } from '@agent-core/harness/core/state'; +import type { AgentMode } from '../core/state.js'; const modeSystemPrompts: Record = { plan: ` diff --git a/packages/agent-core/src/harness/protocol/index.ts b/packages/agent-core/src/harness/protocol/index.ts index e00daa1..99b88da 100644 --- a/packages/agent-core/src/harness/protocol/index.ts +++ b/packages/agent-core/src/harness/protocol/index.ts @@ -1,5 +1,5 @@ -export type { ToolCallAction, FinalAction, AgentAction } from '@agent-core/harness/protocol/action'; -export { isToolCallAction, isFinalAction, isAgentAction } from '@agent-core/harness/protocol/action'; -export type { ToolResultObservation } from '@agent-core/harness/protocol/observation'; -export { createToolResultObservation } from '@agent-core/harness/protocol/observation'; -export { parseAgentResponse, shouldContinueLoop, extractFinalText } from '@agent-core/harness/protocol/parser'; +export type { ToolCallAction, FinalAction, AgentAction } from './action.js'; +export { isToolCallAction, isFinalAction, isAgentAction } from './action.js'; +export type { ToolResultObservation } from './observation.js'; +export { createToolResultObservation } from './observation.js'; +export { parseAgentResponse, shouldContinueLoop, extractFinalText } from './parser.js'; diff --git a/packages/agent-core/src/harness/protocol/parser.ts b/packages/agent-core/src/harness/protocol/parser.ts index 0aa37ca..8c984b8 100644 --- a/packages/agent-core/src/harness/protocol/parser.ts +++ b/packages/agent-core/src/harness/protocol/parser.ts @@ -1,4 +1,4 @@ -import { isAgentAction, isFinalAction, isToolCallAction, type AgentAction } from '@agent-core/harness/protocol/action'; +import { isAgentAction, isFinalAction, isToolCallAction, type AgentAction } from './action.js'; export const parseAgentResponse = (response: string): AgentAction | null => { try { diff --git a/packages/agent-core/src/harness/runtime/index.ts b/packages/agent-core/src/harness/runtime/index.ts index 8c269df..381d086 100644 --- a/packages/agent-core/src/harness/runtime/index.ts +++ b/packages/agent-core/src/harness/runtime/index.ts @@ -1,3 +1,3 @@ -export * from './run-loop'; -export * from './session-runtime'; -export * from './tool-runtime'; +export * from './run-loop.js'; +export * from './session-runtime.js'; +export * from './tool-runtime.js'; diff --git a/packages/agent-core/src/harness/runtime/run-loop.ts b/packages/agent-core/src/harness/runtime/run-loop.ts index 8396c5e..4721afd 100644 --- a/packages/agent-core/src/harness/runtime/run-loop.ts +++ b/packages/agent-core/src/harness/runtime/run-loop.ts @@ -1,23 +1,23 @@ -import { callLLM, streamLLM, type StreamHandlers } from '../core/llm'; -import { ContextBuilder } from '../context/context-builder'; -import { buildRuntimeContext } from '../context/runtime-context'; -import type { ContextMessage } from '../context/context-types'; -import { systemPrompt } from '../prompt/system'; -import { toolPrompt } from '../prompt/tool'; -import { getModePrompt } from '../prompt/modes'; -import type { TaskState } from '../core/state'; +import { callLLM, streamLLM, type StreamHandlers } from '../core/llm.js'; +import { ContextBuilder } from '../context/context-builder.js'; +import { buildRuntimeContext } from '../context/runtime-context.js'; +import type { ContextMessage } from '../context/context-types.js'; +import { systemPrompt } from '../prompt/system.js'; +import { toolPrompt } from '../prompt/tool.js'; +import { getModePrompt } from '../prompt/modes.js'; +import type { TaskState } from '../core/state.js'; import { extractFinalText, parseAgentResponse, shouldContinueLoop, -} from '../protocol/parser'; -import { isToolCallAction } from '../protocol/action'; -import { promoteStableFact } from '../memory/memory-writer'; -import { retrieveMemoryForTask } from '../memory/memory-retriever'; -import { getSharedSessionStoreOrNull } from '../session/store-registry'; -import type { SessionStoreLike } from '../session/store-types'; -import { createSessionRuntime } from './session-runtime'; -import { runToolCall } from './tool-runtime'; +} from '../protocol/parser.js'; +import { isToolCallAction } from '../protocol/action.js'; +import { promoteStableFact } from '../memory/memory-writer.js'; +import { retrieveMemoryForTask } from '../memory/memory-retriever.js'; +import { getSharedSessionStoreOrNull } from '../session/store-registry.js'; +import type { SessionStoreLike } from '../session/store-types.js'; +import { createSessionRuntime } from './session-runtime.js'; +import { runToolCall } from './tool-runtime.js'; import { DEFAULT_COMPACTION_SETTINGS, compact, @@ -26,8 +26,8 @@ import { shouldCompact, type CompactionSettings, type SummarizeFn, -} from '../compaction/compaction'; -import { estimateContextTokens } from '../compaction/utils'; +} from '../compaction/compaction.js'; +import { estimateContextTokens } from '../compaction/utils.js'; const contextBuilder = new ContextBuilder(8000); diff --git a/packages/agent-core/src/harness/runtime/session-runtime.ts b/packages/agent-core/src/harness/runtime/session-runtime.ts index baff470..df12da7 100644 --- a/packages/agent-core/src/harness/runtime/session-runtime.ts +++ b/packages/agent-core/src/harness/runtime/session-runtime.ts @@ -1,14 +1,14 @@ -import type { ContextMessage } from '../context/context-types'; -import { persistCompactionEntry, type CompactResult } from '../compaction/compaction'; -import type { TaskState } from '../core/state'; -import type { ToolCallAction } from '../protocol/action'; -import type { SessionStoreLike } from '../session/store-types'; -import { readTaskHistory } from '../session/history'; +import type { ContextMessage } from '../context/context-types.js'; +import { persistCompactionEntry, type CompactResult } from '../compaction/compaction.js'; +import type { TaskState } from '../core/state.js'; +import type { ToolCallAction } from '../protocol/action.js'; +import type { SessionStoreLike } from '../session/store-types.js'; +import { readTaskHistory } from '../session/history.js'; import { appendTaskEntry, appendTaskRecord, ensureTaskSession, -} from '../session/task-session'; +} from '../session/task-session.js'; export interface SessionRuntime { history: ContextMessage[]; diff --git a/packages/agent-core/src/harness/runtime/tool-runtime.ts b/packages/agent-core/src/harness/runtime/tool-runtime.ts index 393c95e..7f62fe1 100644 --- a/packages/agent-core/src/harness/runtime/tool-runtime.ts +++ b/packages/agent-core/src/harness/runtime/tool-runtime.ts @@ -1,7 +1,7 @@ -import type { TaskState } from '../core/state'; -import type { ToolCallAction } from '../protocol/action'; -import { executeToolCall, type ToolExecutionResult } from '../tools/executor'; -import type { SessionRuntime } from './session-runtime'; +import type { TaskState } from '../core/state.js'; +import type { ToolCallAction } from '../protocol/action.js'; +import { executeToolCall, type ToolExecutionResult } from '../tools/executor.js'; +import type { SessionRuntime } from './session-runtime.js'; /** 执行工具并复用会话运行时记录调用与结果。 */ export const runToolCall = async ( diff --git a/packages/agent-core/src/harness/session/activity-query.ts b/packages/agent-core/src/harness/session/activity-query.ts index b63522e..39953ed 100644 --- a/packages/agent-core/src/harness/session/activity-query.ts +++ b/packages/agent-core/src/harness/session/activity-query.ts @@ -1,5 +1,5 @@ -import type { EntryLike, SessionStoreLike } from './store-types'; -import { getSharedSessionStore } from './store-registry'; +import type { EntryLike, SessionStoreLike } from './store-types.js'; +import { getSharedSessionStore } from './store-registry.js'; /** * 活动面板需要的历史条目。 diff --git a/packages/agent-core/src/harness/session/history.ts b/packages/agent-core/src/harness/session/history.ts index faf7cab..5b3d917 100644 --- a/packages/agent-core/src/harness/session/history.ts +++ b/packages/agent-core/src/harness/session/history.ts @@ -1,6 +1,6 @@ -import type { ContextMessage } from '../context/context-types'; -import type { EntryLike, SessionStoreLike } from './store-types'; -import { getSharedSessionStore } from './store-registry'; +import type { ContextMessage } from '../context/context-types.js'; +import type { EntryLike, SessionStoreLike } from './store-types.js'; +import { getSharedSessionStore } from './store-registry.js'; const entryToContextMessage = (entry: EntryLike): ContextMessage | null => { const payload = entry.payload as { content?: unknown; role?: unknown } | null; diff --git a/packages/agent-core/src/harness/session/index.ts b/packages/agent-core/src/harness/session/index.ts index 05dce44..03e7086 100644 --- a/packages/agent-core/src/harness/session/index.ts +++ b/packages/agent-core/src/harness/session/index.ts @@ -1,5 +1,5 @@ -export * from './activity-query'; -export * from './history'; -export * from './store-registry'; -export * from './store-types'; -export * from './task-session'; +export * from './activity-query.js'; +export * from './history.js'; +export * from './store-registry.js'; +export * from './store-types.js'; +export * from './task-session.js'; diff --git a/packages/agent-core/src/harness/session/store-registry.ts b/packages/agent-core/src/harness/session/store-registry.ts index 8562c5a..6e2c507 100644 --- a/packages/agent-core/src/harness/session/store-registry.ts +++ b/packages/agent-core/src/harness/session/store-registry.ts @@ -1,4 +1,4 @@ -import type { SessionStoreLike } from './store-types'; +import type { SessionStoreLike } from './store-types.js'; let sharedStore: SessionStoreLike | null = null; diff --git a/packages/agent-core/src/harness/session/task-session.ts b/packages/agent-core/src/harness/session/task-session.ts index a3efcbb..0b112de 100644 --- a/packages/agent-core/src/harness/session/task-session.ts +++ b/packages/agent-core/src/harness/session/task-session.ts @@ -1,11 +1,11 @@ -import type { TaskState } from '../core/state'; +import type { TaskState } from '../core/state.js'; import type { EntryLike, RecordLike, SessionLike, SessionStoreLike, -} from './store-types'; -import { getSharedSessionStore } from './store-registry'; +} from './store-types.js'; +import { getSharedSessionStore } from './store-registry.js'; /** 默认泳道名称,与 session-sqlite store 保持一致 */ export const DEFAULT_LANE = 'default'; diff --git a/packages/agent-core/src/harness/tools/bash.ts b/packages/agent-core/src/harness/tools/bash.ts index 86d5414..6f5c43d 100644 --- a/packages/agent-core/src/harness/tools/bash.ts +++ b/packages/agent-core/src/harness/tools/bash.ts @@ -1,6 +1,6 @@ import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; -import { resolveUserPath } from './pathUtils'; +import { resolveUserPath } from './pathUtils.js'; const execFileAsync = promisify(execFile); diff --git a/packages/agent-core/src/harness/tools/executor.ts b/packages/agent-core/src/harness/tools/executor.ts index 7be3fff..6fcaf47 100644 --- a/packages/agent-core/src/harness/tools/executor.ts +++ b/packages/agent-core/src/harness/tools/executor.ts @@ -1,8 +1,8 @@ -import { tools } from './'; -import { enforceToolPermission } from './policy/guard'; -import type { AgentMode } from '@agent-core/harness/core/state'; -import type { ToolCallAction } from '@agent-core/harness/protocol/action'; -import { createToolResultObservation } from '@agent-core/harness/protocol/observation'; +import { tools } from './index.js'; +import { enforceToolPermission } from './policy/guard.js'; +import type { AgentMode } from '../core/state.js'; +import type { ToolCallAction } from '../protocol/action.js'; +import { createToolResultObservation } from '../protocol/observation.js'; export interface ToolExecutionResult { content: string; diff --git a/packages/agent-core/src/harness/tools/getEnvironment.ts b/packages/agent-core/src/harness/tools/getEnvironment.ts index a0d9941..2fa897e 100644 --- a/packages/agent-core/src/harness/tools/getEnvironment.ts +++ b/packages/agent-core/src/harness/tools/getEnvironment.ts @@ -1,6 +1,6 @@ import { access } from 'node:fs/promises'; import process from 'node:process'; -import { getKnownLocations } from './pathUtils'; +import { getKnownLocations } from './pathUtils.js'; const exists = async (path: string) => { try { diff --git a/packages/agent-core/src/harness/tools/gitDiff.ts b/packages/agent-core/src/harness/tools/gitDiff.ts index fdaf997..e8437b7 100644 --- a/packages/agent-core/src/harness/tools/gitDiff.ts +++ b/packages/agent-core/src/harness/tools/gitDiff.ts @@ -1,6 +1,6 @@ import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; -import { resolveUserPath } from './pathUtils'; +import { resolveUserPath } from './pathUtils.js'; const execFileAsync = promisify(execFile); diff --git a/packages/agent-core/src/harness/tools/index.ts b/packages/agent-core/src/harness/tools/index.ts index ad63d90..a8bdb12 100644 --- a/packages/agent-core/src/harness/tools/index.ts +++ b/packages/agent-core/src/harness/tools/index.ts @@ -1,10 +1,10 @@ -import { getEnvironmentTool } from './getEnvironment'; -import { bashTool } from './bash'; -import { ocrImageTool } from './ocr'; -import { gitDiffTool } from './gitDiff'; -import { readFileTool } from './readFile'; -import { searchTool } from './search'; -import { writeFileTool } from './writeFile'; +import { getEnvironmentTool } from './getEnvironment.js'; +import { bashTool } from './bash.js'; +import { ocrImageTool } from './ocr.js'; +import { gitDiffTool } from './gitDiff.js'; +import { readFileTool } from './readFile.js'; +import { searchTool } from './search.js'; +import { writeFileTool } from './writeFile.js'; export { bashTool, getEnvironmentTool, diff --git a/packages/agent-core/src/harness/tools/ocr.ts b/packages/agent-core/src/harness/tools/ocr.ts index 074454a..e89d4a6 100644 --- a/packages/agent-core/src/harness/tools/ocr.ts +++ b/packages/agent-core/src/harness/tools/ocr.ts @@ -1,6 +1,6 @@ import { readFile } from 'node:fs/promises'; import Tesseract from 'tesseract.js'; -import { resolveUserPath } from './pathUtils'; +import { resolveUserPath } from './pathUtils.js'; const DEFAULT_LANG = 'eng'; diff --git a/packages/agent-core/src/harness/tools/policy/guard.ts b/packages/agent-core/src/harness/tools/policy/guard.ts index 458f453..512b7bf 100644 --- a/packages/agent-core/src/harness/tools/policy/guard.ts +++ b/packages/agent-core/src/harness/tools/policy/guard.ts @@ -1,5 +1,5 @@ -import type { AgentMode } from '@agent-core/harness/core/state'; -import { modePolicies } from './modes'; +import type { AgentMode } from '../../core/state.js'; +import { modePolicies } from './modes.js'; const toolPermissionByName = (toolName: string, mode: AgentMode): boolean => { const policy = modePolicies[mode]; diff --git a/packages/agent-core/src/harness/tools/policy/modes.ts b/packages/agent-core/src/harness/tools/policy/modes.ts index dc8ee2d..54b9778 100644 --- a/packages/agent-core/src/harness/tools/policy/modes.ts +++ b/packages/agent-core/src/harness/tools/policy/modes.ts @@ -1,4 +1,4 @@ -import type { AgentMode } from '@agent-core/harness/core/state'; +import type { AgentMode } from '../../core/state.js'; export interface ModePolicy { readonly allowBash: boolean; diff --git a/packages/agent-core/src/harness/tools/readFile.ts b/packages/agent-core/src/harness/tools/readFile.ts index 4871e43..72895d5 100644 --- a/packages/agent-core/src/harness/tools/readFile.ts +++ b/packages/agent-core/src/harness/tools/readFile.ts @@ -1,5 +1,5 @@ import { readFile } from 'node:fs/promises'; -import { resolveUserPath } from './pathUtils'; +import { resolveUserPath } from './pathUtils.js'; export const readFileTool = { name: 'read_file', diff --git a/packages/agent-core/src/harness/tools/search.ts b/packages/agent-core/src/harness/tools/search.ts index 535d030..a834477 100644 --- a/packages/agent-core/src/harness/tools/search.ts +++ b/packages/agent-core/src/harness/tools/search.ts @@ -3,7 +3,7 @@ import type { Dirent } from 'node:fs'; import { readdir, readFile, stat } from 'node:fs/promises'; import path from 'node:path'; import { promisify } from 'node:util'; -import { resolveUserPath } from './pathUtils'; +import { resolveUserPath } from './pathUtils.js'; const execFileAsync = promisify(execFile); const DEFAULT_MAX_RESULTS = 100; diff --git a/packages/agent-core/src/harness/tools/writeFile.ts b/packages/agent-core/src/harness/tools/writeFile.ts index 499742f..a7dc46f 100644 --- a/packages/agent-core/src/harness/tools/writeFile.ts +++ b/packages/agent-core/src/harness/tools/writeFile.ts @@ -1,6 +1,6 @@ import { mkdir, writeFile } from 'node:fs/promises'; import pathModule from 'node:path'; -import { resolveUserPath } from './pathUtils'; +import { resolveUserPath } from './pathUtils.js'; export const writeFileTool = { name: 'write_file', diff --git a/packages/agent-core/src/harness/utils/shell.ts b/packages/agent-core/src/harness/utils/shell.ts index b8ea198..5b49c1f 100644 --- a/packages/agent-core/src/harness/utils/shell.ts +++ b/packages/agent-core/src/harness/utils/shell.ts @@ -1,6 +1,6 @@ import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; -import { truncateOutput, type TruncateOptions } from '@agent-core/harness/utils/truncate'; +import { truncateOutput, type TruncateOptions } from './truncate.js'; const execFileAsync = promisify(execFile); export interface ExecShellOptions { diff --git a/packages/agent-core/src/index.ts b/packages/agent-core/src/index.ts new file mode 100644 index 0000000..324d173 --- /dev/null +++ b/packages/agent-core/src/index.ts @@ -0,0 +1,45 @@ +export { agent, type AgentOptions } from './harness/core/agent.js'; +export { + callLLM, + streamLLM, + llmModel, + type Message as LLMMessage, + type StreamHandlers, +} from './harness/core/llm.js'; +export { + createTaskState, + type AgentMode, + type TaskState, + type CreateTaskStateOptions, +} from './harness/core/state.js'; +export { + runLoop, + type RunLoopOptions, +} from './harness/runtime/run-loop.js'; +export { + executeToolCall, + type ToolExecutionResult, +} from './harness/tools/executor.js'; +export { tools } from './harness/tools/index.js'; +export { + buildWebExport, + writeWebExport, + type WebEntry, + type WebRecord, + type WebFact, + type WebSession, + type WebExport, + type WebStore, +} from './web/export.js'; +export { + parseClientMessage, + type ChatSendPayload, + type WebSocketClientMessage, + type WebSocketServerMessage, +} from './web/protocol.js'; +export { + startWebServer, + type WebServerOptions, + type WebServerHandle, +} from './web/server.js'; +export * from './harness/session/index.js'; diff --git a/packages/agent-core/src/web/export.ts b/packages/agent-core/src/web/export.ts index 2ecf2e7..3cdd95b 100644 --- a/packages/agent-core/src/web/export.ts +++ b/packages/agent-core/src/web/export.ts @@ -4,7 +4,7 @@ import type { EntryLike, SessionStoreLike, SessionStatsLike, -} from '../harness/session/store-types'; +} from '../harness/session/store-types.js'; /** Web 客户端读取的会话快照格式。 */ export interface WebEntry { @@ -61,7 +61,7 @@ export interface WebExport { } /** export.ts 需要的存储最小接口,兼容 SessionStoreLike 及 SQLite 扩展方法。 */ -type WebStore = SessionStoreLike & { +export type WebStore = SessionStoreLike & { getRecords?: (sessionId: string, options?: { limit?: number; offset?: number }) => WebRecord[]; listFacts?: (sessionId: string) => WebFact[]; }; diff --git a/packages/agent-core/src/web/protocol.ts b/packages/agent-core/src/web/protocol.ts new file mode 100644 index 0000000..d311d0e --- /dev/null +++ b/packages/agent-core/src/web/protocol.ts @@ -0,0 +1,62 @@ +import type { AgentMode } from "../harness/core/state.js"; +import type { WebExport } from "./export.js"; + +export type ChatSendPayload = { + type: "chat.send"; + input: string; + mode?: AgentMode; + objective?: string; + constraints?: string[]; + workspace?: string; +}; + +/** 客户端发给 WebSocket 服务的请求。 */ +export type WebSocketClientMessage = + | { type: "sessions.list" } + | ChatSendPayload; + +/** 服务端发给客户端的响应。 */ +export type WebSocketServerMessage = + | { type: "sessions.snapshot"; data: WebExport } + | { + type: "chat.status"; + status: "idle" | "running" | "success" | "error"; + trace?: string; + message?: string; + } + | { type: "error"; message: string }; + +/** 解析客户端文本消息,格式非法或类型未知时返回 null。 */ +export const parseClientMessage = ( + raw: string, +): WebSocketClientMessage | null => { + try { + const value: unknown = JSON.parse(raw); + if (!value || typeof value !== "object") { + return null; + } + + const message = value as { type?: unknown }; + if (message.type === "sessions.list") { + return { type: "sessions.list" }; + } + + if (message.type === "chat.send") { + const rawInput = (message as { input?: unknown }).input; + if (typeof rawInput !== "string" || !rawInput.trim()) { + return null; + } + const rawMode = (message as { mode?: unknown }).mode; + const mode: AgentMode = rawMode === "plan" ? "plan" : "build"; + return { + type: "chat.send", + input: rawInput.trim(), + mode, + }; + } + + return null; + } catch { + return null; + } +}; diff --git a/packages/agent-core/src/web/server.ts b/packages/agent-core/src/web/server.ts index e69de29..356e972 100644 --- a/packages/agent-core/src/web/server.ts +++ b/packages/agent-core/src/web/server.ts @@ -0,0 +1,277 @@ +import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; +import { readFile, stat } from "node:fs/promises"; +import { extname, join, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; +import { WebSocket, WebSocketServer } from "ws"; +import { runLoop } from "../harness/runtime/run-loop.js"; +import { createTaskState } from "../harness/core/state.js"; +import { buildWebExport, type WebStore } from "./export.js"; +import { parseClientMessage, type WebSocketServerMessage } from "./protocol.js"; + +const DEFAULT_CLIENT_DIR = fileURLToPath( + new URL("../../../client/dist", import.meta.url), +); +const DEFAULT_PORT = 4173; + +const MIME_TYPES: Record = { + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".mjs": "text/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".svg": "image/svg+xml", + ".webp": "image/webp", + ".ico": "image/x-icon", + ".map": "application/json; charset=utf-8", + ".txt": "text/plain; charset=utf-8", +}; + +export interface WebServerOptions { + store: WebStore; + clientDir?: string; + host?: string; + port?: number; +} + +export interface WebServerHandle { + host: string; + port: number; + close(): Promise; +} + +const sendJson = (socket: WebSocket, message: WebSocketServerMessage) => { + if (socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify(message)); + } +}; + +const broadcast = (wss: WebSocketServer, message: WebSocketServerMessage) => { + const payload = JSON.stringify(message); + for (const client of wss.clients) { + if (client.readyState === WebSocket.OPEN) { + client.send(payload); + } + } +}; + +const safePath = (root: string, pathname: string): string | null => { + const candidate = resolve(root, `.${pathname}`); + if (candidate !== root && !candidate.startsWith(root + sep)) { + return null; + } + return candidate; +}; + +const serveFile = async ( + res: ServerResponse, + target: string, + headOnly: boolean, +): Promise => { + try { + const info = await stat(target); + if (!info.isFile()) { + return false; + } + + const content = await readFile(target); + res.writeHead(200, { + "Content-Type": MIME_TYPES[extname(target)] ?? "application/octet-stream", + "Content-Length": content.length, + }); + res.end(headOnly ? undefined : content); + return true; + } catch { + return false; + } +}; + +const serveStatic = async ( + req: IncomingMessage, + res: ServerResponse, + clientDir: string, +): Promise => { + if (req.method !== "GET" && req.method !== "HEAD") { + res.writeHead(405, { Allow: "GET, HEAD" }); + res.end(); + return; + } + + let pathname = "/"; + try { + pathname = decodeURIComponent( + new URL(req.url ?? "/", "http://localhost").pathname, + ); + } catch { + res.writeHead(400); + res.end(); + return; + } + + const root = resolve(clientDir); + const requested = pathname === "/" ? "/index.html" : pathname; + const filePath = safePath(root, requested); + if (!filePath) { + res.writeHead(404); + res.end(); + return; + } + + const headOnly = req.method === "HEAD"; + if (await serveFile(res, filePath, headOnly)) { + return; + } + + // 单页应用回退到入口页,避免刷新子路由时 404 + const fallback = join(root, "index.html"); + if (await serveFile(res, fallback, headOnly)) { + return; + } + + res.writeHead(404); + res.end(); +}; + +/** + * 启动 Web 会话面板:静态托管客户端构建产物,并在 /ws 提供会话快照与 Harness 调度。 + */ +export const startWebServer = async ( + options: WebServerOptions, +): Promise => { + const store = options.store; + const clientDir = options.clientDir ?? DEFAULT_CLIENT_DIR; + const host = options.host ?? "127.0.0.1"; + const port = + options.port ?? Number(process.env.CALL_CODE_WEB_PORT ?? DEFAULT_PORT); + + let isRunning = false; + + const httpServer = createServer((req, res) => { + void serveStatic(req, res, clientDir); + }); + + const wss = new WebSocketServer({ server: httpServer, path: "/ws" }); + wss.on("connection", (socket) => { + socket.on("message", async (raw) => { + const text = typeof raw === "string" ? raw : raw.toString(); + const message = parseClientMessage(text); + if (!message) { + sendJson(socket, { type: "error", message: "无法解析请求消息" }); + return; + } + + if (message.type === "sessions.list") { + sendJson(socket, { + type: "sessions.snapshot", + data: buildWebExport(store), + }); + return; + } + + if (message.type === "chat.send") { + if (isRunning) { + sendJson(socket, { + type: "chat.status", + status: "error", + message: "当前已有正在运行的任务,请稍候...", + }); + return; + } + + isRunning = true; + broadcast(wss, { + type: "chat.status", + status: "running", + trace: `任务已启动(模式: ${(message.mode ?? "build").toUpperCase()})`, + }); + + try { + const task = createTaskState(message.input, { + mode: message.mode ?? "build", + objective: message.objective, + constraints: message.constraints, + workspace: message.workspace ?? process.cwd(), + }); + + await runLoop( + task, + { + onTrace: (traceText) => { + broadcast(wss, { + type: "chat.status", + status: "running", + trace: traceText, + }); + }, + onError: (err) => { + const errMsg = err instanceof Error ? err.message : String(err); + broadcast(wss, { + type: "chat.status", + status: "error", + message: errMsg, + }); + }, + }, + { persist: true, sessionStore: store }, + ); + + broadcast(wss, { + type: "chat.status", + status: "success", + trace: "任务完成", + }); + broadcast(wss, { + type: "sessions.snapshot", + data: buildWebExport(store), + }); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + broadcast(wss, { + type: "chat.status", + status: "error", + message: errMsg, + }); + } finally { + isRunning = false; + } + } + }); + }); + + await new Promise((resolveListen, rejectListen) => { + httpServer.once("error", rejectListen); + httpServer.listen(port, host, () => { + httpServer.off("error", rejectListen); + resolveListen(); + }); + }); + + const address = httpServer.address(); + if (!address || typeof address === "string") { + throw new Error("无法获取 Web 服务端口"); + } + + return { + host, + port: address.port, + async close() { + for (const client of wss.clients) { + client.terminate(); + } + await new Promise((resolveClose) => { + wss.close(() => resolveClose()); + }); + await new Promise((resolveClose, rejectClose) => { + httpServer.close((error) => { + if (error) { + rejectClose(error); + } else { + resolveClose(); + } + }); + }); + }, + }; +}; diff --git a/packages/agent-core/tsconfig.json b/packages/agent-core/tsconfig.json index 1af166c..3b88121 100644 --- a/packages/agent-core/tsconfig.json +++ b/packages/agent-core/tsconfig.json @@ -7,7 +7,9 @@ "moduleResolution": "bundler", "declaration": true, - "composite": true + "composite": true, + "noEmit": false, + "allowImportingTsExtensions": false }, "include": ["src"] } diff --git a/packages/client/README.md b/packages/client/README.md index 80f7bfa..3fa6340 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -1,6 +1,6 @@ # Call Code History Client -这是 CLI 会话历史的静态展示端,基于 TypeScript、React 和 Tailwind CSS 构建,当前仅用于本地预览,不再部署到 GitHub Pages。页面默认读取 `data.json`,如果不存在则回退到 `data.example.json` 预览数据;也可以用 `?data=` 指定数据文件,用 `?session=` 直达某个会话。 +这是 CLI 会话历史的 Web 展示端,基于 TypeScript、React 和 Tailwind CSS 构建。页面通过 WebSocket 从会话服务实时读取数据,不再依赖静态 JSON 文件。 ## 构建 @@ -8,33 +8,22 @@ bun run build:client ``` -产物输出到 `packages/client/dist`,用于本地预览。 +产物输出到 `packages/client/dist`。 ## 本地开发 -```bash -bun run dev:client -``` - -## 导出数据 - -在仓库根目录运行: +先启动会话服务,再启动 Vite 开发服务器: ```bash -bun run export:web +bun run web:serve +bun run dev:client ``` -脚本会把当前 `SESSION_DB_PATH` 指向的会话数据库导出为 `packages/client/public/data.json`。也可以在 CLI 中执行 `/export` 完成同样操作。 - -导出文件包含会话、消息、工具调用、统计和事实数据。若后续公开发布,请先检查内容是否包含不该公开的路径、命令输出或密钥。 - -## 本地预览 +`web:serve` 默认监听 `127.0.0.1:4173`,可通过 `CALL_CODE_WEB_PORT` 修改端口;Vite 会把 `/ws` 代理到会话服务。 -预览构建产物: +## 数据服务 -```bash -bun run preview:client -``` +CLI 会话写入 `SESSION_DB_PATH` 指向的 SQLite 数据库,默认 `.agent-sessions/sessions.db`。`web:serve` 读取同一个数据库,并在 `/ws` 提供 `sessions.list` / `sessions.snapshot` 消息。页面默认连接同源 `/ws`,也可以使用 `?ws=` 覆盖服务地址,使用 `?session=` 直达某个会话。 ## 主题 diff --git a/packages/client/dist/assets/index-B7QVdr3Y.css b/packages/client/dist/assets/index-B7QVdr3Y.css deleted file mode 100644 index 90b57dc..0000000 --- a/packages/client/dist/assets/index-B7QVdr3Y.css +++ /dev/null @@ -1,2 +0,0 @@ -/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--font-weight-semibold:600;--tracking-wider:.05em;--leading-tight:1.25;--leading-snug:1.375;--leading-relaxed:1.625;--radius-lg:.5rem;--radius-xl:.75rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.relative{position:relative}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.m-0{margin:0}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.ml-auto{margin-left:auto}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.flex{display:flex}.grid{display:grid}.h-4{height:calc(var(--spacing) * 4)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-full{height:100%}.max-h-40{max-height:calc(var(--spacing) * 40)}.max-h-\[50vh\]{max-height:50vh}.max-h-\[480px\]{max-height:480px}.min-h-0{min-height:0}.min-h-\[60vh\]{min-height:60vh}.min-h-\[120px\]{min-height:120px}.min-h-\[200px\]{min-height:200px}.w-1{width:var(--spacing)}.w-9{width:calc(var(--spacing) * 9)}.w-full{width:100%}.max-w-\[240px\]{max-width:240px}.max-w-\[760px\]{max-width:760px}.min-w-0{min-width:0}.flex-1{flex:1}.shrink-0{flex-shrink:0}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-y-auto{overflow-y:auto}.rounded-full{border-radius:2147483647px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.bg-transparent{background-color:#0000}.object-contain{object-fit:contain}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.\!py-0{padding-block:0!important}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.pt-1\.5{padding-top:calc(var(--spacing) * 1.5)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.text-left{text-align:left}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.\!text-\[10px\]{font-size:10px!important}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.text-\[13\.5px\]{font-size:13.5px}.text-\[13px\]{font-size:13px}.text-\[15px\]{font-size:15px}.text-\[17px\]{font-size:17px}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.break-words{overflow-wrap:break-word}.whitespace-pre-wrap{white-space:pre-wrap}.uppercase{text-transform:uppercase}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.outline-none{--tw-outline-style:none;outline-style:none}@media (width>=40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (width>=64rem){.lg\:max-h-none{max-height:none}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--font-ui:"SF Pro Text", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Microsoft YaHei", sans-serif;--font-mono:"SF Mono", ui-monospace, "JetBrains Mono", "Menlo", "Consolas", monospace;--bg-a:#f3f4f7;--bg-b:#fbfbfd;--glow-1:148 148 158;--glow-2:148 148 158;--glow-3:148 148 158;--panel-bg:255 255 255 / .42;--panel-border:145 145 155 / .14;--panel-shadow:0 1px 0 #ffffffbf inset, 0 1px 3px #1118270f, 0 20px 50px -16px #1118271a;--sidebar-active:255 255 255 / .88;--sidebar-active-shadow:0 1px 3px #11182712, 0 8px 24px -6px #1118271f;--sidebar-hover:255 255 255 / .45;--chip-bg:255 255 255 / .46;--chip-border:145 145 155 / .12;--accent:#111827;--accent-soft:#11182712;--text-primary:#18181b;--text-secondary:#52525b;--text-tertiary:#8e8e93;--role-user:#71717a;--role-assistant:#52525b;--role-tool:#3f3f46;--role-system:#a1a1aa;--segment-active:255 255 255 / .92;--segment-active-text:#18181b;--segment-active-shadow:0 1px 3px #11182714, 0 4px 12px -4px #11182714}:root.dark{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--bg-a:#0a0a0c;--bg-b:#111114;--glow-1:138 138 146;--glow-2:138 138 146;--glow-3:138 138 146;--panel-bg:18 18 22 / .52;--panel-border:255 255 255 / .1;--panel-shadow:0 1px 0 #ffffff0a inset, 0 1px 3px #00000073, 0 40px 80px -20px #0000008c;--sidebar-active:255 255 255 / .12;--sidebar-active-shadow:0 1px 0 #ffffff0d inset, 0 8px 28px -6px #0009;--sidebar-hover:255 255 255 / .07;--chip-bg:255 255 255 / .06;--chip-border:255 255 255 / .1;--accent:#f4f4f5;--accent-soft:#f4f4f51a;--text-primary:#f4f4f5;--text-secondary:#a1a1aa;--text-tertiary:#6f6f76;--role-user:#d4d4d8;--role-assistant:#a1a1aa;--role-tool:#71717a;--role-system:#52525b;--segment-active:255 255 255 / .12;--segment-active-text:#fafafa;--segment-active-shadow:0 1px 0 #ffffff0d inset, 0 1px 3px #00000073, 0 4px 14px -6px #00000080}html{font-family:var(--font-ui);text-rendering:optimizelegibility;-webkit-font-smoothing:antialiased;font-feature-settings:"ss01", "cv11"}body{min-height:100vh;color:var(--text-primary);margin:0}*{scrollbar-width:thin;scrollbar-color:rgb(var(--glow-1) / .2) transparent}body{background:linear-gradient(180deg, var(--bg-a), var(--bg-b));background-attachment:fixed}.app-shell{min-height:100vh;padding:12px 12px 20px;position:relative}@media (width>=1024px){.app-shell{padding:16px 20px 24px}}.app-shell:before{content:"";pointer-events:none;opacity:.35;mix-blend-mode:overlay;z-index:0;background-image:url("data:image/svg+xml;utf8,");position:fixed;inset:0}.app-frame{z-index:1;border:1px solid rgb(var(--panel-border));background:rgb(var(--panel-bg));width:100%;max-width:1480px;min-height:calc(100vh - 28px);box-shadow:var(--panel-shadow);-webkit-backdrop-filter:blur(24px)saturate(180%);border-radius:16px;flex-direction:column;margin:0 auto;display:flex;position:relative;overflow:hidden}@media (width>=1024px){.app-frame{height:calc(100vh - 40px)}}.header-bar{border-bottom:1px solid rgb(var(--panel-border));background:rgb(var(--chip-bg));justify-content:space-between;align-items:center;gap:12px;padding:10px 14px;display:flex}@media (width>=1024px){.header-bar{padding:12px 18px}}.app-workspace{flex:1;grid-template-columns:1fr;min-height:0;display:grid}@media (width>=1024px){.app-workspace{grid-template-columns:300px minmax(0,1fr)}}.sidebar-panel,.main-panel{flex-direction:column;min-height:0;display:flex;position:relative;overflow:hidden}.sidebar-panel{border-bottom:1px solid rgb(var(--panel-border));max-height:50vh}@media (width>=1024px){.sidebar-panel{border-right:1px solid rgb(var(--panel-border));border-bottom:0;max-height:none}}.chip{border:1px solid rgb(var(--chip-border));background:rgb(var(--chip-bg));color:var(--text-secondary);white-space:nowrap;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-radius:999px;align-items:center;gap:5px;padding:2px 9px;font-size:11px;font-weight:500;display:inline-flex}.segmented{border:1px solid rgb(var(--chip-border));background:rgb(var(--chip-bg));-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-radius:10px;padding:3px;display:inline-flex}.segmented button{color:var(--text-secondary);border-radius:7px;padding:4px 10px;font-size:11px;font-weight:500;transition:all .2s}.segmented button:hover{color:var(--text-primary)}.segmented button[aria-pressed=true]{background:rgb(var(--segment-active));color:var(--segment-active-text);box-shadow:var(--segment-active-shadow)}.mono{font-family:var(--font-mono);font-size:12px;line-height:1.55}::selection{background:rgb(var(--glow-1) / .2)}.role-dot{border-radius:50%;flex-shrink:0;width:8px;height:8px;display:block}.role-dot--user{background:var(--role-user);box-shadow:0 0 6px rgb(var(--glow-1) / .4)}.role-dot--assistant{background:var(--role-assistant);box-shadow:0 0 6px rgb(var(--glow-2) / .4)}.role-dot--tool{background:var(--role-tool);box-shadow:0 0 6px 0 rgb(var(--glow-3) / .35)}.role-dot--system{background:var(--role-system)}.msg-bubble{border:1px solid rgb(var(--chip-border));background:rgb(var(--chip-bg));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-radius:12px}.msg-bubble--user{border-left:2px solid var(--role-user)}.msg-bubble--assistant{border-left:2px solid var(--role-assistant)}.msg-bubble--tool{border-left:2px solid var(--role-tool)}.search-box{transition:border-color .2s,box-shadow .2s}.search-box:focus-within{box-shadow:0 0 0 3px rgb(var(--glow-1) / .08);border-color:rgb(var(--glow-1) / .35)!important}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false} diff --git a/packages/client/dist/assets/index-DXeJAcap.js b/packages/client/dist/assets/index-DXeJAcap.js deleted file mode 100644 index fd69d5f..0000000 --- a/packages/client/dist/assets/index-DXeJAcap.js +++ /dev/null @@ -1,9 +0,0 @@ -var e=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports);(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var t=e((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var ee=Array.isArray;function S(){}var C={H:null,A:null,T:null,S:null},te=Object.prototype.hasOwnProperty;function ne(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function re(e,t){return ne(e.type,t,e.props)}function w(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function ie(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var ae=/\/+/g;function oe(e,t){return typeof e==`object`&&e&&e.key!=null?ie(``+e.key):t.toString(36)}function se(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(S,S):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function ce(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,ce(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+oe(e,0):a,ee(o)?(i=``,c!=null&&(i=c.replace(ae,`$&/`)+`/`),ce(o,r,i,``,function(e){return e})):o!=null&&(w(o)&&(o=re(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(ae,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(ee(e))for(var u=0;u{n.exports=t()})),r=e((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,ee||(ee=!0,w());else{var t=n(l);t!==null&&oe(x,t.startTime-e)}}var ee=!1,S=-1,C=5,te=-1;function ne(){return g?!0:!(e.unstable_now()-tet&&ne());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&oe(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}}}finally{i?w():ee=!1}}}var w;if(typeof y==`function`)w=function(){y(re)};else if(typeof MessageChannel<`u`){var ie=new MessageChannel,ae=ie.port2;ie.port1.onmessage=re,w=function(){ae.postMessage(null)}}else w=function(){_(re,0)};function oe(t,n){S=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(S),S=-1):h=!0,oe(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,ee||(ee=!0,w()))),r},e.unstable_shouldYield=ne,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),i=e(((e,t)=>{t.exports=r()})),a=e((e=>{var t=n();function r(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=a()})),s=e((e=>{var t=i(),r=n(),a=o();function s(e){var t=`https://react.dev/errors/`+e;if(1pe||(e.current=fe[pe],fe[pe]=null,pe--)}function O(e,t){pe++,fe[pe]=e.current,e.current=t}var he=me(null),ge=me(null),_e=me(null),ve=me(null);function ye(e,t){switch(O(_e,t),O(ge,e),O(he,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Vd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Vd(t),e=Hd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}D(he),O(he,e)}function be(){D(he),D(ge),D(_e)}function xe(e){e.memoizedState!==null&&O(ve,e);var t=he.current,n=Hd(t,e.type);t!==n&&(O(ge,e),O(he,n))}function Se(e){ge.current===e&&(D(he),D(ge)),ve.current===e&&(D(ve),Qf._currentValue=de)}var Ce,we;function Te(e){if(Ce===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);Ce=t&&t[1]||``,we=-1)`:-1i||c[r]!==l[i]){var u=` -`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{Ee=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?Te(n):``}function Oe(e,t){switch(e.tag){case 26:case 27:case 5:return Te(e.type);case 16:return Te(`Lazy`);case 13:return e.child!==t&&t!==null?Te(`Suspense Fallback`):Te(`Suspense`);case 19:return Te(`SuspenseList`);case 0:case 15:return De(e.type,!1);case 11:return De(e.type.render,!1);case 1:return De(e.type,!0);case 31:return Te(`Activity`);default:return``}}function ke(e){try{var t=``,n=null;do t+=Oe(e,n),n=e,e=e.return;while(e);return t}catch(e){return` -Error generating stack: `+e.message+` -`+e.stack}}var Ae=Object.prototype.hasOwnProperty,je=t.unstable_scheduleCallback,Me=t.unstable_cancelCallback,Ne=t.unstable_shouldYield,Pe=t.unstable_requestPaint,Fe=t.unstable_now,Ie=t.unstable_getCurrentPriorityLevel,Le=t.unstable_ImmediatePriority,Re=t.unstable_UserBlockingPriority,ze=t.unstable_NormalPriority,Be=t.unstable_LowPriority,Ve=t.unstable_IdlePriority,He=t.log,Ue=t.unstable_setDisableYieldValue,We=null,Ge=null;function Ke(e){if(typeof He==`function`&&Ue(e),Ge&&typeof Ge.setStrictMode==`function`)try{Ge.setStrictMode(We,e)}catch{}}var qe=Math.clz32?Math.clz32:Xe,Je=Math.log,Ye=Math.LN2;function Xe(e){return e>>>=0,e===0?32:31-(Je(e)/Ye|0)|0}var Ze=256,Qe=262144,$e=4194304;function et(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function tt(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=et(n))):i=et(o):i=et(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=et(n))):i=et(o)):i=et(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function nt(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function rt(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function it(){var e=$e;return $e<<=1,!($e&62914560)&&($e=4194304),e}function at(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function ot(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function st(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),yn=!1;if(vn)try{var bn={};Object.defineProperty(bn,"passive",{get:function(){yn=!0}}),window.addEventListener(`test`,bn,bn),window.removeEventListener(`test`,bn,bn)}catch{yn=!1}var xn=null,Sn=null,Cn=null;function wn(){if(Cn)return Cn;var e,t=Sn,n=t.length,r,i=`value`in xn?xn.value:xn.textContent,a=i.length;for(e=0;e=nr),ar=` `,or=!1;function sr(e,t){switch(e){case`keyup`:return er.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function cr(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var lr=!1;function ur(e,t){switch(e){case`compositionend`:return cr(t);case`keypress`:return t.which===32?(or=!0,ar):null;case`textInput`:return e=t.data,e===ar&&or?null:e;default:return null}}function dr(e,t){if(lr)return e===`compositionend`||!tr&&sr(e,t)?(e=wn(),Cn=Sn=xn=null,lr=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=Nr(n)}}function Fr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Fr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Ir(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Gt(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Gt(e.document)}return t}function Lr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var Rr=vn&&`documentMode`in document&&11>=document.documentMode,zr=null,Br=null,Vr=null,Hr=!1;function Ur(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Hr||zr==null||zr!==Gt(r)||(r=zr,`selectionStart`in r&&Lr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Vr&&Mr(Vr,r)||(Vr=r,r=Ed(Br,`onSelect`),0>=o,i-=o,Fi=1<<32-qe(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),o=a(_,o,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),j&&Li(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(i,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(i,h),o=a(y,o,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(i,h),j&&Li(i,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(i,v.value,l),v!==null&&(o=a(v,o,g),d===null?u=v:d.sibling=v,d=v);return j&&Li(i,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,i,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),o=a(v,o,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(i,e)}),j&&Li(i,g),u}function b(e,r,a,c){if(typeof a==`object`&&a&&a.type===y&&a.key===null&&(a=a.props.children),typeof a==`object`&&a){switch(a.$$typeof){case _:a:{for(var l=a.key;r!==null;){if(r.key===l){if(l=a.type,l===y){if(r.tag===7){n(e,r.sibling),c=i(r,a.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===w&&Fa(l)===r.type){n(e,r.sibling),c=i(r,a.props),Ha(c,a),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}a.type===y?(c=Si(a.props.children,e.mode,c,a.key),c.return=e,e=c):(c=xi(a.type,a.key,a.props,null,e.mode,c),Ha(c,a),c.return=e,e=c)}return o(e);case v:a:{for(l=a.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===a.containerInfo&&r.stateNode.implementation===a.implementation){n(e,r.sibling),c=i(r,a.children||[]),c.return=e,e=c;break a}else{n(e,r);break}t(e,r),r=r.sibling}c=Ti(a,e.mode,c),c.return=e,e=c}return o(e);case w:return a=Fa(a),b(e,r,a,c)}if(ue(a))return h(e,r,a,c);if(se(a)){if(l=se(a),typeof l!=`function`)throw Error(s(150));return a=l.call(a),g(e,r,a,c)}if(typeof a.then==`function`)return b(e,r,Va(a),c);if(a.$$typeof===S)return b(e,r,ua(e,a),c);Ua(e,a)}return typeof a==`string`&&a!==``||typeof a==`number`||typeof a==`bigint`?(a=``+a,r!==null&&r.tag===6?(n(e,r.sibling),c=i(r,a),c.return=e,e=c):(n(e,r),c=Ci(a,e.mode,c),c.return=e,e=c),o(e)):n(e,r)}return function(e,t,n,r){try{Ba=0;var i=b(e,t,n,r);return za=null,i}catch(t){if(t===ka||t===ja)throw t;var a=_i(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Ga=Wa(!0),Ka=Wa(!1),qa=!1;function Ja(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Ya(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Xa(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Za(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,W&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=mi(e),pi(e,null,n),t}return ui(e,r,t,n),mi(e)}function Qa(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,lt(e,n)}}function $a(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var eo=!1;function to(){if(eo){var e=ba;if(e!==null)throw e}}function no(e,t,n,r){eo=!1;var i=e.updateQueue;qa=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(q&f)===f:(r&f)===f){f!==0&&f===ya&&(eo=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var m=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(m=g.payload,typeof m==`function`){d=m.call(_,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=g.payload,f=typeof m==`function`?m.call(_,d,f):m,f==null)break a;d=h({},d,f);break a;case 2:qa=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Kl|=o,e.lanes=o,e.memoizedState=d}}function ro(e,t){if(typeof e!=`function`)throw Error(s(191,e));e.call(t)}function io(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=T.T,s={};T.T=s,zs(e,!1,t,n);try{var c=i(),l=T.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Rs(e,t,Ca(c,r),pu(e)):Rs(e,t,r,pu(e))}catch(n){Rs(e,t,{then:function(){},status:`rejected`,reason:n},pu())}finally{E.p=a,o!==null&&s.types!==null&&(o.types=s.types),T.T=o}}function Os(){}function ks(e,t,n,r){if(e.tag!==5)throw Error(s(476));var i=As(e).queue;Ds(e,i,t,de,n===null?Os:function(){return js(e),n(r)})}function As(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:de,baseState:de,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Bo,lastRenderedState:de},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Bo,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function js(e){var t=As(e);t.next===null&&(t=e.alternate.memoizedState),Rs(e,t.next.queue,{},pu())}function Ms(){return la(Qf)}function Ns(){return R().memoizedState}function Ps(){return R().memoizedState}function Fs(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=pu();e=Xa(n);var r=Za(t,e,n);r!==null&&(hu(r,t,n),Qa(r,t,n)),t={cache:ha()},e.payload=t;return}t=t.return}}function Is(e,t,n){var r=pu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Bs(e)?Vs(t,n):(n=di(e,t,n,r),n!==null&&(hu(n,e,r),Hs(n,t,r)))}function Ls(e,t,n){Rs(e,t,n,pu())}function Rs(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Bs(e))Vs(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,jr(s,o))return ui(e,t,i,0),G===null&&li(),!1}catch{}if(n=di(e,t,i,r),n!==null)return hu(n,e,r),Hs(n,t,r),!0}return!1}function zs(e,t,n,r){if(r={lane:2,revertLane:dd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Bs(e)){if(t)throw Error(s(479))}else t=di(e,n,r,2),t!==null&&hu(t,e,2)}function Bs(e){var t=e.alternate;return e===P||t!==null&&t===P}function Vs(e,t){xo=bo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Hs(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,lt(e,n)}}var Us={readContext:la,use:Ro,useCallback:L,useContext:L,useEffect:L,useImperativeHandle:L,useLayoutEffect:L,useInsertionEffect:L,useMemo:L,useReducer:L,useRef:L,useState:L,useDebugValue:L,useDeferredValue:L,useTransition:L,useSyncExternalStore:L,useId:L,useHostTransitionStatus:L,useFormState:L,useActionState:L,useOptimistic:L,useMemoCache:L,useCacheRefresh:L};Us.useEffectEvent=L;var Ws={readContext:la,use:Ro,useCallback:function(e,t){return Fo().memoizedState=[e,t===void 0?null:t],e},useContext:la,useEffect:ms,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),fs(4194308,4,bs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return fs(4194308,4,e,t)},useInsertionEffect:function(e,t){fs(4,2,e,t)},useMemo:function(e,t){var n=Fo();t=t===void 0?null:t;var r=e();if(So){Ke(!0);try{e()}finally{Ke(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=Fo();if(n!==void 0){var i=n(t);if(So){Ke(!0);try{n(t)}finally{Ke(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Is.bind(null,P,e),[r.memoizedState,e]},useRef:function(e){var t=Fo();return e={current:e},t.memoizedState=e},useState:function(e){e=Xo(e);var t=e.queue,n=Ls.bind(null,P,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:Ss,useDeferredValue:function(e,t){return Ts(Fo(),e,t)},useTransition:function(){var e=Xo(!1);return e=Ds.bind(null,P,e.queue,!0,!1),Fo().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=P,i=Fo();if(j){if(n===void 0)throw Error(s(407));n=n()}else{if(n=t(),G===null)throw Error(s(349));q&127||Go(r,t,n)}i.memoizedState=n;var a={value:n,getSnapshot:t};return i.queue=a,ms(qo.bind(null,r,a,e),[e]),r.flags|=2048,us(9,{destroy:void 0},Ko.bind(null,r,a,n,t),null),n},useId:function(){var e=Fo(),t=G.identifierPrefix;if(j){var n=Ii,r=Fi;n=(r&~(1<<32-qe(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=Co++,0<\/script>`,a=a.removeChild(a.firstChild);break;case`select`:a=typeof r.is==`string`?o.createElement(`select`,{is:r.is}):o.createElement(`select`),r.multiple?a.multiple=!0:r.size&&(a.size=r.size);break;default:a=typeof r.is==`string`?o.createElement(i,{is:r.is}):o.createElement(i)}}a[gt]=t,a[_t]=r;a:for(o=t.child;o!==null;){if(o.tag===5||o.tag===6)a.appendChild(o.stateNode);else if(o.tag!==4&&o.tag!==27&&o.child!==null){o.child.return=o,o=o.child;continue}if(o===t)break a;for(;o.sibling===null;){if(o.return===null||o.return===t)break a;o=o.return}o.sibling.return=o.return,o=o.sibling}t.stateNode=a;a:switch(Pd(a,i,r),i){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Lc(t)}}return B(t),Rc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Lc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(s(166));if(e=_e.current,Yi(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,i=Hi,i!==null)switch(i.tag){case 27:case 5:r=i.memoizedProps}e[gt]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Md(e.nodeValue,n)),e||Ki(t,!0)}else e=Bd(e).createTextNode(r),e[gt]=t,t.stateNode=e}return B(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Yi(t),n!==null){if(e===null){if(!r)throw Error(s(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(s(557));e[gt]=t}else Xi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;B(t),e=!1}else n=Zi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(_o(t),t):(_o(t),null);if(t.flags&128)throw Error(s(558))}return B(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(i=Yi(t),r!==null&&r.dehydrated!==null){if(e===null){if(!i)throw Error(s(318));if(i=t.memoizedState,i=i===null?null:i.dehydrated,!i)throw Error(s(317));i[gt]=t}else Xi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;B(t),i=!1}else i=Zi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=i),i=!0;if(!i)return t.flags&256?(_o(t),t):(_o(t),null)}return _o(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,i=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(i=r.alternate.memoizedState.cachePool.pool),a=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(a=r.memoizedState.cachePool.pool),a!==i&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Bc(t,t.updateQueue),B(t),null);case 4:return be(),e===null&&Sd(t.stateNode.containerInfo),B(t),null;case 10:return ra(t.type),B(t),null;case 19:if(D(N),r=t.memoizedState,r===null)return B(t),null;if(i=!!(t.flags&128),a=r.rendering,a===null)if(i)Vc(r,!1);else{if(Y!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(a=vo(e),a!==null){for(t.flags|=128,Vc(r,!1),e=a.updateQueue,t.updateQueue=e,Bc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)bi(n,e),n=n.sibling;return O(N,N.current&1|2),j&&Li(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&Fe()>nu&&(t.flags|=128,i=!0,Vc(r,!1),t.lanes=4194304)}else{if(!i)if(e=vo(a),e!==null){if(t.flags|=128,i=!0,e=e.updateQueue,t.updateQueue=e,Bc(t,e),Vc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!a.alternate&&!j)return B(t),null}else 2*Fe()-r.renderingStartTime>nu&&n!==536870912&&(t.flags|=128,i=!0,Vc(r,!1),t.lanes=4194304);r.isBackwards?(a.sibling=t.child,t.child=a):(e=r.last,e===null?t.child=a:e.sibling=a,r.last=a)}return r.tail===null?(B(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=Fe(),e.sibling=null,n=N.current,O(N,i?n&1|2:n&1),j&&Li(t,r.treeForkCount),e);case 22:case 23:return _o(t),lo(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(B(t),t.subtreeFlags&6&&(t.flags|=8192)):B(t),n=t.updateQueue,n!==null&&Bc(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&D(Ta),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),ra(M),B(t),null;case 25:return null;case 30:return null}throw Error(s(156,t.tag))}function Uc(e,t){switch(Bi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return ra(M),be(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return Se(t),null;case 31:if(t.memoizedState!==null){if(_o(t),t.alternate===null)throw Error(s(340));Xi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(_o(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(s(340));Xi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return D(N),null;case 4:return be(),null;case 10:return ra(t.type),null;case 22:case 23:return _o(t),lo(),e!==null&&D(Ta),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return ra(M),null;case 25:return null;default:return null}}function Wc(e,t){switch(Bi(t),t.tag){case 3:ra(M),be();break;case 26:case 27:case 5:Se(t);break;case 4:be();break;case 31:t.memoizedState!==null&&_o(t);break;case 13:_o(t);break;case 19:D(N);break;case 10:ra(t.type);break;case 22:case 23:_o(t),lo(),e!==null&&D(Ta);break;case 24:ra(M)}}function Gc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Z(t,t.return,e)}}function Kc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Z(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Z(t,t.return,e)}}function qc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{io(t,n)}catch(t){Z(e,e.return,t)}}}function Jc(e,t,n){n.props=Zs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Z(e,t,n)}}function Yc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Z(e,t,n)}}function Xc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Z(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Z(e,t,n)}else n.current=null}function Zc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Z(e,e.return,t)}}function Qc(e,t,n){try{var r=e.stateNode;Fd(r,e.type,n,t),r[_t]=t}catch(t){Z(e,e.return,t)}}function $c(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Zd(e.type)||e.tag===4}function el(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||$c(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Zd(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function tl(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=ln));else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(tl(e,t,n),e=e.sibling;e!==null;)tl(e,t,n),e=e.sibling}function nl(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(nl(e,t,n),e=e.sibling;e!==null;)nl(e,t,n),e=e.sibling}function rl(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Pd(t,r,n),t[gt]=e,t[_t]=n}catch(t){Z(e,e.return,t)}}var il=!1,V=!1,al=!1,ol=typeof WeakSet==`function`?WeakSet:Set,H=null;function sl(e,t){if(e=e.containerInfo,Rd=sp,e=Ir(e),Lr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var i=r.anchorOffset,a=r.focusNode;r=r.focusOffset;try{n.nodeType,a.nodeType}catch{n=null;break a}var o=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||i!==0&&f.nodeType!==3||(c=o+i),f!==a||r!==0&&f.nodeType!==3||(l=o+r),f.nodeType===3&&(o+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===i&&(c=o),p===a&&++d===r&&(l=o),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(zd={focusedElem:e,selectionRange:n},sp=!1,H=t;H!==null;)if(t=H,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,H=e;else for(;H!==null;){switch(t=H,a=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Pd(a,r,n),a[gt]=e,k(a),r=a;break a;case`link`:var o=Vf(`link`,`href`,i).get(r+(n.href||``));if(o){for(var c=0;cg&&(o=g,g=h,h=o);var _=Pr(s,h),v=Pr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,T.T=null,n=lu,lu=null;var a=au,o=su;if(X=0,ou=au=null,su=0,W&6)throw Error(s(331));var c=W;if(W|=4,Il(a.current),Ol(a,a.current,o,n),W=c,id(0,!1),Ge&&typeof Ge.onPostCommitFiberRoot==`function`)try{Ge.onPostCommitFiberRoot(We,a)}catch{}return!0}finally{E.p=i,T.T=r,Vu(e,t)}}function Wu(e,t,n){t=Di(n,t),t=rc(e.stateNode,t,2),e=Za(e,t,2),e!==null&&(ot(e,2),rd(e))}function Z(e,t,n){if(e.tag===3)Wu(e,e,n);else for(;t!==null;){if(t.tag===3){Wu(t,e,n);break}if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(iu===null||!iu.has(r))){e=Di(n,e),n=ic(2),r=Za(t,n,2),r!==null&&(ac(n,r,t,e),ot(r,2),rd(r));break}}t=t.return}}function Gu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Bl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Wl=!0,i.add(n),e=Ku.bind(null,e,t,n),t.then(e,e))}function Ku(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,G===e&&(q&n)===n&&(Y===4||Y===3&&(q&62914560)===q&&300>Fe()-eu?!(W&2)&&Su(e,0):Jl|=n,Xl===q&&(Xl=0)),rd(e)}function qu(e,t){t===0&&(t=it()),e=fi(e,t),e!==null&&(ot(e,t),rd(e))}function Ju(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),qu(e,n)}function Yu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,i=e.memoizedState;i!==null&&(n=i.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(s(314))}r!==null&&r.delete(t),qu(e,n)}function Xu(e,t){return je(e,t)}var Zu=null,Qu=null,$u=!1,ed=!1,td=!1,nd=0;function rd(e){e!==Qu&&e.next===null&&(Qu===null?Zu=Qu=e:Qu=Qu.next=e),ed=!0,$u||($u=!0,ud())}function id(e,t){if(!td&&ed){td=!0;do for(var n=!1,r=Zu;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-qe(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,ld(r,a))}else a=q,a=tt(r,r===G?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||nt(r,a)||(n=!0,ld(r,a));r=r.next}while(n);td=!1}}function ad(){od()}function od(){ed=$u=!1;var e=0;nd!==0&&Gd()&&(e=nd);for(var t=Fe(),n=null,r=Zu;r!==null;){var i=r.next,a=sd(r,t);a===0?(r.next=null,n===null?Zu=i:n.next=i,i===null&&(Qu=n)):(n=r,(e!==0||a&3)&&(ed=!0)),r=i}X!==0&&X!==5||id(e,!1),nd!==0&&(nd=0)}function sd(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Id(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function xf(e,t,n){var r=bf;if(r&&typeof t==`string`&&t){var i=qt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),hf.has(i)||(hf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Pd(t,`link`,e),k(t),r.head.appendChild(t)))}}function Sf(e){_f.D(e),xf(`dns-prefetch`,e,null)}function Cf(e,t){_f.C(e,t),xf(`preconnect`,e,t)}function wf(e,t,n){_f.L(e,t,n);var r=bf;if(r&&e&&t){var i=`link[rel="preload"][as="`+qt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+qt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+qt(n.imageSizes)+`"]`)):i+=`[href="`+qt(e)+`"]`;var a=i;switch(t){case`style`:a=Af(e);break;case`script`:a=Pf(e)}mf.has(a)||(e=h({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),mf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(jf(a))||t===`script`&&r.querySelector(Ff(a))||(t=r.createElement(`link`),Pd(t,`link`,e),k(t),r.head.appendChild(t)))}}function Tf(e,t){_f.m(e,t);var n=bf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+qt(r)+`"][href="`+qt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Pf(e)}if(!mf.has(a)&&(e=h({rel:`modulepreload`,href:e},t),mf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Ff(a)))return}r=n.createElement(`link`),Pd(r,`link`,e),k(r),n.head.appendChild(r)}}}function Ef(e,t,n){_f.S(e,t,n);var r=bf;if(r&&e){var i=Ot(r).hoistableStyles,a=Af(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(jf(a)))s.loading=5;else{e=h({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=mf.get(a))&&Rf(e,n);var c=o=r.createElement(`link`);k(c),Pd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Lf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Df(e,t){_f.X(e,t);var n=bf;if(n&&e){var r=Ot(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),k(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Of(e,t){_f.M(e,t);var n=bf;if(n&&e){var r=Ot(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0,type:`module`},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),k(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t,n,r){var i=(i=_e.current)?gf(i):null;if(!i)throw Error(s(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Af(n.href),n=Ot(i).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Af(n.href);var a=Ot(i).hoistableStyles,o=a.get(e);if(o||(i=i.ownerDocument||i,o={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},a.set(e,o),(a=i.querySelector(jf(e)))&&!a._p&&(o.instance=a,o.state.loading=5),mf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},mf.set(e,n),a||Nf(i,e,n,o.state))),t&&r===null)throw Error(s(528,``));return o}if(t&&r!==null)throw Error(s(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Pf(n),n=Ot(i).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(s(444,e))}}function Af(e){return`href="`+qt(e)+`"`}function jf(e){return`link[rel="stylesheet"][`+e+`]`}function Mf(e){return h({},e,{"data-precedence":e.precedence,precedence:null})}function Nf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Pd(t,`link`,n),k(t),e.head.appendChild(t))}function Pf(e){return`[src="`+qt(e)+`"]`}function Ff(e){return`script[async]`+e}function If(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+qt(n.href)+`"]`);if(r)return t.instance=r,k(r),r;var i=h({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),k(r),Pd(r,`style`,i),Lf(r,n.precedence,e),t.instance=r;case`stylesheet`:i=Af(n.href);var a=e.querySelector(jf(i));if(a)return t.state.loading|=4,t.instance=a,k(a),a;r=Mf(n),(i=mf.get(i))&&Rf(r,i),a=(e.ownerDocument||e).createElement(`link`),k(a);var o=a;return o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),t.state.loading|=4,Lf(a,n.precedence,e),t.instance=a;case`script`:return a=Pf(n.src),(i=e.querySelector(Ff(a)))?(t.instance=i,k(i),i):(r=n,(i=mf.get(a))&&(r=h({},n),zf(r,i)),e=e.ownerDocument||e,i=e.createElement(`script`),k(i),Pd(i,`link`,r),e.head.appendChild(i),t.instance=i);case`void`:return null;default:throw Error(s(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Lf(r,n.precedence,e));return t.instance}function Lf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Uf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Wf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Gf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Af(r.href),a=t.querySelector(jf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Jf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,k(a);return}a=t.ownerDocument||t,r=Mf(r),(i=mf.get(i))&&Rf(r,i),a=a.createElement(`link`),k(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Jf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Kf=0;function qf(e,t){return e.stylesheets&&e.count===0&&Xf(e,e.stylesheets),0Kf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Jf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Xf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Yf=null;function Xf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Yf=new Map,t.forEach(Zf,e),Yf=null,Jf.call(e))}function Zf(e,t){if(!(t.state.loading&4)){var n=Yf.get(e);if(n)var r=n.get(null);else{n=new Map,Yf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=s()})),l=n(),u=c(),d=e=>{let t=e.role||e.type;return t===`user`||t===`assistant`||t===`tool`||t===`system`?t:`assistant`},f=e=>{if(e.text)return e.text;if(e.payload&&typeof e.payload==`object`&&`content`in e.payload){let t=e.payload.content;if(typeof t==`string`)return t}return``},p=e=>{let t=e.entries.find(e=>d(e)===`user`);if(t){let e=f(t).trim();if(e)return e}let n=e.metadata?.objective;return typeof n==`string`&&n.trim()?n:e.id},m=e=>{let t=new Date(e);return Number.isNaN(t.getTime())?``:new Intl.DateTimeFormat(`zh-CN`,{month:`short`,day:`numeric`,hour:`2-digit`,minute:`2-digit`}).format(t)},h=(e,t)=>{let n=t.trim().toLowerCase();if(!n)return!0;let r=[e.id,e.cwd,p(e)].map(e=>e.toLowerCase());for(let t of e.entries)r.push(f(t).toLowerCase()),t.tool&&r.push(t.tool.toLowerCase());return r.some(e=>e.includes(n))},g=(e,t)=>e.filter(e=>h(e,t)),_=(e,t)=>t===`all`?e:e.filter(e=>d(e)===t),v=``+new URL(`call-code-BBgvhvPd.png`,import.meta.url).href,y=e((e=>{var t=Symbol.for(`react.transitional.element`);function n(e,n,r){var i=null;if(r!==void 0&&(i=``+r),n.key!==void 0&&(i=``+n.key),`key`in n)for(var a in r={},n)a!==`key`&&(r[a]=n[a]);else r=n;return n=r.ref,{$$typeof:t,type:e,key:i,ref:n===void 0?null:n,props:r}}e.jsx=n,e.jsxs=n})),b=e(((e,t)=>{t.exports=y()}))();function x({sessions:e,theme:t,onThemeChange:n}){let r=e.reduce((e,t)=>(e.messages+=t.entries.length,e.tools+=t.entries.filter(e=>d(e)===`tool`).length,e),{messages:0,tools:0});return(0,b.jsxs)(`header`,{className:`header-bar`,children:[(0,b.jsxs)(`div`,{className:`flex min-w-0 items-center gap-3`,children:[(0,b.jsx)(`img`,{src:v,alt:`Call Code`,className:`h-9 w-9 shrink-0 rounded-xl border object-contain`,style:{borderColor:`rgb(var(--chip-border))`,background:`rgb(var(--chip-bg))`}}),(0,b.jsxs)(`div`,{className:`min-w-0`,children:[(0,b.jsx)(`h1`,{className:`truncate text-[17px] font-semibold leading-tight`,style:{color:`var(--text-primary)`},children:`Call Code`}),(0,b.jsxs)(`div`,{className:`mt-0.5 truncate text-[11px]`,style:{color:`var(--text-tertiary)`},children:[e.length,` 个会话 · `,r.messages,` 条消息 · `,r.tools,` `,`次工具`]})]})]}),(0,b.jsx)(`div`,{className:`segmented shrink-0`,role:`tablist`,"aria-label":`主题切换`,children:[`light`,`dark`].map(e=>(0,b.jsx)(`button`,{type:`button`,role:`tab`,"aria-pressed":t===e,onClick:()=>n(e),children:e===`light`?`毛玻璃`:`高级黑`},e))})]})}function ee({sessions:e,activeId:t,query:n,onSelect:r,onQueryChange:i}){return(0,b.jsxs)(`aside`,{className:`sidebar-panel max-h-[50vh] lg:max-h-none`,children:[(0,b.jsx)(`header`,{className:`border-b px-3 py-3`,style:{borderColor:`rgb(var(--panel-border))`},children:(0,b.jsxs)(`div`,{className:`search-box flex h-8 min-w-0 items-center gap-2 rounded-lg border px-2.5`,style:{borderColor:`rgb(var(--chip-border))`,background:`rgb(var(--chip-bg))`},children:[(0,b.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,style:{color:`var(--text-tertiary)`},"aria-hidden":`true`,children:[(0,b.jsx)(`circle`,{cx:`11`,cy:`11`,r:`7`}),(0,b.jsx)(`path`,{d:`m20 20-3.5-3.5`})]}),(0,b.jsx)(`input`,{type:`search`,value:n,onChange:e=>i(e.target.value),placeholder:`搜索会话`,"aria-label":`搜索会话`,className:`h-full w-full min-w-0 bg-transparent text-[12px] outline-none`,style:{color:`var(--text-primary)`}})]})}),(0,b.jsxs)(`div`,{className:`min-h-0 flex-1 overflow-y-auto px-2 pt-3 pb-3`,children:[e.length===0&&(0,b.jsx)(`div`,{className:`grid min-h-[120px] place-items-center text-[12px]`,style:{color:`var(--text-tertiary)`},children:`暂无会话`}),(0,b.jsx)(`div`,{className:`flex flex-col gap-0.5`,children:e.map(e=>{let n=e.id===t;return(0,b.jsxs)(`button`,{type:`button`,onClick:()=>r(e.id),"aria-current":n?`true`:void 0,className:`group relative flex items-start gap-2.5 rounded-xl px-2.5 py-2 text-left transition-all duration-200`,style:n?{background:`rgb(var(--sidebar-active))`,boxShadow:`var(--sidebar-active-shadow)`}:void 0,onMouseEnter:e=>{n||(e.currentTarget.style.background=`rgb(var(--sidebar-hover))`)},onMouseLeave:e=>{n||(e.currentTarget.style.background=``)},children:[(0,b.jsx)(`span`,{className:`mt-1 h-4 w-1 shrink-0 rounded-full transition-opacity`,style:{background:n?`var(--text-primary)`:`transparent`,opacity:+!!n},"aria-hidden":`true`}),(0,b.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,b.jsx)(`div`,{className:`line-clamp-2 text-[13px] font-medium leading-snug`,style:{color:n?`var(--text-primary)`:`var(--text-secondary)`},children:p(e)}),(0,b.jsxs)(`div`,{className:`mt-1 flex items-center gap-1.5 text-[11px]`,style:{color:`var(--text-tertiary)`},children:[(0,b.jsxs)(`span`,{children:[e.entries.length,` 条`]}),(0,b.jsx)(`span`,{style:{opacity:.4},children:`·`}),(0,b.jsx)(`span`,{className:`truncate`,children:m(e.createdAt)})]})]})]},e.id)})})]})]})}var S={user:{label:`用户`,dotClass:`role-dot role-dot--user`,bubbleClass:`msg-bubble--user`},assistant:{label:`助手`,dotClass:`role-dot role-dot--assistant`,bubbleClass:`msg-bubble--assistant`},tool:{label:`工具`,dotClass:`role-dot role-dot--tool`,bubbleClass:`msg-bubble--tool`},system:{label:`系统`,dotClass:`role-dot role-dot--system`,bubbleClass:``}};function C({entry:e}){let t=d(e),n=S[t],r=f(e),i=t===`tool`||r.length>320;return(0,b.jsxs)(`article`,{className:`flex gap-3`,children:[(0,b.jsx)(`div`,{className:`pt-1.5`,children:(0,b.jsx)(`span`,{className:n.dotClass,"aria-hidden":`true`})}),(0,b.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,b.jsxs)(`div`,{className:`mb-1.5 flex items-center gap-2 text-[11px] font-medium`,style:{color:`var(--text-tertiary)`},children:[(0,b.jsx)(`span`,{style:{color:`var(--role-${t})`},children:n.label}),e.tool?(0,b.jsx)(`span`,{className:`chip !py-0 !text-[10px]`,children:e.tool}):null,(0,b.jsx)(`span`,{className:`ml-auto shrink-0`,children:m(e.timestamp)})]}),i?(0,b.jsx)(`pre`,{className:`msg-bubble ${n.bubbleClass} mono m-0 max-h-[480px] overflow-auto px-3.5 py-2.5 text-[12px]`,style:{color:`var(--text-secondary)`},children:r}):(0,b.jsx)(`div`,{className:`msg-bubble ${n.bubbleClass} whitespace-pre-wrap break-words px-3.5 py-2.5 text-[13.5px] leading-relaxed`,style:{color:`var(--text-primary)`},children:r})]})]})}var te=[{value:`all`,label:`全部`},{value:`user`,label:`用户`},{value:`assistant`,label:`助手`},{value:`tool`,label:`工具`}];function ne({session:e}){let t=[[`消息`,`${e.entries.length}`],[`tokens`,e.stats?.totalTokens?.toLocaleString()??`0`],[`成本`,`$${(e.stats?.costTotal??0).toFixed(3)}`],[`目录`,e.cwd]];return(0,b.jsx)(`div`,{className:`flex flex-wrap items-center gap-1.5`,children:t.map(([e,t])=>(0,b.jsxs)(`span`,{className:`chip max-w-[240px] truncate`,children:[(0,b.jsx)(`span`,{style:{opacity:.6},children:e}),(0,b.jsx)(`span`,{className:`truncate`,children:t})]},e))})}function re({facts:e}){return e.length===0?null:(0,b.jsxs)(`section`,{className:`mt-8`,children:[(0,b.jsx)(`div`,{className:`mb-3 text-[11px] font-medium uppercase tracking-wider`,style:{color:`var(--text-tertiary)`},children:`事实`}),(0,b.jsx)(`div`,{className:`grid gap-2 sm:grid-cols-2 lg:grid-cols-3`,children:e.slice(0,12).map((e,t)=>(0,b.jsxs)(`div`,{className:`msg-bubble px-3 py-2.5`,children:[(0,b.jsx)(`div`,{className:`text-[11px]`,style:{color:`var(--text-tertiary)`},children:e.kind}),(0,b.jsx)(`div`,{className:`mt-0.5 truncate text-[13px]`,style:{color:`var(--text-primary)`},children:e.value||e.key||String(e.seq)})]},`${e.seq}-${t}`))})]})}var w=e=>typeof e==`string`?e:e&&typeof e==`object`?JSON.stringify(e,null,2):String(e??``);function ie({records:e}){return e.length===0?null:(0,b.jsxs)(`section`,{className:`mt-8`,children:[(0,b.jsxs)(`div`,{className:`mb-3 text-[11px] font-medium uppercase tracking-wider`,style:{color:`var(--text-tertiary)`},children:[`运行记录 · 最近 `,Math.min(20,e.length),` 条`]}),(0,b.jsx)(`div`,{className:`flex flex-col gap-1.5`,children:e.slice(-20).reverse().map(e=>(0,b.jsxs)(`div`,{className:`msg-bubble px-3 py-2.5`,children:[(0,b.jsxs)(`div`,{className:`flex items-center justify-between gap-2 text-[11px]`,style:{color:`var(--text-tertiary)`},children:[(0,b.jsxs)(`span`,{className:`truncate`,children:[e.type,e.opKind?` / ${e.opKind}`:``]}),(0,b.jsx)(`span`,{className:`shrink-0`,children:m(e.timestamp)})]}),(0,b.jsx)(`div`,{className:`mt-1.5 max-h-40 overflow-auto whitespace-pre-wrap break-words mono`,style:{color:`var(--text-secondary)`},children:w(e.payload)})]},e.id))})]})}function ae({session:e,filter:t,onFilterChange:n}){let r=e?_(e.entries,t):[];return(0,b.jsxs)(`main`,{className:`main-panel`,children:[(0,b.jsxs)(`header`,{className:`flex flex-col gap-3 px-6 pt-5 pb-4`,children:[(0,b.jsx)(`h2`,{className:`truncate text-[15px] font-semibold leading-snug`,style:{color:`var(--text-primary)`},children:e?p(e):`暂无会话`}),e?(0,b.jsx)(ne,{session:e}):null]}),(0,b.jsx)(`div`,{className:`flex items-center gap-3 border-t px-6 py-2.5`,style:{borderColor:`rgb(var(--panel-border))`},children:(0,b.jsx)(`div`,{className:`segmented`,role:`tablist`,"aria-label":`消息过滤`,children:te.map(e=>(0,b.jsx)(`button`,{type:`button`,role:`tab`,"aria-pressed":t===e.value,onClick:()=>n(e.value),children:e.label},e.value))})}),(0,b.jsx)(`div`,{className:`min-h-0 flex-1 overflow-y-auto px-6 py-6`,children:(0,b.jsx)(`div`,{className:`mx-auto max-w-[760px]`,children:e?r.length===0?(0,b.jsx)(`div`,{className:`grid min-h-[200px] place-items-center text-[13px]`,style:{color:`var(--text-tertiary)`},children:`没有匹配的消息`}):(0,b.jsxs)(`div`,{className:`flex flex-col gap-4`,children:[r.map(e=>(0,b.jsx)(C,{entry:e},e.id)),e.facts?(0,b.jsx)(re,{facts:e.facts}):null,e.records?(0,b.jsx)(ie,{records:e.records}):null]}):(0,b.jsx)(`div`,{className:`grid min-h-[200px] place-items-center text-[13px]`,style:{color:`var(--text-tertiary)`},children:`暂无会话数据`})})})]})}var oe=[`./data.json`,`./data.example.json`],se=()=>{let e=new URLSearchParams(window.location.search),t=e.get(`data`)||e.get(`dataUrl`);return t?[t,...oe]:oe},ce=e=>{if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.id==`string`&&typeof t.createdAt==`string`&&typeof t.cwd==`string`&&Array.isArray(t.entries)},le=e=>{if(!e||typeof e!=`object`)return!1;let t=e;return t.schemaVersion===1&&typeof t.exportedAt==`string`&&Array.isArray(t.sessions)&&t.sessions.every(ce)},ue=async()=>{for(let e of se())try{let t=new URL(e,window.location.href),n=await fetch(t,{cache:`no-cache`});if(!n.ok)continue;let r=await n.json();if(le(r))return r}catch{}return null},T=`call-code-theme`,E=()=>localStorage.getItem(T)===`light`?`light`:`dark`;function de(){let[e,t]=(0,l.useState)(null),[n,r]=(0,l.useState)(`loading`),[i,a]=(0,l.useState)(E),[o,s]=(0,l.useState)(null),[c,u]=(0,l.useState)(``),[d,f]=(0,l.useState)(`all`);(0,l.useEffect)(()=>{let e=!1;return ue().then(n=>{if(e)return;t(n);let i=new URLSearchParams(window.location.search).get(`session`);s(i&&n?.sessions.some(e=>e.id===i)?i:n?.sessions[0]?.id??null),r(n?`ready`:`error`)}),()=>{e=!0}},[]),(0,l.useEffect)(()=>{document.documentElement.classList.toggle(`dark`,i===`dark`),localStorage.setItem(T,i)},[i]);let p=(0,l.useMemo)(()=>e?.sessions??[],[e]),m=(0,l.useMemo)(()=>g(p,c),[p,c]),h=(0,l.useMemo)(()=>m.find(e=>e.id===o)??m[0]??null,[m,o]);return n===`loading`?(0,b.jsx)(`div`,{className:`app-shell`,children:(0,b.jsx)(`div`,{className:`grid min-h-[60vh] place-items-center text-sm`,style:{color:`var(--text-tertiary)`},children:`正在读取会话`})}):n===`error`||!e?(0,b.jsx)(`div`,{className:`app-shell`,children:(0,b.jsx)(`div`,{className:`grid min-h-[60vh] place-items-center text-sm`,style:{color:`var(--text-tertiary)`},children:`无法读取会话数据`})}):(0,b.jsx)(`div`,{className:`app-shell`,children:(0,b.jsxs)(`div`,{className:`app-frame`,children:[(0,b.jsx)(x,{sessions:m,theme:i,onThemeChange:a}),(0,b.jsxs)(`div`,{className:`app-workspace`,children:[(0,b.jsx)(ee,{sessions:m,activeId:o,query:c,onSelect:s,onQueryChange:u}),(0,b.jsx)(ae,{session:h,filter:d,onFilterChange:f})]})]})})}var fe=document.getElementById(`root`);if(!fe)throw Error(`未找到 #root 容器`);(0,u.createRoot)(fe).render((0,b.jsx)(l.StrictMode,{children:(0,b.jsx)(de,{})})); \ No newline at end of file diff --git a/packages/client/dist/data.example.json b/packages/client/dist/data.example.json deleted file mode 100644 index e8e8f15..0000000 --- a/packages/client/dist/data.example.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "schemaVersion": 1, - "exportedAt": "2026-08-05T12:00:00.000Z", - "sessions": [ - { - "id": "demo-build", - "createdAt": "2026-08-05T10:10:00.000Z", - "cwd": "/Users/allure/Desktop/demo-app", - "parentSessionId": null, - "metadata": { - "mode": "build", - "objective": "为仪表盘增加搜索过滤" - }, - "stats": { - "messageCount": 4, - "cachedTokens": 1200, - "uncachedTokens": 860, - "totalTokens": 2060, - "costTotal": 0.04 - }, - "entries": [ - { - "seq": 1, - "id": "e-user-1", - "parentId": null, - "type": "user", - "role": "user", - "timestamp": "2026-08-05T10:10:00.000Z", - "text": "给仪表盘加一个搜索框,按任务状态过滤列表。", - "payload": { - "content": "给仪表盘加一个搜索框,按任务状态过滤列表。", - "tags": ["task-input"] - } - }, - { - "seq": 2, - "id": "e-assistant-1", - "parentId": "e-user-1", - "type": "assistant", - "role": "assistant", - "timestamp": "2026-08-05T10:10:12.000Z", - "text": "我会先读取现有仪表盘组件,找出状态字段和列表渲染位置,再补充搜索过滤。", - "payload": { - "content": "我会先读取现有仪表盘组件,找出状态字段和列表渲染位置,再补充搜索过滤。", - "tags": ["model-response"] - } - }, - { - "seq": 3, - "id": "e-tool-1", - "parentId": "e-assistant-1", - "type": "tool", - "role": "tool", - "timestamp": "2026-08-05T10:10:18.000Z", - "tool": "read_file", - "text": "{\"path\":\"src/pages/dashboard.tsx\"}\nsuccess", - "payload": { - "content": "{\"path\":\"src/pages/dashboard.tsx\"}\nsuccess", - "tool": "read_file", - "tags": ["tool-result", "read_file"] - } - }, - { - "seq": 4, - "id": "e-assistant-2", - "parentId": "e-tool-1", - "type": "assistant", - "role": "assistant", - "timestamp": "2026-08-05T10:10:30.000Z", - "text": "已完成搜索框和状态过滤,列表会按状态字段实时过滤。", - "payload": { - "content": "已完成搜索框和状态过滤,列表会按状态字段实时过滤。", - "tags": ["model-response"] - } - } - ], - "records": [ - { - "seq": 3, - "id": "r-tool-1", - "lane": "run", - "runId": "demo-build", - "type": "tool_call", - "opKind": "read_file", - "timestamp": "2026-08-05T10:10:18.000Z", - "payload": { - "tool": "read_file", - "path": "src/pages/dashboard.tsx" - } - } - ], - "facts": [ - { - "seq": 1, - "kind": "task-objective", - "key": "objective", - "value": "为仪表盘增加搜索过滤" - } - ] - }, - { - "id": "demo-plan", - "createdAt": "2026-08-04T09:20:00.000Z", - "cwd": "/Users/allure/Desktop/call-code", - "parentSessionId": null, - "metadata": { - "mode": "plan", - "objective": "评估 GitHub Pages 静态发布方案" - }, - "stats": { - "messageCount": 3, - "cachedTokens": 900, - "uncachedTokens": 640, - "totalTokens": 1540, - "costTotal": 0.02 - }, - "entries": [ - { - "seq": 1, - "id": "e-user-p1", - "parentId": null, - "type": "user", - "role": "user", - "timestamp": "2026-08-04T09:20:00.000Z", - "text": "梳理 GitHub Pages 部署路径和需要提交的静态文件。", - "payload": { - "content": "梳理 GitHub Pages 部署路径和需要提交的静态文件。", - "tags": ["task-input"] - } - }, - { - "seq": 2, - "id": "e-assistant-p1", - "parentId": "e-user-p1", - "type": "assistant", - "role": "assistant", - "timestamp": "2026-08-04T09:20:08.000Z", - "text": "可以使用 GitHub Actions 将 packages/client 发布到 Pages,数据由 pnpm export:web 生成并提交。", - "payload": { - "content": "可以使用 GitHub Actions 将 packages/client 发布到 Pages,数据由 pnpm export:web 生成并提交。", - "tags": ["model-response"] - } - }, - { - "seq": 3, - "id": "e-assistant-p2", - "parentId": "e-assistant-p1", - "type": "assistant", - "role": "assistant", - "timestamp": "2026-08-04T09:20:16.000Z", - "text": "计划完成,确认后进入 BUILD 模式部署工作流。", - "payload": { - "content": "计划完成,确认后进入 BUILD 模式部署工作流。", - "tags": ["model-response"] - } - } - ], - "records": [], - "facts": [ - { - "seq": 1, - "kind": "task-objective", - "key": "objective", - "value": "评估 GitHub Pages 静态发布方案" - } - ] - } - ] -} diff --git a/packages/client/dist/index.html b/packages/client/dist/index.html index e7c1b22..c3ad76e 100644 --- a/packages/client/dist/index.html +++ b/packages/client/dist/index.html @@ -4,8 +4,8 @@ Call Code History - - + +
diff --git a/packages/client/package.json b/packages/client/package.json index 03bb50c..86d0d92 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -10,6 +10,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { + "@call-code/server": "workspace:*", "react": "^19.2.8", "react-dom": "^19.2.8" }, diff --git a/packages/client/public/data.example.json b/packages/client/public/data.example.json deleted file mode 100644 index e8e8f15..0000000 --- a/packages/client/public/data.example.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "schemaVersion": 1, - "exportedAt": "2026-08-05T12:00:00.000Z", - "sessions": [ - { - "id": "demo-build", - "createdAt": "2026-08-05T10:10:00.000Z", - "cwd": "/Users/allure/Desktop/demo-app", - "parentSessionId": null, - "metadata": { - "mode": "build", - "objective": "为仪表盘增加搜索过滤" - }, - "stats": { - "messageCount": 4, - "cachedTokens": 1200, - "uncachedTokens": 860, - "totalTokens": 2060, - "costTotal": 0.04 - }, - "entries": [ - { - "seq": 1, - "id": "e-user-1", - "parentId": null, - "type": "user", - "role": "user", - "timestamp": "2026-08-05T10:10:00.000Z", - "text": "给仪表盘加一个搜索框,按任务状态过滤列表。", - "payload": { - "content": "给仪表盘加一个搜索框,按任务状态过滤列表。", - "tags": ["task-input"] - } - }, - { - "seq": 2, - "id": "e-assistant-1", - "parentId": "e-user-1", - "type": "assistant", - "role": "assistant", - "timestamp": "2026-08-05T10:10:12.000Z", - "text": "我会先读取现有仪表盘组件,找出状态字段和列表渲染位置,再补充搜索过滤。", - "payload": { - "content": "我会先读取现有仪表盘组件,找出状态字段和列表渲染位置,再补充搜索过滤。", - "tags": ["model-response"] - } - }, - { - "seq": 3, - "id": "e-tool-1", - "parentId": "e-assistant-1", - "type": "tool", - "role": "tool", - "timestamp": "2026-08-05T10:10:18.000Z", - "tool": "read_file", - "text": "{\"path\":\"src/pages/dashboard.tsx\"}\nsuccess", - "payload": { - "content": "{\"path\":\"src/pages/dashboard.tsx\"}\nsuccess", - "tool": "read_file", - "tags": ["tool-result", "read_file"] - } - }, - { - "seq": 4, - "id": "e-assistant-2", - "parentId": "e-tool-1", - "type": "assistant", - "role": "assistant", - "timestamp": "2026-08-05T10:10:30.000Z", - "text": "已完成搜索框和状态过滤,列表会按状态字段实时过滤。", - "payload": { - "content": "已完成搜索框和状态过滤,列表会按状态字段实时过滤。", - "tags": ["model-response"] - } - } - ], - "records": [ - { - "seq": 3, - "id": "r-tool-1", - "lane": "run", - "runId": "demo-build", - "type": "tool_call", - "opKind": "read_file", - "timestamp": "2026-08-05T10:10:18.000Z", - "payload": { - "tool": "read_file", - "path": "src/pages/dashboard.tsx" - } - } - ], - "facts": [ - { - "seq": 1, - "kind": "task-objective", - "key": "objective", - "value": "为仪表盘增加搜索过滤" - } - ] - }, - { - "id": "demo-plan", - "createdAt": "2026-08-04T09:20:00.000Z", - "cwd": "/Users/allure/Desktop/call-code", - "parentSessionId": null, - "metadata": { - "mode": "plan", - "objective": "评估 GitHub Pages 静态发布方案" - }, - "stats": { - "messageCount": 3, - "cachedTokens": 900, - "uncachedTokens": 640, - "totalTokens": 1540, - "costTotal": 0.02 - }, - "entries": [ - { - "seq": 1, - "id": "e-user-p1", - "parentId": null, - "type": "user", - "role": "user", - "timestamp": "2026-08-04T09:20:00.000Z", - "text": "梳理 GitHub Pages 部署路径和需要提交的静态文件。", - "payload": { - "content": "梳理 GitHub Pages 部署路径和需要提交的静态文件。", - "tags": ["task-input"] - } - }, - { - "seq": 2, - "id": "e-assistant-p1", - "parentId": "e-user-p1", - "type": "assistant", - "role": "assistant", - "timestamp": "2026-08-04T09:20:08.000Z", - "text": "可以使用 GitHub Actions 将 packages/client 发布到 Pages,数据由 pnpm export:web 生成并提交。", - "payload": { - "content": "可以使用 GitHub Actions 将 packages/client 发布到 Pages,数据由 pnpm export:web 生成并提交。", - "tags": ["model-response"] - } - }, - { - "seq": 3, - "id": "e-assistant-p2", - "parentId": "e-assistant-p1", - "type": "assistant", - "role": "assistant", - "timestamp": "2026-08-04T09:20:16.000Z", - "text": "计划完成,确认后进入 BUILD 模式部署工作流。", - "payload": { - "content": "计划完成,确认后进入 BUILD 模式部署工作流。", - "tags": ["model-response"] - } - } - ], - "records": [], - "facts": [ - { - "seq": 1, - "kind": "task-objective", - "key": "objective", - "value": "评估 GitHub Pages 静态发布方案" - } - ] - } - ] -} diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index 81fc869..2ed643f 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -1,60 +1,102 @@ -import { useEffect, useMemo, useState } from 'react'; -import { HeaderBar } from './components/HeaderBar'; -import { Sidebar } from './components/Sidebar'; -import { MainPanel } from './components/MainPanel'; -import { loadWebExport } from './data'; -import type { Filter, Theme, WebExport } from './types'; -import { filterSessions } from './utils'; +import { useEffect, useMemo, useRef, useState } from "react"; +import { HeaderBar } from "./components/HeaderBar"; +import { Sidebar } from "./components/Sidebar"; +import { MainPanel } from "./components/MainPanel"; +import { connectLiveExport, type LiveExportConnection } from "./ws"; +import type { AgentMode, ChatStatusMessage, Filter, Theme, WebExport } from "./types"; +import { filterSessions } from "./utils"; +import { ParticleField } from "./components/ParticleField"; -const THEME_KEY = 'call-code-theme'; +const THEME_KEY = "call-code-theme"; const initialTheme = (): Theme => { const stored = localStorage.getItem(THEME_KEY); - return stored === 'light' ? 'light' : 'dark'; + return stored === "light" ? "light" : "dark"; }; export default function App() { const [data, setData] = useState(null); - const [loadState, setLoadState] = useState<'loading' | 'ready' | 'error'>( - 'loading', + const [loadState, setLoadState] = useState<"loading" | "ready" | "error">( + "loading", ); + const [chatStatus, setChatStatus] = useState({ + status: "idle", + }); const [theme, setTheme] = useState(initialTheme); const [activeId, setActiveId] = useState(null); - const [query, setQuery] = useState(''); - const [filter, setFilter] = useState('all'); + const [query, setQuery] = useState(""); + const [filter, setFilter] = useState("all"); + + const connectionRef = useRef(null); useEffect(() => { let cancelled = false; - loadWebExport().then((result) => { + const conn = connectLiveExport({ + onSnapshot: (result) => { + if (cancelled) { + return; + } + + setData(result); + setLoadState("ready"); + setActiveId((current) => { + const requestedId = new URLSearchParams(window.location.search).get( + "session", + ); + const candidates = [ + requestedId, + current, + result.sessions[0]?.id ?? null, + ]; + return ( + candidates.find( + (id) => + id !== null && + id !== undefined && + result.sessions.some((session) => session.id === id), + ) ?? null + ); + }); + }, + onChatStatus: (statusMsg) => { + if (cancelled) { + return; + } + setChatStatus(statusMsg); + }, + }); + + connectionRef.current = conn; + + conn.ready.then((result) => { if (cancelled) { return; } - - setData(result); - const requestedId = new URLSearchParams(window.location.search).get( - 'session', - ); - setActiveId( - requestedId && - result?.sessions.some((session) => session.id === requestedId) - ? requestedId - : (result?.sessions[0]?.id ?? null), - ); - setLoadState(result ? 'ready' : 'error'); + setLoadState(result ? "ready" : "error"); }); return () => { cancelled = true; + conn.close(); + connectionRef.current = null; }; }, []); useEffect(() => { const root = document.documentElement; - root.classList.toggle('dark', theme === 'dark'); + root.classList.toggle("dark", theme === "dark"); + root.dataset.theme = theme; localStorage.setItem(THEME_KEY, theme); }, [theme]); + const handleSendMessage = (payload: { input: string; mode: AgentMode }) => { + if (!connectionRef.current) { + return false; + } + return connectionRef.current.sendMessage(payload); + }; + const sessions = useMemo(() => data?.sessions ?? [], [data]); const filteredSessions = useMemo( () => filterSessions(sessions, query), @@ -68,39 +110,15 @@ export default function App() { [filteredSessions, activeId], ); - if (loadState === 'loading') { - return ( -
-
- 正在读取会话 -
-
- ); - } - - if (loadState === 'error' || !data) { - return ( -
-
- 无法读取会话数据 -
-
- ); - } - return (
+
diff --git a/packages/client/src/components/HeaderBar.tsx b/packages/client/src/components/HeaderBar.tsx index a6a652a..8d08937 100644 --- a/packages/client/src/components/HeaderBar.tsx +++ b/packages/client/src/components/HeaderBar.tsx @@ -6,9 +6,15 @@ interface HeaderBarProps { sessions: WebSession[]; theme: Theme; onThemeChange: (theme: Theme) => void; + connectionState?: 'loading' | 'ready' | 'error'; } -export function HeaderBar({ sessions, theme, onThemeChange }: HeaderBarProps) { +export function HeaderBar({ + sessions, + theme, + onThemeChange, + connectionState = 'ready', +}: HeaderBarProps) { const count = sessions.reduce( (acc, session) => { acc.messages += session.entries.length; @@ -22,34 +28,43 @@ export function HeaderBar({ sessions, theme, onThemeChange }: HeaderBarProps) { return (
-
+
Call Code
-

- Call Code -

+
+

+ Call Code +

+
+
+
- {sessions.length} 个会话 · {count.messages} 条消息 · {count.tools}{' '} - 次工具 + {sessions.length} 个会话 · {count.messages} 条消息 · {count.tools} 次工具
-
+
{(['light', 'dark'] as const).map((value) => ( ))}
diff --git a/packages/client/src/components/MainPanel.tsx b/packages/client/src/components/MainPanel.tsx index 5277646..bcc55ad 100644 --- a/packages/client/src/components/MainPanel.tsx +++ b/packages/client/src/components/MainPanel.tsx @@ -1,26 +1,29 @@ -import type { Filter, WebFact, WebRecord, WebSession } from '../types'; -import { filterEntries, formatTime, getSessionTitle } from '../utils'; -import { MessageItem } from './MessageItem'; +import { useState, type KeyboardEvent } from "react"; +import type { AgentMode, ChatStatusMessage, Filter, WebFact, WebRecord, WebSession } from "../types"; +import { filterEntries, formatTime, getSessionTitle } from "../utils"; +import { MessageItem } from "./MessageItem"; interface MainPanelProps { session: WebSession | null; filter: Filter; onFilterChange: (filter: Filter) => void; + chatStatus: ChatStatusMessage; + onSendMessage: (payload: { input: string; mode: AgentMode }) => boolean; } const filters: Array<{ value: Filter; label: string }> = [ - { value: 'all', label: '全部' }, - { value: 'user', label: '用户' }, - { value: 'assistant', label: '助手' }, - { value: 'tool', label: '工具' }, + { value: "all", label: "全部" }, + { value: "user", label: "用户" }, + { value: "assistant", label: "助手" }, + { value: "tool", label: "工具" }, ]; function StatRow({ session }: { session: WebSession }) { const items = [ - ['消息', `${session.entries.length}`], - ['tokens', session.stats?.totalTokens?.toLocaleString() ?? '0'], - ['成本', `$${(session.stats?.costTotal ?? 0).toFixed(3)}`], - ['目录', session.cwd], + ["消息", `${session.entries.length}`], + ["tokens", session.stats?.totalTokens?.toLocaleString() ?? "0"], + ["成本", `$${(session.stats?.costTotal ?? 0).toFixed(3)}`], + ["目录", session.cwd], ] as const; return ( @@ -44,7 +47,7 @@ function FactGrid({ facts }: { facts: WebFact[] }) {
事实
@@ -53,13 +56,13 @@ function FactGrid({ facts }: { facts: WebFact[] }) {
{fact.kind}
{fact.value || fact.key || String(fact.seq)}
@@ -71,13 +74,13 @@ function FactGrid({ facts }: { facts: WebFact[] }) { } const payloadText = (payload: unknown): string => { - if (typeof payload === 'string') { + if (typeof payload === "string") { return payload; } - if (payload && typeof payload === 'object') { + if (payload && typeof payload === "object") { return JSON.stringify(payload, null, 2); } - return String(payload ?? ''); + return String(payload ?? ""); }; function RecordList({ records }: { records: WebRecord[] }) { @@ -89,7 +92,7 @@ function RecordList({ records }: { records: WebRecord[] }) {
运行记录 · 最近 {Math.min(20, records.length)} 条
@@ -101,17 +104,17 @@ function RecordList({ records }: { records: WebRecord[] }) {
{record.type} - {record.opKind ? ` / ${record.opKind}` : ''} + {record.opKind ? ` / ${record.opKind}` : ""} {formatTime(record.timestamp)}
{payloadText(record.payload)}
@@ -122,26 +125,51 @@ function RecordList({ records }: { records: WebRecord[] }) { ); } -export function MainPanel({ session, filter, onFilterChange }: MainPanelProps) { +export function MainPanel({ + session, + filter, + onFilterChange, + chatStatus, + onSendMessage, +}: MainPanelProps) { + const [input, setInput] = useState(""); + const [mode, setMode] = useState("build"); const entries = session ? filterEntries(session.entries, filter) : []; + const handleSend = () => { + if (!input.trim() || chatStatus.status === "running") { + return; + } + const success = onSendMessage({ input: input.trim(), mode }); + if (success) { + setInput(""); + } + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + return (
{/* 标题栏 */}

- {session ? getSessionTitle(session) : '暂无会话'} + {session ? getSessionTitle(session) : "暂无会话"}

{session ? : null}
- {/* 过滤器 */} + {/* 过滤器与模式栏 */}
{filters.map((item) => ( @@ -156,6 +184,27 @@ export function MainPanel({ session, filter, onFilterChange }: MainPanelProps) { ))}
+ +
+
+ + +
+
{/* 消息列表 */} @@ -164,14 +213,14 @@ export function MainPanel({ session, filter, onFilterChange }: MainPanelProps) { {!session ? (
- 暂无会话数据 + 暂无会话数据,可在下方直接输入需求启动 Harness
) : entries.length === 0 ? (
没有匹配的消息
@@ -188,6 +237,85 @@ export function MainPanel({ session, filter, onFilterChange }: MainPanelProps) { )}
+ + {/* 底部输入与运行状态栏 */} +
+
+ {chatStatus.status !== "idle" && ( +
+ + + {chatStatus.message || chatStatus.trace || "处理中..."} + +
+ )} + +
+ + [{mode.toUpperCase()}] + + setInput(e.target.value)} + onKeyDown={handleKeyDown} + disabled={chatStatus.status === "running"} + placeholder={ + chatStatus.status === "running" + ? "Harness 正在执行任务中..." + : "输入指令或任务,回车直接调用 Harness CLI..." + } + className="h-8 flex-1 bg-transparent text-[13px] outline-none disabled:opacity-50" + style={{ color: "var(--text-primary)" }} + /> + +
+
+
); } diff --git a/packages/client/src/components/ParticleField.tsx b/packages/client/src/components/ParticleField.tsx new file mode 100644 index 0000000..57f56f7 --- /dev/null +++ b/packages/client/src/components/ParticleField.tsx @@ -0,0 +1,187 @@ +import { useEffect, useRef } from 'react'; + +interface Particle { + x: number; + y: number; + originX: number; + originY: number; + vx: number; + vy: number; + size: number; + alpha: number; + baseAlpha: number; + isChar: boolean; +} + +export function ParticleField() { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + let animationFrameId: number; + let particles: Particle[] = []; + let width = (canvas.width = window.innerWidth); + let height = (canvas.height = window.innerHeight); + const mouse = { x: -9999, y: -9999, radius: 120 }; + + const createTextParticles = () => { + const offscreen = document.createElement('canvas'); + const offCtx = offscreen.getContext('2d'); + if (!offCtx) return []; + + offscreen.width = width; + offscreen.height = height; + + const fontSize = Math.min(width / 7.5, 140); + offCtx.font = `900 ${fontSize}px "SF Mono", Monaco, Consolas, monospace`; + offCtx.textAlign = 'center'; + offCtx.textBaseline = 'middle'; + offCtx.fillStyle = '#ffffff'; + offCtx.fillText('CALL CODE', width / 2, height / 2); + + const imageData = offCtx.getImageData(0, 0, width, height); + const data = imageData.data; + const sampled: Particle[] = []; + const step = Math.max(Math.floor(fontSize / 24), 4); + + for (let y = 0; y < height; y += step) { + for (let x = 0; x < width; x += step) { + const index = (y * width + x) * 4; + if (data[index + 3] > 128) { + sampled.push({ + x: Math.random() * width, + y: Math.random() * height, + originX: x, + originY: y, + vx: 0, + vy: 0, + size: Math.random() * 1.5 + 1.2, + alpha: Math.random() * 0.4 + 0.35, + baseAlpha: Math.random() * 0.4 + 0.35, + isChar: true, + }); + } + } + } + return sampled; + }; + + const createAmbientParticles = (count: number): Particle[] => { + const ambient: Particle[] = []; + for (let i = 0; i < count; i++) { + const x = Math.random() * width; + const y = Math.random() * height; + ambient.push({ + x, + y, + originX: x, + originY: y, + vx: (Math.random() - 0.5) * 0.3, + vy: (Math.random() - 0.5) * 0.3, + size: Math.random() * 1.2 + 0.6, + alpha: Math.random() * 0.15 + 0.05, + baseAlpha: Math.random() * 0.15 + 0.05, + isChar: false, + }); + } + return ambient; + }; + + const init = () => { + width = canvas.width = window.innerWidth; + height = canvas.height = window.innerHeight; + const charParticles = createTextParticles(); + const ambientCount = Math.floor((width * height) / 18000); + const ambientParticles = createAmbientParticles(ambientCount); + particles = [...charParticles, ...ambientParticles]; + }; + + init(); + + const onResize = () => init(); + const onMouseMove = (e: MouseEvent) => { + mouse.x = e.clientX; + mouse.y = e.clientY; + }; + const onMouseLeave = () => { + mouse.x = -9999; + mouse.y = -9999; + }; + + window.addEventListener('resize', onResize); + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseleave', onMouseLeave); + + const render = () => { + ctx.clearRect(0, 0, width, height); + + const isDark = + document.documentElement.classList.contains('dark') || + document.documentElement.dataset.theme === 'dark'; + const particleRgb = isDark ? '255, 255, 255' : '15, 23, 42'; + + for (let i = 0; i < particles.length; i++) { + const p = particles[i]; + + if (p.isChar) { + const dx = mouse.x - p.x; + const dy = mouse.y - p.y; + const dist = Math.hypot(dx, dy); + + if (dist < mouse.radius) { + const force = (1 - dist / mouse.radius) * 6; + const angle = Math.atan2(dy, dx); + p.vx -= Math.cos(angle) * force; + p.vy -= Math.sin(angle) * force; + } + + const homeDx = p.originX - p.x; + const homeDy = p.originY - p.y; + p.vx += homeDx * 0.04; + p.vy += homeDy * 0.04; + p.vx *= 0.85; + p.vy *= 0.85; + + p.x += p.vx; + p.y += p.vy; + } else { + p.x += p.vx; + p.y += p.vy; + if (p.x < 0) p.x = width; + if (p.x > width) p.x = 0; + if (p.y < 0) p.y = height; + if (p.y > height) p.y = 0; + } + + ctx.fillStyle = `rgba(${particleRgb}, ${p.alpha})`; + ctx.beginPath(); + ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); + ctx.fill(); + } + + animationFrameId = requestAnimationFrame(render); + }; + + render(); + + return () => { + window.removeEventListener('resize', onResize); + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseleave', onMouseLeave); + cancelAnimationFrame(animationFrameId); + }; + }, []); + + return ( +