diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c874782..a83408d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,6 +30,9 @@ jobs: - run: npm install + - name: Type check + run: node node_modules/typescript/bin/tsc -p tsconfig.main.json --noEmit && node node_modules/typescript/bin/tsc -p tsconfig.preload.json --noEmit && node node_modules/typescript/bin/tsc -p tsconfig.renderer.json --noEmit + - run: npm run build - name: Build distributables diff --git a/.gitignore b/.gitignore index c9b4a37..90357ee 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ release/ .claude/ *.env log.md + +# Agent Loop logs (迭代日志,不提交到仓库) +agent-loop-logs/ +agent-loop.yaml diff --git a/CLAUDE.md b/CLAUDE.md index a375f95..5de5a19 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,68 +1,89 @@ -# ColaMD +# CLAUDE.md -## 产品定位 +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -**Agent Native 的 Markdown 编辑器。** +## Build & Development Commands -面向 Agent-to-Agent、Agent-to-Human 之间的新型协作方式。当前 v1.0 聚焦于核心体验:AI Agent 修改 .md 文件时,ColaMD 自动检测变化并实时显示最新内容,让人类和 Agent 的协作像结对编程一样流畅。 +```bash +npm run dev # Start dev server with HMR (electron-vite dev) +npm run build # Compile all 3 bundles (main, preload, renderer) +npm run preview # Preview production build +npm run dist # Build + package for current platform +npm run dist:mac # macOS build (.dmg + .zip) +npm run dist:win # Windows build (.exe NSIS installer) +npm run dist:linux # Linux build (.AppImage + .deb) +``` + +No test framework, linter, or formatter is configured. Type checking: `npx tsc --noEmit`. -后续会随着 Agent 生态的发展持续迭代。 +Release is triggered by pushing `v*` tags (see `.github/workflows/release.yml`). -## 设计哲学 +## Architecture -### 如非必要,勿增实体 +Electron 3-process model with a minimal, no-framework renderer: -这是 ColaMD 的第一原则。每增加一个 UI 元素、一个功能、一行代码,都要问:这是绝对必要的吗?默认答案是否。 +``` +Main Process (src/main/index.ts) + Node.js APIs: fs.watch, dialog, shell, Menu + Responsibilities: window lifecycle, file I/O, file watching, agent detection, + menus, PDF/HTML export, custom theme storage + ↕ IPC (ipcMain / ipcRenderer) +Preload (src/preload/index.ts) + contextBridge.exposeInMainWorld → window.electronAPI + Defines typed ElectronAPI interface (invoke + event listener patterns) + ↕ +Renderer (src/renderer/) + Plain TypeScript, no React/Vue — Milkdown IS the UI + main.ts → wires IPC events to editor actions + editor/editor.ts → Milkdown setup (commonmark, gfm, history, listener, clipboard) + editor/html-view.ts → custom inline HTML node view + themes/base.css → ALL CSS: reset, layout, 4 built-in themes (via CSS custom properties) + themes/theme-manager.ts → theme switching & localStorage persistence +``` -- 不要工具栏(用户会用快捷键和 Markdown 语法) -- 不要侧边栏 -- 不要状态栏 -- 界面只有:标题栏(拖拽用)+ 编辑器 -- 追求极致的简单,一个功能做到极致 +### Key Mechanism: File Hot Update (Core Feature) -### 核心功能优先级 +The main process uses `fs.watch()` on the open file. When external changes are detected: -1. **文件热更新**(核心卖点)— 外部 Agent 修改 .md 时自动刷新,实时看到 Agent 的工作 -2. **所见即所得** — 输入 Markdown 即刻渲染为富文本 -3. **主题系统** — CSS 主题,可导入自定义主题 -4. **导出** — PDF、HTML +1. Check `isInternalSave` flag — if true (our own save within 100ms), ignore to prevent feedback loops +2. Agent activity state machine: `idle` → `active` (rapid writes, gap < 2s) → `cooldown` (3s after last write) → `idle` (2s later). Visual feedback via CSS animation on `#agent-dot`. +3. Debounce 100ms → read file → send `file-changed` IPC to renderer → `setMarkdown()` replaces all content -### 不做的事情 +Each BrowserWindow has independent `WindowState` (filePath, watcher, agent state). Opening a file already open in another window focuses that window instead. -- 不做文件管理、文件树、工作区 -- 不做知识库管理 -- 不做云同步、协作编辑 -- 不做笔记组织和标签系统 -- 不加不必要的 UI 元素(工具栏、侧边栏等) +### IPC Pattern -## 技术栈 +- **Renderer → Main**: `ipcRenderer.invoke()` for request/response (openFile, saveFile, exportPDF, etc.) +- **Main → Renderer**: `webContents.send()` for push events (file-changed, set-theme, agent-activity, menu-* actions) +- All methods are typed via the `ElectronAPI` interface in `src/preload/index.ts` -- Electron(桌面跨平台) -- Milkdown(基于 ProseMirror 的 WYSIWYG Markdown 框架) -- TypeScript 严格模式 -- electron-vite(构建) -- electron-builder(打包) +### Theme System -## 项目结构 +- Built-in themes: `light`, `dark`, `elegant` (default), `newsprint` — switched by CSS class on `` +- Custom themes: stored as `.css` files in `~/.colamd/themes/`, injected via ` -${getHTML()}` +${sanitizeHTMLFragment(getHTML())}` api.exportHTML(html) }) - api.onNewFile(() => setMarkdown('')) - api.onFileOpened((data) => setMarkdown(data.content)) - api.onFileChanged((content) => setMarkdown(content)) + + api.onNewFile(() => { + currentMarkdown = '' + pendingOpenedContent = null + resetAgentTimeline() + setMarkdown('') + }) + + // file-changed: show diff highlight for agent changes + api.onFileChanged((content) => { + if (content === currentMarkdown) return + + const previousMarkdown = currentMarkdown + const editorEl = document.getElementById('editor') + const scrollTop = editorEl?.scrollTop ?? 0 + currentMarkdown = content + lastAgentUpdateAt = Date.now() + agentUpdateCount += 1 + const changeInfo = analyzeAgentChange(previousMarkdown, content) + agentChanges = [ + { + id: ++agentChangeId, + summary: changeInfo.summary, + timestamp: lastAgentUpdateAt, + targetText: changeInfo.targetText + }, + ...agentChanges + ].slice(0, MAX_AGENT_CHANGES) + renderAgentUI() + setMarkdown(content, true) + requestAnimationFrame(() => { + if (editorEl) editorEl.scrollTop = scrollTop + }) + }) + api.onSetTheme((theme) => applyTheme(theme)) api.onSetCustomCSS((css) => { const theme = loadSavedTheme() @@ -90,6 +441,8 @@ img{max-width:100%} const agentDot = document.getElementById('agent-dot') api.onAgentActivity((state) => { + agentState = state as 'idle' | 'active' | 'cooldown' + renderAgentUI() if (agentDot) agentDot.className = state === 'idle' ? '' : state }) @@ -102,7 +455,11 @@ img{max-width:100%} const filePath = api.getPathForFile(file) if (!filePath) return const result = await api.openFilePath(filePath) - if (result) setMarkdown(result.content) + if (result) { + currentMarkdown = result.content + resetAgentTimeline() + setMarkdown(result.content) + } }) } diff --git a/src/renderer/themes/base.css b/src/renderer/themes/base.css index 75a9a1e..b71d24d 100644 --- a/src/renderer/themes/base.css +++ b/src/renderer/themes/base.css @@ -19,19 +19,138 @@ html, body { position: relative; } -#agent-dot { +#agent-meta { position: absolute; - top: 22px; + top: 14px; right: 16px; + display: flex; + align-items: center; + gap: 10px; + -webkit-app-region: no-drag; +} + +#agent-status { + max-width: min(38vw, 320px); + color: var(--text-muted); + font-size: 12px; + line-height: 1.3; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +#agent-panel-toggle { + border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--bg-color) 90%, var(--text-color) 10%); + color: var(--text-color); + border-radius: 999px; + padding: 5px 10px; + font-size: 12px; + cursor: pointer; +} + +#agent-panel-toggle:hover { + background: color-mix(in srgb, var(--bg-color) 82%, var(--text-color) 18%); +} + +#agent-dot { + position: static; width: 8px; height: 8px; border-radius: 50%; background: var(--text-muted); opacity: 0.3; - -webkit-app-region: no-drag; transition: background 0.3s ease, opacity 0.3s ease; } +#agent-panel { + margin: 0 40px 16px; + padding: 14px 16px; + border: 1px solid var(--border-color); + border-radius: 12px; + background: color-mix(in srgb, var(--bg-color) 96%, var(--text-color) 4%); +} + +#agent-panel[hidden] { + display: none; +} + +#agent-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +#agent-summary { + color: var(--text-muted); + font-size: 12px; +} + +#agent-change-list { + padding-left: 18px; +} + +#agent-change-list li + li { + margin-top: 10px; +} + +.agent-change-time { + display: block; + margin-bottom: 4px; + color: var(--text-muted); + font-size: 12px; +} + +.agent-change-link { + display: block; + width: 100%; + padding: 0; + border: 0; + background: transparent; + text-align: left; + cursor: pointer; +} + +.agent-change-link:hover .agent-change-summary { + color: var(--link-color); +} + +.agent-change-summary { + color: var(--text-color); + font-size: 14px; + line-height: 1.5; +} + +#editor .ProseMirror .agent-change-target { + animation: agent-jump-highlight 1.8s ease; +} + +#error-toast { + position: fixed; + right: 16px; + bottom: 16px; + max-width: min(420px, calc(100vw - 32px)); + padding: 12px 14px; + border-radius: 10px; + background: rgba(120, 25, 25, 0.94); + color: #fff; + font-size: 13px; + line-height: 1.45; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + opacity: 0; + pointer-events: none; + transform: translateY(8px); + transition: opacity 0.18s ease, transform 0.18s ease; + z-index: 20; +} + +#error-toast.visible { + opacity: 1; + transform: translateY(0); +} + #agent-dot.active { background: #e8913a; opacity: 1; @@ -44,11 +163,36 @@ html, body { animation: none; } +/* Agent diff highlight */ +#editor .ProseMirror .agent-diff-added { + background: rgba(63, 185, 80, 0.15); + border-radius: 3px; + transition: background 0.3s ease; +} + +#editor .ProseMirror .agent-diff-fadeout { + animation: agent-diff-fadeout 0.4s ease forwards; +} + +@keyframes agent-diff-fadeout { + 0% { background: rgba(63, 185, 80, 0.25); } + 100% { background: transparent; } +} + @keyframes agent-breathe { 0%, 100% { opacity: 0.45; transform: scale(0.95); } 50% { opacity: 1; transform: scale(1.05); } } +@keyframes agent-jump-highlight { + 0% { + background: color-mix(in srgb, var(--link-color) 22%, transparent); + } + 100% { + background: transparent; + } +} + #editor { height: calc(100vh - 52px); overflow-y: auto;