From b10b3ff7098661124f43030af933ab1438f73615 Mon Sep 17 00:00:00 2001 From: Jiayuan Date: Fri, 16 Jan 2026 00:12:47 +0800 Subject: [PATCH] Add AI code safety defense system: tests, pre-commit hooks, and CI Implemented three-layer defense mechanism to prevent AI-generated code from breaking the codebase: - Layer 1: AGENTS.md with universal AI agent instructions - Layer 2: Husky pre-commit hooks with test file checking - Layer 3: GitHub Actions CI with TypeScript, ESLint, tests, and coverage Includes design document, test infrastructure, and coverage thresholds. Co-Authored-By: Claude Haiku 4.5 --- .github/workflows/ci.yml | 65 ++ .husky/pre-commit | 5 + AGENTS.md | 105 +++ CLAUDE.md | 28 + README.ja.md | 52 +- README.ko.md | 52 +- README.md | 52 +- README.zh-CN.md | 52 +- README.zh-TW.md | 52 +- .../plans/2026-01-15-ai-code-safety-design.md | 124 ++++ ...026-01-15-chatbox-agent-selector-design.md | 12 +- .../2026-01-15-chatbox-agent-selector.md | 100 +-- ...2026-01-15-right-sidebar-directory-tree.md | 78 +-- docs/plans/2026-01-15-testing-strategy.md | 14 +- docs/system-design.md | 609 +++++++++--------- electron.vite.config.ts | 20 +- eslint.config.mjs | 18 +- package.json | 15 +- pnpm-lock.yaml | 387 ++++++++++- scripts/check-tests.ts | 127 ++++ src/main/cli.ts | 86 ++- src/main/conductor/AcpClientFactory.ts | 31 +- src/main/conductor/AgentProcess.ts | 2 +- src/main/conductor/Conductor.ts | 98 +-- src/main/conductor/historyReplay.ts | 14 +- src/main/index.ts | 14 +- src/main/permission/AskUserQuestionHandler.ts | 4 +- src/main/permission/PermissionManager.ts | 24 +- src/main/permission/QuestionToolWorkaround.ts | 8 +- src/main/session/SessionStore.ts | 10 +- src/main/utils/agent-check.ts | 12 +- src/main/utils/agent-install.ts | 15 +- src/main/utils/path.ts | 2 +- src/preload/index.ts | 2 +- src/renderer/src/App.tsx | 8 +- src/renderer/src/components/AgentSelector.tsx | 39 +- src/renderer/src/components/AppSidebar.tsx | 51 +- src/renderer/src/components/ChatView.tsx | 192 +++--- src/renderer/src/components/FileIcons.tsx | 33 +- src/renderer/src/components/FileTree.tsx | 39 +- src/renderer/src/components/MessageInput.tsx | 109 ++-- src/renderer/src/components/Modals.tsx | 16 +- src/renderer/src/components/Settings.tsx | 10 +- src/renderer/src/components/StatusBar.tsx | 16 +- src/renderer/src/components/ToolCallItem.tsx | 78 +-- .../src/components/layout/RightPanel.tsx | 48 +- src/renderer/src/components/layout/index.ts | 2 +- .../AskUserQuestion/AskUserQuestionUI.tsx | 45 +- .../AskUserQuestion/CompletedAnswer.tsx | 2 +- .../AskUserQuestion/CustomInput.tsx | 11 +- .../AskUserQuestion/QuestionOptions.tsx | 2 +- .../permission/StandardPermissionUI.tsx | 6 +- src/renderer/src/components/ui/badge.tsx | 39 +- src/renderer/src/components/ui/button.tsx | 50 +- src/renderer/src/components/ui/card.tsx | 51 +- .../src/components/ui/collapsible.tsx | 22 +- .../src/components/ui/context-menu.tsx | 70 +- src/renderer/src/components/ui/dialog.tsx | 50 +- .../src/components/ui/dropdown-menu.tsx | 26 +- src/renderer/src/components/ui/input.tsx | 12 +- src/renderer/src/components/ui/separator.tsx | 10 +- src/renderer/src/components/ui/sheet.tsx | 65 +- src/renderer/src/components/ui/sidebar.tsx | 346 +++++----- src/renderer/src/components/ui/skeleton.tsx | 6 +- src/renderer/src/components/ui/sonner.tsx | 40 +- src/renderer/src/components/ui/textarea.tsx | 8 +- .../src/components/ui/toggle-group.tsx | 31 +- src/renderer/src/components/ui/toggle.tsx | 29 +- src/renderer/src/components/ui/tooltip.tsx | 16 +- src/renderer/src/hooks/use-mobile.ts | 6 +- src/renderer/src/hooks/useApp.ts | 168 ++--- src/renderer/src/hooks/useResize.ts | 10 +- src/renderer/src/lib/utils.ts | 4 +- src/renderer/src/stores/fileChangeStore.ts | 2 +- src/renderer/src/stores/modalStore.ts | 16 +- src/renderer/src/stores/permissionStore.ts | 58 +- src/renderer/src/stores/uiStore.ts | 6 +- src/renderer/src/styles/index.css | 5 +- src/shared/electron-api.d.ts | 14 +- src/shared/ipc-channels.ts | 2 +- src/shared/tool-names.ts | 2 +- src/shared/types/session.ts | 2 +- tests/integration/conductor/Conductor.test.ts | 20 +- tests/integration/ipc/handlers.test.ts | 34 +- tests/setup/mocks/acp-sdk.ts | 8 +- tests/setup/mocks/electron.ts | 10 +- tests/setup/vitest.setup.ts | 4 +- tests/unit/main/session/SessionStore.test.ts | 44 +- tests/unit/main/utils/agent-check.test.ts | 193 +++--- vitest.config.ts | 15 +- 90 files changed, 2633 insertions(+), 1857 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .husky/pre-commit create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 docs/plans/2026-01-15-ai-code-safety-design.md create mode 100644 scripts/check-tests.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..a5ae40da2d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,65 @@ +name: CI + +on: + push: + branches: ['*'] + pull_request: + branches: [main] + +jobs: + ci: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: TypeScript check + run: pnpm typecheck + + - name: ESLint + # TODO: Reduce --max-warnings to 0 as pre-existing issues are fixed + run: pnpm lint --max-warnings 250 + + - name: Prettier check + run: pnpm format:check + + - name: Run tests with coverage + run: pnpm test:coverage + + - name: Build application + run: pnpm build + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage-report + path: coverage/ + retention-days: 7 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000000..0c4bd6f474 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +# Run the test checker script (advisory only) +npx tsx scripts/check-tests.ts + +# Run lint-staged for formatting and linting +npx lint-staged diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..b42feef3a6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,105 @@ +# AI Agent Guidelines for Multica + +This document provides guidelines for AI coding agents working on this repository. Following these guidelines helps maintain code quality and prevents regressions. + +## Testing Requirements + +When modifying code in this repository, you MUST: + +1. **Write tests for all new functionality** - Every new function, class, or module must have corresponding unit tests + +2. **Update tests when modifying existing code** - If you change behavior, update the related tests to reflect the new behavior + +3. **Run tests before committing** - Execute `pnpm test:run` and ensure all tests pass + +4. **Maintain coverage** - New code should have at least 80% test coverage. The CI enforces a minimum of 50% overall coverage. + +## Test File Conventions + +Follow these conventions for test file locations: + +| Source File | Test File Location | +| -------------------- | -------------------------------- | +| `src/main/**/*.ts` | `tests/unit/main/**/*.test.ts` | +| `src/shared/**/*.ts` | `tests/unit/shared/**/*.test.ts` | +| Integration tests | `tests/integration/**/*.test.ts` | + +### Example + +If you modify `src/main/conductor/Conductor.ts`, ensure tests exist at `tests/unit/main/conductor/Conductor.test.ts` or create them if they don't exist. + +## Before Committing + +Run these commands to verify your changes: + +```bash +# TypeScript must compile without errors +pnpm typecheck + +# No ESLint errors +pnpm lint + +# Code must be properly formatted +pnpm format:check + +# All tests must pass +pnpm test:run + +# (Optional) Check coverage +pnpm test:coverage +``` + +## CI Pipeline + +The following checks run automatically on every pull request: + +1. **TypeScript** - Code must compile without errors +2. **ESLint** - No linting errors allowed +3. **Prettier** - Code must be properly formatted +4. **Tests** - All tests must pass +5. **Coverage** - Minimum 50% code coverage required +6. **Build** - Application must build successfully + +PRs that fail any of these checks should not be merged. + +## Writing Good Tests + +### Unit Tests + +- Test one thing per test case +- Use descriptive test names that explain the expected behavior +- Mock external dependencies (Electron APIs, file system, network) +- Test both success and error cases + +### Example Test Structure + +```typescript +import { describe, it, expect, vi, beforeEach } from 'vitest' + +describe('MyModule', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('myFunction', () => { + it('should return expected result when given valid input', () => { + const result = myFunction('valid input') + expect(result).toBe('expected output') + }) + + it('should throw error when given invalid input', () => { + expect(() => myFunction('')).toThrow('Invalid input') + }) + }) +}) +``` + +## Common Mocks + +The test setup provides mocks for: + +- Electron APIs (`app`, `ipcMain`, `BrowserWindow`, etc.) +- ACP SDK (`@agentclientprotocol/sdk`) +- File system operations + +See `tests/setup/mocks/` for available mocks. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..f7268c2e1d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,28 @@ +# Claude Code Instructions + +This file contains instructions for Claude Code when working on this repository. + +## Important: Read AGENTS.md First + +Before making any code changes, read and follow the guidelines in [AGENTS.md](./AGENTS.md). + +## Key Requirements + +1. **Always write tests** - Every code change must include corresponding unit tests +2. **Run checks before committing** - Use `pnpm typecheck && pnpm lint && pnpm test:run` +3. **Follow test conventions** - See AGENTS.md for test file location patterns + +## Quick Reference + +```bash +# Development +pnpm dev # Start development server +pnpm build # Build for production + +# Quality checks +pnpm typecheck # TypeScript compilation check +pnpm lint # ESLint check +pnpm format:check # Prettier format check +pnpm test:run # Run all tests +pnpm test:coverage # Run tests with coverage report +``` diff --git a/README.ja.md b/README.ja.md index 70bbb26401..50d2f7e9ec 100644 --- a/README.ja.md +++ b/README.ja.md @@ -17,16 +17,19 @@ コーディングエージェント(Claude Code、Codex、Gemini CLIなど)は2025年に非常に強力になり、単なるコード作成をはるかに超えた複雑なタスクを解決できるようになりました。しかし、95%のナレッジワーカーは3つの主要な障壁によってこれらの能力を利用できません: **1. インタラクションのミスマッチ** + - CLIベースのツールは、ターミナルの概念、ファイルパス、環境変数の理解を必要とする - 現在のツールはビジネス成果ではなく、コード出力(差分、コミット、リンティング)に焦点を当てている - ナレッジワーカーが気にするのは結果(チャート、レポート、分析)であり、それらを生成するスクリプトではない **2. ローカル環境の課題** + - Webベースのエージェントはローカルファイル、フォルダ、ネイティブアプリケーションにアクセスできない - Python、Node.js、その他の依存関係のセットアップは大きな障壁となる - すべての依存関係を処理する「すぐに使える」サンドボックス環境がない **3. プライバシーと信頼** + - 機密性の高いビジネスデータ(財務分析、法的文書、医療記録)はサードパーティのサーバーにアップロードできない - データはローカルに保持し、インテリジェンスはクラウドから得るモデルが必要 @@ -42,11 +45,11 @@ Multicaは、データをローカルに保持しながらコーディングエ ## サポートされているエージェント -| エージェント | コマンド | インストール | -|-------|---------|---------| -| [OpenCode](https://github.com/opencode-ai/opencode) | `opencode acp` | `go install github.com/opencode-ai/opencode@latest` | -| [Codex CLI (ACP)](https://github.com/zed-industries/codex-acp) | `codex-acp` | `npm install -g codex-acp` | -| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini acp` | `npm install -g @google/gemini-cli` | +| エージェント | コマンド | インストール | +| -------------------------------------------------------------- | -------------- | --------------------------------------------------- | +| [OpenCode](https://github.com/opencode-ai/opencode) | `opencode acp` | `go install github.com/opencode-ai/opencode@latest` | +| [Codex CLI (ACP)](https://github.com/zed-industries/codex-acp) | `codex-acp` | `npm install -g codex-acp` | +| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini acp` | `npm install -g @google/gemini-cli` | ## クイックスタート @@ -84,20 +87,20 @@ pnpm cli 利用可能なコマンド: -| コマンド | 説明 | -|---------|-------------| -| `/help` | ヘルプを表示 | -| `/new [cwd]` | 新しいセッションを作成(デフォルト:カレントディレクトリ) | -| `/sessions` | すべてのセッションを一覧表示 | -| `/resume ` | IDプレフィックスでセッションを再開 | -| `/delete ` | セッションを削除 | -| `/history` | 現在のセッションのメッセージ履歴を表示 | -| `/agent ` | 別のエージェントに切り替え | -| `/agents` | 利用可能なエージェントを一覧表示 | -| `/doctor` | エージェントのインストール状況を確認 | -| `/status` | 現在のステータスを表示 | -| `/cancel` | 現在のリクエストをキャンセル | -| `/quit` | CLIを終了 | +| コマンド | 説明 | +| --------------- | ---------------------------------------------------------- | +| `/help` | ヘルプを表示 | +| `/new [cwd]` | 新しいセッションを作成(デフォルト:カレントディレクトリ) | +| `/sessions` | すべてのセッションを一覧表示 | +| `/resume ` | IDプレフィックスでセッションを再開 | +| `/delete ` | セッションを削除 | +| `/history` | 現在のセッションのメッセージ履歴を表示 | +| `/agent ` | 別のエージェントに切り替え | +| `/agents` | 利用可能なエージェントを一覧表示 | +| `/doctor` | エージェントのインストール状況を確認 | +| `/status` | 現在のステータスを表示 | +| `/cancel` | 現在のリクエストをキャンセル | +| `/quit` | CLIを終了 | ### ワンショットプロンプト @@ -110,11 +113,11 @@ pnpm cli prompt "ファイルを一覧表示" --cwd=/tmp ### オプション -| オプション | 説明 | -|--------|-------------| -| `--cwd=PATH` | エージェントの作業ディレクトリ | -| `--log` | セッションログを `logs/` ディレクトリに保存 | -| `--log=PATH` | セッションログを指定したファイルに保存 | +| オプション | 説明 | +| ------------ | ------------------------------------------- | +| `--cwd=PATH` | エージェントの作業ディレクトリ | +| `--log` | セッションログを `logs/` ディレクトリに保存 | +| `--log=PATH` | セッションログを指定したファイルに保存 | ## 開発 @@ -169,6 +172,7 @@ MulticaはACPの上に独自のセッションレイヤーを維持します: ``` **主要な設計上の決定:** + - **クライアントサイドストレージ**:MulticaはUI表示用に生の `session/update` データを保存 - **エージェント非依存**:各エージェントは独自の内部状態を個別に管理 - **再開動作**:新しいACPセッションを作成し、保存された履歴をUIに表示 diff --git a/README.ko.md b/README.ko.md index 4ab2e58ab4..0de3cc413e 100644 --- a/README.ko.md +++ b/README.ko.md @@ -17,16 +17,19 @@ 코딩 에이전트(Claude Code, Codex, Gemini CLI 등)는 2025년에 매우 강력해져서 단순한 코드 작성을 훨씬 넘어서는 복잡한 작업을 해결할 수 있게 되었습니다. 그러나 95%의 지식 근로자들은 세 가지 핵심 장벽으로 인해 이러한 기능을 활용하지 못하고 있습니다: **1. 상호작용 불일치** + - CLI 기반 도구는 터미널 개념, 파일 경로, 환경 변수에 대한 이해를 요구함 - 현재 도구들은 비즈니스 결과가 아닌 코드 출력(diff, 커밋, 린팅)에 초점을 맞춤 - 지식 근로자들이 관심 있는 것은 결과(차트, 보고서, 분석)이지, 그것을 생성하는 스크립트가 아님 **2. 로컬 환경 문제** + - 웹 기반 에이전트는 로컬 파일, 폴더 또는 네이티브 애플리케이션에 접근할 수 없음 - Python, Node.js 또는 기타 종속성 설정은 상당한 장벽임 - 모든 종속성을 처리하는 "바로 사용 가능한" 샌드박스 환경이 없음 **3. 개인정보 보호 및 신뢰** + - 민감한 비즈니스 데이터(재무 분석, 법률 문서, 의료 기록)는 타사 서버에 업로드할 수 없음 - 데이터는 로컬에 유지하고 인텔리전스는 클라우드에서 가져오는 모델이 필요함 @@ -42,11 +45,11 @@ Multica는 데이터를 로컬에 유지하면서 코딩 에이전트의 기능 ## 지원되는 에이전트 -| 에이전트 | 명령어 | 설치 | -|-------|---------|---------| -| [OpenCode](https://github.com/opencode-ai/opencode) | `opencode acp` | `go install github.com/opencode-ai/opencode@latest` | -| [Codex CLI (ACP)](https://github.com/zed-industries/codex-acp) | `codex-acp` | `npm install -g codex-acp` | -| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini acp` | `npm install -g @google/gemini-cli` | +| 에이전트 | 명령어 | 설치 | +| -------------------------------------------------------------- | -------------- | --------------------------------------------------- | +| [OpenCode](https://github.com/opencode-ai/opencode) | `opencode acp` | `go install github.com/opencode-ai/opencode@latest` | +| [Codex CLI (ACP)](https://github.com/zed-industries/codex-acp) | `codex-acp` | `npm install -g codex-acp` | +| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini acp` | `npm install -g @google/gemini-cli` | ## 빠른 시작 @@ -84,20 +87,20 @@ pnpm cli 사용 가능한 명령어: -| 명령어 | 설명 | -|---------|-------------| -| `/help` | 도움말 표시 | -| `/new [cwd]` | 새 세션 생성 (기본값: 현재 디렉토리) | -| `/sessions` | 모든 세션 나열 | -| `/resume ` | ID 접두사로 세션 재개 | -| `/delete ` | 세션 삭제 | -| `/history` | 현재 세션의 메시지 기록 표시 | -| `/agent ` | 다른 에이전트로 전환 | -| `/agents` | 사용 가능한 에이전트 나열 | -| `/doctor` | 에이전트 설치 확인 | -| `/status` | 현재 상태 표시 | -| `/cancel` | 현재 요청 취소 | -| `/quit` | CLI 종료 | +| 명령어 | 설명 | +| --------------- | ------------------------------------ | +| `/help` | 도움말 표시 | +| `/new [cwd]` | 새 세션 생성 (기본값: 현재 디렉토리) | +| `/sessions` | 모든 세션 나열 | +| `/resume ` | ID 접두사로 세션 재개 | +| `/delete ` | 세션 삭제 | +| `/history` | 현재 세션의 메시지 기록 표시 | +| `/agent ` | 다른 에이전트로 전환 | +| `/agents` | 사용 가능한 에이전트 나열 | +| `/doctor` | 에이전트 설치 확인 | +| `/status` | 현재 상태 표시 | +| `/cancel` | 현재 요청 취소 | +| `/quit` | CLI 종료 | ### 단일 프롬프트 @@ -110,11 +113,11 @@ pnpm cli prompt "파일 목록" --cwd=/tmp ### 옵션 -| 옵션 | 설명 | -|--------|-------------| -| `--cwd=PATH` | 에이전트의 작업 디렉토리 | -| `--log` | 세션 로그를 `logs/` 디렉토리에 저장 | -| `--log=PATH` | 세션 로그를 지정된 파일에 저장 | +| 옵션 | 설명 | +| ------------ | ----------------------------------- | +| `--cwd=PATH` | 에이전트의 작업 디렉토리 | +| `--log` | 세션 로그를 `logs/` 디렉토리에 저장 | +| `--log=PATH` | 세션 로그를 지정된 파일에 저장 | ## 개발 @@ -169,6 +172,7 @@ Multica는 ACP 위에 자체 세션 레이어를 유지합니다: ``` **주요 설계 결정:** + - **클라이언트 측 저장소**: Multica는 UI 표시를 위해 원시 `session/update` 데이터를 저장 - **에이전트 독립적**: 각 에이전트가 자체 내부 상태를 별도로 관리 - **재개 동작**: 새 ACP 세션을 생성하고 저장된 기록을 UI에 표시 diff --git a/README.md b/README.md index 3d0e1c99d2..ee3a18b3eb 100644 --- a/README.md +++ b/README.md @@ -17,16 +17,19 @@ The name is inspired by [Multics](https://en.wikipedia.org/wiki/Multics) (Multip Coding agents (like Claude Code, Codex, Gemini CLI) have become incredibly powerful in 2025, capable of solving complex tasks far beyond just writing code. However, 95% of knowledge workers are locked out of these capabilities due to three core barriers: **1. Interaction Mismatch** + - CLI-based tools require understanding of terminal concepts, file paths, and environment variables - Current tools focus on code output (diffs, commits, linting) rather than business outcomes - Knowledge workers care about results (charts, reports, analysis), not the scripts that generate them **2. Local Environment Challenges** + - Web-based agents can't access local files, folders, or native applications - Setting up Python, Node.js, or other dependencies is a significant barrier - Missing the "just works" sandbox environment that handles all dependencies **3. Privacy & Trust** + - Sensitive business data (financial analysis, legal documents, medical records) can't be uploaded to third-party servers - Need a model where data stays local while intelligence comes from the cloud @@ -42,11 +45,11 @@ Multica bridges this gap by providing a visual, native desktop interface that le ## Supported Agents -| Agent | Command | Install | -|-------|---------|---------| -| [OpenCode](https://github.com/opencode-ai/opencode) | `opencode acp` | `go install github.com/opencode-ai/opencode@latest` | -| [Codex CLI (ACP)](https://github.com/zed-industries/codex-acp) | `codex-acp` | `npm install -g codex-acp` | -| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini acp` | `npm install -g @google/gemini-cli` | +| Agent | Command | Install | +| -------------------------------------------------------------- | -------------- | --------------------------------------------------- | +| [OpenCode](https://github.com/opencode-ai/opencode) | `opencode acp` | `go install github.com/opencode-ai/opencode@latest` | +| [Codex CLI (ACP)](https://github.com/zed-industries/codex-acp) | `codex-acp` | `npm install -g codex-acp` | +| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini acp` | `npm install -g @google/gemini-cli` | ## Quick Start @@ -84,20 +87,20 @@ pnpm cli Available commands: -| Command | Description | -|---------|-------------| -| `/help` | Show help | -| `/new [cwd]` | Create new session (default: current directory) | -| `/sessions` | List all sessions | -| `/resume ` | Resume session by ID prefix | -| `/delete ` | Delete a session | -| `/history` | Show current session message history | -| `/agent ` | Switch to a different agent | -| `/agents` | List available agents | -| `/doctor` | Check agent installations | -| `/status` | Show current status | -| `/cancel` | Cancel current request | -| `/quit` | Exit CLI | +| Command | Description | +| --------------- | ----------------------------------------------- | +| `/help` | Show help | +| `/new [cwd]` | Create new session (default: current directory) | +| `/sessions` | List all sessions | +| `/resume ` | Resume session by ID prefix | +| `/delete ` | Delete a session | +| `/history` | Show current session message history | +| `/agent ` | Switch to a different agent | +| `/agents` | List available agents | +| `/doctor` | Check agent installations | +| `/status` | Show current status | +| `/cancel` | Cancel current request | +| `/quit` | Exit CLI | ### One-Shot Prompt @@ -110,11 +113,11 @@ pnpm cli prompt "List files" --cwd=/tmp ### Options -| Option | Description | -|--------|-------------| -| `--cwd=PATH` | Working directory for the agent | -| `--log` | Save session log to `logs/` directory | -| `--log=PATH` | Save session log to specified file | +| Option | Description | +| ------------ | ------------------------------------- | +| `--cwd=PATH` | Working directory for the agent | +| `--log` | Save session log to `logs/` directory | +| `--log=PATH` | Save session log to specified file | ## Development @@ -169,6 +172,7 @@ Multica maintains its own session layer on top of ACP: ``` **Key design decisions:** + - **Client-side storage**: Multica stores raw `session/update` data for UI display - **Agent-agnostic**: Each agent manages its own internal state separately - **Resume behavior**: Creates new ACP session, displays stored history in UI diff --git a/README.zh-CN.md b/README.zh-CN.md index a7e48aa28c..c32b730fb4 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -17,16 +17,19 @@ 编程智能体(如 Claude Code、Codex、Gemini CLI)在 2025 年变得极其强大,其能力已经远远超出了单纯的代码编写。然而,95% 的知识工作者因为三个核心障碍而无法使用这些能力: **1. 交互形态的错配** + - 基于命令行的工具需要理解终端概念、文件路径和环境变量 - 现有工具聚焦于代码输出(差异对比、提交、代码检查),而非业务成果 - 知识工作者关心的是结果(图表、报告、分析),而不是生成这些结果的脚本 **2. 本地环境的挑战** + - 基于网页的智能体无法访问本地文件、文件夹或原生应用 - 设置 Python、Node.js 或其他依赖是一个巨大的障碍 - 缺少一个"开箱即用"、处理好所有依赖的沙盒环境 **3. 隐私与信任** + - 敏感的业务数据(财务分析、法律文件、医疗记录)不能上传到第三方服务器 - 需要一种数据留在本地、智能来自云端的模式 @@ -42,11 +45,11 @@ Multica 通过提供可视化的原生桌面界面来弥合这一鸿沟,在保 ## 支持的智能体 -| 智能体 | 命令 | 安装方式 | -|-------|---------|---------| -| [OpenCode](https://github.com/opencode-ai/opencode) | `opencode acp` | `go install github.com/opencode-ai/opencode@latest` | -| [Codex CLI (ACP)](https://github.com/zed-industries/codex-acp) | `codex-acp` | `npm install -g codex-acp` | -| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini acp` | `npm install -g @google/gemini-cli` | +| 智能体 | 命令 | 安装方式 | +| -------------------------------------------------------------- | -------------- | --------------------------------------------------- | +| [OpenCode](https://github.com/opencode-ai/opencode) | `opencode acp` | `go install github.com/opencode-ai/opencode@latest` | +| [Codex CLI (ACP)](https://github.com/zed-industries/codex-acp) | `codex-acp` | `npm install -g codex-acp` | +| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini acp` | `npm install -g @google/gemini-cli` | ## 快速开始 @@ -84,20 +87,20 @@ pnpm cli 可用命令: -| 命令 | 描述 | -|---------|-------------| -| `/help` | 显示帮助 | -| `/new [cwd]` | 创建新会话(默认:当前目录) | -| `/sessions` | 列出所有会话 | -| `/resume ` | 通过 ID 前缀恢复会话 | -| `/delete ` | 删除会话 | -| `/history` | 显示当前会话的消息历史 | -| `/agent ` | 切换到其他智能体 | -| `/agents` | 列出可用智能体 | -| `/doctor` | 检查智能体安装状态 | -| `/status` | 显示当前状态 | -| `/cancel` | 取消当前请求 | -| `/quit` | 退出 CLI | +| 命令 | 描述 | +| --------------- | ---------------------------- | +| `/help` | 显示帮助 | +| `/new [cwd]` | 创建新会话(默认:当前目录) | +| `/sessions` | 列出所有会话 | +| `/resume ` | 通过 ID 前缀恢复会话 | +| `/delete ` | 删除会话 | +| `/history` | 显示当前会话的消息历史 | +| `/agent ` | 切换到其他智能体 | +| `/agents` | 列出可用智能体 | +| `/doctor` | 检查智能体安装状态 | +| `/status` | 显示当前状态 | +| `/cancel` | 取消当前请求 | +| `/quit` | 退出 CLI | ### 单次提问 @@ -110,11 +113,11 @@ pnpm cli prompt "列出文件" --cwd=/tmp ### 选项 -| 选项 | 描述 | -|--------|-------------| -| `--cwd=PATH` | 智能体的工作目录 | -| `--log` | 将会话日志保存到 `logs/` 目录 | -| `--log=PATH` | 将会话日志保存到指定文件 | +| 选项 | 描述 | +| ------------ | ----------------------------- | +| `--cwd=PATH` | 智能体的工作目录 | +| `--log` | 将会话日志保存到 `logs/` 目录 | +| `--log=PATH` | 将会话日志保存到指定文件 | ## 开发 @@ -169,6 +172,7 @@ Multica 在 ACP 之上维护自己的会话层: ``` **关键设计决策:** + - **客户端存储**:Multica 存储原始的 `session/update` 数据用于 UI 展示 - **智能体无关**:每个智能体独立管理自己的内部状态 - **恢复行为**:创建新的 ACP 会话,在 UI 中显示存储的历史记录 diff --git a/README.zh-TW.md b/README.zh-TW.md index 6743d089fe..8f82eeff6d 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -17,16 +17,19 @@ 程式智能體(如 Claude Code、Codex、Gemini CLI)在 2025 年變得極其強大,其能力已經遠遠超出了單純的程式碼編寫。然而,95% 的知識工作者因為三個核心障礙而無法使用這些能力: **1. 互動形態的錯配** + - 基於命令列的工具需要理解終端機概念、檔案路徑和環境變數 - 現有工具聚焦於程式碼輸出(差異比對、提交、程式碼檢查),而非業務成果 - 知識工作者關心的是結果(圖表、報告、分析),而不是產生這些結果的腳本 **2. 本機環境的挑戰** + - 基於網頁的智能體無法存取本機檔案、資料夾或原生應用程式 - 設定 Python、Node.js 或其他相依套件是一個巨大的障礙 - 缺少一個「開箱即用」、處理好所有相依套件的沙盒環境 **3. 隱私與信任** + - 敏感的業務資料(財務分析、法律文件、醫療紀錄)不能上傳到第三方伺服器 - 需要一種資料留在本機、智能來自雲端的模式 @@ -42,11 +45,11 @@ Multica 透過提供視覺化的原生桌面介面來彌合這一鴻溝,在保 ## 支援的智能體 -| 智能體 | 命令 | 安裝方式 | -|-------|---------|---------| -| [OpenCode](https://github.com/opencode-ai/opencode) | `opencode acp` | `go install github.com/opencode-ai/opencode@latest` | -| [Codex CLI (ACP)](https://github.com/zed-industries/codex-acp) | `codex-acp` | `npm install -g codex-acp` | -| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini acp` | `npm install -g @google/gemini-cli` | +| 智能體 | 命令 | 安裝方式 | +| -------------------------------------------------------------- | -------------- | --------------------------------------------------- | +| [OpenCode](https://github.com/opencode-ai/opencode) | `opencode acp` | `go install github.com/opencode-ai/opencode@latest` | +| [Codex CLI (ACP)](https://github.com/zed-industries/codex-acp) | `codex-acp` | `npm install -g codex-acp` | +| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini acp` | `npm install -g @google/gemini-cli` | ## 快速開始 @@ -84,20 +87,20 @@ pnpm cli 可用命令: -| 命令 | 描述 | -|---------|-------------| -| `/help` | 顯示說明 | -| `/new [cwd]` | 建立新工作階段(預設:目前目錄) | -| `/sessions` | 列出所有工作階段 | -| `/resume ` | 透過 ID 前綴恢復工作階段 | -| `/delete ` | 刪除工作階段 | -| `/history` | 顯示目前工作階段的訊息歷史 | -| `/agent ` | 切換到其他智能體 | -| `/agents` | 列出可用智能體 | -| `/doctor` | 檢查智能體安裝狀態 | -| `/status` | 顯示目前狀態 | -| `/cancel` | 取消目前請求 | -| `/quit` | 退出 CLI | +| 命令 | 描述 | +| --------------- | -------------------------------- | +| `/help` | 顯示說明 | +| `/new [cwd]` | 建立新工作階段(預設:目前目錄) | +| `/sessions` | 列出所有工作階段 | +| `/resume ` | 透過 ID 前綴恢復工作階段 | +| `/delete ` | 刪除工作階段 | +| `/history` | 顯示目前工作階段的訊息歷史 | +| `/agent ` | 切換到其他智能體 | +| `/agents` | 列出可用智能體 | +| `/doctor` | 檢查智能體安裝狀態 | +| `/status` | 顯示目前狀態 | +| `/cancel` | 取消目前請求 | +| `/quit` | 退出 CLI | ### 單次提問 @@ -110,11 +113,11 @@ pnpm cli prompt "列出檔案" --cwd=/tmp ### 選項 -| 選項 | 描述 | -|--------|-------------| -| `--cwd=PATH` | 智能體的工作目錄 | -| `--log` | 將工作階段日誌儲存到 `logs/` 目錄 | -| `--log=PATH` | 將工作階段日誌儲存到指定檔案 | +| 選項 | 描述 | +| ------------ | --------------------------------- | +| `--cwd=PATH` | 智能體的工作目錄 | +| `--log` | 將工作階段日誌儲存到 `logs/` 目錄 | +| `--log=PATH` | 將工作階段日誌儲存到指定檔案 | ## 開發 @@ -169,6 +172,7 @@ Multica 在 ACP 之上維護自己的工作階段層: ``` **關鍵設計決策:** + - **客戶端儲存**:Multica 儲存原始的 `session/update` 資料用於 UI 展示 - **智能體無關**:每個智能體獨立管理自己的內部狀態 - **恢復行為**:建立新的 ACP 工作階段,在 UI 中顯示儲存的歷史紀錄 diff --git a/docs/plans/2026-01-15-ai-code-safety-design.md b/docs/plans/2026-01-15-ai-code-safety-design.md new file mode 100644 index 0000000000..8bff6302e3 --- /dev/null +++ b/docs/plans/2026-01-15-ai-code-safety-design.md @@ -0,0 +1,124 @@ +# AI Code Safety System Design + +## Overview + +A three-layer defense mechanism to prevent AI-generated code from breaking the codebase during multi-person collaboration. + +## Problem Statement + +When multiple people collaborate using AI to generate code, changes made by AI in one area can inadvertently break other parts of the codebase. We need a comprehensive defense mechanism to: + +1. Ensure AI always writes tests after modifications +2. Verify all changes pass CI before merging + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ DEFENSE LAYERS │ +├─────────────────────────────────────────────────────────────┤ +│ Layer 1: AI Instructions (AGENTS.md + .claude/) │ +│ ↓ Tells AI agents to write tests │ +├─────────────────────────────────────────────────────────────┤ +│ Layer 2: Pre-commit Hook (husky + custom script) │ +│ ↓ Warns developer if tests are missing │ +├─────────────────────────────────────────────────────────────┤ +│ Layer 3: GitHub Actions CI │ +│ ↓ Blocks merge if any check fails │ +│ - TypeScript compilation │ +│ - ESLint linting │ +│ - Prettier formatting │ +│ - Vitest tests │ +│ - Coverage threshold (50% → increasing) │ +│ - Build verification │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Layer 1: AI Instructions + +### AGENTS.md (Universal) + +A markdown file at the project root that any AI coding agent can reference. Contains: + +- Testing requirements for all code changes +- Test file naming conventions +- Commands to run before committing +- Coverage expectations + +### Claude-specific Config + +`.claude/settings.local.json` with Claude Code-specific behaviors that reference AGENTS.md. + +## Layer 2: Pre-commit Hook + +### Behavior + +1. Detects which files are staged for commit +2. If any `src/main/**` or `src/shared/**` files are modified, checks if corresponding test files are also modified +3. If tests are missing, prints a **warning** (advisory only, does not block) +4. Runs quick TypeScript and ESLint checks on staged files + +### Tools + +- `husky` - Git hooks manager +- `lint-staged` - Run linters on staged files only +- Custom `scripts/check-tests.ts` - Test file checker + +## Layer 3: GitHub Actions CI + +### Triggers + +- Every push to any branch +- Every pull request to `main` + +### Checks + +1. TypeScript compilation (`pnpm typecheck`) +2. ESLint (`pnpm lint`) +3. Prettier format check (`pnpm format:check`) +4. Vitest tests with coverage (`pnpm test:coverage`) +5. Coverage threshold enforcement (50% minimum) +6. Build verification (`pnpm build`) + +### Coverage Strategy + +- Initial threshold: 50% +- Plan to increase incrementally over time (e.g., +5% each quarter) + +## Files to Create + +| File | Purpose | +| ----------------------------- | ------------------------- | +| `AGENTS.md` | Universal AI instructions | +| `.claude/settings.local.json` | Claude Code config | +| `.github/workflows/ci.yml` | GitHub Actions workflow | +| `.husky/pre-commit` | Pre-commit hook | +| `scripts/check-tests.ts` | Test file checker script | + +## Files to Modify + +| File | Change | +| ------------------ | ------------------------------------------ | +| `package.json` | Add husky, lint-staged; add prepare script | +| `vitest.config.ts` | Add coverage thresholds | + +## New npm Scripts + +```json +{ + "prepare": "husky", + "format:check": "prettier --check ." +} +``` + +## Success Criteria + +1. All AI agents receive clear instructions to write tests +2. Developers are warned (not blocked) when committing without tests +3. CI prevents merging code that: + - Has TypeScript errors + - Has ESLint errors + - Has formatting issues + - Fails tests + - Falls below 50% coverage + - Fails to build diff --git a/docs/plans/2026-01-15-chatbox-agent-selector-design.md b/docs/plans/2026-01-15-chatbox-agent-selector-design.md index 535a93a29c..48df37e6b4 100644 --- a/docs/plans/2026-01-15-chatbox-agent-selector-design.md +++ b/docs/plans/2026-01-15-chatbox-agent-selector-design.md @@ -64,12 +64,12 @@ Add to MessageInput.tsx in the bottom toolbar, positioned left of folder indicat ## Edge Cases -| Scenario | Behavior | -|----------|----------| -| No session active | Changes default agent for next session | -| Agent switch fails | Error toast, revert to previous agent | -| All agents unavailable | Disabled selector with tooltip | -| Loading agents | Show spinner instead of chevron | +| Scenario | Behavior | +| ---------------------- | -------------------------------------- | +| No session active | Changes default agent for next session | +| Agent switch fails | Error toast, revert to previous agent | +| All agents unavailable | Disabled selector with tooltip | +| Loading agents | Show spinner instead of chevron | ## Files to Modify diff --git a/docs/plans/2026-01-15-chatbox-agent-selector.md b/docs/plans/2026-01-15-chatbox-agent-selector.md index 11ed86ea8a..c295c88fe6 100644 --- a/docs/plans/2026-01-15-chatbox-agent-selector.md +++ b/docs/plans/2026-01-15-chatbox-agent-selector.md @@ -13,6 +13,7 @@ ### Task 1: Create DropdownMenu UI Component **Files:** + - Create: `src/renderer/src/components/ui/dropdown-menu.tsx` **Step 1: Install Radix UI dropdown-menu package** @@ -23,24 +24,20 @@ Expected: Package added to package.json **Step 2: Create the DropdownMenu component** ```tsx -import * as React from "react" -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" -import { CheckIcon } from "lucide-react" +import * as React from 'react' +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' +import { CheckIcon } from 'lucide-react' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' -function DropdownMenu({ - ...props -}: React.ComponentProps) { +function DropdownMenu({ ...props }: React.ComponentProps) { return } function DropdownMenuTrigger({ ...props }: React.ComponentProps) { - return ( - - ) + return } function DropdownMenuContent({ @@ -54,7 +51,7 @@ function DropdownMenuContent({ data-slot="dropdown-menu-content" sideOffset={sideOffset} className={cn( - "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md", + 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md', className )} {...props} @@ -71,7 +68,7 @@ function DropdownMenuItem({ ) @@ -124,7 +121,7 @@ export { DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, - DropdownMenuSeparator, + DropdownMenuSeparator } ``` @@ -145,6 +142,7 @@ git commit -m "feat: add DropdownMenu UI component" ### Task 2: Add switchSessionAgent to Backend **Files:** + - Modify: `src/main/conductor/Conductor.ts:468-476` (add new method after updateSessionMeta) - Modify: `src/main/ipc/handlers.ts:106-111` (add IPC handler after SESSION_UPDATE) - Modify: `src/shared/ipc-channels.ts` (add new channel) @@ -265,6 +263,7 @@ git commit -m "feat: add switchSessionAgent backend support" ### Task 3: Add switchSessionAgent to useApp Hook **Files:** + - Modify: `src/renderer/src/hooks/useApp.ts:27-41` (add to AppActions interface) - Modify: `src/renderer/src/hooks/useApp.ts:293-297` (add implementation before clearError) - Modify: `src/renderer/src/hooks/useApp.ts:299-317` (add to return object) @@ -282,21 +281,27 @@ switchSessionAgent: (newAgentId: string) => Promise In `src/renderer/src/hooks/useApp.ts`, add before `clearError` (around line 295): ```typescript -const switchSessionAgent = useCallback(async (newAgentId: string) => { - if (!currentSession) { - setError('No active session') - return - } +const switchSessionAgent = useCallback( + async (newAgentId: string) => { + if (!currentSession) { + setError('No active session') + return + } - try { - setError(null) - const updatedSession = await window.electronAPI.switchSessionAgent(currentSession.id, newAgentId) - setCurrentSession(updatedSession) - await loadRunningStatus() - } catch (err) { - setError(`Failed to switch agent: ${err}`) - } -}, [currentSession, loadRunningStatus]) + try { + setError(null) + const updatedSession = await window.electronAPI.switchSessionAgent( + currentSession.id, + newAgentId + ) + setCurrentSession(updatedSession) + await loadRunningStatus() + } catch (err) { + setError(`Failed to switch agent: ${err}`) + } + }, + [currentSession, loadRunningStatus] +) ``` **Step 3: Add to return object** @@ -320,6 +325,7 @@ git commit -m "feat: add switchSessionAgent to useApp hook" ### Task 4: Create AgentSelector Component **Files:** + - Create: `src/renderer/src/components/AgentSelector.tsx` **Step 1: Create the component** @@ -335,7 +341,7 @@ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, - DropdownMenuItem, + DropdownMenuItem } from '@/components/ui/dropdown-menu' import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' @@ -349,7 +355,7 @@ interface AgentSelectorProps { export function AgentSelector({ currentAgentId, onAgentChange, - disabled = false, + disabled = false }: AgentSelectorProps) { const [agents, setAgents] = useState([]) const [loading, setLoading] = useState(true) @@ -386,8 +392,8 @@ export function AgentSelector({ - - {isProcessing ? 'Stop' : 'Send message'} - + {isProcessing ? 'Stop' : 'Send message'} diff --git a/src/renderer/src/components/Modals.tsx b/src/renderer/src/components/Modals.tsx index 388f733bd4..b99cb637f7 100644 --- a/src/renderer/src/components/Modals.tsx +++ b/src/renderer/src/components/Modals.tsx @@ -12,7 +12,7 @@ import { DialogDescription, DialogFooter, DialogHeader, - DialogTitle, + DialogTitle } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -31,7 +31,7 @@ export function Modals({ defaultAgentId, onSetDefaultAgent, onCreateSession, - onDeleteSession, + onDeleteSession }: ModalsProps) { const closeModal = useModalStore((s) => s.closeModal) @@ -43,10 +43,7 @@ export function Modals({ onCreateSession={onCreateSession} onClose={() => closeModal('settings')} /> - closeModal('newSession')} - /> + closeModal('newSession')} /> closeModal('deleteSession')} @@ -63,7 +60,12 @@ interface SettingsModalProps { onClose: () => void } -function SettingsModal({ defaultAgentId, onSetDefaultAgent, onCreateSession, onClose }: SettingsModalProps) { +function SettingsModal({ + defaultAgentId, + onSetDefaultAgent, + onCreateSession, + onClose +}: SettingsModalProps) { const { isOpen, data } = useModal('settings') const handleClose = async () => { diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx index 5dc085e12a..d10b7bd117 100644 --- a/src/renderer/src/components/Settings.tsx +++ b/src/renderer/src/components/Settings.tsx @@ -21,8 +21,8 @@ import opencodeIcon from '../assets/agents/opencode.png' const AGENT_ICONS: Record = { 'claude-code': claudeIcon, - 'codex': openaiIcon, - 'opencode': opencodeIcon, + codex: openaiIcon, + opencode: opencodeIcon } // Icons that need dark mode inversion (monochrome black icons) @@ -33,7 +33,7 @@ interface SettingsProps { onClose: () => void defaultAgentId: string onSetDefaultAgent: (agentId: string) => void - highlightAgent?: string // Agent to highlight (when opened due to missing dependency) + highlightAgent?: string // Agent to highlight (when opened due to missing dependency) } type ThemeMode = 'light' | 'dark' | 'system' @@ -247,7 +247,7 @@ interface AgentItemProps { onSelect: (agentId: string) => void installStatus: InstallStatus onInstall: (agentId: string) => void - isHighlighted?: boolean // When true, auto-expand and highlight this agent + isHighlighted?: boolean // When true, auto-expand and highlight this agent } function AgentItem({ @@ -320,7 +320,7 @@ function AgentItem({ )} diff --git a/src/renderer/src/components/StatusBar.tsx b/src/renderer/src/components/StatusBar.tsx index 6aa683f702..2f2ebaaf0b 100644 --- a/src/renderer/src/components/StatusBar.tsx +++ b/src/renderer/src/components/StatusBar.tsx @@ -15,7 +15,7 @@ interface StatusBarProps { export function StatusBar({ runningSessionsCount, currentSession, - isCurrentSessionRunning, + isCurrentSessionRunning }: StatusBarProps) { const { state, isMobile } = useSidebar() @@ -23,10 +23,12 @@ export function StatusBar({ const needsTrafficLightPadding = state === 'collapsed' || isMobile return ( -
+
{/* Left: Sidebar trigger + Session info */}
@@ -35,9 +37,7 @@ export function StatusBar({ {currentSession.title || currentSession.workingDirectory.split('/').pop()} - - {currentSession.workingDirectory} - + {currentSession.workingDirectory} ) : ( No session selected diff --git a/src/renderer/src/components/ToolCallItem.tsx b/src/renderer/src/components/ToolCallItem.tsx index bfc2be22dd..a44050f144 100644 --- a/src/renderer/src/components/ToolCallItem.tsx +++ b/src/renderer/src/components/ToolCallItem.tsx @@ -12,7 +12,7 @@ import { Bot, ListTodo, Circle, - MessageSquare, + MessageSquare } from 'lucide-react' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' import { cn } from '@/lib/utils' @@ -23,7 +23,7 @@ import { isQuestionTool } from '../../../shared/tool-names' * Persisted response from AskUserQuestion (restored from session updates) */ export interface AnsweredResponse { - optionId?: string // Optional: permission optionId (not needed for display) + optionId?: string // Optional: permission optionId (not needed for display) selectedOption?: string selectedOptions?: string[] customText?: string @@ -35,8 +35,8 @@ export interface ToolCall { title: string status: string kind?: string - toolName?: string // From _meta.claudeCode.toolName, most reliable tool name - rawInput?: Record // Raw input for subtitle display + toolName?: string // From _meta.claudeCode.toolName, most reliable tool name + rawInput?: Record // Raw input for subtitle display input?: string output?: string /** Persisted response for AskUserQuestion (restored from disk on restart) */ @@ -56,7 +56,7 @@ function countLines(output: string | undefined): number { function extractMatchCount(output: string | undefined): number | undefined { if (!output) return undefined // Count non-empty lines as match count - const lines = output.split('\n').filter(line => line.trim()) + const lines = output.split('\n').filter((line) => line.trim()) return lines.length > 0 ? lines.length : undefined } @@ -72,7 +72,7 @@ function getDisplayInfo(toolCall: ToolCall): { icon: ReactNode name: string subtitle?: string - stats?: string // Right-side stats, e.g. "1027 lines" or "51 matches" + stats?: string // Right-side stats, e.g. "1027 lines" or "51 matches" } { const { toolName, kind, title, rawInput, output, status } = toolCall const input = rawInput || {} @@ -84,12 +84,13 @@ function getDisplayInfo(toolCall: ToolCall): { switch (name.toLowerCase() || kind) { case 'read': { const lines = isCompleted ? countLines(output) : 0 - const stats = isCompleted && lines > 0 ? `${lines} lines` : isPending ? 'reading...' : undefined + const stats = + isCompleted && lines > 0 ? `${lines} lines` : isPending ? 'reading...' : undefined return { icon: , name: 'Read', subtitle: formatPath(input.file_path as string), - stats, + stats } } case 'write': { @@ -100,14 +101,14 @@ function getDisplayInfo(toolCall: ToolCall): { icon: , name: 'Write', subtitle: formatPath(input.file_path as string), - stats, + stats } } case 'edit': { return { icon: , name: 'Edit', - subtitle: formatPath(input.file_path as string), + subtitle: formatPath(input.file_path as string) } } case 'bash': @@ -115,38 +116,41 @@ function getDisplayInfo(toolCall: ToolCall): { return { icon: , name: 'Terminal', - subtitle: input.description as string || (input.command as string)?.slice(0, 50), - stats: isPending ? 'running...' : undefined, + subtitle: (input.description as string) || (input.command as string)?.slice(0, 50), + stats: isPending ? 'running...' : undefined } } case 'grep': { const matches = isCompleted ? extractMatchCount(output) : undefined - const stats = matches !== undefined ? `${matches} matches` : isPending ? 'searching...' : undefined + const stats = + matches !== undefined ? `${matches} matches` : isPending ? 'searching...' : undefined return { icon: , name: `grep '${(input.pattern as string)?.slice(0, 20) || '...'}'`, subtitle: formatPath(input.path as string), - stats, + stats } } case 'glob': { const matches = isCompleted ? extractMatchCount(output) : undefined - const stats = matches !== undefined ? `${matches} files` : isPending ? 'searching...' : undefined + const stats = + matches !== undefined ? `${matches} files` : isPending ? 'searching...' : undefined return { icon: , name: 'Glob', subtitle: input.pattern as string, - stats, + stats } } case 'search': { const matches = isCompleted ? extractMatchCount(output) : undefined - const stats = matches !== undefined ? `${matches} matches` : isPending ? 'searching...' : undefined + const stats = + matches !== undefined ? `${matches} matches` : isPending ? 'searching...' : undefined return { icon: , name: 'Search', subtitle: input.pattern as string, - stats, + stats } } case 'websearch': { @@ -154,7 +158,7 @@ function getDisplayInfo(toolCall: ToolCall): { icon: , name: 'Web Search', subtitle: input.query as string, - stats: isPending ? 'searching...' : undefined, + stats: isPending ? 'searching...' : undefined } } case 'webfetch': { @@ -162,15 +166,15 @@ function getDisplayInfo(toolCall: ToolCall): { icon: , name: 'Web Fetch', subtitle: input.url as string, - stats: isPending ? 'fetching...' : undefined, + stats: isPending ? 'fetching...' : undefined } } case 'fetch': { return { icon: , name: 'Web Search', - subtitle: input.query as string || input.url as string, - stats: isPending ? 'searching...' : undefined, + subtitle: (input.query as string) || (input.url as string), + stats: isPending ? 'searching...' : undefined } } case 'task': { @@ -178,7 +182,7 @@ function getDisplayInfo(toolCall: ToolCall): { icon: , name: `${input.subagent_type || 'Task'} Agent`, subtitle: input.description as string, - stats: isPending ? 'working...' : undefined, + stats: isPending ? 'working...' : undefined } } case 'todowrite': { @@ -195,7 +199,7 @@ function getDisplayInfo(toolCall: ToolCall): { return { icon: , name: header, - stats: isPending ? 'waiting...' : undefined, + stats: isPending ? 'waiting...' : undefined } } default: { @@ -212,7 +216,7 @@ function StatusDot({ status }: { status: string }) { running: 'bg-[var(--tool-running)] animate-[glow-pulse_2s_ease-in-out_infinite]', in_progress: 'bg-[var(--tool-running)] animate-[glow-pulse_2s_ease-in-out_infinite]', completed: 'bg-[var(--tool-success)]', - failed: 'bg-[var(--tool-error)]', + failed: 'bg-[var(--tool-error)]' } return ( @@ -239,9 +243,7 @@ function ToolCallDetails({ toolCall }: { toolCall: ToolCall }) { )} {/* Separator */} - {toolCall.input && toolCall.output && ( -
- )} + {toolCall.input && toolCall.output &&
} {/* Output */} {toolCall.output && ( @@ -288,11 +290,12 @@ export function ToolCallItem({ toolCall }: { toolCall: ToolCall }) { // Prefer memory response, fallback to persisted response const response = respondedData?.response || persistedResponse // Handle both single and multi-select answers - const selectedAnswer = response?.selectedOptions?.join(', ') || - response?.selectedOption || - response?.customText || - response?.answers?.[0]?.answer || - 'Answered' + const selectedAnswer = + response?.selectedOptions?.join(', ') || + response?.selectedOption || + response?.customText || + response?.answers?.[0]?.answer || + 'Answered' const questions = (toolCall.rawInput?.questions as Array<{ header?: string }>) || [] const header = questions[0]?.header || 'Question' @@ -311,7 +314,10 @@ export function ToolCallItem({ toolCall }: { toolCall: ToolCall }) { // Use getDisplayInfo to get icon, name, subtitle, and stats const { icon, name, subtitle, stats } = getDisplayInfo(toolCall) - const isPending = toolCall.status === 'pending' || toolCall.status === 'running' || toolCall.status === 'in_progress' + const isPending = + toolCall.status === 'pending' || + toolCall.status === 'running' || + toolCall.status === 'in_progress' return ( @@ -343,9 +349,7 @@ export function ToolCallItem({ toolCall }: { toolCall: ToolCall }) { {/* Subtitle (path, query, etc.) */} {subtitle && ( - - {subtitle} - + {subtitle} )} {/* Stats (line count, match count, etc.) - shown on the right */} diff --git a/src/renderer/src/components/layout/RightPanel.tsx b/src/renderer/src/components/layout/RightPanel.tsx index cecd60aa53..7e02159dfb 100644 --- a/src/renderer/src/components/layout/RightPanel.tsx +++ b/src/renderer/src/components/layout/RightPanel.tsx @@ -6,11 +6,7 @@ import * as React from 'react' import { PanelLeftIcon, PanelRightIcon } from 'lucide-react' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' -import { - useUIStore, - RIGHT_PANEL_MIN_WIDTH, - RIGHT_PANEL_MAX_WIDTH, -} from '@/stores/uiStore' +import { useUIStore, RIGHT_PANEL_MIN_WIDTH, RIGHT_PANEL_MAX_WIDTH } from '@/stores/uiStore' import { useSidebar } from '@/components/ui/sidebar' import { useResize } from '@/hooks/useResize' @@ -29,7 +25,7 @@ export function RightPanel({ children, className }: RightPanelProps) { minWidth: RIGHT_PANEL_MIN_WIDTH, maxWidth: RIGHT_PANEL_MAX_WIDTH, onWidthChange: setRightPanelWidth, - direction: 'left', + direction: 'left' }) return ( @@ -69,10 +65,7 @@ export function RightPanel({ children, className }: RightPanelProps) { } // Trigger button - desktop only, secondary variant when panel is open -export function RightPanelTrigger({ - className, - ...props -}: React.ComponentProps) { +export function RightPanelTrigger({ className, ...props }: React.ComponentProps) { const isOpen = useUIStore((s) => s.rightPanelOpen) const toggle = useUIStore((s) => s.toggleRightPanel) @@ -117,41 +110,18 @@ export function SidebarTrigger({ } // Sub-components for consistent structure -export function RightPanelHeader({ - className, - children, - ...props -}: React.ComponentProps<'div'>) { +export function RightPanelHeader({ className, children, ...props }: React.ComponentProps<'div'>) { return ( -
+
{children}
) } -export function RightPanelContent({ - className, - ...props -}: React.ComponentProps<'div'>) { - return ( -
- ) +export function RightPanelContent({ className, ...props }: React.ComponentProps<'div'>) { + return
} -export function RightPanelFooter({ - className, - ...props -}: React.ComponentProps<'div'>) { - return ( -
- ) +export function RightPanelFooter({ className, ...props }: React.ComponentProps<'div'>) { + return
} diff --git a/src/renderer/src/components/layout/index.ts b/src/renderer/src/components/layout/index.ts index e2441b2cfa..e0e834d652 100644 --- a/src/renderer/src/components/layout/index.ts +++ b/src/renderer/src/components/layout/index.ts @@ -4,5 +4,5 @@ export { RightPanelHeader, RightPanelContent, RightPanelFooter, - SidebarTrigger, + SidebarTrigger } from './RightPanel' diff --git a/src/renderer/src/components/permission/AskUserQuestion/AskUserQuestionUI.tsx b/src/renderer/src/components/permission/AskUserQuestion/AskUserQuestionUI.tsx index 3946ded07f..d31a02b98b 100644 --- a/src/renderer/src/components/permission/AskUserQuestion/AskUserQuestionUI.tsx +++ b/src/renderer/src/components/permission/AskUserQuestion/AskUserQuestionUI.tsx @@ -16,7 +16,11 @@ const OTHER_PATTERNS = ['other', '其他', '另外'] const isOtherOption = (label: string): boolean => OTHER_PATTERNS.some((pattern) => label.toLowerCase().includes(pattern)) -export function AskUserQuestionUI({ request, questions, currentQuestionIndex }: AskUserQuestionUIProps) { +export function AskUserQuestionUI({ + request, + questions, + currentQuestionIndex +}: AskUserQuestionUIProps) { const respondToRequest = usePermissionStore((s) => s.respondToRequest) const answerCurrentQuestion = usePermissionStore((s) => s.answerCurrentQuestion) const [customInput, setCustomInput] = useState('') @@ -68,9 +72,7 @@ export function AskUserQuestionUI({ request, questions, currentQuestionIndex }: // If "Other" is selected and has custom input, replace "Other" with custom text if (hasOtherSelected && customInput.trim()) { - finalOptions = finalOptions.map((opt) => - isOtherOption(opt) ? customInput.trim() : opt - ) + finalOptions = finalOptions.map((opt) => (isOtherOption(opt) ? customInput.trim() : opt)) } // If custom input is filled but "Other" is not selected, add custom input as an option else if (customInput.trim() && !hasOtherSelected) { @@ -140,24 +142,25 @@ export function AskUserQuestionUI({ request, questions, currentQuestionIndex }: /> {/* Submit button for multi-select (after custom input) */} - {isMultiSelect && (() => { - const hasCustom = customInput.trim().length > 0 - const totalCount = selectedOptions.length + (hasCustom && !hasOtherSelected ? 1 : 0) - const canSubmit = totalCount > 0 && !(hasOtherSelected && !hasCustom) + {isMultiSelect && + (() => { + const hasCustom = customInput.trim().length > 0 + const totalCount = selectedOptions.length + (hasCustom && !hasOtherSelected ? 1 : 0) + const canSubmit = totalCount > 0 && !(hasOtherSelected && !hasCustom) - return ( - - ) - })()} + return ( + + ) + })()}
) } diff --git a/src/renderer/src/components/permission/AskUserQuestion/CompletedAnswer.tsx b/src/renderer/src/components/permission/AskUserQuestion/CompletedAnswer.tsx index baa58be94b..79ef4a8546 100644 --- a/src/renderer/src/components/permission/AskUserQuestion/CompletedAnswer.tsx +++ b/src/renderer/src/components/permission/AskUserQuestion/CompletedAnswer.tsx @@ -9,7 +9,7 @@ export function CompletedAnswer({ selectedOption, selectedOptions, customText, - firstQuestionHeader, + firstQuestionHeader }: CompletedAnswerProps) { // Multi-question: simply show "Answered" if (answers && answers.length > 1) { diff --git a/src/renderer/src/components/permission/AskUserQuestion/CustomInput.tsx b/src/renderer/src/components/permission/AskUserQuestion/CustomInput.tsx index 40b435cc52..c73f2ee893 100644 --- a/src/renderer/src/components/permission/AskUserQuestion/CustomInput.tsx +++ b/src/renderer/src/components/permission/AskUserQuestion/CustomInput.tsx @@ -5,7 +5,12 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import type { CustomInputProps } from '../types' -export function CustomInput({ value, onChange, onSubmit, isMultiSelect = false }: CustomInputProps) { +export function CustomInput({ + value, + onChange, + onSubmit, + isMultiSelect = false +}: CustomInputProps) { // In multi-select mode, the Send button is hidden // User should use the Submit button in QuestionOptions instead const handleKeyDown = (e: React.KeyboardEvent) => { @@ -17,7 +22,9 @@ export function CustomInput({ value, onChange, onSubmit, isMultiSelect = false } return (
onChange(e.target.value)} onKeyDown={handleKeyDown} diff --git a/src/renderer/src/components/permission/AskUserQuestion/QuestionOptions.tsx b/src/renderer/src/components/permission/AskUserQuestion/QuestionOptions.tsx index 62e77bd595..82e82b99a4 100644 --- a/src/renderer/src/components/permission/AskUserQuestion/QuestionOptions.tsx +++ b/src/renderer/src/components/permission/AskUserQuestion/QuestionOptions.tsx @@ -10,7 +10,7 @@ export function QuestionOptions({ options, isMultiSelect, selectedOptions, - onOptionClick, + onOptionClick }: QuestionOptionsProps) { return (
diff --git a/src/renderer/src/components/permission/StandardPermissionUI.tsx b/src/renderer/src/components/permission/StandardPermissionUI.tsx index 6b55a3a7e9..d3a065875e 100644 --- a/src/renderer/src/components/permission/StandardPermissionUI.tsx +++ b/src/renderer/src/components/permission/StandardPermissionUI.tsx @@ -81,7 +81,11 @@ export function StandardPermissionUI({ request, isPending }: StandardPermissionU ) : ( // Simple allow/deny <> -