diff --git a/.claude/commands/review-pr-comments.md b/.claude/commands/review-pr-comments.md deleted file mode 100644 index 0a17a1bd..00000000 --- a/.claude/commands/review-pr-comments.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -agent: "agent" -description: "PR のレビューコメントを分析し、対応要否を判断・提案する" -argument-hint: "PR の URL(例: https://github.com/owner/repo/pull/123)" ---- - -# PR レビューコメントの対応判断 - -対象 PR: $ARGUMENTS - -## 手順 - -1. **PR 情報の取得** - - `gh pr view <番号> --repo ` で PR の概要・本文を取得 - - `gh pr diff <番号> --repo ` で差分を取得 - - `gh api repos///pulls/<番号>/comments` でレビューコメント一覧を取得 - - `gh api repos///pulls/<番号>/reviews` でレビュー全体も確認 - -2. **コメントごとの分析** - 各レビューコメントについて以下を行う: - - コメントが指摘しているコードの該当箇所を特定する - - PR の変更意図(コミットログ、PR 本文)と照合する - - 必要に応じて関連するソースコードを読み、文脈を理解する - - 指摘が技術的に正しいか、プロジェクトの規約に合致するかを検証する - -3. **判断基準** - 各コメントを以下の2択で判断する(「今後対応」は選ばない): - - **対応する** — 以下のいずれかに該当する場合: - - バグや論理エラーの正しい指摘である - - セキュリティ上のリスクがある - - 型安全性やエラーハンドリングに明確な不備がある - - パフォーマンス上の実害がある - - プロジェクト規約への明確な違反である - - **対応しない** — 以下のいずれかに該当する場合: - - 指摘が技術的に誤っている、または誤解に基づいている - - 現在の実装で仕様上問題がない - - 好みやスタイルの違いでありプロジェクト規約に反していない - - PR のスコープ外の改善提案である - - 既に別の方法で対処されている - -4. **結果の報告** - コメントごとに以下の形式で報告する: - - ``` - ### コメント #N: [コメントの要約] - **投稿者:** @username - **対象:** `ファイルパス` L行番号 - **指摘内容:** コメントの要点 - **判断:** 対応する / 対応しない - **理由:** 判断の根拠(コードや仕様を引用して具体的に) - **対応案:** (対応する場合のみ)具体的な修正内容 - ``` - -5. **サマリー** - 最後に全コメントの対応判断を一覧表でまとめる: - - | # | 指摘内容 | 判断 | 理由(一言) | - | --- | -------- | --------------------- | ------------ | - | 1 | ... | 対応する / 対応しない | ... | diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..87c0a177 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,5 @@ +{ + "permissions": { + "allow": ["Bash(gh api:*)", "Bash(bun run:*)"] + } +} diff --git a/.cursor/skills/handle-pr-review/SKILL.md b/.cursor/skills/handle-pr-review/SKILL.md index 537db08a..23f21ca4 100644 --- a/.cursor/skills/handle-pr-review/SKILL.md +++ b/.cursor/skills/handle-pr-review/SKILL.md @@ -1,14 +1,23 @@ --- name: handle-pr-review description: > - PR レビューコメントの取得・分析・修正・返信・再レビュー依頼をワンストップで行う。 + PR レビューコメントを仕様に照らして分析し、対応方針を提案・実行する。 + コメントをそのまま受け入れるのではなく、TSDoc/テスト/型定義に基づいて + 妥当性を検証し、修正・代替案・対応不要を判断する。 "レビュー対応して", "PRコメントに対応", "review PR comments", "レビューコメントを確認", "PRのレビューを処理" などで起動する。 + Cursor / Claude Code 共通で使用可能。 --- -# PR レビュー対応 +# PR レビュー対応(仕様駆動) -PR のレビューコメントを取得し、分析・修正・返信・再レビュー依頼まで一気通貫で行う。 +PR のレビューコメントを取得し、**プロジェクトの仕様(TSDoc/JSDoc・テスト・型定義)に照らして妥当性を検証**したうえで、対応方針を提案・実行する。 + +## 基本方針 + +- レビューコメントを**そのまま受け入れない**。仕様と文脈に基づいて判断する。 +- 指摘の問題意識が正しくても、提案された解決策が最適とは限らない。代替案を検討する。 +- 対応不要と判断した場合は、仕様根拠を添えて丁寧に説明する。 ## Step 0: PR の特定 @@ -20,12 +29,21 @@ gh pr list --head "$(git branch --show-current)" --json number,url,title --jq '. セッション中に既に PR を扱っている場合は、その情報を再利用する。 -## Step 1: 未返信コメントの取得 +## Step 1: PR コンテキストの把握 + +レビューコメントを読む**前に**、PR の変更意図と仕様を理解する: + +1. `gh pr view <番号>` で PR の概要・本文を取得 +2. `gh pr diff <番号>` で差分を取得 +3. 変更対象ファイルの TSDoc/JSDoc コメントを読み、仕様上の意図を把握する +4. 関連するテストコードを読み、振る舞いの契約を確認する +5. `AGENTS.md` / `SPECIFICATION_POLICY.md` の方針を参照する + +**この段階で「この PR は何を達成しようとしているか」を明確にする。** -**返信済みのコメントを除外し、未返信のトップレベルコメントを取得する。** -新規チャットでも前回のコンテキスト不要で正しく動作する。 +## Step 2: 未返信コメントの取得 -**注意**: この方式は「未返信」を対象とする。GitHub の `Require conversation resolution before merging` は「未解決のスレッド」をブロックするため、返信済みだが未解決のスレッドは検出されない。マージ可否の完全な判定には `gh pr view --json mergeable,reviewDecision` の確認を併用すること。コメントが 30 件超の場合は `?per_page=100` や `--paginate` でページネーションを指定すること。 +返信済みコメントを除外し、未返信のトップレベルコメントを取得する: ```bash gh api repos/{owner}/{repo}/pulls/{number}/comments \ @@ -42,52 +60,94 @@ gh api repos/{owner}/{repo}/pulls/{number}/comments \ 3. 「未返信」≠「未解決」。返信済みだがスレッドが未解決の場合はこの方式では検出されない 4. `body` は先頭 300 文字に切り詰め、全文が必要なら個別に読む 5. bot の自動コメント(coderabbitai の summary 等)は分析対象外 +6. コメントが 30 件超の場合は `?per_page=100` や `--paginate` でページネーションを指定する + +## Step 3: 仕様に基づくコメント分析 + +各コメントについて以下を行う。 + +### 3.1 指摘箇所の文脈確認 + +- コメントが指摘するコードの該当箇所を読む +- その関数・モジュールの TSDoc/JSDoc を確認し、設計意図を把握する +- 関連するテストがある場合、テストが表現する振る舞いの契約を確認する +- PR の変更意図(Step 1 で把握済み)と照合する -## Step 2: コメント分析 +### 3.2 指摘の妥当性を検証 -各コメントを以下の 2 択で判断する: +各コメントを以下の **3 択** で判断する: -**対応する:** +**A. 修正する** — 以下のいずれかに該当する場合: - バグ・論理エラーの正しい指摘 -- セキュリティリスク -- 型安全性・エラーハンドリングの不備 -- プロジェクト規約への違反 +- セキュリティ上のリスク +- 型安全性・エラーハンドリングの明確な不備 +- パフォーマンス上の実害 +- プロジェクト規約(AGENTS.md)への明確な違反 -**対応しない:** +**B. 代替案で対応する** — 以下のいずれかに該当する場合: -- 技術的に誤った指摘 -- 現実装で問題がない -- ESLint 等の制約で採用不可 -- PR スコープ外の改善提案 +- 指摘の問題意識は正しいが、提案された解決策が仕様や設計意図と合わない +- 提案を取り入れると別の問題(型安全性の低下、テスト破壊等)が生じる +- 同じ問題をより適切に解決する方法がある -分析結果をサマリーテーブルで報告する: +**C. 対応不要** — 以下のいずれかに該当する場合: -| # | ファイル | 指摘 | 判断 | 理由 | -| --- | -------- | ---- | ----------- | ---- | -| 1 | ... | ... | 対応 / 不要 | ... | +- 指摘が技術的に誤っている、または誤解に基づいている +- 現在の実装が仕様上正しい(TSDoc/テストで裏付け可能) +- 好みやスタイルの違いでありプロジェクト規約に反していない +- PR のスコープ外の改善提案 +- 既に別の方法で対処されている +- ESLint 等のツール制約で採用不可 -**ユーザーの承認を得てから Step 3 に進む。** +### 3.3 分析結果の報告 -## Step 3: 修正の実装 +コメントごとに以下の形式で報告する: -承認されたコメントに対して修正を行う: +``` +### コメント #N: [コメントの要約] +**投稿者:** @username +**対象:** `ファイルパス` L行番号 +**指摘内容:** コメントの要点 +**判断:** 修正する / 代替案で対応 / 対応不要 +**仕様根拠:** 判断の裏付け(TSDoc、テスト、型定義、AGENTS.md 等を引用) +**対応案:** 具体的な修正内容 / 代替案の詳細 / 不要の理由 +``` + +最後にサマリーテーブルを提示する: + +| # | ファイル | 指摘 | 判断 | 仕様根拠(一言) | 対応案 | +| --- | -------- | ---- | ----- | ---------------- | ------ | +| 1 | ... | ... | A/B/C | ... | ... | + +**ユーザーの承認を得てから Step 4 に進む。** + +## Step 4: 修正の実装 + +承認された対応方針に基づき修正を行う: 1. 対象ファイルを読み、指摘箇所を確認 -2. 修正を実装 -3. `bun run lint` でエラーがないか確認 -4. 全修正をまとめて 1 コミット: +2. 修正を実装(代替案の場合はその方法で) +3. 既存のテストが通ることを確認する +4. `bun run lint` でエラーがないか確認 +5. 全修正をまとめて 1 コミット: ```text fix: address PR #{number} review comments ``` -## Step 4: 返信の投稿 +## Step 5: 返信の投稿 + +各コメントに対する返信を作成し、投稿前にテーブルで一覧表示する: + +| # | コメント要約 | 対応 | 返信内容 | +| --- | ------------ | ----- | -------- | +| 1 | ... | A/B/C | ... | -各コメントに対して返信を作成し、投稿前にテーブルで一覧表示する: +返信の書き方: -| # | コメント要約 | 対応 | 返信内容 | -| --- | ------------ | ----------- | -------- | -| 1 | ... | 修正 / 不要 | ... | +- **修正する場合**: 何を修正したか簡潔に伝える +- **代替案の場合**: 指摘の問題意識に同意しつつ、別の方法を選んだ理由を仕様根拠とともに説明する +- **対応不要の場合**: 丁寧に理由を説明し、仕様やテストの根拠を示す **ユーザーが承認するまで投稿しない。** @@ -98,24 +158,25 @@ gh api -X POST repos/{owner}/{repo}/pulls/{number}/comments/{comment_id}/replies -f body="返信内容" ``` -## Step 5: 再レビュー依頼 +## Step 6: 再レビュー依頼 修正コミットを push し、再レビューを依頼する。 ### 対象 PR が develop → main(リリース PR)の場合 -**develop に直接 push できない場合は、作業ブランチを作成し develop 向け PR を出す。** +develop に直接 push できない場合は、作業ブランチを作成し develop 向け PR を出す: -1. 修正をコミットした状態で、main や develop とは別の作業ブランチを作成する: +1. 修正をコミットした状態で作業ブランチを作成: ```bash git checkout -b fix/pr-{number}-review-comment ``` -2. そのブランチを push し、**develop** をベースに PR を作成する: +2. push して develop ベースの PR を作成: ```bash git push -u origin fix/pr-{number}-review-comment - gh pr create --base develop --head fix/pr-{number}-review-comment --title "fix: address PR #{number} review comments" --body "..." + gh pr create --base develop --head fix/pr-{number}-review-comment \ + --title "fix: address PR #{number} review comments" --body "..." ``` -3. 元のリリース PR(#311 など)には「レビュー対応は別 PR で develop に出す予定です」などとコメントし、必要ならその PR の再レビュー依頼は develop にマージ後に行う。 +3. 元のリリース PR にはコメントで報告する。 ### 通常(feature ブランチなど)の場合 diff --git a/.env.example b/.env.example index 6a62aed3..3098b4d8 100644 --- a/.env.example +++ b/.env.example @@ -29,6 +29,10 @@ POLAR_PRO_YEARLY_PRODUCT_ID=YOUR_POLAR_YEARLY_PRODUCT_ID # For Chrome extension: add chrome-extension:// (get ID from chrome://extensions). # CORS_ORIGIN= +# GitHub MCP Server (Claude Code): used by .mcp.json for GitHub integration +# Create a PAT at https://github.com/settings/tokens with repo scope. +# GITHUB_PERSONAL_ACCESS_TOKEN= + # Docker Compose (docker-compose.dev.yml) # Override defaults for local dev; required in shared/production. Do not commit real secrets. # POSTGRES_USER=zedi diff --git a/.gitignore b/.gitignore index ecde627d..92490a92 100644 --- a/.gitignore +++ b/.gitignore @@ -58,5 +58,11 @@ test-results/ # TypeScript build info *.tsbuildinfo +# Tauri +src-tauri/target/ +src-tauri/gen/ +# Sidecar executable for externalBin (build with `bun run sidecar:build` or `tauri:dev` ensure step) +src-tauri/binaries/claude-sidecar* + # Local-only notes (not tracked). See AGENTS.md / SPECIFICATION_POLICY.md. /docs/ diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 00000000..4eee0f05 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "github": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}" + } + } + } +} diff --git a/AGENTS.md b/AGENTS.md index 4eb987af..caac60c1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,39 +71,9 @@ bun run test:run # Vitest 単体テスト ## PR レビューコメント対応フロー -レビューコメントへの対応は以下の手順で行う。 +レビューコメントへの対応は [`.cursor/skills/handle-pr-review/SKILL.md`](.cursor/skills/handle-pr-review/SKILL.md) の手順に従う(Cursor / Claude Code 共通)。 -### 1. 未対応コメントの取得 - -**注意**: 以下は「未返信」のトップレベルコメントを取得する方式。GitHub の `Require conversation resolution before merging` は「未解決のスレッド」をブロックするため、返信済みだが未解決のスレッドはこの方式では検出されない。マージ可否の完全な判定には、PR の `mergeable` 状態や `reviewDecision` の確認を併用すること。 - -返信済みコメントを除外し、未返信のトップレベルコメントを取得する(新規セッションでも動作する): - -```bash -gh api repos/{owner}/{repo}/pulls/{number}/comments \ - --jq ' - [.[] | select(.in_reply_to_id != null) | .in_reply_to_id] as $replied | - [.[] | select(.in_reply_to_id == null and (.id | IN($replied[]) | not))] - | .[] | {id, path, line, body: (.body | .[0:300]), user: .user.login}' -``` - -コメントが 30 件を超える場合は `?per_page=100` や `--paginate` でページネーションを指定すること。 - -### 2. PR の自動検出 - -ブランチ名から PR を特定する: - -```bash -gh pr list --head "$(git branch --show-current)" --json number,url --jq '.[0]' -``` - -### 3. 再レビュー依頼 - -```bash -gh pr comment {number} --body "レビューコメントへの対応をコミットしました。最新の変更に対する再レビューをお願いします。 - -@coderabbitai review" -``` +**基本方針**: コメントをそのまま受け入れるのではなく、TSDoc/テスト/型定義に照らして妥当性を検証し、「修正する / 代替案で対応 / 対応不要」の 3 択で判断する。対応不要の場合は仕様根拠を添えて丁寧に説明する。 ## ディレクトリ構成 diff --git a/README.md b/README.md index c6fc0a85..53abd756 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,36 @@ Docker で複数インスタンスを動かす場合は、ポートをずらし > **Note:** Dockerを使う場合、最低8GBのRAM(推奨: 16GB以上)が必要です。軽量な並列開発が必要な場合は、環境変数によるポート設定の方が適しています。 +### デスクトップアプリ(Tauri) + +Zedi は [Tauri 2.0](https://v2.tauri.app/) によるデスクトップアプリとしても起動できます。 + +#### 前提条件(Desktop) + +- [Rust](https://www.rust-lang.org/tools/install) (rustup で stable を導入) +- **Windows**: Microsoft C++ Build Tools + Windows 11 SDK +- **macOS**: Xcode Command Line Tools (`xcode-select --install`) +- **Linux**: `libwebkit2gtk-4.1-dev`, `build-essential`, `libssl-dev` 等([詳細](https://v2.tauri.app/guides/prerequisites/)) + +#### デスクトップアプリの起動 + +```bash +# 開発モード(Vite dev server + Tauri WebView) +bun run tauri:dev + +# プロダクションビルド(インストーラー生成) +bun run tauri:build +``` + +- **Claude Code sidecar** ([Issue #456](https://github.com/otomatty/zedi/issues/456)): Tauri bundles a compiled helper under `src-tauri/binaries/` (`externalBin`). On first `bun run tauri:dev`, a missing binary is built automatically; run `bun run sidecar:build` manually if needed. / `externalBin` 用の sidecar は `src-tauri/binaries/` に配置する。初回 `tauri:dev` で自動ビルド、手動は `bun run sidecar:build`。 + +> **Windows + Git Bash の場合**: MSVC のビルドツールが PATH に含まれていない場合、 +> Developer Command Prompt for VS 2022 から実行するか、 +> `.bashrc` に `LIB`, `INCLUDE`, `PATH` を設定してください。 +> 詳細は [Issue #49](https://github.com/otomatty/zedi/issues/49) を参照。 +> **Note**: デスクトップ版は現在 Phase D(開発中)です。ストレージは暫定的に IndexedDB を使用しており、 +> Tauri 固有のストレージ (SQLite) は [#50](https://github.com/otomatty/zedi/issues/50) で対応予定です。 + ### 環境変数の設定(オプション) AI 機能・認証・API 連携を使う場合は、`.env.local` を作成してください。サンプルは [.env.example](.env.example) を参照してください。 @@ -211,8 +241,9 @@ VITE_ZEDI_API_BASE_URL=https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.c | Category | Technology | | -------------- | --------------------------------------------------------- | -| **Frontend** | React 18 + TypeScript | +| **Frontend** | React 19 + TypeScript | | **Build Tool** | Vite | +| **Desktop** | Tauri 2.0 (Rust) | | **Editor** | Tiptap (ProseMirror) | | **Styling** | Tailwind CSS + shadcn/ui | | **State** | Zustand + TanStack Query | @@ -249,8 +280,6 @@ VITE_ZEDI_API_BASE_URL=https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.c - [ ] iOS / Android モバイルアプリ - [ ] Share Sheet 連携 - [ ] Magic Split / Flick-to-Split -- [ ] Ghost Link System -- [ ] Semantic Search(ベクトル検索) ロードマップの詳細は Issue / Discussions を参照してください。 @@ -280,7 +309,7 @@ bun run test:mutation ## 📁 Project Structure ``` -src/ +src/ # フロントエンド(React) ├── components/ # React コンポーネント │ ├── editor/ # エディタ関連 │ ├── page/ # ページ表示関連 @@ -288,10 +317,25 @@ src/ │ ├── layout/ # レイアウト │ └── ui/ # shadcn/ui コンポーネント ├── hooks/ # カスタムフック -├── lib/ # ユーティリティ +├── lib/ # ユーティリティ(`claudeCode/` = Tauri↔Claude Code ブリッジ) ├── pages/ # ページコンポーネント ├── stores/ # Zustand ストア └── types/ # TypeScript 型定義 + +src-tauri/ # Tauri デスクトップバックエンド(Rust) +├── src/ +│ ├── main.rs # デスクトップ エントリポイント +│ ├── lib.rs # Tauri アプリ本体・Commands 定義 +│ └── claude_sidecar.rs # Claude Code sidecar プロセス管理(Issue #456) +├── binaries/ # externalBin(`bun run sidecar:build`、gitignore) +├── capabilities/ # セキュリティ権限定義 +├── icons/ # アプリアイコン(各 OS 用) +├── Cargo.toml # Rust 依存管理 +└── tauri.conf.json # Tauri 設定 + +server/ +├── api/ # API サーバー(Hono on Bun) +└── hocuspocus/ # リアルタイム同期サーバー(Y.js) ``` --- diff --git a/bun.lock b/bun.lock index 43e3999d..058fa491 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "vite_react_shadcn_ts", "dependencies": { - "@anthropic-ai/sdk": "^0.80.0", + "@anthropic-ai/sdk": "^0.82.0", "@dagrejs/dagre": "^3.0.0", "@google/genai": "^1.34.0", "@google/generative-ai": "^0.24.1", @@ -41,6 +41,11 @@ "@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-visually-hidden": "^1.2.3", "@tanstack/react-query": "^5.83.0", + "@tauri-apps/api": "^2.10.1", + "@tauri-apps/plugin-dialog": "^2", + "@tauri-apps/plugin-fs": "^2", + "@tauri-apps/plugin-shell": "^2.3.5", + "@tauri-apps/plugin-store": "^2.4.2", "@tiptap/core": "^3.20.0", "@tiptap/extension-bubble-menu": "^3.20.0", "@tiptap/extension-code-block-lowlight": "^3.20.0", @@ -76,6 +81,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", + "dompurify": "^3.3.3", "embla-carousel-react": "^8.6.0", "highlight.js": "^11.11.1", "i18next": "^26.0.1", @@ -125,6 +131,7 @@ "@stryker-mutator/vitest-runner": "^9.6.0", "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/typography": "^0.5.16", + "@tauri-apps/cli": "^2.10.1", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.1", @@ -192,6 +199,17 @@ "vitest": "^4.0.16", }, }, + "packages/claude-sidecar": { + "name": "@zedi/claude-sidecar", + "version": "0.1.0", + "dependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.2.90", + }, + "devDependencies": { + "typescript": "^6.0.2", + "vitest": "^4.0.16", + }, + }, "packages/ui": { "name": "@zedi/ui", "version": "0.1.0", @@ -252,7 +270,9 @@ "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.80.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-WeXLn7zNVk3yjeshn+xZHvld6AoFUOR3Sep6pSoHho5YbSi6HwcirqgPA5ccFuW8QTVJAAU7N8uQQC6Wa9TG+g=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.90", "", { "dependencies": { "@anthropic-ai/sdk": "^0.74.0", "@modelcontextprotocol/sdk": "^1.27.1" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-up5bK0pUbthKIZtNE18WDrIYi0KNpZUhdgjGbkfH/mFQJxI6W/uE3mTiLrCX3UF0SqNl0fMtojBTZPJr2b3O4g=="], + + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.82.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-xdHTjL1GlUlDugHq/I47qdOKp/ROPvuHl7ROJCgUQigbvPu7asf9KcAcU1EqdrP2LuVhEKaTs7Z+ShpZDRzHdQ=="], "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.2.6" } }, "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw=="], @@ -532,6 +552,8 @@ "@hocuspocus/provider": ["@hocuspocus/provider@3.4.4", "", { "dependencies": { "@hocuspocus/common": "^3.4.4", "@lifeomic/attempt": "^3.0.2", "lib0": "^0.2.87", "ws": "^8.17.1" }, "peerDependencies": { "y-protocols": "^1.0.6", "yjs": "^13.6.8" } }, "sha512-KbsMAfdYcIJD8eMU/5QnpXcSOvIWAcCNI33FSRSaKCIpYBFtAwkYIwWnZJmPZ8a1BMAtqQc+uvy9+UQf7GHnGQ=="], + "@hono/node-server": ["@hono/node-server@1.19.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw=="], + "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -676,6 +698,8 @@ "@microsoft/tsdoc-config": ["@microsoft/tsdoc-config@0.18.1", "", { "dependencies": { "@microsoft/tsdoc": "0.16.0", "ajv": "~8.18.0", "jju": "~1.4.0", "resolve": "~1.22.2" } }, "sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + "@mozilla/readability": ["@mozilla/readability@0.6.0", "", {}, "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], @@ -1070,6 +1094,40 @@ "@tanstack/react-query": ["@tanstack/react-query@5.83.0", "", { "dependencies": { "@tanstack/query-core": "5.83.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ=="], + "@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="], + + "@tauri-apps/cli": ["@tauri-apps/cli@2.10.1", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.10.1", "@tauri-apps/cli-darwin-x64": "2.10.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", "@tauri-apps/cli-linux-arm64-musl": "2.10.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-musl": "2.10.1", "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", "@tauri-apps/cli-win32-x64-msvc": "2.10.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g=="], + + "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.10.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ=="], + + "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.10.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw=="], + + "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.10.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w=="], + + "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA=="], + + "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg=="], + + "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.10.1", "", { "os": "linux", "cpu": "none" }, "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw=="], + + "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw=="], + + "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ=="], + + "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.10.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg=="], + + "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.10.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw=="], + + "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg=="], + + "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.6.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg=="], + + "@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.5.0", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-c83kbz61AK+rKjhS+je9+stIO27nXj7p9cqeg36TwkIUtxpCFTttlHHtqon6h6FN54cXjyAjlMPOJcW3mwE5XQ=="], + + "@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.5", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg=="], + + "@tauri-apps/plugin-store": ["@tauri-apps/plugin-store@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-0ClHS50Oq9HEvLPhNzTNFxbWVOqoAp3dRvtewQBeqfIQ0z5m3JRnOISIn2ZVPCrQC0MyGyhTS9DWhHjpigQE7A=="], + "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], @@ -1334,8 +1392,12 @@ "@xyflow/system": ["@xyflow/system@0.0.75", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ=="], + "@zedi/claude-sidecar": ["@zedi/claude-sidecar@workspace:packages/claude-sidecar"], + "@zedi/ui": ["@zedi/ui@workspace:packages/ui"], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -1344,6 +1406,8 @@ "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "angular-html-parser": ["angular-html-parser@10.4.0", "", {}, "sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -1408,6 +1472,8 @@ "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + "brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -1420,6 +1486,8 @@ "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -1482,6 +1550,10 @@ "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "conventional-changelog-angular": ["conventional-changelog-angular@8.1.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-GGf2Nipn1RUCAktxuVauVr1e3r8QrLP/B0lEUsFktmGqc3ddbQkhoJZHJctVU829U1c6mTSWftrVOCHaL85Q3w=="], "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@9.1.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-MnbEysR8wWa8dAEvbj5xcBgJKQlX/m0lhS8DsyAAWDHdfs2faDJxTgzRYlRYpXSe7UiKrIIlB4TrBKU9q9DgkA=="], @@ -1492,6 +1564,10 @@ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="], @@ -1622,6 +1698,8 @@ "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "des.js": ["des.js@1.1.0", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg=="], @@ -1642,7 +1720,7 @@ "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], - "dompurify": ["dompurify@3.3.1", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q=="], + "dompurify": ["dompurify@3.3.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="], "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], @@ -1656,6 +1734,8 @@ "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "electron-to-chromium": ["electron-to-chromium@1.5.192", "", {}, "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg=="], "embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="], @@ -1666,6 +1746,8 @@ "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="], "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], @@ -1702,6 +1784,8 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], @@ -1736,12 +1820,22 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@8.3.2", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -1776,6 +1870,8 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], @@ -1790,6 +1886,10 @@ "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -1876,6 +1976,8 @@ "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], + "hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="], + "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="], @@ -1886,6 +1988,8 @@ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], @@ -1896,7 +2000,7 @@ "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw=="], - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "idb-keyval": ["idb-keyval@6.2.2", "", {}, "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg=="], @@ -1924,6 +2028,10 @@ "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], @@ -1978,6 +2086,8 @@ "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], @@ -2048,6 +2158,8 @@ "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], @@ -2198,8 +2310,12 @@ "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "meow": ["meow@12.1.1", "", {}, "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw=="], + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "mermaid": ["mermaid@11.12.2", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^0.6.3", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.21", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w=="], @@ -2262,6 +2378,10 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], "miniflare": ["miniflare@4.20260301.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260301.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-fqkHx0QMKswRH9uqQQQOU/RoaS3Wjckxy3CUX3YGJr0ZIMu7ObvI+NovdYi6RIsSPthNtq+3TPmRNxjeRiasog=="], @@ -2296,6 +2416,8 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], @@ -2330,6 +2452,10 @@ "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "openai": ["openai@6.15.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-F1Lvs5BoVvmZtzkUEVyh8mDQPPFolq4F+xdsx/DO8Hee8YF3IGAlZqUIsF+DVGhqf4aU0a3bTghsxB6OIsRy1g=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -2364,6 +2490,8 @@ "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], @@ -2402,6 +2530,8 @@ "pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="], + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], "playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="], @@ -2490,6 +2620,8 @@ "prosemirror-view": ["prosemirror-view@1.41.4", "", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], @@ -2498,6 +2630,10 @@ "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], "react-day-picker": ["react-day-picker@9.14.0", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "@tabby_ai/hijri-converter": "1.0.5", "date-fns": "^4.1.0", "date-fns-jalali": "4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA=="], @@ -2586,6 +2722,8 @@ "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], @@ -2612,6 +2750,10 @@ "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], @@ -2620,6 +2762,8 @@ "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -2662,6 +2806,8 @@ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], @@ -2736,6 +2882,8 @@ "to-valid-identifier": ["to-valid-identifier@1.0.0", "", { "dependencies": { "@sindresorhus/base62": "^1.0.0", "reserved-identifiers": "^1.0.0" } }, "sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="], "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], @@ -2764,6 +2912,8 @@ "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], @@ -2812,6 +2962,8 @@ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -2826,6 +2978,8 @@ "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], @@ -2894,6 +3048,8 @@ "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], @@ -2932,12 +3088,16 @@ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], "zustand": ["zustand@5.0.9", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["immer"] }, "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@anthropic-ai/claude-agent-sdk/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.74.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-srbJV7JKsc5cQ6eVuFzjZO7UR3xEPJqPamHFIe29bs38Ij2IripoAhC0S5NslNbaFUYqBKypmmpzMTpqfHEUDw=="], + "@asamuzakjp/css-color/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], "@babel/core/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], @@ -3016,8 +3176,6 @@ "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], - "@inquirer/external-editor/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], @@ -3142,6 +3300,8 @@ "ast-v8-to-istanbul/js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], + "body-parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "browserslist/caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="], "bun-types/@types/node": ["@types/node@22.16.5", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ=="], @@ -3166,6 +3326,8 @@ "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + "d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], @@ -3192,8 +3354,14 @@ "estree-walker/@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], + "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "express/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "finalhandler/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "happy-dom/@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], @@ -3220,6 +3388,8 @@ "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "mermaid/dompurify": ["dompurify@3.3.1", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q=="], + "mermaid/katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="], "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], @@ -3264,6 +3434,12 @@ "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.9", "", {}, "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw=="], + "router/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + + "send/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], diff --git a/knip.json b/knip.json index ad5392a2..c5b8c68b 100644 --- a/knip.json +++ b/knip.json @@ -8,7 +8,12 @@ "ignoreBinaries": ["docker-compose"], "workspaces": { ".": { - "entry": ["scripts/**/*.ts", "e2e/**/*.ts"], + "entry": [ + "scripts/**/*.ts", + "e2e/**/*.ts", + "src/lib/claudeCode/index.ts", + "src/lib/agentSlashCommands/index.ts" + ], "project": ["src/**/*.{ts,tsx}", "scripts/**/*.ts", "e2e/**/*.{ts,tsx}"] }, "admin": { @@ -17,6 +22,9 @@ "packages/ui": { "project": ["src/**/*.{ts,tsx}"] }, + "packages/claude-sidecar": { + "project": ["src/**/*.ts"] + }, "server/api": { "entry": ["scripts/**/*.ts"], "project": ["src/**/*.ts", "scripts/**/*.ts"] @@ -82,7 +90,9 @@ "@types/sql.js", "@types/uuid", "@vitejs/plugin-react", - "wrangler" + "wrangler", + "@tauri-apps/plugin-shell", + "@tauri-apps/plugin-store" ], "ignoreExportsUsedInFile": { "interface": true, diff --git a/package.json b/package.json index d362962f..98426ef4 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "format:check": "prettier --check .", "preview": "vite preview", "test": "vitest", - "test:run": "vitest run && vitest run --config server/hocuspocus/vitest.config.ts", + "test:run": "vitest run && vitest run --config packages/claude-sidecar/vitest.config.ts && vitest run --config server/hocuspocus/vitest.config.ts", "test:coverage": "vitest run --coverage", "test:ui": "vitest --ui", "test:e2e": "playwright test", @@ -58,6 +58,10 @@ "prepare:extension:prod": "node scripts/prepare-extension.js prod", "build:extension": "npm run prepare:extension:prod", "build:extension:dev": "npm run prepare:extension:dev", + "sidecar:build": "node scripts/build-claude-sidecar.mjs", + "tauri": "tauri", + "tauri:dev": "node scripts/ensure-claude-sidecar-bin.mjs && tauri dev", + "tauri:build": "bun run sidecar:build && tauri build", "prepare": "husky" }, "lint-staged": { @@ -78,7 +82,7 @@ "bun": ">=1.0.0" }, "dependencies": { - "@anthropic-ai/sdk": "^0.80.0", + "@anthropic-ai/sdk": "^0.82.0", "@dagrejs/dagre": "^3.0.0", "@google/genai": "^1.34.0", "@google/generative-ai": "^0.24.1", @@ -114,6 +118,11 @@ "@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-visually-hidden": "^1.2.3", "@tanstack/react-query": "^5.83.0", + "@tauri-apps/api": "^2.10.1", + "@tauri-apps/plugin-dialog": "^2", + "@tauri-apps/plugin-fs": "^2", + "@tauri-apps/plugin-shell": "^2.3.5", + "@tauri-apps/plugin-store": "^2.4.2", "@tiptap/core": "^3.20.0", "@tiptap/extension-bubble-menu": "^3.20.0", "@tiptap/extension-code-block-lowlight": "^3.20.0", @@ -149,6 +158,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", + "dompurify": "^3.3.3", "embla-carousel-react": "^8.6.0", "highlight.js": "^11.11.1", "i18next": "^26.0.1", @@ -198,6 +208,7 @@ "@stryker-mutator/vitest-runner": "^9.6.0", "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/typography": "^0.5.16", + "@tauri-apps/cli": "^2.10.1", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.1", diff --git a/packages/claude-sidecar/package.json b/packages/claude-sidecar/package.json new file mode 100644 index 00000000..a2ab801a --- /dev/null +++ b/packages/claude-sidecar/package.json @@ -0,0 +1,20 @@ +{ + "name": "@zedi/claude-sidecar", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "JSONL stdin/stdout bridge between Zedi Tauri and @anthropic-ai/claude-agent-sdk. / Tauri と Claude Agent SDK をつなぐ JSONL ブリッジ。", + "main": "src/index.ts", + "scripts": { + "start": "bun run src/index.ts", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.2.90" + }, + "devDependencies": { + "typescript": "^6.0.2", + "vitest": "^4.0.16" + } +} diff --git a/packages/claude-sidecar/src/handlers/installation.ts b/packages/claude-sidecar/src/handlers/installation.ts new file mode 100644 index 00000000..57bb7ab8 --- /dev/null +++ b/packages/claude-sidecar/src/handlers/installation.ts @@ -0,0 +1,44 @@ +/** + * Detects whether the `claude` CLI is on PATH and returns `--version` output when possible. + * `claude` CLI が PATH にあり `--version` を取得できるか検査する。 + */ + +/** Outcome of a Claude CLI presence check. / Claude CLI 存在確認の結果 */ +export interface InstallationCheckResult { + installed: boolean; + version?: string; +} + +const CLAUDE = process.platform === "win32" ? "claude.exe" : "claude"; + +/** argv for `claude --version` (Windows uses `cmd /c` for npm-style shims). / Windows は npm 系シム解決のため cmd 経由 */ +function claudeVersionArgv(): string[] { + if (process.platform === "win32") { + return ["cmd.exe", "/c", "claude", "--version"]; + } + return [CLAUDE, "--version"]; +} + +/** + * Runs `claude --version` (or `claude.exe` on Windows). + * `claude --version` を実行する(Windows は `claude.exe`)。 + */ +export async function checkClaudeInstallation(): Promise { + try { + const proc = Bun.spawn(claudeVersionArgv(), { + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + }); + const stdoutText = await new Response(proc.stdout).text(); + const stderrText = await new Response(proc.stderr).text(); + const exitCode = await proc.exited; + if (exitCode === 0) { + const version = stdoutText.trim() || stderrText.trim() || undefined; + return { installed: true, version }; + } + } catch { + /* not on PATH or spawn failed */ + } + return { installed: false }; +} diff --git a/packages/claude-sidecar/src/handlers/models.ts b/packages/claude-sidecar/src/handlers/models.ts new file mode 100644 index 00000000..0c5daa1a --- /dev/null +++ b/packages/claude-sidecar/src/handlers/models.ts @@ -0,0 +1,40 @@ +/** + * Lists available Claude models via the SDK Query control API. + * SDK の Query コントロール API 経由で利用可能な Claude モデル一覧を取得する。 + */ + +import { query } from "@anthropic-ai/claude-agent-sdk"; + +/** + * SDK の initializationResult から取得する Claude モデル情報。 + * Claude model info extracted from the SDK initialization result. + */ +export interface ClaudeModelInfo { + value: string; + displayName: string; + description: string; +} + +/** + * Creates a minimal SDK query to retrieve the available model list via `initializationResult()`. + * 最小限の SDK クエリを作成し、`initializationResult()` でモデル一覧を取得する。 + */ +export async function listClaudeModels(): Promise { + const q = query({ + prompt: "", + options: { + maxTurns: 0, + permissionMode: "plan", + }, + }); + try { + const initResult = await q.initializationResult(); + return initResult.models.map((m) => ({ + value: m.value, + displayName: m.displayName, + description: m.description, + })); + } finally { + q.close(); + } +} diff --git a/packages/claude-sidecar/src/handlers/query.ts b/packages/claude-sidecar/src/handlers/query.ts new file mode 100644 index 00000000..a70e56e0 --- /dev/null +++ b/packages/claude-sidecar/src/handlers/query.ts @@ -0,0 +1,322 @@ +/** + * Runs one Claude Agent SDK `query()` and maps SDK stream events to sidecar stdout lines. + * Claude Agent SDK の `query()` を 1 回実行し、ストリームを sidecar の stdout 行に写す。 + */ + +import { query } from "@anthropic-ai/claude-agent-sdk"; +import type { Options } from "@anthropic-ai/claude-agent-sdk"; +import type { + SDKAssistantMessage, + SDKMessage, + SDKPartialAssistantMessage, + SDKResultMessage, + SDKToolProgressMessage, +} from "@anthropic-ai/claude-agent-sdk"; +import type { SidecarMcpServerConfig, SidecarResponse } from "../protocol"; +import { formatResponseLine } from "../protocol"; +import type { QueryActivityTracker } from "./status"; + +/** Writes one newline-terminated JSON line to the host. / ホストへ改行付き JSON 1 行を書く */ +export type WriteLine = (line: string) => void; + +const DEFAULT_TOOLS = ["Read", "Write", "Bash", "WebSearch"] as const; + +function extractTextDelta(msg: SDKPartialAssistantMessage): string | null { + const ev = msg.event as unknown as Record | undefined; + if (!ev || typeof ev !== "object") return null; + if (ev.type !== "content_block_delta") return null; + const delta = ev.delta as Record | undefined; + if (!delta || delta.type !== "text_delta") return null; + const text = delta.text; + return typeof text === "string" ? text : null; +} + +/** + * Detects a tool_use content_block_start from a stream event. + * ストリームイベントから tool_use の content_block_start を検出する。 + */ +function extractToolUseStart( + msg: SDKPartialAssistantMessage, +): { name: string; input: string } | null { + const ev = msg.event as unknown as Record | undefined; + if (!ev || typeof ev !== "object") return null; + if (ev.type !== "content_block_start") return null; + const block = ev.content_block as Record | undefined; + if (!block || block.type !== "tool_use") return null; + const name = typeof block.name === "string" ? block.name : "unknown"; + const input = typeof block.input === "string" ? block.input : JSON.stringify(block.input ?? ""); + return { name, input }; +} + +/** + * Detects a content_block_stop from a stream event (to mark tool use complete). + * ストリームイベントから content_block_stop を検出する。 + */ +function isContentBlockStop(msg: SDKPartialAssistantMessage): boolean { + const ev = msg.event as { type?: string } | undefined; + return ev?.type === "content_block_stop"; +} + +function isToolProgressMessage(msg: SDKMessage): msg is SDKToolProgressMessage { + return typeof msg === "object" && msg !== null && "type" in msg && msg.type === "tool_progress"; +} + +function extractAssistantText(msg: SDKAssistantMessage): string { + const message = msg.message as { content?: unknown }; + const content = message.content; + if (!Array.isArray(content)) return ""; + let out = ""; + for (const block of content) { + if ( + block && + typeof block === "object" && + "type" in block && + (block as { type: string }).type === "text" && + "text" in block + ) { + out += String((block as { text: string }).text); + } + } + return out; +} + +function isResultMessage(msg: SDKMessage): msg is SDKResultMessage { + return typeof msg === "object" && msg !== null && "type" in msg && msg.type === "result"; +} + +/** + * Checks if a message is a system init message containing MCP server status. + * system init メッセージ(MCP サーバーステータスを含む)かどうかを判定する。 + */ +function isSystemInitMessage(msg: SDKMessage): boolean { + return ( + typeof msg === "object" && + msg !== null && + "type" in msg && + (msg as { type: string }).type === "system" && + "subtype" in msg && + (msg as { subtype: string }).subtype === "init" + ); +} + +/** + * Emits stream-complete on success or error line on failure. + * 成功時は stream-complete、失敗時は error 行を出す。 + */ +function emitResultOrError( + id: string, + msg: SDKResultMessage, + aggregated: string, + emit: (r: SidecarResponse) => void, +): void { + if (msg.subtype === "success") { + const finalText = msg.result ?? aggregated; + emit({ + type: "stream-complete", + id, + result: { content: finalText }, + }); + return; + } + const errors = "errors" in msg && Array.isArray(msg.errors) ? msg.errors.join("; ") : ""; + emit({ + type: "error", + id, + error: errors || `Claude Code finished with subtype ${msg.subtype}`, + code: msg.subtype, + }); +} + +/** + * Executes a query; writes {@link SidecarResponse} lines via `writeLine`. + * `writeLine` 経由で {@link SidecarResponse} 行を書き出す。 + */ +export async function runQuery(params: { + id: string; + prompt: string; + model?: string; + cwd?: string; + maxTurns?: number; + allowedTools?: string[]; + resume?: string; + mcpServers?: Record; + writeLine: WriteLine; + abortController: AbortController; + tracker: QueryActivityTracker; +}): Promise { + const { + id, + prompt, + model, + cwd, + maxTurns, + allowedTools, + resume, + mcpServers, + writeLine, + abortController, + tracker, + } = params; + + const emit = (response: SidecarResponse): void => { + writeLine(formatResponseLine(response)); + }; + + tracker.start(id); + let aggregated = ""; + let activeToolName: string | null = null; + + try { + const hasMcp = mcpServers && Object.keys(mcpServers).length > 0; + + const resolvedTools: string[] | undefined = + allowedTools !== undefined + ? allowedTools + : hasMcp + ? [...DEFAULT_TOOLS, "mcp__*"] + : [...DEFAULT_TOOLS]; + + const q = query({ + prompt, + options: { + model: model || undefined, + cwd: cwd ?? process.cwd(), + maxTurns: maxTurns ?? 25, + allowedTools: resolvedTools, + // SDK `Options.mcpServers` と protocol の SidecarMcpServerConfig は形状が一致するが、型は別定義のため unknown 経由で渡す。 + // Shapes match SDK `Options.mcpServers`; cast via `unknown` because types differ from protocol. + mcpServers: hasMcp + ? (mcpServers as unknown as NonNullable) + : undefined, + abortController, + includePartialMessages: true, + resume, + permissionMode: "acceptEdits", + settingSources: ["user", "project", "local"], + }, + }); + + for await (const msg of q) { + if (abortController.signal.aborted) { + break; + } + + if (msg.type === "stream_event") { + const toolStart = extractToolUseStart(msg); + if (toolStart) { + activeToolName = toolStart.name; + emit({ + type: "tool-use-start", + id, + toolName: toolStart.name, + toolInput: toolStart.input, + }); + continue; + } + + if (isContentBlockStop(msg) && activeToolName) { + emit({ type: "tool-use-complete", id, toolName: activeToolName }); + activeToolName = null; + continue; + } + + const delta = extractTextDelta(msg); + if (delta) { + aggregated += delta; + emit({ type: "stream-chunk", id, content: delta }); + } + continue; + } + + if (isToolProgressMessage(msg)) { + // Complete the previous tool before starting the next when SDK switches tools mid-stream. + // SDK がツールを切り替えたとき、次の tool_progress の前に前ツールを完了扱いにする。 + if (activeToolName && activeToolName !== msg.tool_name) { + emit({ type: "tool-use-complete", id, toolName: activeToolName }); + } + if (!activeToolName || activeToolName !== msg.tool_name) { + activeToolName = msg.tool_name; + emit({ + type: "tool-use-start", + id, + toolName: msg.tool_name, + toolInput: "", + }); + } + continue; + } + + if (msg.type === "assistant") { + if (activeToolName) { + emit({ type: "tool-use-complete", id, toolName: activeToolName }); + activeToolName = null; + } + const text = extractAssistantText(msg); + const delta = text.length > 0 ? text.slice(aggregated.length) : ""; + if (delta.length > 0) { + aggregated += delta; + emit({ type: "stream-chunk", id, content: delta }); + } + continue; + } + + if (isSystemInitMessage(msg)) { + const sysMsg = msg as unknown as { mcp_servers?: unknown[] }; + const mcpArr = sysMsg.mcp_servers; + if (Array.isArray(mcpArr) && mcpArr.length > 0) { + const servers = mcpArr.map((raw: unknown) => { + const s = raw as Record; + return { + name: String(s.name ?? ""), + status: String(s.status ?? "unknown"), + error: s.error ? String(s.error) : undefined, + tools: Array.isArray(s.tools) + ? (s.tools as Array>).map((t) => ({ + name: String(t.name ?? ""), + description: t.description ? String(t.description) : undefined, + })) + : undefined, + }; + }); + emit({ type: "mcp-status", id, servers }); + } + continue; + } + + if (isResultMessage(msg)) { + if (activeToolName) { + emit({ type: "tool-use-complete", id, toolName: activeToolName }); + activeToolName = null; + } + emitResultOrError(id, msg, aggregated, emit); + return; + } + } + + if (abortController.signal.aborted) { + emit({ + type: "error", + id, + error: "Query aborted", + code: "aborted", + }); + return; + } + + emit({ + type: "stream-complete", + id, + result: { content: aggregated }, + }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + emit({ + type: "error", + id, + error: message, + code: "query_exception", + }); + } finally { + tracker.end(id); + } +} diff --git a/packages/claude-sidecar/src/handlers/status.ts b/packages/claude-sidecar/src/handlers/status.ts new file mode 100644 index 00000000..6f273d6a --- /dev/null +++ b/packages/claude-sidecar/src/handlers/status.ts @@ -0,0 +1,33 @@ +/** + * Tracks active query ids for status reporting. + * ステータス報告用にアクティブなクエリ ID を追跡する。 + */ + +/** Tracks in-flight query ids for `status` RPC. / `status` RPC 用の実行中クエリ ID */ +export class QueryActivityTracker { + private readonly active = new Set(); + + /** Marks a query as running. / クエリ開始 */ + start(id: string): void { + this.active.add(id); + } + + /** Marks a query as finished. / クエリ終了 */ + end(id: string): void { + this.active.delete(id); + } + + /** Clears all tracked ids without aborting controllers (e.g. shutdown). / コントローラは中断せず追跡 ID のみクリア */ + clearAll(): void { + this.active.clear(); + } + + /** Current activity for sidecar status responses. / ステータス応答用スナップショット */ + snapshot(): { status: "idle" | "processing"; activeQueryIds: string[] } { + const ids = [...this.active]; + return { + status: ids.length > 0 ? "processing" : "idle", + activeQueryIds: ids, + }; + } +} diff --git a/packages/claude-sidecar/src/index.ts b/packages/claude-sidecar/src/index.ts new file mode 100644 index 00000000..d128b13e --- /dev/null +++ b/packages/claude-sidecar/src/index.ts @@ -0,0 +1,130 @@ +/** + * Claude Code sidecar entry: JSONL on stdin/stdout for the Tauri host process. + * Tauri ホスト向け stdin/stdout JSONL の Claude Code sidecar エントリ。 + * + * @packageDocumentation + */ + +import * as readline from "node:readline"; +import { formatResponseLine, parseRequestLine } from "./protocol"; +import { checkClaudeInstallation } from "./handlers/installation"; +import { listClaudeModels } from "./handlers/models"; +import { runQuery } from "./handlers/query"; +import { QueryActivityTracker } from "./handlers/status"; + +const tracker = new QueryActivityTracker(); +const abortById = new Map(); + +function writeLine(line: string): void { + process.stdout.write(line); +} + +function emitError(id: string, error: string, code?: string): void { + writeLine(formatResponseLine({ type: "error", id, error, code })); +} + +async function handleRequest(raw: string): Promise { + let req; + try { + req = parseRequestLine(raw); + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + emitError("protocol", message, "parse_error"); + return; + } + + switch (req.type) { + case "shutdown": { + tracker.clearAll(); + writeLine(formatResponseLine({ type: "shutdown-ack" })); + process.exit(0); + return; + } + case "status": { + const snap = tracker.snapshot(); + writeLine( + formatResponseLine({ + type: "status-response", + correlationId: req.correlationId, + status: snap.status, + activeQueryIds: snap.activeQueryIds, + }), + ); + return; + } + case "check_installation": { + const inst = await checkClaudeInstallation(); + writeLine( + formatResponseLine({ + type: "installation-status", + correlationId: req.correlationId, + installed: inst.installed, + version: inst.version, + }), + ); + return; + } + case "abort": { + abortById.get(req.id)?.abort(); + return; + } + case "list_models": { + try { + const models = await listClaudeModels(); + writeLine( + formatResponseLine({ + type: "models-list", + correlationId: req.correlationId, + models, + }), + ); + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + // Use request correlation id so the Tauri host can resolve the pending RPC (avoids 30s timeout). + // リクエストの correlation id を使い、Tauri 側の保留 RPC を解決する(30 秒タイムアウトを避ける)。 + emitError(req.correlationId, message, "list_models_error"); + } + return; + } + case "query": { + if (abortById.has(req.id)) { + emitError(req.id, "duplicate query id", "duplicate_id"); + return; + } + const ac = new AbortController(); + abortById.set(req.id, ac); + void runQuery({ + id: req.id, + prompt: req.prompt, + model: req.model, + cwd: req.cwd, + maxTurns: req.maxTurns, + allowedTools: req.allowedTools, + resume: req.resume, + mcpServers: req.mcpServers, + writeLine, + abortController: ac, + tracker, + }).finally(() => { + abortById.delete(req.id); + }); + return; + } + default: { + emitError("protocol", "unknown request type", "unknown_type"); + } + } +} + +const rl = readline.createInterface({ + input: process.stdin, + crlfDelay: Infinity, +}); + +rl.on("line", (line) => { + void handleRequest(line); +}); + +rl.on("close", () => { + process.exit(0); +}); diff --git a/packages/claude-sidecar/src/protocol.test.ts b/packages/claude-sidecar/src/protocol.test.ts new file mode 100644 index 00000000..a9e8d02f --- /dev/null +++ b/packages/claude-sidecar/src/protocol.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; +import { formatResponseLine, parseRequestLine } from "./protocol"; + +describe("parseRequestLine", () => { + it("parses query request", () => { + const r = parseRequestLine( + JSON.stringify({ + type: "query", + id: "a", + prompt: "hi", + cwd: "/tmp", + maxTurns: 3, + }), + ); + expect(r).toEqual({ + type: "query", + id: "a", + prompt: "hi", + cwd: "/tmp", + maxTurns: 3, + }); + }); + + it("throws on empty", () => { + expect(() => parseRequestLine(" ")).toThrow(/empty/); + }); +}); + +describe("formatResponseLine", () => { + it("ends with newline and round-trips JSON", () => { + const line = formatResponseLine({ + type: "stream-chunk", + id: "x", + content: "hello", + }); + expect(line.endsWith("\n")).toBe(true); + expect(JSON.parse(line.trim())).toEqual({ + type: "stream-chunk", + id: "x", + content: "hello", + }); + }); +}); diff --git a/packages/claude-sidecar/src/protocol.ts b/packages/claude-sidecar/src/protocol.ts new file mode 100644 index 00000000..7a23e147 --- /dev/null +++ b/packages/claude-sidecar/src/protocol.ts @@ -0,0 +1,110 @@ +/** + * JSONL protocol between the Tauri host and this sidecar process. + * Tauri ホストとこの sidecar プロセス間の JSONL プロトコル。 + * + * Each line on stdin is one JSON request; each line on stdout is one JSON response. + * stdin の 1 行が 1 リクエスト、stdout の 1 行が 1 レスポンス。 + */ + +/** + * MCP サーバー設定(シリアライズ可能なトランスポート設定のみ)。 + * MCP server config (serializable process transports only). + */ +export type SidecarMcpServerConfig = + | { type?: "stdio"; command: string; args?: string[]; env?: Record } + | { type: "http"; url: string; headers?: Record } + | { type: "sse"; url: string; headers?: Record }; + +/** Inbound message from the host (one JSON object per line on stdin). */ +export type SidecarRequest = + | { + type: "query"; + id: string; + prompt: string; + model?: string; + cwd?: string; + maxTurns?: number; + allowedTools?: string[]; + /** Optional Claude session to resume (SDK `resume`). */ + resume?: string; + /** + * MCP サーバー設定。SDK の `options.mcpServers` に渡す。 + * MCP server configs passed to SDK's `options.mcpServers`. + */ + mcpServers?: Record; + correlationId?: string; + } + | { type: "abort"; id: string } + | { type: "status"; correlationId: string } + | { type: "check_installation"; correlationId: string } + | { type: "list_models"; correlationId: string } + | { type: "shutdown" }; + +/** Outbound message to the host (one JSON object per line on stdout). */ +export type SidecarResponse = + | { type: "stream-chunk"; id: string; content: string } + | { type: "stream-complete"; id: string; result: { content: string } } + | { type: "error"; id: string; error: string; code?: string } + | { + type: "tool-use-start"; + id: string; + toolName: string; + toolInput: string; + } + | { + type: "tool-use-complete"; + id: string; + toolName: string; + } + | { + type: "status-response"; + correlationId: string; + status: "idle" | "processing"; + activeQueryIds: string[]; + } + | { + type: "installation-status"; + correlationId: string; + installed: boolean; + version?: string; + } + | { + type: "models-list"; + correlationId: string; + models: Array<{ value: string; displayName: string; description: string }>; + } + | { + type: "mcp-status"; + id: string; + servers: Array<{ + name: string; + status: string; + error?: string; + tools?: Array<{ name: string; description?: string }>; + }>; + } + | { type: "shutdown-ack" }; + +/** + * Parses a single JSON line into {@link SidecarRequest}. + * 1 行を {@link SidecarRequest} としてパースする。 + */ +export function parseRequestLine(line: string): SidecarRequest { + const trimmed = line.trim(); + if (!trimmed) { + throw new Error("empty line"); + } + const value: unknown = JSON.parse(trimmed); + if (!value || typeof value !== "object" || !("type" in value)) { + throw new Error("invalid request: missing type"); + } + return value as SidecarRequest; +} + +/** + * Serializes a response as one JSON line (with trailing newline for writing). + * レスポンスを JSON 1 行(書き込み用に末尾改行付き)にする。 + */ +export function formatResponseLine(response: SidecarResponse): string { + return `${JSON.stringify(response)}\n`; +} diff --git a/packages/claude-sidecar/tsconfig.json b/packages/claude-sidecar/tsconfig.json new file mode 100644 index 00000000..3b7d0c2a --- /dev/null +++ b/packages/claude-sidecar/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["bun-types"] + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/claude-sidecar/vitest.config.ts b/packages/claude-sidecar/vitest.config.ts new file mode 100644 index 00000000..9ca71dba --- /dev/null +++ b/packages/claude-sidecar/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + root: import.meta.dirname, + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +}); diff --git a/scripts/build-claude-sidecar.mjs b/scripts/build-claude-sidecar.mjs new file mode 100644 index 00000000..7d46e4ba --- /dev/null +++ b/scripts/build-claude-sidecar.mjs @@ -0,0 +1,55 @@ +#!/usr/bin/env node +/** + * Compiles `packages/claude-sidecar` with `bun build --compile` and renames the + * binary to `claude-sidecar-[.exe]` under `src-tauri/binaries/` + * for Tauri `bundle.externalBin`. + * + * `bun build --compile` でコンパイルし、Tauri `externalBin` 用に + * `src-tauri/binaries/claude-sidecar-` にリネームする。 + */ + +import { spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, renameSync, unlinkSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(fileURLToPath(new URL(".", import.meta.url)), ".."); +const outDir = join(root, "src-tauri", "binaries"); + +function run(cmd, args, options = {}) { + const r = spawnSync(cmd, args, { + cwd: root, + stdio: "inherit", + shell: process.platform === "win32", + ...options, + }); + if (r.status !== 0) { + process.exit(r.status ?? 1); + } +} + +const triple = spawnSync("rustc", ["--print", "host-tuple"], { + encoding: "utf8", + cwd: root, +}); +if (triple.error || triple.status !== 0) { + console.error("Failed to run rustc --print host-tuple"); + process.exit(1); +} + +const hostTuple = triple.stdout.trim(); +const ext = process.platform === "win32" ? ".exe" : ""; +const finalName = `claude-sidecar-${hostTuple}${ext}`; +const tempName = `claude-sidecar-build-temp${ext}`; +const tempPath = join(outDir, tempName); +const finalPath = join(outDir, finalName); + +mkdirSync(outDir, { recursive: true }); + +run("bun", ["build", "packages/claude-sidecar/src/index.ts", "--compile", `--outfile=${tempPath}`]); + +if (existsSync(finalPath)) { + unlinkSync(finalPath); +} +renameSync(tempPath, finalPath); +console.log(`Sidecar binary: ${finalPath}`); diff --git a/scripts/ensure-claude-sidecar-bin.mjs b/scripts/ensure-claude-sidecar-bin.mjs new file mode 100644 index 00000000..cb3bfe8b --- /dev/null +++ b/scripts/ensure-claude-sidecar-bin.mjs @@ -0,0 +1,37 @@ +#!/usr/bin/env node +/** + * Ensures `src-tauri/binaries/claude-sidecar-` exists; runs + * `sidecar:build` if missing (so `tauri dev` / `cargo` can validate externalBin). + * + * バイナリが無ければ `sidecar:build` を実行する(`externalBin` 検証用)。 + */ + +import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(fileURLToPath(new URL(".", import.meta.url)), ".."); +const triple = spawnSync("rustc", ["--print", "host-tuple"], { + encoding: "utf8", + cwd: root, +}); +if (triple.error || triple.status !== 0) { + console.error("ensure-claude-sidecar-bin: rustc --print host-tuple failed"); + process.exit(1); +} +const hostTuple = triple.stdout.trim(); +const ext = process.platform === "win32" ? ".exe" : ""; +const finalPath = join(root, "src-tauri", "binaries", `claude-sidecar-${hostTuple}${ext}`); + +if (existsSync(finalPath)) { + process.exit(0); +} + +console.log("Claude sidecar binary missing; running sidecar:build…"); +const r = spawnSync("bun", ["run", "sidecar:build"], { + cwd: root, + stdio: "inherit", + shell: process.platform === "win32", +}); +process.exit(r.status ?? 1); diff --git a/server/api/src/routes/pages.ts b/server/api/src/routes/pages.ts index 854aaa1c..97447de6 100644 --- a/server/api/src/routes/pages.ts +++ b/server/api/src/routes/pages.ts @@ -12,10 +12,27 @@ import { HTTPException } from "hono/http-exception"; import { eq, and, sql } from "drizzle-orm"; import { pages, pageContents } from "../schema/index.js"; import { authRequired } from "../middleware/auth.js"; -import type { AppEnv } from "../types/index.js"; +import type { AppEnv, Database } from "../types/index.js"; const app = new Hono(); +/** + * PUT /content リクエストから pages テーブルの更新セットを構築し、変更があれば適用する。 + * Build and apply pages-table updates (title, content_preview, updated_at) from PUT body. + */ +async function applyPagesMetadataUpdate( + db: { update: Database["update"] }, + pageId: string, + body: { title?: string; content_preview?: string }, +): Promise { + const set: Record = {}; + if (body.title !== undefined) set.title = body.title; + if (body.content_preview !== undefined) set.contentPreview = body.content_preview; + if (Object.keys(set).length === 0) return; + set.updatedAt = new Date(); + await db.update(pages).set(set).where(eq(pages.id, pageId)); +} + // ── GET /pages/:id/content ────────────────────────────────────────────────── app.get("/:id/content", authRequired, async (c) => { const pageId = c.req.param("id"); @@ -73,6 +90,7 @@ app.put("/:id/content", authRequired, async (c) => { ydoc_state: string; // base64-encoded Y.Doc state expected_version?: number; content_text?: string; + content_preview?: string; title?: string; }>(); @@ -119,12 +137,7 @@ app.put("/:id/content", authRequired, async (c) => { const insertedRow = inserted[0]; if (!insertedRow) throw new HTTPException(500, { message: "Insert failed" }); - if (body.title !== undefined) { - await tx - .update(pages) - .set({ title: body.title, updatedAt: new Date() }) - .where(eq(pages.id, pageId)); - } + await applyPagesMetadataUpdate(tx, pageId, body); return { done: true as const, version: insertedRow.version ?? 1 }; }); @@ -162,12 +175,7 @@ app.put("/:id/content", authRequired, async (c) => { const updatedRow = updated[0]; if (!updatedRow) throw new HTTPException(500, { message: "Update failed" }); - if (body.title !== undefined) { - await db - .update(pages) - .set({ title: body.title, updatedAt: new Date() }) - .where(eq(pages.id, pageId)); - } + await applyPagesMetadataUpdate(db, pageId, body); return c.json({ version: updatedRow.version ?? 0 }); } @@ -192,12 +200,7 @@ app.put("/:id/content", authRequired, async (c) => { }) .returning(); - if (body.title !== undefined) { - await db - .update(pages) - .set({ title: body.title, updatedAt: new Date() }) - .where(eq(pages.id, pageId)); - } + await applyPagesMetadataUpdate(db, pageId, body); const resultRow = result[0]; if (!resultRow) throw new HTTPException(500, { message: "Upsert failed" }); diff --git a/server/hocuspocus/src/extractPlainTextFromYXml.test.ts b/server/hocuspocus/src/extractPlainTextFromYXml.test.ts new file mode 100644 index 00000000..edac4134 --- /dev/null +++ b/server/hocuspocus/src/extractPlainTextFromYXml.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from "vitest"; +import * as Y from "yjs"; +import { buildContentPreview, extractTextFromYXml } from "./extractPlainTextFromYXml.js"; + +describe("extractTextFromYXml", () => { + it("does not insert a newline inside a paragraph between inline bold and following text", () => { + const doc = new Y.Doc(); + doc.transact(() => { + const fragment = doc.getXmlFragment("default"); + const paragraph = new Y.XmlElement("paragraph"); + fragment.push([paragraph]); + const t1 = new Y.XmlText(); + t1.insert(0, "Hello "); + paragraph.push([t1]); + const bold = new Y.XmlElement("bold"); + paragraph.push([bold]); + const tBold = new Y.XmlText(); + tBold.insert(0, "world"); + bold.push([tBold]); + const t2 = new Y.XmlText(); + t2.insert(0, "!"); + paragraph.push([t2]); + }); + + const plain = extractTextFromYXml(doc.getXmlFragment("default")).trim(); + expect(plain).not.toMatch(/world\s*\n\s*!/); + expect(plain.replace(/\s+/g, " ").trim()).toBe("Hello world !"); + }); + + it("separates block-level paragraphs with newlines", () => { + const doc = new Y.Doc(); + doc.transact(() => { + const fragment = doc.getXmlFragment("default"); + const p1 = new Y.XmlElement("paragraph"); + fragment.push([p1]); + const a = new Y.XmlText(); + a.insert(0, "First"); + p1.push([a]); + const p2 = new Y.XmlElement("paragraph"); + fragment.push([p2]); + const b = new Y.XmlText(); + b.insert(0, "Second"); + p2.push([b]); + }); + + const plain = extractTextFromYXml(doc.getXmlFragment("default")).trim(); + expect(plain).toMatch(/First/); + expect(plain).toMatch(/Second/); + expect(plain.includes("First") && plain.includes("Second")).toBe(true); + expect(/\n/.test(plain)).toBe(true); + }); +}); + +describe("buildContentPreview", () => { + it("collapses whitespace and truncates", () => { + expect(buildContentPreview(" a \n b ")).toBe("a b"); + const long = "x".repeat(200); + const prev = buildContentPreview(long); + expect(prev.endsWith("...")).toBe(true); + expect(prev.length).toBeLessThanOrEqual(124); + }); +}); diff --git a/server/hocuspocus/src/extractPlainTextFromYXml.ts b/server/hocuspocus/src/extractPlainTextFromYXml.ts new file mode 100644 index 00000000..06ae89ca --- /dev/null +++ b/server/hocuspocus/src/extractPlainTextFromYXml.ts @@ -0,0 +1,65 @@ +import * as Y from "yjs"; + +/** + * TipTap / ProseMirror のマーク相当で、Y.Xml 上に子要素として現れるインライン名。 + * 兄弟インライン間では改行ではなくスペースを挟み、プレビュー用テキストの不自然な改行を防ぐ。 + * + * Mark-like XmlElement names in the Y.Xml tree; use a space (not a newline) between + * inline siblings so plain-text extraction does not split words (e.g. `Hello world!`). + */ +const INLINE_XML_ELEMENT_NAMES = new Set([ + "bold", + "italic", + "strike", + "code", + "link", + "underline", + "highlight", + "subscript", + "superscript", + "textStyle", +]); + +/** + * インライン要素かどうかを nodeName で判定する。 + * Determine whether an XmlElement is inline-only (no trailing newline after its subtree). + */ +function isInlineXmlElement(node: Y.XmlElement): boolean { + return INLINE_XML_ELEMENT_NAMES.has(node.nodeName); +} + +/** + * Y.Doc の XmlFragment(または XmlElement 根)からプレーンテキストを再帰的に抽出する。 + * Recursively extract plain text from a Y.XmlFragment or Y.XmlElement subtree. + */ +export function extractTextFromYXml(node: Y.XmlFragment | Y.XmlElement): string { + let text = ""; + + for (let i = 0; i < node.length; i++) { + const child = node.get(i); + if (child instanceof Y.XmlText) { + text += child.toString(); + } else if (child instanceof Y.XmlElement) { + const inner = extractTextFromYXml(child); + const suffix = isInlineXmlElement(child) ? " " : "\n"; + text += inner + suffix; + } + } + return text; +} + +/** + * プレビュー文字列の最大長(DB の content_preview と一致させる)。 + * Max length for content preview (aligned with `pages.content_preview`). + */ +export const CONTENT_PREVIEW_MAX_LENGTH = 120; + +/** + * プレーンテキストからコンテンツプレビュー(先頭120文字)を生成する。 + * Generate content preview (first 120 chars) from plain text. + */ +export function buildContentPreview(text: string): string { + const trimmed = text.trim().replace(/\s+/g, " "); + if (trimmed.length <= CONTENT_PREVIEW_MAX_LENGTH) return trimmed; + return trimmed.slice(0, CONTENT_PREVIEW_MAX_LENGTH).trim() + "..."; +} diff --git a/server/hocuspocus/src/index.ts b/server/hocuspocus/src/index.ts index 51b0a0d0..03c8e6ac 100644 --- a/server/hocuspocus/src/index.ts +++ b/server/hocuspocus/src/index.ts @@ -9,6 +9,7 @@ import { isTruthyEnvFlag, warnDevAuthBypassOnce, } from "./dev-auth-bypass.js"; +import { buildContentPreview, extractTextFromYXml } from "./extractPlainTextFromYXml.js"; const PORT = parseInt(process.env.PORT || "1234", 10); const REDIS_URL = process.env.REDIS_URL; @@ -195,19 +196,33 @@ async function loadDocumentFromDb(pageId: string): Promise { async function saveDocumentToDb(pageId: string, document: Y.Doc): Promise { const encodedState = Buffer.from(Y.encodeStateAsUpdate(document)); + const contentText = extractTextFromYXml(document.getXmlFragment("default")); + const contentPreview = buildContentPreview(contentText); const client = await getPool().connect(); try { + await client.query("BEGIN"); await client.query( ` INSERT INTO page_contents (page_id, ydoc_state, version, content_text, updated_at) - VALUES ($1, $2, 1, '', NOW()) + VALUES ($1, $2, 1, $3, NOW()) ON CONFLICT (page_id) DO UPDATE SET ydoc_state = EXCLUDED.ydoc_state, version = page_contents.version + 1, + content_text = EXCLUDED.content_text, updated_at = NOW() `, - [pageId, encodedState], + [pageId, encodedState, contentText], ); + // ページ一覧用のプレビューを pages テーブルにも同期 + // Sync content preview to the pages table for page list display + await client.query(`UPDATE pages SET content_preview = $1, updated_at = NOW() WHERE id = $2`, [ + contentPreview, + pageId, + ]); + await client.query("COMMIT"); + } catch (error) { + await client.query("ROLLBACK"); + throw error; } finally { client.release(); } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 00000000..da4d56a8 --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,5334 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.0", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser 0.36.0", + "foldhash 0.2.0", + "html5ever 0.38.0", + "precomputed-hash", + "selectors 0.36.1", + "tendril 0.5.0", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63a1d0de4f2249aa0ff5884d7080814f446bb241a559af6c170a41e878ed2d45" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.12+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever 0.14.1", + "match_token", +] + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever 0.38.0", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "js-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser 0.29.6", + "html5ever 0.29.1", + "indexmap 2.13.0", + "selectors 0.24.0", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache 0.8.9", + "string_cache_codegen 0.5.4", + "tendril 0.4.3", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril 0.5.0", + "web_atoms", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.13.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.10+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser 0.29.6", + "derive_more 0.99.20", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.0", + "cssparser 0.36.0", + "derive_more 2.1.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc 0.4.3", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" +dependencies = [ + "bitflags 2.11.0", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "parking_lot", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs 6.0.0", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs 6.0.0", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "tauri-plugin-store" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca1a8ff83c269b115e98726ffc13f9e548a10161544a92ad121d6d0a96e16ea" +dependencies = [ + "dunce", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "tauri-runtime" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever 0.29.1", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +dependencies = [ + "dunce", + "embed-resource", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.10+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82418ca169e235e6c399a84e395ab6debeb3bc90edc959bf0f48647c6a32d1b" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.1", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.1", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +dependencies = [ + "crossbeam-channel", + "dirs 6.0.0", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache 0.9.0", + "string_cache_codegen 0.6.1", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wry" +version = "0.54.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a8135d8676225e5744de000d4dff5a082501bf7db6a1c1495034f8c314edbc" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs 6.0.0", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zedi" +version = "0.10.0" +dependencies = [ + "dirs 5.0.1", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-fs", + "tauri-plugin-shell", + "tauri-plugin-store", + "tempfile", + "tokio", + "uuid", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 00000000..47390fee --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "zedi" +version = "0.10.0" +description = "Zero-Friction Knowledge Network" +authors = ["Zedi Team"] +edition = "2021" + +[lib] +name = "app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +dirs = "5" +tauri-plugin-dialog = "2" +tauri-plugin-fs = "2" +tauri-plugin-shell = "2" +tauri-plugin-store = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["sync", "time", "macros", "rt-multi-thread"] } +uuid = { version = "1", features = ["v4"] } + +[profile.dev] +incremental = true + +[dev-dependencies] +tempfile = "3" + +[profile.release] +codegen-units = 1 +lto = true +opt-level = "s" +strip = true diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 00000000..d860e1e6 --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 00000000..8aca9f20 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,25 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default capability for the main window", + "windows": ["main"], + "permissions": [ + "core:default", + "dialog:default", + "fs:default", + { + "identifier": "fs:allow-read-text-file", + "allow": [{ "path": "$HOME/.claude/**" }, { "path": "$HOME/.claude.json" }] + }, + { + "identifier": "shell:allow-execute", + "allow": [ + { + "name": "binaries/claude-sidecar", + "sidecar": true, + "args": false + } + ] + } + ] +} diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 00000000..662ddb8d Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 00000000..41332d2f Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 00000000..3b703948 Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png new file mode 100644 index 00000000..2d22918e Binary files /dev/null and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 00000000..85c6cc55 Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 00000000..f485a64c Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 00000000..d78cf2ce Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 00000000..121978fc Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 00000000..28b81542 Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 00000000..0c7c7060 Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 00000000..08e0bd8d Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 00000000..d39ffdf8 Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 00000000..15d90b8c Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png new file mode 100644 index 00000000..243d329f Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..2ffbf24b --- /dev/null +++ b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..d2320c39 Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..52a352fe Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..0395ec39 Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..f9e083fc Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..7d084f67 Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..98a45dd0 Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..dc346bb8 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..00cce8e4 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..21e0ed46 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..7b53a4ed Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..e7e89215 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..9b8cd949 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..12a4c0f7 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..13b4b5c1 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..9b24e080 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/values/ic_launcher_background.xml b/src-tauri/icons/android/values/ic_launcher_background.xml new file mode 100644 index 00000000..ea9c223a --- /dev/null +++ b/src-tauri/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #fff + \ No newline at end of file diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 00000000..0c39de14 Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 00000000..3c2c3eda Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 00000000..1953ceea Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@1x.png b/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 00000000..efd90c9f Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 00000000..41e5cdd4 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x.png b/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 00000000..41e5cdd4 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@3x.png b/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 00000000..4ab8682e Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@1x.png b/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 00000000..66f79cc5 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 00000000..d72b4866 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x.png b/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 00000000..d72b4866 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@3x.png b/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 00000000..d4581a05 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@1x.png b/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 00000000..41e5cdd4 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 00000000..74ac52c2 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x.png b/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 00000000..74ac52c2 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@3x.png b/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 00000000..b49db92f Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-512@2x.png b/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 00000000..7de3fc76 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@2x.png b/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 00000000..b49db92f Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@3x.png b/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 00000000..82e3fe39 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@1x.png b/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 00000000..de33f9b1 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@2x.png b/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 00000000..9ad2dd29 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 00000000..0bba2ec9 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/src-tauri/src/claude_sidecar.rs b/src-tauri/src/claude_sidecar.rs new file mode 100644 index 00000000..e339882c --- /dev/null +++ b/src-tauri/src/claude_sidecar.rs @@ -0,0 +1,402 @@ +//! Claude Code sidecar: spawns the JSONL bridge process and exposes Tauri commands + events. +//! Claude Code sidecar — JSONL ブリッジプロセスを起動し Tauri コマンド・イベントを提供する。 +//! +//! Events: `claude-stream-chunk`, `claude-stream-complete`, `claude-error` + +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +use serde_json::Value; +use tauri::{AppHandle, Emitter, State}; +use tauri_plugin_shell::process::{CommandChild, CommandEvent}; +use tauri_plugin_shell::ShellExt; +use tokio::sync::{oneshot, Mutex as TokioMutex}; + +/// Shared state for the sidecar child stdin and RPC correlation. +/// sidecar の子プロセス stdin と相関 ID 用の共有状態。 +#[derive(Debug)] +pub struct ClaudeSidecarState { + /// Dev-only: repo root for `bun packages/claude-sidecar/...`. Omitted in release builds. + /// 開発時のみ: `bun` で sidecar を起動する際のリポジトリルート。release では未使用。 + repo_root: Option, + child: Arc>>, + pending: Arc>>>, +} + +impl ClaudeSidecarState { + /// Builds state. `repo_root` is only set in debug builds (no embedded path in release). + /// `repo_root` はデバッグビルドでのみ設定(release にビルドマシン絶対パスを埋め込まない)。 + pub fn new() -> Self { + let repo_root = if cfg!(debug_assertions) { + Some( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(".")), + ) + } else { + None + }; + Self { + repo_root, + child: Arc::new(TokioMutex::new(None)), + pending: Arc::new(TokioMutex::new(HashMap::new())), + } + } +} + +impl Default for ClaudeSidecarState { + fn default() -> Self { + Self::new() + } +} + +fn map_shell_err(e: tauri_plugin_shell::Error) -> String { + e.to_string() +} + +/// Completes all pending RPC waiters after sidecar failure (timeout avoided). +/// sidecar 失敗後に保留中 RPC を完了させる(タイムアウト待ちを避ける)。 +async fn fail_pending_rpc( + pending: &Arc>>>, + message: &str, + code: &str, +) { + let mut guard = pending.lock().await; + for (cid, tx) in guard.drain() { + let _ = tx.send(serde_json::json!({ + "type": "error", + "correlationId": cid, + "error": message, + "code": code, + })); + } +} + +async fn process_sidecar_line( + app: &AppHandle, + pending: &Arc>>>, + line: &str, +) { + let value: Value = match serde_json::from_str(line) { + Ok(v) => v, + Err(e) => { + eprintln!("claude-sidecar: stdout JSON parse error: {e}: {line}"); + return; + } + }; + + let Some(typ) = value.get("type").and_then(|t| t.as_str()) else { + return; + }; + + match typ { + "stream-chunk" => { + let _ = app.emit("claude-stream-chunk", &value); + } + "stream-complete" => { + let _ = app.emit("claude-stream-complete", &value); + } + "tool-use-start" => { + let _ = app.emit("claude-tool-use-start", &value); + } + "tool-use-complete" => { + let _ = app.emit("claude-tool-use-complete", &value); + } + "error" => { + // RPC waiters (status, installation, list_models) key pending by correlation id. + // Sidecar errors use `id` (or `correlationId` from fail_pending_rpc) matching that id. + let cid = value + .get("correlationId") + .and_then(|c| c.as_str()) + .or_else(|| value.get("id").and_then(|c| c.as_str())); + if let Some(cid) = cid { + let mut guard = pending.lock().await; + if let Some(tx) = guard.remove(cid) { + let _ = tx.send(value); + return; + } + } + let _ = app.emit("claude-error", &value); + } + "mcp-status" => { + let _ = app.emit("claude-mcp-status", &value); + } + "status-response" | "installation-status" | "models-list" => { + if let Some(cid) = value.get("correlationId").and_then(|c| c.as_str()) { + let mut guard = pending.lock().await; + if let Some(tx) = guard.remove(cid) { + let _ = tx.send(value); + } + } + } + _ => {} + } +} + +async fn ensure_sidecar(app: &AppHandle, state: &ClaudeSidecarState) -> Result<(), String> { + let mut slot = state.child.lock().await; + if slot.is_some() { + return Ok(()); + } + + let (mut rx, child) = if cfg!(debug_assertions) { + let root = state + .repo_root + .as_ref() + .ok_or_else(|| "internal error: missing repo root for dev sidecar".to_string())?; + app.shell() + .command("bun") + .args(["packages/claude-sidecar/src/index.ts"]) + .current_dir(root) + .spawn() + .map_err(map_shell_err)? + } else { + app.shell() + .sidecar("binaries/claude-sidecar") + .map_err(map_shell_err)? + .spawn() + .map_err(map_shell_err)? + }; + + let app_handle = app.clone(); + let pending = state.pending.clone(); + let child_slot = state.child.clone(); + + tauri::async_runtime::spawn(async move { + let mut line_buf: Vec = Vec::new(); + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Stdout(bytes) => { + line_buf.extend_from_slice(&bytes); + while let Some(pos) = line_buf.iter().position(|&b| b == b'\n') { + let line_bytes: Vec = line_buf.drain(..=pos).collect(); + let without_nl = line_bytes.strip_suffix(b"\n").unwrap_or(&line_bytes); + let without_cr = without_nl.strip_suffix(b"\r").unwrap_or(without_nl); + let line = String::from_utf8_lossy(without_cr).trim().to_string(); + if !line.is_empty() { + process_sidecar_line(&app_handle, &pending, &line).await; + } + } + } + CommandEvent::Stderr(bytes) => { + eprintln!("claude-sidecar stderr: {}", String::from_utf8_lossy(&bytes)); + } + CommandEvent::Error(e) => { + eprintln!("claude-sidecar command error: {e}"); + fail_pending_rpc(&pending, "sidecar command error", "sidecar_io_error").await; + let _ = app_handle.emit( + "claude-error", + serde_json::json!({ + "id": "sidecar", + "error": e.to_string(), + "code": "sidecar_io_error", + "type": "error", + }), + ); + } + CommandEvent::Terminated(payload) => { + eprintln!("claude-sidecar terminated: {payload:?}"); + fail_pending_rpc( + &pending, + "Claude sidecar process terminated", + "sidecar_terminated", + ) + .await; + *child_slot.lock().await = None; + let _ = app_handle.emit( + "claude-error", + serde_json::json!({ + "id": "sidecar", + "error": "Claude sidecar process terminated", + "code": "sidecar_terminated", + "type": "error", + "exitCode": payload.code, + "signal": payload.signal, + }), + ); + break; + } + _ => {} + } + } + }); + + *slot = Some(child); + Ok(()) +} + +async fn write_line(state: &ClaudeSidecarState, line: &str) -> Result<(), String> { + let mut guard = state.child.lock().await; + let child = guard + .as_mut() + .ok_or_else(|| "Claude sidecar is not running".to_string())?; + let bytes = format!("{}\n", line.trim_end()); + let bytes_buf = bytes.into_bytes(); + tokio::task::block_in_place(|| { + child + .write(&bytes_buf) + .map_err(|e| format!("failed to write sidecar stdin: {e}")) + })?; + Ok(()) +} + +async fn rpc_json( + _app: &AppHandle, + state: &ClaudeSidecarState, + request: Value, + correlation_id: &str, +) -> Result { + let (tx, rx) = oneshot::channel(); + { + let mut p = state.pending.lock().await; + p.insert(correlation_id.to_string(), tx); + } + + let line = serde_json::to_string(&request).map_err(|e| e.to_string())?; + if let Err(e) = write_line(state, &line).await { + state.pending.lock().await.remove(correlation_id); + return Err(e); + } + + let out = tokio::time::timeout(Duration::from_secs(30), rx).await; + if out.is_err() { + state.pending.lock().await.remove(correlation_id); + return Err("sidecar RPC timed out".to_string()); + } + + let value = out + .unwrap() + .map_err(|_| "sidecar RPC channel closed".to_string())?; + + if value.get("type").and_then(|t| t.as_str()) == Some("error") { + let msg = value + .get("error") + .and_then(|e| e.as_str()) + .unwrap_or("sidecar RPC error"); + return Err(msg.to_string()); + } + + Ok(value) +} + +/// Sends a prompt to Claude Code via the sidecar; returns the request id for event correlation. +/// sidecar 経由でプロンプトを送り、イベント相関用のリクエスト ID を返す。 +#[tauri::command] +pub async fn claude_query( + app: AppHandle, + state: State<'_, ClaudeSidecarState>, + prompt: String, + model: Option, + cwd: Option, + max_turns: Option, + allowed_tools: Option>, + resume: Option, + mcp_servers: Option, +) -> Result { + ensure_sidecar(&app, &state).await?; + + let id = uuid::Uuid::new_v4().to_string(); + let mut req = serde_json::json!({ + "type": "query", + "id": id, + "prompt": prompt, + }); + if let Some(m) = &model { + req["model"] = Value::String(m.clone()); + } + if let Some(c) = cwd { + req["cwd"] = Value::String(c); + } + if let Some(m) = max_turns { + req["maxTurns"] = Value::Number(m.into()); + } + if let Some(tools) = allowed_tools { + req["allowedTools"] = serde_json::to_value(tools).map_err(|e| e.to_string())?; + } + if let Some(r) = resume { + req["resume"] = Value::String(r); + } + if let Some(mcp) = mcp_servers { + if mcp.is_object() { + req["mcpServers"] = mcp; + } + } + + let line = serde_json::to_string(&req).map_err(|e| e.to_string())?; + write_line(&state, &line).await?; + Ok(id) +} + +/// Aborts a running query by id. +/// 実行中のクエリを ID で中断する。 +#[tauri::command] +pub async fn claude_abort( + app: AppHandle, + state: State<'_, ClaudeSidecarState>, + request_id: String, +) -> Result<(), String> { + ensure_sidecar(&app, &state).await?; + let req = serde_json::json!({ + "type": "abort", + "id": request_id, + }); + let line = serde_json::to_string(&req).map_err(|e| e.to_string())?; + write_line(&state, &line).await +} + +/// Returns sidecar processing status (idle / active query ids). +/// sidecar の処理状態(アイドル / アクティブなクエリ ID)を返す。 +#[tauri::command] +pub async fn claude_status( + app: AppHandle, + state: State<'_, ClaudeSidecarState>, +) -> Result { + ensure_sidecar(&app, &state).await?; + + let correlation_id = uuid::Uuid::new_v4().to_string(); + let req = serde_json::json!({ + "type": "status", + "correlationId": correlation_id, + }); + + rpc_json(&app, &state, req, &correlation_id).await +} + +/// Checks whether the Claude Code CLI is installed (`claude --version`). +/// Claude Code CLI がインストールされているか(`claude --version`)を確認する。 +#[tauri::command] +pub async fn check_claude_installation( + app: AppHandle, + state: State<'_, ClaudeSidecarState>, +) -> Result { + ensure_sidecar(&app, &state).await?; + + let correlation_id = uuid::Uuid::new_v4().to_string(); + let req = serde_json::json!({ + "type": "check_installation", + "correlationId": correlation_id, + }); + + rpc_json(&app, &state, req, &correlation_id).await +} + +/// Lists available Claude models via the sidecar (SDK `supportedModels()`). +/// sidecar 経由で利用可能な Claude モデル一覧を取得する。 +#[tauri::command] +pub async fn claude_list_models( + app: AppHandle, + state: State<'_, ClaudeSidecarState>, +) -> Result { + ensure_sidecar(&app, &state).await?; + + let correlation_id = uuid::Uuid::new_v4().to_string(); + let req = serde_json::json!({ + "type": "list_models", + "correlationId": correlation_id, + }); + + rpc_json(&app, &state, req, &correlation_id).await +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 00000000..424dead2 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,31 @@ +mod claude_sidecar; +mod workspace_paths; + +/// Tauri アプリケーション共通エントリポイント。 +/// デスクトップ (main.rs) とモバイル (mobile entry point) の両方から呼ばれる。 +/// +/// Common entry point for the Tauri application. +/// Called from both desktop (main.rs) and mobile entry points. +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_store::Builder::default().build()) + .manage(claude_sidecar::ClaudeSidecarState::new()) + .invoke_handler(tauri::generate_handler![ + claude_sidecar::claude_query, + claude_sidecar::claude_abort, + claude_sidecar::claude_status, + claude_sidecar::check_claude_installation, + claude_sidecar::claude_list_models, + workspace_paths::list_workspace_directory_entries, + workspace_paths::register_note_workspace_root, + workspace_paths::clear_note_workspace_root, + workspace_paths::read_note_workspace_file, + workspace_paths::list_note_workspace_entries, + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 00000000..d475a598 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + app_lib::run() +} diff --git a/src-tauri/src/workspace_paths.rs b/src-tauri/src/workspace_paths.rs new file mode 100644 index 00000000..fab569c9 --- /dev/null +++ b/src-tauri/src/workspace_paths.rs @@ -0,0 +1,365 @@ +//! Workspace-relative paths: process cwd (slash completion) and note-linked roots (Issue #461). +//! プロセス cwd 基準(スラッシュ補完)とノート紐付けルート(Issue #461)。 + +use std::collections::HashMap; +use std::fs; +use std::fs::File; +use std::io::Read; +use std::path::{Component, Path, PathBuf}; +use std::sync::{Mutex, OnceLock}; + +use serde::{Deserialize, Serialize}; + +/// Serializes read-modify-write on the note workspace registry file (concurrent IPC). +/// ノートワークスペースレジストリの read-modify-write を直列化する(並行 IPC)。 +static REGISTRY_MUTEX: OnceLock> = OnceLock::new(); + +fn registry_mutex() -> &'static Mutex<()> { + REGISTRY_MUTEX.get_or_init(|| Mutex::new(())) +} + +/// Runs `f` while holding the registry lock (register/clear only). +/// レジストリロック保持中に `f` を実行する(register/clear のみ)。 +fn with_registry_write_lock(f: F) -> Result +where + F: FnOnce() -> Result, +{ + let _guard = registry_mutex() + .lock() + .map_err(|e| format!("registry mutex poisoned: {e}"))?; + f() +} + +/// Writes `contents` to `path` via a same-directory temp file + rename (crash-safe partial writes). +/// 同一ディレクトリの一時ファイルへ書き込み後にリネーム(クラッシュ時の部分書き込みを避ける)。 +fn atomic_write_file(path: &Path, contents: &[u8]) -> Result<(), String> { + let file_name = path + .file_name() + .ok_or_else(|| "registry path has no file name".to_string())?; + let mut tmp_name = file_name.to_os_string(); + tmp_name.push(".tmp"); + let parent = path + .parent() + .ok_or_else(|| "registry path has no parent".to_string())?; + let tmp_path = parent.join(tmp_name); + fs::write(&tmp_path, contents).map_err(|e| e.to_string())?; + fs::rename(&tmp_path, path).map_err(|e| e.to_string()) +} + +/// Maximum bytes returned by {@link read_note_workspace_file}. +/// {@link read_note_workspace_file} が返す最大バイト数。 +const MAX_NOTE_WORKSPACE_FILE_BYTES: u64 = 512 * 1024; + +/// Default cap for {@link list_note_workspace_entries}. +/// {@link list_note_workspace_entries} の既定上限。 +const DEFAULT_NOTE_WORKSPACE_MAX_ENTRIES: u32 = 500; + +/// Hard cap for list entries (API + UI abuse mitigation). +/// 列挙件数の上限(API 悪用緩和)。 +const HARD_MAX_NOTE_WORKSPACE_ENTRIES: u32 = 2000; + +/// Persisted mapping note id → canonical workspace root (desktop; Issue #461). +/// ノート ID → 正規化済みワークスペースルートの永続マップ(デスクトップ、Issue #461)。 +#[derive(Debug, Default, Serialize, Deserialize)] +struct NoteWorkspaceRegistry { + roots: HashMap, +} + +/// Resolves `relative` under an already-canonicalized root (traversal-safe). +/// Lexical joins first, then `canonicalize` on the resolved path or the longest existing prefix. +/// 正規化済みルート配下に解決。字句結合後に終端または最長の存在接頭辞を `canonicalize`。 +pub(crate) fn resolve_under_root(root_canon: &PathBuf, relative: &str) -> Result { + let trimmed = relative.trim(); + if trimmed.is_empty() { + return Ok(root_canon.clone()); + } + let mut acc = root_canon.clone(); + for comp in Path::new(trimmed).components() { + match comp { + Component::Normal(c) => acc.push(c), + Component::ParentDir => { + acc.pop(); + if !acc.starts_with(root_canon) { + return Err("path outside workspace".into()); + } + } + Component::CurDir => {} + Component::RootDir | Component::Prefix(_) => { + return Err("invalid path".into()); + } + } + } + if acc.exists() { + let canon = acc.canonicalize().map_err(|e| e.to_string())?; + if !canon.starts_with(root_canon) { + return Err("path outside workspace".into()); + } + return Ok(canon); + } + // Non-existent leaf: walk up to the longest existing prefix so symlink escapes are still detected. + let mut check = acc.clone(); + loop { + if check.exists() { + let canon = check.canonicalize().map_err(|e| e.to_string())?; + if !canon.starts_with(root_canon) { + return Err("path outside workspace".into()); + } + break; + } + if check == *root_canon { + break; + } + if !check.pop() { + break; + } + } + if !acc.starts_with(root_canon) { + return Err("path outside workspace".into()); + } + Ok(acc) +} + +/// Re-canonicalize immediately before opening to narrow TOCTOU vs symlink swap (not full `openat` hardening). +/// オープン直前に再 canonicalize して TOCTOU を狭める(openat 相当の完全対策ではない)。 +fn assert_still_under_root(root_canon: &PathBuf, path: &Path) -> Result { + let canon = path.canonicalize().map_err(|e| e.to_string())?; + if !canon.starts_with(root_canon) { + return Err("path outside workspace".into()); + } + Ok(canon) +} + +fn canonical_note_workspace_root(workspace_root: &str) -> Result { + let trimmed = workspace_root.trim(); + if trimmed.is_empty() { + return Err("empty workspace root".into()); + } + let p = PathBuf::from(trimmed); + let canon = p.canonicalize().map_err(|e| e.to_string())?; + if !canon.is_dir() { + return Err("workspace root is not a directory".into()); + } + Ok(canon) +} + +/// Validates reserved keys and returns a trimmed registry key (must match lookup keys). +/// 予約キーを検証し、レジストリキー用に trim 済み文字列を返す(参照キーと一致させる)。 +fn parse_note_id_key(note_id: String) -> Result { + let t = note_id.trim(); + if t.is_empty() { + return Err("invalid note id".into()); + } + match t { + "__proto__" | "prototype" | "constructor" => Err("invalid note id".into()), + _ => Ok(t.to_string()), + } +} + +fn registry_file() -> Result { + let dir = dirs::data_dir() + .ok_or_else(|| "no data directory".to_string())? + .join("zedi"); + fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + Ok(dir.join("note_workspace_roots.json")) +} + +fn load_registry() -> Result { + let path = registry_file()?; + if !path.exists() { + return Ok(NoteWorkspaceRegistry::default()); + } + let raw = fs::read_to_string(&path).map_err(|e| e.to_string())?; + serde_json::from_str(&raw).map_err(|e| e.to_string()) +} + +fn save_registry(reg: &NoteWorkspaceRegistry) -> Result<(), String> { + let path = registry_file()?; + let raw = serde_json::to_string_pretty(reg).map_err(|e| e.to_string())?; + atomic_write_file(&path, raw.as_bytes()) +} + +fn resolve_registered_root(note_id: &str) -> Result { + let reg = load_registry()?; + let s = reg + .roots + .get(note_id) + .ok_or_else(|| "note workspace not registered".to_string())?; + canonical_note_workspace_root(s) +} + +/// Registers the canonical workspace root for a note (used by read/list commands; do not trust raw paths from IPC alone). +/// ノートのワークスペースルートを登録する(読み取りはここ経由。IPC の生パスだけは信用しない)。 +#[tauri::command] +pub fn register_note_workspace_root(note_id: String, workspace_root: String) -> Result<(), String> { + let note_id = parse_note_id_key(note_id)?; + let canon = canonical_note_workspace_root(&workspace_root)?; + with_registry_write_lock(|| { + let mut reg = load_registry()?; + reg.roots + .insert(note_id, canon.to_string_lossy().to_string()); + save_registry(®) + }) +} + +/// Removes the registered workspace root for a note. +/// ノートの登録済みワークスペースルートを削除する。 +#[tauri::command] +pub fn clear_note_workspace_root(note_id: String) -> Result<(), String> { + let note_id = parse_note_id_key(note_id)?; + with_registry_write_lock(|| { + let mut reg = load_registry()?; + reg.roots.remove(¬e_id); + save_registry(®) + }) +} + +/// Reads UTF-8 under `root_canon` with a single file handle and a hard byte cap (no metadata/read split). +/// 単一ファイルハンドルでバイト上限を強制(metadata と read の分離によるレースを避ける)。 +fn read_utf8_file_under_root(root_canon: &PathBuf, relative_path: &str) -> Result { + let target = resolve_under_root(root_canon, relative_path)?; + if !target.exists() { + return Err("not a file".into()); + } + let target = assert_still_under_root(root_canon, target.as_path())?; + if !target.is_file() { + return Err("not a file".into()); + } + let cap = MAX_NOTE_WORKSPACE_FILE_BYTES.saturating_add(1); + let mut buf = Vec::new(); + File::open(&target) + .map_err(|e| e.to_string())? + .take(cap) + .read_to_end(&mut buf) + .map_err(|e| e.to_string())?; + if buf.len() as u64 > MAX_NOTE_WORKSPACE_FILE_BYTES { + return Err("file too large".into()); + } + String::from_utf8(buf).map_err(|e| e.to_string()) +} + +/// Lists file and subdirectory names under `relative_dir` (relative to process cwd). +/// `relative_dir` が空なら cwd 直下を列挙する。 +/// Directories are suffixed with `/`. Hidden names (leading `.`) are skipped. +/// +/// 列挙先が cwd 外に出る場合はエラーにする(パストラバーサル対策)。 +/// 存在するパスは `canonicalize` してシンボリックリンク越しのエスケープを検出する。 +#[tauri::command] +pub fn list_workspace_directory_entries(relative_dir: String) -> Result, String> { + let cwd = std::env::current_dir().map_err(|e| e.to_string())?; + let cwd_canon = cwd.canonicalize().map_err(|e| e.to_string())?; + let target = resolve_under_root(&cwd_canon, &relative_dir)?; + if !target.exists() { + return Ok(vec![]); + } + let target = assert_still_under_root(&cwd_canon, target.as_path())?; + if !target.is_dir() { + return Ok(vec![]); + } + list_directory_names(&target, DEFAULT_NOTE_WORKSPACE_MAX_ENTRIES, None) +} + +/// Reads a UTF-8 text file under the registered workspace for `note_id`; size-capped via one handle. +/// 登録済み `note_id` のワークスペース配下の UTF-8 を読む(単一ハンドルでサイズ上限)。 +#[tauri::command] +pub fn read_note_workspace_file(note_id: String, relative_path: String) -> Result { + let note_id = parse_note_id_key(note_id)?; + let root_canon = resolve_registered_root(¬e_id)?; + let rel = relative_path.replace('\\', "/"); + read_utf8_file_under_root(&root_canon, &rel) +} + +/// Lists names in `relative_dir` under the registered workspace for `note_id`. +/// Optional `name_prefix` filters case-insensitively before the entry cap (large-dir completions). +/// 登録済み `note_id` のワークスペース配下で `relative_dir` を列挙する。 +/// `name_prefix` は上限適用前に大小無視で絞り込み(大きいディレクトリの補完向け)。 +#[tauri::command] +pub fn list_note_workspace_entries( + note_id: String, + relative_dir: String, + max_entries: Option, + name_prefix: Option, +) -> Result, String> { + let note_id = parse_note_id_key(note_id)?; + let cap = max_entries + .unwrap_or(DEFAULT_NOTE_WORKSPACE_MAX_ENTRIES) + .min(HARD_MAX_NOTE_WORKSPACE_ENTRIES); + let root_canon = resolve_registered_root(¬e_id)?; + let rel = relative_dir.replace('\\', "/"); + let target = resolve_under_root(&root_canon, &rel)?; + if !target.exists() { + return Ok(vec![]); + } + let target = assert_still_under_root(&root_canon, target.as_path())?; + if !target.is_dir() { + return Ok(vec![]); + } + let prefix = name_prefix + .as_deref() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()); + list_directory_names(&target, cap, prefix) +} + +/// Lists entry display names (file name or `name/` for dirs), optional case-insensitive prefix, then sort and cap. +/// エントリ名を列挙し、任意のプレフィックス(大小無視)で絞ってからソートして上限適用。 +fn list_directory_names( + target: &Path, + max_entries: u32, + name_prefix: Option<&str>, +) -> Result, String> { + let prefix_lower = name_prefix.map(|s| s.to_lowercase()); + let mut out: Vec = Vec::new(); + for entry in fs::read_dir(target).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let name = entry.file_name().to_string_lossy().to_string(); + let is_dir = entry.file_type().map_err(|e| e.to_string())?.is_dir(); + // Skip dotfiles except `.zedi/` (workspace metadata; Issue #461). + if name.starts_with('.') && !(name == ".zedi" && is_dir) { + continue; + } + let display = if is_dir { + format!("{name}/") + } else { + name + }; + if let Some(ref pl) = prefix_lower { + if !display.to_lowercase().starts_with(pl.as_str()) { + continue; + } + } + out.push(display); + } + out.sort(); + out.truncate(max_entries as usize); + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::tempdir; + + #[test] + fn resolve_under_root_rejects_parent_escape() { + let tmp = tempdir().unwrap(); + let root = tmp.path().canonicalize().unwrap(); + let err = resolve_under_root(&root, "..").unwrap_err(); + assert!(err.contains("outside") || err.contains("workspace")); + } + + #[test] + fn read_utf8_file_under_root_reads_utf8() { + let tmp = tempdir().unwrap(); + let sub = tmp.path().join("proj"); + fs::create_dir(&sub).unwrap(); + let f = sub.join("hello.txt"); + let mut file = fs::File::create(&f).unwrap(); + writeln!(file, "hi").unwrap(); + drop(file); + + let root = sub.canonicalize().unwrap(); + let text = read_utf8_file_under_root(&root, "hello.txt").unwrap(); + assert!(text.contains("hi")); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 00000000..4dddec33 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Zedi", + "version": "0.10.0", + "identifier": "com.zedi.app", + "build": { + "beforeDevCommand": "bun run dev", + "beforeBuildCommand": "bun run build", + "devUrl": "http://localhost:30000", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "Zedi", + "width": 1200, + "height": 800, + "minWidth": 800, + "minHeight": 600, + "resizable": true, + "fullscreen": false + } + ] + }, + "bundle": { + "active": true, + "targets": "all", + "externalBin": ["binaries/claude-sidecar"], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/src/App.tsx b/src/App.tsx index f06f4aed..48cd55f0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,6 +32,7 @@ import { GlobalShortcutsProvider } from "./components/layout/GlobalShortcutsProv import { ProtectedRoute } from "./components/auth/ProtectedRoute"; import { AIChatProvider } from "./contexts/AIChatContext"; import { AIChatConversationsProvider } from "./hooks/useAIChatConversations"; +import { FilePreviewDialogHost } from "./components/note/FilePreviewDialogHost"; const queryClient = new QueryClient(); @@ -57,6 +58,7 @@ const App = () => ( {/* unstable_useTransitions を無効化: リンク後の表示が遅れる問題を防ぐ(RR v7 は future 廃止) Disable unstable_useTransitions: prevents display delay after link navigation (RR v7 removed future flags) */} + diff --git a/src/components/ai-chat/AIChatHeader.tsx b/src/components/ai-chat/AIChatHeader.tsx index 9d3a3d2e..c0b59f79 100644 --- a/src/components/ai-chat/AIChatHeader.tsx +++ b/src/components/ai-chat/AIChatHeader.tsx @@ -1,19 +1,56 @@ +import { useState, useEffect } from "react"; import { Sparkles, ClipboardList, Plus, X, Maximize2 } from "lucide-react"; import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import { useAIChatStore } from "../../stores/aiChatStore"; import { AI_CHAT_BASE_PATH, aiChatConversationPath } from "@/constants/aiChatSidebar"; +import { AI_SETTINGS_CHANGED_EVENT, loadAISettings } from "@/lib/aiSettings"; +import { + type AIInteractionMode, + getInteractionMode, + getProviderById, + DEFAULT_AI_SETTINGS, +} from "@/types/ai"; +import { McpStatusIndicator } from "./McpStatusIndicator"; /** - * AI chat dock header: list, new chat, open full page, close. - * AI チャットドックのヘッダー(一覧・新規・フルページで開く・閉じる)。 + * AI chat dock header: list, new chat, open full page, close, and mode badge. + * AI チャットドックのヘッダー(一覧・新規・フルページで開く・閉じる・モードバッジ)。 */ export function AIChatHeader() { const { t } = useTranslation(); const navigate = useNavigate(); + const location = useLocation(); const { closePanel, toggleConversationList, setActiveConversation, activeConversationId } = useAIChatStore(); + const [modeInfo, setModeInfo] = useState<{ + mode: AIInteractionMode; + providerName?: string; + }>({ mode: "default" }); + + useEffect(() => { + let cancelled = false; + const refresh = async () => { + const settings = await loadAISettings(); + if (cancelled) return; + const s = settings ?? DEFAULT_AI_SETTINGS; + const mode = getInteractionMode(s); + const providerName = + mode === "user_api_key" ? (getProviderById(s.provider)?.name ?? s.provider) : undefined; + setModeInfo({ mode, providerName }); + }; + void refresh(); + const onSettingsChanged = () => { + void refresh(); + }; + window.addEventListener(AI_SETTINGS_CHANGED_EVENT, onSettingsChanged); + return () => { + cancelled = true; + window.removeEventListener(AI_SETTINGS_CHANGED_EVENT, onSettingsChanged); + }; + }, [location.pathname, location.search]); + const handleNewConversation = () => { setActiveConversation(null); }; @@ -27,11 +64,38 @@ export function AIChatHeader() { closePanel(); }; + const handleOpenSettings = () => { + navigate("/settings?section=ai"); + closePanel(); + }; + + const modeLabel = (() => { + switch (modeInfo.mode) { + case "default": + return t("aiChat.mode.default"); + case "user_api_key": + return modeInfo.providerName + ? t("aiChat.mode.apiKeyWithProvider", { provider: modeInfo.providerName }) + : t("aiChat.mode.apiKey"); + case "claude_code": + return t("aiChat.mode.claudeCode"); + } + })(); + return (

{t("aiChat.title")}

+ + {modeInfo.mode === "claude_code" && }
+ )} + {!isUser && !message.isStreaming && displayContent && ( + + )} + {message.modelDisplayName && ( + + {message.modelDisplayName} + + )} +
)} {siblingTotal != null && siblingIndex != null && onSwitchBranch && ( @@ -129,6 +180,10 @@ export function AIChatMessage({ /> )} + {!isUser && message.toolExecutions && message.toolExecutions.length > 0 && ( + + )} + {message.error &&
{message.error}
} {message.actions && message.actions.length > 0 && onExecuteAction && ( diff --git a/src/components/ai-chat/AIChatMessages.tsx b/src/components/ai-chat/AIChatMessages.tsx index 010a8ed9..a995a648 100644 --- a/src/components/ai-chat/AIChatMessages.tsx +++ b/src/components/ai-chat/AIChatMessages.tsx @@ -16,6 +16,11 @@ interface AIChatMessagesProps { onSuggestionClick: (text: string) => void; onExecuteAction?: (action: ChatAction) => void; onEditMessage?: (messageId: string, newContent: string) => void; + /** + * カーソル位置にメッセージ内容を挿入するコールバック。 + * Callback to insert message content at editor cursor position. + */ + onInsertToNote?: (markdown: string) => void; /** Switch active branch at a fork. / 分岐点で表示ブランチを切り替え */ onSwitchBranch?: (messageId: string, direction: "prev" | "next") => void; isStreaming?: boolean; @@ -33,6 +38,7 @@ export function AIChatMessages({ onSuggestionClick, onExecuteAction, onEditMessage, + onInsertToNote, onSwitchBranch, isStreaming = false, className, @@ -70,6 +76,7 @@ export function AIChatMessages({ message={message} onExecuteAction={onExecuteAction} onEditMessage={onEditMessage} + onInsertToNote={onInsertToNote} siblingIndex={hasSiblings ? index : undefined} siblingTotal={hasSiblings ? siblings.length : undefined} onSwitchBranch={ diff --git a/src/components/ai-chat/AIChatModelSelector.tsx b/src/components/ai-chat/AIChatModelSelector.tsx index f54d2494..9aa12351 100644 --- a/src/components/ai-chat/AIChatModelSelector.tsx +++ b/src/components/ai-chat/AIChatModelSelector.tsx @@ -5,77 +5,149 @@ import { useAIChatStore } from "../../stores/aiChatStore"; import { fetchServerModels } from "../../lib/aiService"; import { getSonnetBaseline, formatCostMultiplierLabel } from "../../lib/aiCostUtils"; import { loadAISettings } from "../../lib/aiSettings"; -import type { AIModel } from "../../types/ai"; +import { isTauriDesktop } from "../../lib/platform"; +import type { AIModel, AIInteractionMode, AIProviderType } from "../../types/ai"; +import { getInteractionMode } from "../../types/ai"; import { cn } from "@zedi/ui"; +interface DisplayModel { + id: string; + provider: AIProviderType; + modelId: string; + displayName: string; + inputCostUnits?: number; + outputCostUnits?: number; +} + +/** + * Fallback when sidecar is unavailable or the app is not running in Tauri (web build). + * sidecar 不可時、または Tauri 外(Web ビルド)のときの Claude Code モデル一覧の既定。 + */ +const DEFAULT_CLAUDE_CODE_MODELS: DisplayModel[] = [ + { + id: "claude-code:claude-sonnet-4-6", + provider: "claude-code", + modelId: "claude-sonnet-4-6", + displayName: "Claude Sonnet 4.6", + }, +]; + +/** + * Resolves which Claude Code model to select after loading the list. + * 一覧取得後に選ぶ Claude Code モデルを解決する。 + */ +function resolveClaudeInitialSelection( + claudeModels: DisplayModel[], + current: { id: string; provider: AIProviderType } | null, + savedModelId: string | undefined, +): DisplayModel | undefined { + if (claudeModels.length === 0) return undefined; + const matchedCurrent = + current?.provider === "claude-code" ? claudeModels.find((m) => m.id === current.id) : undefined; + if (matchedCurrent) return undefined; + const matchedSaved = savedModelId ? claudeModels.find((m) => m.id === savedModelId) : undefined; + return matchedSaved ?? claudeModels[0]; +} + +/** + * Resolves which server model to select when the current store id is not in the list. + * ストアの選択が一覧に無いときに選ぶサーバーモデルを解決する。 + */ +function resolveServerInitialSelection( + available: AIModel[], + current: { id: string } | null, + savedModelId: string | undefined, +): AIModel | undefined { + if (available.length === 0) return undefined; + const matchedCurrent = current ? available.find((m) => m.id === current.id) : undefined; + if (matchedCurrent) return undefined; + const matched = savedModelId ? available.find((m) => m.id === savedModelId) : null; + return matched ?? available[0]; +} + /** + * Chat-panel model selector. Behaviour varies by interaction mode: + * - default: shows all available server models + * - user_api_key: filters to the configured provider only + * - claude_code: fetches available Claude models from the sidecar * + * チャットパネルのモデルセレクター。利用モードに応じて動作が変わる。 */ export function AIChatModelSelector() { - /** - * - */ const { t } = useTranslation(); - /** - * - */ - const { selectedModel, setSelectedModel, isStreaming } = useAIChatStore(); - /** - * - */ - const [models, setModels] = useState([]); - /** - * - */ + const { setSelectedModel, isStreaming } = useAIChatStore(); + const [models, setModels] = useState([]); + const [serverAIModels, setServerAIModels] = useState([]); const [loading, setLoading] = useState(false); - /** - * - */ const [open, setOpen] = useState(false); - /** - * - */ + const [mode, setMode] = useState("default"); const containerRef = useRef(null); - // モデル一覧をロード - /** - * - */ const loadModels = useCallback(async () => { setLoading(true); try { - /** - * - */ + const settings = await loadAISettings(); + const currentMode = settings ? getInteractionMode(settings) : "default"; + setMode(currentMode); + + if (currentMode === "claude_code") { + let claudeModels: DisplayModel[] = [...DEFAULT_CLAUDE_CODE_MODELS]; + if (isTauriDesktop()) { + try { + const { claudeListModels } = await import("@/lib/claudeCode/bridge"); + const result = await claudeListModels(); + claudeModels = result.models.map((m) => ({ + id: `claude-code:${m.value}`, + provider: "claude-code", + modelId: m.value, + displayName: m.displayName, + })); + } catch { + claudeModels = [...DEFAULT_CLAUDE_CODE_MODELS]; + } + } + setModels(claudeModels); + setServerAIModels([]); + + const current = useAIChatStore.getState().selectedModel; + if (claudeModels.length === 0) { + setSelectedModel(null); + } else { + const initial = resolveClaudeInitialSelection(claudeModels, current, settings?.modelId); + if (initial) { + setSelectedModel({ + id: initial.id, + provider: "claude-code", + model: initial.modelId, + displayName: initial.displayName, + }); + } + } + return; + } + const { models: serverModels } = await fetchServerModels(); - /** - * - */ - const available = serverModels.filter((m) => m.available); - setModels(available); - - // 初回: selectedModel がまだ null なら設定画面のモデル or デフォルトを選択 - if (!selectedModel && available.length > 0) { - /** - * - */ - const settings = await loadAISettings(); - /** - * - */ - const savedModelId = settings?.modelId; - /** - * - */ - const matched = savedModelId ? available.find((m) => m.id === savedModelId) : null; - /** - * - */ - const first = available[0]; - /** - * - */ - const initial = matched ?? first; + let available = serverModels.filter((m) => m.available); + if (currentMode === "user_api_key" && settings) { + available = available.filter((m) => m.provider === settings.provider); + } + setServerAIModels(available); + setModels( + available.map((m) => ({ + id: m.id, + provider: m.provider, + modelId: m.modelId, + displayName: m.displayName, + inputCostUnits: m.inputCostUnits, + outputCostUnits: m.outputCostUnits, + })), + ); + + const current = useAIChatStore.getState().selectedModel; + if (available.length === 0) { + setSelectedModel(null); + } else { + const initial = resolveServerInitialSelection(available, current, settings?.modelId); if (initial) { setSelectedModel({ id: initial.id, @@ -88,22 +160,18 @@ export function AIChatModelSelector() { } } } catch { - // フォールバック: 設定画面のモデルを使用 + // fallback: use settings model } finally { setLoading(false); } - }, [selectedModel, setSelectedModel]); + }, [setSelectedModel]); useEffect(() => { loadModels(); }, [loadModels]); - // 外部クリックで閉じる useEffect(() => { if (!open) return; - /** - * - */ const handleClick = (e: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setOpen(false); @@ -113,11 +181,8 @@ export function AIChatModelSelector() { return () => document.removeEventListener("mousedown", handleClick); }, [open]); - /** - * - */ const handleSelect = useCallback( - (model: AIModel) => { + (model: DisplayModel) => { setSelectedModel({ id: model.id, provider: model.provider, @@ -131,20 +196,10 @@ export function AIChatModelSelector() { [setSelectedModel], ); - /** - * - */ + const selectedModel = useAIChatStore((s) => s.selectedModel); const displayLabel = selectedModel?.displayName ?? t("aiChat.modelSelector.select"); - - /** - * - */ - const sonnetBaseline = getSonnetBaseline(models); - /** - * - */ - const getCostLabel = (model: AIModel) => - formatCostMultiplierLabel(model.inputCostUnits, sonnetBaseline); + const sonnetBaseline = getSonnetBaseline(serverAIModels); + const showCost = mode !== "claude_code"; return (
@@ -173,18 +228,13 @@ export function AIChatModelSelector() { {open && models.length > 0 && (
{models.map((model) => { - /** - * - */ const isSelected = selectedModel?.id === model.id; - /** - * - */ - const costLabel = getCostLabel(model); - /** - * - */ - const isCheaperOrBaseline = model.inputCostUnits <= sonnetBaseline; + const costLabel = + showCost && model.inputCostUnits != null + ? formatCostMultiplierLabel(model.inputCostUnits, sonnetBaseline) + : null; + const isCheaperOrBaseline = + model.inputCostUnits != null && model.inputCostUnits <= sonnetBaseline; return ( diff --git a/src/components/ai-chat/AIChatPanelContent.tsx b/src/components/ai-chat/AIChatPanelContent.tsx index 9601550c..06ceed75 100644 --- a/src/components/ai-chat/AIChatPanelContent.tsx +++ b/src/components/ai-chat/AIChatPanelContent.tsx @@ -1,4 +1,6 @@ -import { Suspense, lazy } from "react"; +import { Suspense, lazy, useCallback, useLayoutEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { cn, useToast } from "@zedi/ui"; import { AIChatHeader } from "./AIChatHeader"; import { AIChatViewTabs } from "./AIChatViewTabs"; import { AIChatInput } from "./AIChatInput"; @@ -6,11 +8,16 @@ import { AIChatMessages } from "./AIChatMessages"; import { AIChatContextBar } from "./AIChatContextBar"; import { AIChatConversationList } from "./AIChatConversationList"; import { useAIChatPanelContentLogic } from "@/hooks/useAIChatPanelContentLogic"; +import { useAIChatContext } from "@/contexts/AIChatContext"; const AIChatBranchTree = lazy(() => import("./AIChatBranchTree").then((m) => ({ default: m.AIChatBranchTree })), ); +const AIChatWorkflowPanel = lazy(() => + import("./AIChatWorkflowPanel").then((m) => ({ default: m.AIChatWorkflowPanel })), +); + /** * Props for {@link AIChatPanelContent}. * {@link AIChatPanelContent} 向けプロパティ。 @@ -63,6 +70,40 @@ export function AIChatPanelContent({ contextEnabled, }); + const { t } = useTranslation(); + const { toast } = useToast(); + const { insertAtCursorRef, pageContext } = useAIChatContext(); + + const handleInsertToNote = useCallback( + (markdown: string) => { + const ok = insertAtCursorRef.current?.(markdown); + if (ok) { + toast({ title: t("aiChat.notifications.insertSuccess") }); + } else { + toast({ + title: t("aiChat.notifications.insertUnavailable"), + variant: "destructive", + }); + } + }, + [insertAtCursorRef, t, toast], + ); + + const canInsert = pageContext?.type === "editor"; + + /** After the workflow tab is visited once, keep the panel mounted so run state survives tab switches. / ワークフロータブを一度開いたらマウントを維持し、タブ切替で実行状態を失わない */ + const [keepWorkflowMounted, setKeepWorkflowMounted] = useState( + () => activeViewTab === "workflow", + ); + useLayoutEffect(() => { + if (activeViewTab === "workflow") { + // Latch: after first visit to the workflow tab, keep the panel mounted so run/pause state survives tab switches. + // 初回表示後はマウントを維持し、タブ切替で実行状態を失わない。 + // eslint-disable-next-line react-hooks/set-state-in-effect -- one-way latch from tab selection (not external sync) + setKeepWorkflowMounted(true); + } + }, [activeViewTab]); + return (
@@ -88,10 +129,11 @@ export function AIChatPanelContent({ onSuggestionClick={handleSendMessage} onExecuteAction={handleExecuteAction} onEditMessage={handleEditMessage} + onInsertToNote={canInsert ? handleInsertToNote : undefined} onSwitchBranch={switchBranch} isStreaming={isStreaming} /> - ) : ( + ) : activeViewTab === "branch" ? ( - )} + ) : null} + {keepWorkflowMounted ? ( +
+ + + +
+ ) : null}
-
+ {/* Stay mounted on workflow tab so uncontrolled input draft is not lost. / ワークフロー切替で下書きを失わない */} +
{ vi.clearAllMocks(); }); - it("renders Chat and Branch tabs", () => { + it("renders Chat, Branch, and Workflow tabs", () => { const onTabChange = vi.fn(); render(); expect(screen.getByRole("tab", { name: "aiChat.viewTabs.chat" })).toBeInTheDocument(); expect(screen.getByRole("tab", { name: "aiChat.viewTabs.branch" })).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: "aiChat.viewTabs.workflow" })).toBeInTheDocument(); }); it("Branch tab is always enabled", () => { @@ -47,4 +48,12 @@ describe("AIChatViewTabs", () => { await user.click(screen.getByRole("tab", { name: "aiChat.viewTabs.chat" })); expect(onTabChange).toHaveBeenCalledWith("chat"); }); + + it("calls onTabChange with workflow when Workflow tab is clicked", async () => { + const user = userEvent.setup(); + const onTabChange = vi.fn(); + render(); + await user.click(screen.getByRole("tab", { name: "aiChat.viewTabs.workflow" })); + expect(onTabChange).toHaveBeenCalledWith("workflow"); + }); }); diff --git a/src/components/ai-chat/AIChatViewTabs.tsx b/src/components/ai-chat/AIChatViewTabs.tsx index 2512a43d..b158b146 100644 --- a/src/components/ai-chat/AIChatViewTabs.tsx +++ b/src/components/ai-chat/AIChatViewTabs.tsx @@ -1,12 +1,12 @@ import { useTranslation } from "react-i18next"; -import { MessageSquare, GitBranch } from "lucide-react"; +import { MessageSquare, GitBranch, ListChecks } from "lucide-react"; import { Tabs, TabsList, TabsTrigger } from "@zedi/ui"; /** * Active AI chat panel view: threaded messages or branch tree. * AI チャットパネルの表示:スレッド表示かブランチツリーか。 */ -export type AIChatViewTab = "chat" | "branch"; +export type AIChatViewTab = "chat" | "branch" | "workflow"; /** * Props for {@link AIChatViewTabs}. @@ -43,6 +43,13 @@ export function AIChatViewTabs({ activeTab, onTabChange }: AIChatViewTabsProps) {t("aiChat.viewTabs.branch")} + + + {t("aiChat.viewTabs.workflow")} + ); diff --git a/src/components/ai-chat/AIChatWikiLink.tsx b/src/components/ai-chat/AIChatWikiLink.tsx index 7f2631b9..5330374a 100644 --- a/src/components/ai-chat/AIChatWikiLink.tsx +++ b/src/components/ai-chat/AIChatWikiLink.tsx @@ -1,33 +1,178 @@ +import { useState, useRef, useCallback, useEffect } from "react"; import { Link } from "react-router-dom"; +import { HoverCard, HoverCardTrigger, HoverCardContent } from "@zedi/ui"; +import { useWikiLinkNavigation } from "@/components/editor/TiptapEditor/useWikiLinkNavigation"; +import { CreatePageDialog } from "@/components/editor/TiptapEditor/CreatePageDialog"; import { usePageStore } from "../../stores/pageStore"; +import { WikiLinkPreviewContent } from "../wiki-link/WikiLinkPreviewContent"; interface AIChatWikiLinkProps { /** WikiLink title (e.g. from [[Title]]) */ title: string; } +const OPEN_DELAY_MS = 300; +const CLOSE_DELAY_MS = 200; +const LONG_PRESS_MS = 500; + /** - * Renders a clickable WikiLink in AI chat. Resolves title to page id; - * existing pages link to /page/:id, missing pages render as ghost style. + * AI チャット内の WikiLink をレンダリングする。 + * ホバーでページプレビューを表示し、クリックでページ遷移する。 + * モバイルでは長押しでプレビューを表示する。 + * + * Renders a clickable WikiLink in AI chat with hover preview. + * Existing pages link to /page/:id, missing pages render as ghost style. + * Supports long-press preview on mobile. */ export function AIChatWikiLink({ title }: AIChatWikiLinkProps) { const normalizedTitle = title.trim(); const page = usePageStore((state) => state.getPageByTitle(normalizedTitle)); + const referenced = usePageStore( + (state) => !page && state.ghostLinks.some((gl) => gl.linkText === normalizedTitle), + ); + + const { + handleLinkClick: navigateWikiLinkByTitle, + createPageDialogOpen, + pendingCreatePageTitle, + handleConfirmCreate, + handleCancelCreate, + } = useWikiLinkNavigation(); + + const [isOpen, setIsOpen] = useState(false); + const contentRef = useRef(null); + const longPressTimerRef = useRef(undefined); + const preventClickResetTimerRef = useRef(undefined); + const preventClickRef = useRef(false); + + const handleTouchStart = useCallback(() => { + longPressTimerRef.current = window.setTimeout(() => { + setIsOpen(true); + preventClickRef.current = true; + }, LONG_PRESS_MS); + }, []); + + const handleTouchEnd = useCallback(() => { + clearTimeout(longPressTimerRef.current); + clearTimeout(preventClickResetTimerRef.current); + preventClickResetTimerRef.current = window.setTimeout(() => { + preventClickRef.current = false; + }, 100); + }, []); + + const handleTouchMove = useCallback(() => { + clearTimeout(longPressTimerRef.current); + }, []); + + /** + * OS ジェスチャ等で touch がキャンセルされたとき long-press が遅延発火しないようクリアする。 + * Clears long-press timers when touch is cancelled (e.g. OS gestures) so the card does not open late. + */ + const handleTouchCancel = useCallback(() => { + clearTimeout(longPressTimerRef.current); + clearTimeout(preventClickResetTimerRef.current); + preventClickRef.current = false; + }, []); + + const handleAnchorClick = useCallback((e: React.MouseEvent) => { + if (preventClickRef.current) { + e.preventDefault(); + } + }, []); + + const handleCardClick = useCallback(() => { + setIsOpen(false); + navigateWikiLinkByTitle(normalizedTitle); + }, [normalizedTitle, navigateWikiLinkByTitle]); + + const handleGhostTriggerClick = useCallback( + (e: React.MouseEvent) => { + if (preventClickRef.current) { + e.preventDefault(); + return; + } + navigateWikiLinkByTitle(normalizedTitle); + }, + [normalizedTitle, navigateWikiLinkByTitle], + ); + + useEffect(() => { + return () => { + clearTimeout(longPressTimerRef.current); + clearTimeout(preventClickResetTimerRef.current); + }; + }, []); + + // Close on outside touch and scroll (mobile) + // モバイルで外部タッチ・スクロール時にカードを閉じる + useEffect(() => { + if (!isOpen) return; + const handleOutsideTouch = (e: TouchEvent) => { + if (contentRef.current?.contains(e.target as Node)) return; + setIsOpen(false); + }; + const handleScroll = () => setIsOpen(false); + const timer = window.setTimeout(() => { + document.addEventListener("touchstart", handleOutsideTouch); + }, 100); + window.addEventListener("scroll", handleScroll, true); + return () => { + clearTimeout(timer); + document.removeEventListener("touchstart", handleOutsideTouch); + window.removeEventListener("scroll", handleScroll, true); + }; + }, [isOpen]); - if (page) { - return ( - - [[{normalizedTitle}]] - - ); - } + const touchProps = { + onTouchStart: handleTouchStart, + onTouchEnd: handleTouchEnd, + onTouchMove: handleTouchMove, + onTouchCancel: handleTouchCancel, + }; return ( - - [[{normalizedTitle}]] - + + + {page ? ( + + [[{normalizedTitle}]] + + ) : ( + + )} + + + + + + ); } diff --git a/src/components/ai-chat/AIChatWorkflowPanel.tsx b/src/components/ai-chat/AIChatWorkflowPanel.tsx new file mode 100644 index 00000000..a474adff --- /dev/null +++ b/src/components/ai-chat/AIChatWorkflowPanel.tsx @@ -0,0 +1,16 @@ +/** + * Multi-step Claude Code workflow UI (Issue #462). + * Claude Code マルチステップワークフロー UI(Issue #462)。 + */ + +import { useWorkflowPanelLogic } from "@/hooks/useWorkflowPanelLogic"; +import { WorkflowPanelForm } from "./WorkflowPanelForm"; + +/** + * Workflow editor and runner embedded in the AI chat panel. + * AI チャットパネルに埋め込むワークフロー編集・実行。 + */ +export function AIChatWorkflowPanel() { + const logic = useWorkflowPanelLogic(); + return ; +} diff --git a/src/components/ai-chat/McpStatusIndicator.tsx b/src/components/ai-chat/McpStatusIndicator.tsx new file mode 100644 index 00000000..5d35ed89 --- /dev/null +++ b/src/components/ai-chat/McpStatusIndicator.tsx @@ -0,0 +1,97 @@ +/** + * MCP サーバー接続状態インジケーター(Issue #463)。 + * MCP server connection status indicator (Issue #463). + * + * AI チャットヘッダーに表示し、接続中のサーバー数をバッジで示す。 + * Shown in the AI chat header with a badge indicating connected server count. + */ + +import { Plug } from "lucide-react"; +import { + Badge, + Popover, + PopoverContent, + PopoverTrigger, + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@zedi/ui"; +import { useTranslation } from "react-i18next"; +import { useMcpConfigStore } from "@/stores/mcpConfigStore"; +import type { McpConnectionStatus } from "@/types/mcp"; + +/** + * MCP ステータスのドット色を返す。 + * Returns a dot color class for MCP status. + */ +function statusDotClass(status: McpConnectionStatus): string { + switch (status) { + case "connected": + return "bg-green-500"; + case "pending": + return "bg-yellow-500"; + case "failed": + case "needs-auth": + return "bg-red-500"; + case "disabled": + return "bg-gray-400"; + case "unknown": + default: + return "bg-gray-300"; + } +} + +/** + * MCP サーバー接続状態インジケーター。サーバーが 1 つ以上あるときのみ表示。 + * MCP server connection status indicator. Only renders when at least one server exists. + */ +export function McpStatusIndicator() { + const { t } = useTranslation(); + const servers = useMcpConfigStore((s) => s.servers); + + if (servers.length === 0) return null; + + const enabledServers = servers.filter((s) => s.enabled); + const connectedCount = enabledServers.filter((s) => s.status === "connected").length; + const totalEnabled = enabledServers.length; + + return ( + + + + + + + + {t("aiSettings.mcp.title")} + + + +

{t("aiSettings.mcp.title")}

+
+ {servers.map((server) => ( +
+ + {server.name} + + {t(`aiSettings.mcp.status.${server.status}`)} + +
+ ))} +
+
+
+ ); +} diff --git a/src/components/ai-chat/ToolExecutionStatus.tsx b/src/components/ai-chat/ToolExecutionStatus.tsx new file mode 100644 index 00000000..f9778e59 --- /dev/null +++ b/src/components/ai-chat/ToolExecutionStatus.tsx @@ -0,0 +1,71 @@ +import { CheckCircle2, Loader2 } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import type { ToolExecution } from "../../types/aiChat"; +import { cn } from "@zedi/ui"; + +/** + * Maps known Claude Code tool names to user-friendly i18n keys. + * Claude Code の既知ツール名をユーザーフレンドリーな i18n キーに対応付ける。 + */ +function getToolI18nKey(toolName: string): string { + switch (toolName) { + case "Read": + return "aiChat.toolStatus.read"; + case "Write": + return "aiChat.toolStatus.write"; + case "Bash": + return "aiChat.toolStatus.bash"; + case "WebSearch": + return "aiChat.toolStatus.webSearch"; + case "Glob": + return "aiChat.toolStatus.glob"; + case "Grep": + return "aiChat.toolStatus.grep"; + case "LS": + return "aiChat.toolStatus.ls"; + default: + return ""; + } +} + +interface ToolExecutionStatusProps { + toolExecutions: ToolExecution[]; + className?: string; +} + +/** + * Displays a compact list of active/completed tool executions during Claude Code streaming. + * Claude Code ストリーミング中のツール実行状況をコンパクトに表示する。 + */ +export function ToolExecutionStatus({ toolExecutions, className }: ToolExecutionStatusProps) { + const { t } = useTranslation(); + + if (toolExecutions.length === 0) return null; + + return ( +
+ {toolExecutions.map((exec, idx) => { + const isRunning = exec.status === "running"; + const i18nKey = getToolI18nKey(exec.toolName); + const label = i18nKey ? t(i18nKey) : exec.toolName; + + return ( +
+ {isRunning ? ( + + ) : ( + + )} + {label} +
+ ); + })} +
+ ); +} diff --git a/src/components/ai-chat/WorkflowPanelForm.tsx b/src/components/ai-chat/WorkflowPanelForm.tsx new file mode 100644 index 00000000..e922fd63 --- /dev/null +++ b/src/components/ai-chat/WorkflowPanelForm.tsx @@ -0,0 +1,135 @@ +/** + * Form layout for the workflow panel (Issue #462). + * ワークフローパネルのフォームレイアウト(Issue #462)。 + */ + +import { Button, ScrollArea } from "@zedi/ui"; +import { ListChecks, Pause, Play, Square } from "lucide-react"; +import { isTauriDesktop } from "@/lib/platform"; +import { WorkflowPanelMetaSection } from "./WorkflowPanelMetaSection"; +import { WorkflowPanelStepsAndProgress } from "./WorkflowPanelStepsAndProgress"; +import type { WorkflowPanelFormProps } from "./workflowPanelTypes"; + +export type { WorkflowPanelFormProps } from "./workflowPanelTypes"; + +/** + * Renders workflow name, templates, steps, progress, and run controls. + * ワークフロー名・テンプレート・ステップ・進捗・実行操作を描画する。 + */ +export function WorkflowPanelForm(props: WorkflowPanelFormProps) { + const { + t, + draft, + setDraft, + definitions, + selectedSavedId, + progress, + activeRunSteps, + pausedState, + importInputRef, + isEditor, + running, + runExecution, + handlePause, + handleStop, + addStep, + removeStep, + updateStep, + loadTemplate, + saveCustom, + exportJson, + onImportFile, + loadSaved, + deleteSaved, + } = props; + + const canRunWorkflow = isTauriDesktop() && isEditor; + + return ( +
+
+ + {t("aiChat.workflow.subtitle")} +
+ + {!isTauriDesktop() && ( +

{t("aiChat.workflow.desktopOnly")}

+ )} + + +
+ + +
+
+ +
+ + + + +
+
+ ); +} diff --git a/src/components/ai-chat/WorkflowPanelMetaSection.tsx b/src/components/ai-chat/WorkflowPanelMetaSection.tsx new file mode 100644 index 00000000..ce08566c --- /dev/null +++ b/src/components/ai-chat/WorkflowPanelMetaSection.tsx @@ -0,0 +1,174 @@ +/** + * Workflow name, templates, saved definitions, and import/export (Issue #462). + * ワークフロー名・テンプレート・保存定義・インポート/エクスポート(Issue #462)。 + */ + +import { + Button, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@zedi/ui"; +import { Download, Trash2, Upload } from "lucide-react"; +import { + WORKFLOW_TEMPLATE_IDS, + WORKFLOW_TEMPLATE_NAME_KEYS, + type WorkflowTemplateId, +} from "@/lib/workflow/templates"; +import type { WorkflowPanelFormProps } from "./workflowPanelTypes"; + +type Props = Pick< + WorkflowPanelFormProps, + | "t" + | "draft" + | "setDraft" + | "definitions" + | "selectedSavedId" + | "importInputRef" + | "running" + | "loadTemplate" + | "saveCustom" + | "exportJson" + | "onImportFile" + | "loadSaved" + | "deleteSaved" +>; + +/** + * Name field, template selector, saved workflow picker, and JSON import/export. + * 名前・テンプレート・保存済み選択・JSON インポート/エクスポート。 + */ +export function WorkflowPanelMetaSection(props: Props) { + const { + t, + draft, + setDraft, + definitions, + selectedSavedId, + importInputRef, + running, + loadTemplate, + saveCustom, + exportJson, + onImportFile, + loadSaved, + deleteSaved, + } = props; + + return ( + <> +
+ + setDraft((d) => ({ ...d, name: e.target.value, updatedAt: Date.now() }))} + placeholder={t("aiChat.workflow.workflowNamePlaceholder")} + disabled={running} + /> +
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ + + + +
+ + ); +} diff --git a/src/components/ai-chat/WorkflowPanelStepsAndProgress.tsx b/src/components/ai-chat/WorkflowPanelStepsAndProgress.tsx new file mode 100644 index 00000000..521b0cc2 --- /dev/null +++ b/src/components/ai-chat/WorkflowPanelStepsAndProgress.tsx @@ -0,0 +1,115 @@ +/** + * Step editor list and run progress summary (Issue #462). + * ステップ編集リストと実行進捗サマリー(Issue #462)。 + */ + +import { Button, Input, Label } from "@zedi/ui"; +import { Plus, Trash2 } from "lucide-react"; +import type { WorkflowPanelFormProps } from "./workflowPanelTypes"; + +type Props = Pick< + WorkflowPanelFormProps, + | "t" + | "draft" + | "running" + | "progress" + | "activeRunSteps" + | "addStep" + | "removeStep" + | "updateStep" +>; + +/** + * Editable steps and optional streaming progress block. + * 編集可能なステップとストリーミング進捗ブロック。 + */ +export function WorkflowPanelStepsAndProgress(props: Props) { + const { t, draft, running, progress, activeRunSteps, addStep, removeStep, updateStep } = props; + + return ( + <> +
+
+ + +
+ + {draft.steps.map((step, index) => ( +
+
+ + {t("aiChat.workflow.stepLabel", { n: index + 1 })} + + +
+ updateStep(index, { title: e.target.value })} + /> +

ok

', + ); + expect(result).not.toContain(" { + const result = sanitizeHtml('

text

'); + expect(result).not.toContain("onclick"); + expect(result).not.toContain("onmouseover"); + expect(result).toContain("

text

"); + }); + + it("javascript: URL を除去する / removes javascript: URLs", () => { + const result = sanitizeHtml('click'); + expect(result).not.toContain("javascript:"); + }); + + it("data: URL を除去する / removes data: URLs in href", () => { + const result = sanitizeHtml('x'); + expect(result).not.toContain("data:"); + }); + + it("style 属性を除去する / removes style attributes", () => { + const result = sanitizeHtml('

text

'); + expect(result).not.toContain("style"); + expect(result).toContain("text"); + }); + + it("svg タグを除去する / removes svg tags", () => { + const result = sanitizeHtml('

ok

'); + expect(result).not.toContain("ok

"); + }); + + it("math タグを除去する / removes math tags", () => { + const result = sanitizeHtml("x

ok

"); + expect(result).not.toContain(" { + const result = sanitizeHtml("

ok

"); + expect(result).not.toContain(" { + const result = sanitizeHtml('

ok

'); + expect(result).not.toContain(" { + const result = sanitizeHtml( + '

ok

', + ); + expect(result).not.toContain(" { + const html = + '

Title

linkphoto'; + const result = sanitizeHtml(html); + expect(result).toContain("

"); + expect(result).toContain('href="https://example.com"'); + expect(result).toContain('title="link"'); + expect(result).toContain('src="photo.jpg"'); + expect(result).toContain('alt="photo"'); + expect(result).toContain('loading="lazy"'); + }); + + it("テーブル要素を保持する / preserves table elements", () => { + const html = + "
A
1
"; + const result = sanitizeHtml(html); + expect(result).toContain(""); + expect(result).toContain(""); + expect(result).toContain(""); + }); + + it("空文字列を処理できる / handles empty string", () => { + expect(sanitizeHtml("")).toBe(""); + }); + + it("制御文字で難読化された javascript: スキームを除去する / removes obfuscated javascript: scheme", () => { + const result = sanitizeHtml('x'); + expect(result).not.toContain("javascript"); + }); + + it("noscript タグを除去する / removes noscript tags", () => { + const result = sanitizeHtml("

ok

"); + expect(result).not.toContain(" { + const result = sanitizeHtml('click'); + expect(result).not.toContain("vbscript:"); + }); + + it("data:image URI は img src で許可する / allows safe data:image URIs in img src", () => { + const result = sanitizeHtml('icon'); + expect(result).toContain("data:image/png;base64,iVBORw0KGgo="); + expect(result).toContain('alt="icon"'); + }); + + it("autoplay 属性を除去する / strips autoplay attribute", () => { + const result = sanitizeHtml(''); + expect(result).not.toContain("autoplay"); + expect(result).toContain("controls"); + }); + + it("許可されていない属性を除去する / strips non-allowlisted attributes", () => { + const result = sanitizeHtml('
text
'); + expect(result).not.toContain("data-custom"); + expect(result).not.toContain("tabindex"); + expect(result).toContain('class="ok"'); + }); +}); diff --git a/src/lib/webClipper/sanitizeHtml.ts b/src/lib/webClipper/sanitizeHtml.ts index 5b1b3727..3093e01e 100644 --- a/src/lib/webClipper/sanitizeHtml.ts +++ b/src/lib/webClipper/sanitizeHtml.ts @@ -1,67 +1,24 @@ /** * HTML サニタイズ(危険なタグ・属性を除去)。 * Sanitizes HTML by removing dangerous tags and attributes. + * + * DOMPurify を使用し、Mutation XSS を含む幅広い攻撃ベクトルに対応する。 + * Uses DOMPurify to handle a wide range of attack vectors including Mutation XSS. */ +import DOMPurify from "dompurify"; +import { ALLOWED_ATTR, ALLOWED_TAGS } from "./sanitizeHtmlConfig"; /** * 危険な要素・属性を除去した HTML を返す。 * Returns HTML with dangerous elements and attributes removed. + * + * @param html - サニタイズ対象の HTML 文字列 / Raw HTML string to sanitize + * @returns サニタイズ済み HTML / Sanitized HTML string */ export function sanitizeHtml(html: string): string { - const div = document.createElement("div"); - div.innerHTML = html; - - const unwantedSelectors = [ - "script", - "style", - "iframe", - "noscript", - "object", - "embed", - "form", - "input", - "button", - "select", - "textarea", - "[onclick]", - "[onerror]", - "[onload]", - ]; - - unwantedSelectors.forEach((selector) => { - const elements = div.querySelectorAll(selector); - elements.forEach((el) => el.remove()); + return DOMPurify.sanitize(html, { + ALLOWED_TAGS: [...ALLOWED_TAGS], + ALLOWED_ATTR: [...ALLOWED_ATTR], + ALLOW_DATA_ATTR: false, }); - - const DANGEROUS_SCHEMES = /^(javascript:|data:|vbscript:)/i; - const URL_ATTRS = ["href", "src", "xlink:href", "formaction", "poster", "cite", "srcset"]; - - /** スキーム判定用に ASCII 制御文字・空白を除去して正規化。Bypass 防止(e.g. java\\nscript:)。 */ - function normalizedScheme(value: string): string { - let s = ""; - for (let i = 0; i < value.length; i++) { - const code = value.charCodeAt(i); - if (code > 0x20 && code !== 0x7f) s += value[i]; - } - return s.trim().toLowerCase(); - } - - const allElements = div.querySelectorAll("*"); - allElements.forEach((el) => { - const attributesToRemove: string[] = []; - for (const attr of el.attributes) { - const name = attr.name.toLowerCase(); - const value = attr.value ?? ""; - if (name.startsWith("on") || name === "style") { - attributesToRemove.push(attr.name); - } else if (URL_ATTRS.includes(name) && DANGEROUS_SCHEMES.test(normalizedScheme(value))) { - attributesToRemove.push(attr.name); - } - } - for (const name of attributesToRemove) { - el.removeAttribute(name); - } - }); - - return div.innerHTML; } diff --git a/src/lib/webClipper/sanitizeHtmlConfig.ts b/src/lib/webClipper/sanitizeHtmlConfig.ts new file mode 100644 index 00000000..771e7fec --- /dev/null +++ b/src/lib/webClipper/sanitizeHtmlConfig.ts @@ -0,0 +1,150 @@ +/** + * sanitizeHtml の DOMPurify 設定。 + * DOMPurify configuration for sanitizeHtml. + * + * ホワイトリスト方式で許可するタグ・属性を定義する。 + * ここに含まれないタグ・属性はすべて除去される。 + * Defines allowlists for tags and attributes; anything not listed is stripped. + */ + +/** + * Web Clipper で保持するタグのホワイトリスト。 + * Allowlist of tags retained for clipped web content. + */ +export const ALLOWED_TAGS = [ + // 見出し / Headings + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + + // ブロック / Block + "p", + "br", + "hr", + "blockquote", + "pre", + "code", + + // リスト / Lists + "ul", + "ol", + "li", + "dl", + "dt", + "dd", + + // リンク・画像 / Links & images + "a", + "img", + "figure", + "figcaption", + "picture", + "source", + + // テーブル / Tables + "table", + "thead", + "tbody", + "tfoot", + "tr", + "th", + "td", + "caption", + + // インライン書式 / Inline formatting + "strong", + "em", + "b", + "i", + "u", + "s", + "del", + "ins", + "mark", + "small", + "sub", + "sup", + "abbr", + "cite", + "q", + "dfn", + "time", + "var", + "kbd", + "samp", + "span", + + // セクション / Sections + "div", + "section", + "article", + "aside", + "header", + "footer", + "nav", + "main", + "details", + "summary", + + // ルビ / Ruby + "ruby", + "rt", + "rp", + + // その他 / Misc + "wbr", + + // メディア / Media + // NOTE: `autoplay` 属性は ALLOWED_ATTR に含めないこと。 + // メディア自動再生による UX 悪化・セキュリティリスクを防止する。 + // NOTE: Do NOT add `autoplay` to ALLOWED_ATTR. + // Prevents UX degradation and security risks from auto-playing media. + "video", + "audio", +] as const; + +/** + * 許可する属性のホワイトリスト。 + * Allowlist of attributes retained for clipped web content. + */ +export const ALLOWED_ATTR = [ + // リンク・画像 / Links & images + "href", + "src", + "alt", + "title", + "srcset", + "sizes", + "loading", + "decoding", + + // レイアウト / Layout + "width", + "height", + "class", + "id", + + // 国際化 / i18n + "lang", + "dir", + + // テーブル / Tables + "colspan", + "rowspan", + "scope", + "headers", + + // セマンティック / Semantic + "datetime", + "cite", + + // メディア / Media + "poster", + "controls", + "preload", + "type", + "media", +] as const; diff --git a/src/lib/wikiGenerator.ts b/src/lib/wikiGenerator.ts index 04d27d4e..497913bf 100644 --- a/src/lib/wikiGenerator.ts +++ b/src/lib/wikiGenerator.ts @@ -36,6 +36,10 @@ export async function getAISettingsOrThrow(): Promise { return { ...settings, isConfigured: true }; } + if (settings.provider === "claude-code") { + return { ...settings, isConfigured: true }; + } + if (!settings.isConfigured || !settings.apiKey) { throw new Error("AI_NOT_CONFIGURED"); } @@ -44,7 +48,7 @@ export async function getAISettingsOrThrow(): Promise { /** * Wikiコンテンツをストリーミング生成 - * api_serverモード: callAIService経由でサーバーに委譲 + * api_serverモード / claude-code: callAIService経由でサーバーに委譲 * user_api_keyモード: 直接SDKで呼び出し(既存動作) */ export async function generateWikiContentStream( @@ -56,7 +60,7 @@ export async function generateWikiContentStream( const settings = await getAISettingsOrThrow(); const effectiveMode = settings.apiMode || (settings.apiKey ? "user_api_key" : "api_server"); - if (effectiveMode === "api_server") { + if (settings.provider === "claude-code" || effectiveMode === "api_server") { const { callAIService } = await import("@/lib/aiService"); const prompt = WIKI_GENERATOR_PROMPT.replace("{{title}}", title); let fullContent = ""; @@ -103,8 +107,12 @@ export async function generateWikiContentStream( case "google": await generateWithGoogle(settings, title, callbacks, abortSignal); break; - default: - throw new Error(`Unknown provider: ${settings.provider}`); + case "claude-code": + throw new Error("Wiki generation is not supported with Claude Code provider."); + default: { + const _exhaustive: never = settings.provider; + throw new Error(`Unknown provider: ${_exhaustive}`); + } } } catch (error) { if (error instanceof Error) { @@ -131,7 +139,7 @@ export async function generateWikiContentFromChatOutlineStream( const settings = await getAISettingsOrThrow(); const effectiveMode = settings.apiMode || (settings.apiKey ? "user_api_key" : "api_server"); - if (effectiveMode === "api_server") { + if (settings.provider === "claude-code" || effectiveMode === "api_server") { const { callAIService } = await import("@/lib/aiService"); let fullContent = ""; diff --git a/src/lib/wikiGenerator/wikiGeneratorStreamFullPrompt.ts b/src/lib/wikiGenerator/wikiGeneratorStreamFullPrompt.ts index 007c0fd3..9e778512 100644 --- a/src/lib/wikiGenerator/wikiGeneratorStreamFullPrompt.ts +++ b/src/lib/wikiGenerator/wikiGeneratorStreamFullPrompt.ts @@ -126,7 +126,11 @@ export async function streamWikiStyleFromFullPrompt( case "google": await streamGoogleFullPrompt(settings, fullUserPrompt, callbacks, abortSignal); break; - default: - throw new Error(`Unknown provider: ${settings.provider}`); + case "claude-code": + throw new Error("Wiki generation is not supported with Claude Code provider."); + default: { + const _exhaustive: never = settings.provider; + throw new Error(`Unknown provider: ${_exhaustive}`); + } } } diff --git a/src/lib/workflow/buildWorkflowStepPrompt.test.ts b/src/lib/workflow/buildWorkflowStepPrompt.test.ts new file mode 100644 index 00000000..86bd2bcb --- /dev/null +++ b/src/lib/workflow/buildWorkflowStepPrompt.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { buildWorkflowStepPrompt, defaultWorkflowStepMaxTurns } from "./buildWorkflowStepPrompt"; + +describe("buildWorkflowStepPrompt", () => { + it("includes step meta, instruction, and prior outputs", () => { + const text = buildWorkflowStepPrompt({ + workflowName: "W", + step: { id: "s2", title: "Design", instruction: "Propose schema." }, + stepIndex: 1, + totalSteps: 3, + priorOutputs: ["analysis done"], + }); + expect(text).toContain("step 2 of 3"); + expect(text).toContain("Design"); + expect(text).toContain("Propose schema."); + expect(text).toContain("analysis done"); + }); + + it("adds page excerpt and resume partial when provided", () => { + const text = buildWorkflowStepPrompt({ + workflowName: "W", + step: { id: "s1", title: "T", instruction: "Go" }, + stepIndex: 0, + totalSteps: 1, + pageExcerpt: "Note body", + priorOutputs: [], + resumeFromPartial: "half-done", + }); + expect(text).toContain("Note body"); + expect(text).toContain("half-done"); + }); +}); + +describe("defaultWorkflowStepMaxTurns", () => { + it("falls back to 15 when maxTurns is missing", () => { + expect(defaultWorkflowStepMaxTurns({ id: "a", title: "t", instruction: "i" })).toBe(15); + }); + + it("uses explicit maxTurns", () => { + expect( + defaultWorkflowStepMaxTurns({ id: "a", title: "t", instruction: "i", maxTurns: 7 }), + ).toBe(7); + }); +}); diff --git a/src/lib/workflow/buildWorkflowStepPrompt.ts b/src/lib/workflow/buildWorkflowStepPrompt.ts new file mode 100644 index 00000000..a927afb7 --- /dev/null +++ b/src/lib/workflow/buildWorkflowStepPrompt.ts @@ -0,0 +1,80 @@ +/** + * Builds the user prompt for one workflow step (Issue #462). + * ワークフロー 1 ステップ分のユーザープロンプトを組み立てる(Issue #462)。 + */ + +import type { WorkflowStepDefinition } from "./types"; + +const DEFAULT_MAX_TURNS = 15; + +/** + * Returns default max turns when a step omits `maxTurns`. + * ステップが `maxTurns` を省略したときの既定値。 + */ +export function defaultWorkflowStepMaxTurns(step: WorkflowStepDefinition): number { + return step.maxTurns ?? DEFAULT_MAX_TURNS; +} + +/** + * Builds Claude Code prompt text for `stepIndex` including prior step outputs. + * 先行ステップの出力を含めた Claude Code 用プロンプトを組み立てる。 + */ +export function buildWorkflowStepPrompt(options: { + workflowName: string; + step: WorkflowStepDefinition; + stepIndex: number; + totalSteps: number; + /** Plain-text excerpt of the open page (optional). / 開いているページの抜粋(任意) */ + pageExcerpt?: string; + /** Completed assistant outputs from earlier steps. / 先行ステップの完了出力 */ + priorOutputs: string[]; + /** When resuming after pause, partial text from the interrupted attempt. / 一時停止後の再開時、中断前の部分テキスト */ + resumeFromPartial?: string; +}): string { + const { + workflowName, + step, + stepIndex, + totalSteps, + pageExcerpt, + priorOutputs, + resumeFromPartial, + } = options; + + const parts: string[] = [ + `You are executing step ${stepIndex + 1} of ${totalSteps} in a workflow named "${workflowName}".`, + "", + `## Step title`, + step.title, + "", + `## Instructions`, + step.instruction.trim() || "(no additional instructions)", + "", + ]; + + if (pageExcerpt?.trim()) { + parts.push(`## Context from the open note (excerpt)`, pageExcerpt.trim(), ""); + } + + if (priorOutputs.length > 0) { + parts.push(`## Outputs from previous steps`); + for (let i = 0; i < priorOutputs.length; i += 1) { + parts.push(`### Step ${i + 1}`, priorOutputs[i].trim() || "(empty)", ""); + } + } + + if (resumeFromPartial?.trim()) { + parts.push( + `## Resume`, + "The previous attempt was interrupted. Continue and improve from this partial output:", + resumeFromPartial.trim(), + "", + ); + } + + parts.push( + `Respond with the result for this step only. Use clear Markdown. Be concise unless the instructions ask for detail.`, + ); + + return parts.join("\n"); +} diff --git a/src/lib/workflow/formatWorkflowNoteMarkdown.test.ts b/src/lib/workflow/formatWorkflowNoteMarkdown.test.ts new file mode 100644 index 00000000..9236cbb7 --- /dev/null +++ b/src/lib/workflow/formatWorkflowNoteMarkdown.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { formatWorkflowNoteMarkdown } from "./formatWorkflowNoteMarkdown"; + +describe("formatWorkflowNoteMarkdown", () => { + it("renders title and step markers", () => { + const md = formatWorkflowNoteMarkdown({ + title: "Test", + stepTitles: ["A", "B"], + stepStatuses: ["done", "pending"], + stepOutputs: ["out-a", ""], + streamingStepIndex: null, + streamingText: "", + }); + expect(md).toContain("## 📋 Workflow: Test"); + expect(md).toContain("### ☑ 1. A"); + expect(md).toContain("out-a"); + expect(md).toContain("### ⬜ 2. B"); + }); + + it("shows (empty) for done step with empty output", () => { + const md = formatWorkflowNoteMarkdown({ + title: "E", + stepTitles: ["Only"], + stepStatuses: ["done"], + stepOutputs: [""], + streamingStepIndex: null, + streamingText: "", + }); + expect(md).toContain("(empty)"); + }); + + it("includes streaming text for the running step", () => { + const md = formatWorkflowNoteMarkdown({ + title: "S", + stepTitles: ["One"], + stepStatuses: ["running"], + stepOutputs: [""], + streamingStepIndex: 0, + streamingText: "partial...", + }); + expect(md).toContain("### 🔄 1. One"); + expect(md).toContain("partial..."); + }); +}); diff --git a/src/lib/workflow/formatWorkflowNoteMarkdown.ts b/src/lib/workflow/formatWorkflowNoteMarkdown.ts new file mode 100644 index 00000000..68ef6a83 --- /dev/null +++ b/src/lib/workflow/formatWorkflowNoteMarkdown.ts @@ -0,0 +1,57 @@ +/** + * Builds Markdown for embedding workflow progress into a note (Issue #462). + * ノートへ進捗を埋め込む Markdown を組み立てる(Issue #462)。 + */ + +import type { WorkflowStepRunStatus } from "./types"; + +const STATUS_PREFIX: Record = { + pending: "⬜", + running: "🔄", + done: "☑", + error: "⚠️", +}; + +/** + * Formats a workflow block with step headings, optional streaming text, and outputs. + * ステップ見出し・ストリーミングテキスト・出力付きのワークフローブロックを整形する。 + */ +export function formatWorkflowNoteMarkdown(options: { + /** Workflow title. / ワークフロー名 */ + title: string; + /** Step titles in order. / ステップタイトル(順序どおり) */ + stepTitles: string[]; + /** Status per step. / ステップごとの状態 */ + stepStatuses: WorkflowStepRunStatus[]; + /** Final text for steps that finished successfully. / 成功完了したステップの最終テキスト */ + stepOutputs: string[]; + /** Index of the step currently streaming, or null. / ストリーム中のステップ index、なければ null */ + streamingStepIndex: number | null; + /** Partial text for the streaming step. / ストリーム中ステップの部分テキスト */ + streamingText: string; +}): string { + const { title, stepTitles, stepStatuses, stepOutputs, streamingStepIndex, streamingText } = + options; + + const lines: string[] = [`## 📋 Workflow: ${title}`, ""]; + + for (let i = 0; i < stepTitles.length; i += 1) { + const status = stepStatuses[i] ?? "pending"; + const prefix = STATUS_PREFIX[status]; + lines.push(`### ${prefix} ${i + 1}. ${stepTitles[i]}`); + lines.push(""); + + if (status === "done") { + lines.push((stepOutputs[i] ?? "").trim() || "(empty)"); + lines.push(""); + } else if (status === "running" && streamingStepIndex === i && streamingText.trim()) { + lines.push(streamingText.trim()); + lines.push(""); + } else if (status === "error") { + lines.push("(step failed)"); + lines.push(""); + } + } + + return lines.join("\n").trimEnd(); +} diff --git a/src/lib/workflow/newWorkflowId.ts b/src/lib/workflow/newWorkflowId.ts new file mode 100644 index 00000000..a119ae81 --- /dev/null +++ b/src/lib/workflow/newWorkflowId.ts @@ -0,0 +1,15 @@ +/** + * Generates a random id for workflow definitions and steps. + * ワークフロー定義・ステップ用のランダム ID を生成する。 + */ + +/** + * Returns a UUID when `crypto.randomUUID` exists; otherwise a fallback string. + * `crypto.randomUUID` があれば UUID、なければフォールバック文字列。 + */ +export function newWorkflowId(): string { + if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { + return crypto.randomUUID(); + } + return `wf-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; +} diff --git a/src/lib/workflow/parseWorkflowDefinitionImport.test.ts b/src/lib/workflow/parseWorkflowDefinitionImport.test.ts new file mode 100644 index 00000000..ded387ec --- /dev/null +++ b/src/lib/workflow/parseWorkflowDefinitionImport.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; +import { parseWorkflowDefinitionImport } from "./parseWorkflowDefinitionImport"; + +describe("parseWorkflowDefinitionImport", () => { + it("parses minimal valid JSON", () => { + const r = parseWorkflowDefinitionImport({ + name: "W", + steps: [{ id: "a", title: "T", instruction: "Do" }], + }); + expect(r.name).toBe("W"); + expect(r.steps).toHaveLength(1); + expect(r.steps[0].title).toBe("T"); + expect(r.steps[0].instruction).toBe("Do"); + }); + + it("rejects non-object root", () => { + expect(() => parseWorkflowDefinitionImport(null)).toThrow(); + }); + + it("rejects missing steps array", () => { + expect(() => parseWorkflowDefinitionImport({ name: "x" })).toThrow(); + }); + + it("rejects step without string title/instruction", () => { + expect(() => + parseWorkflowDefinitionImport({ + name: "x", + steps: [{ id: "1", title: 1, instruction: "a" }], + }), + ).toThrow(); + }); + + it("re-issues step ids when duplicates appear", () => { + const r = parseWorkflowDefinitionImport({ + name: "Dup", + steps: [ + { id: "same", title: "A", instruction: "a" }, + { id: "same", title: "B", instruction: "b" }, + ], + }); + expect(r.steps[0].id).toBe("same"); + expect(r.steps[1].id).not.toBe("same"); + expect(r.steps[1].title).toBe("B"); + }); + + it("accepts optional maxTurns and allowedTools", () => { + const r = parseWorkflowDefinitionImport({ + name: "x", + steps: [ + { + title: "a", + instruction: "b", + maxTurns: 10, + allowedTools: ["Read", "Bash"], + }, + ], + }); + expect(r.steps[0].maxTurns).toBe(10); + expect(r.steps[0].allowedTools).toEqual(["Read", "Bash"]); + }); +}); diff --git a/src/lib/workflow/parseWorkflowDefinitionImport.ts b/src/lib/workflow/parseWorkflowDefinitionImport.ts new file mode 100644 index 00000000..b4589459 --- /dev/null +++ b/src/lib/workflow/parseWorkflowDefinitionImport.ts @@ -0,0 +1,94 @@ +/** + * Validates JSON imported as a workflow definition (Issue #462). + * ワークフロー定義としてインポートする JSON を検証する(Issue #462)。 + */ + +import { newWorkflowId } from "./newWorkflowId"; +import type { WorkflowStepDefinition } from "./types"; + +const MAX_STEPS = 50; +const MAX_FIELD_LEN = 50_000; +const MAX_TOOL_NAME_LEN = 64; + +const INVALID = "invalid workflow JSON"; + +/** + * Parses one step object from imported JSON. + * インポート JSON のステップオブジェクトを 1 件パースする。 + */ +function parseWorkflowStepImport(s: Record): WorkflowStepDefinition { + if (typeof s.title !== "string" || typeof s.instruction !== "string") { + throw new Error(INVALID); + } + const title = s.title.slice(0, MAX_FIELD_LEN); + const instruction = s.instruction.slice(0, MAX_FIELD_LEN); + const id = + typeof s.id === "string" && s.id.trim().length > 0 ? s.id.slice(0, 200) : newWorkflowId(); + + const step: WorkflowStepDefinition = { id, title, instruction }; + + if (s.maxTurns !== undefined) { + if (typeof s.maxTurns !== "number" || !Number.isFinite(s.maxTurns)) { + throw new Error(INVALID); + } + const mt = Math.floor(s.maxTurns); + if (mt < 1 || mt > 500) { + throw new Error(INVALID); + } + step.maxTurns = mt; + } + + if (s.allowedTools !== undefined) { + if (!Array.isArray(s.allowedTools)) { + throw new Error(INVALID); + } + step.allowedTools = s.allowedTools.map((t) => { + if (typeof t !== "string") { + throw new Error(INVALID); + } + return t.slice(0, MAX_TOOL_NAME_LEN); + }); + } + + return step; +} + +/** + * Parses and validates unknown JSON from file import. + * ファイルインポート由来の不明な JSON をパースし検証する。 + * + * @throws Error when the shape is invalid or limits are exceeded. + */ +export function parseWorkflowDefinitionImport(raw: unknown): { + name: string; + steps: WorkflowStepDefinition[]; +} { + if (!raw || typeof raw !== "object") { + throw new Error(INVALID); + } + const o = raw as Record; + const name = typeof o.name === "string" ? o.name.slice(0, MAX_FIELD_LEN) : ""; + + if (!Array.isArray(o.steps)) { + throw new Error(INVALID); + } + if (o.steps.length === 0 || o.steps.length > MAX_STEPS) { + throw new Error(INVALID); + } + + const steps: WorkflowStepDefinition[] = []; + const seenIds = new Set(); + for (const item of o.steps) { + if (!item || typeof item !== "object") { + throw new Error(INVALID); + } + let step = parseWorkflowStepImport(item as Record); + if (seenIds.has(step.id)) { + step = { ...step, id: newWorkflowId() }; + } + seenIds.add(step.id); + steps.push(step); + } + + return { name, steps }; +} diff --git a/src/lib/workflow/runWorkflowExecution.test.ts b/src/lib/workflow/runWorkflowExecution.test.ts new file mode 100644 index 00000000..cbbdac28 --- /dev/null +++ b/src/lib/workflow/runWorkflowExecution.test.ts @@ -0,0 +1,99 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { runWorkflowExecution } from "./runWorkflowExecution"; + +vi.mock("@/lib/claudeCode/streamClaudeQuery", () => ({ + streamClaudeQuery: vi.fn(), +})); + +import { streamClaudeQuery } from "@/lib/claudeCode/streamClaudeQuery"; + +describe("runWorkflowExecution", () => { + beforeEach(() => { + vi.mocked(streamClaudeQuery).mockReset(); + }); + + it("returns error when there are no steps", async () => { + const r = await runWorkflowExecution({ + definition: { id: "w", name: "W", steps: [], createdAt: 0, updatedAt: 0 }, + workflowSignal: new AbortController().signal, + createStepAbort: () => new AbortController(), + startStepIndex: 0, + stepOutputs: [], + onProgress: vi.fn(), + onNoteMarkdown: vi.fn(), + baseContentBeforeWorkflow: "", + }); + expect(r).toEqual({ outcome: "error", error: "Workflow has no steps." }); + }); + + it("runs one step and completes", async () => { + vi.mocked(streamClaudeQuery).mockResolvedValue({ ok: true, content: "done" }); + + const onNote = vi.fn(); + const r = await runWorkflowExecution({ + definition: { + id: "w", + name: "W", + steps: [{ id: "s1", title: "Only", instruction: "Do work" }], + createdAt: 0, + updatedAt: 0, + }, + workflowSignal: new AbortController().signal, + createStepAbort: () => new AbortController(), + startStepIndex: 0, + stepOutputs: [], + onProgress: vi.fn(), + onNoteMarkdown: onNote, + baseContentBeforeWorkflow: "base", + }); + + expect(r).toEqual({ outcome: "completed" }); + expect(streamClaudeQuery).toHaveBeenCalledTimes(1); + const lastNote = onNote.mock.calls.at(-1)?.[0] as string; + expect(lastNote).toContain("base"); + expect(lastNote).toContain("Workflow: W"); + expect(lastNote).toContain("done"); + }); + + it("returns paused when only the step signal aborts", async () => { + vi.mocked(streamClaudeQuery).mockImplementation(async (_p, _o, signal) => { + if (signal?.aborted) { + return { ok: false, error: "Aborted" }; + } + return { ok: false, error: "Aborted" }; + }); + + const workflow = new AbortController(); + const step = new AbortController(); + let createCount = 0; + const r = await runWorkflowExecution({ + definition: { + id: "w", + name: "W", + steps: [{ id: "s1", title: "A", instruction: "x" }], + createdAt: 0, + updatedAt: 0, + }, + workflowSignal: workflow.signal, + createStepAbort: () => { + createCount += 1; + step.abort(); + return step; + }, + startStepIndex: 0, + stepOutputs: [], + onProgress: vi.fn(), + onNoteMarkdown: vi.fn(), + baseContentBeforeWorkflow: "", + }); + + expect(r.outcome).toBe("paused"); + if (r.outcome === "paused") { + expect(r.pausedAtStepIndex).toBe(0); + expect(r.pausedStepId).toBe("s1"); + expect(r.stepOutputsById).toEqual({}); + expect(r.stepOutputs).toEqual([""]); + } + expect(createCount).toBe(1); + }); +}); diff --git a/src/lib/workflow/runWorkflowExecution.ts b/src/lib/workflow/runWorkflowExecution.ts new file mode 100644 index 00000000..aafceb59 --- /dev/null +++ b/src/lib/workflow/runWorkflowExecution.ts @@ -0,0 +1,290 @@ +/** + * Orchestrates multi-step Claude Code workflow runs (Issue #462). + * Claude Code のマルチステップワークフロー実行をオーケストレーションする(Issue #462)。 + */ + +import { streamClaudeQuery } from "@/lib/claudeCode/streamClaudeQuery"; +import type { WorkflowDefinition, WorkflowRunProgress, WorkflowStepRunStatus } from "./types"; +import { buildWorkflowStepPrompt, defaultWorkflowStepMaxTurns } from "./buildWorkflowStepPrompt"; +import { formatWorkflowNoteMarkdown } from "./formatWorkflowNoteMarkdown"; + +/** Outcome when {@link runWorkflowExecution} finishes or yields control. */ +export type WorkflowExecutionOutcome = + | { outcome: "completed" } + | { outcome: "stopped" } + | { + outcome: "paused"; + /** Step index where the run stopped (same step on resume). / 停止したステップ index(再開時も同じ) */ + pausedAtStepIndex: number; + /** Step id at pause (stable across draft edits). / 一時停止時のステップ id(ドラフト編集後も追跡) */ + pausedStepId: string; + /** Completed outputs keyed by step id. / 完了ステップの出力(id キー) */ + stepOutputsById: Record; + /** Snapshot of outputs for completed steps. / 完了ステップの出力スナップショット */ + stepOutputs: string[]; + /** Streaming buffer at pause time for the active step. / 停止時点のアクティブステップのストリーム */ + partialForStep: string; + } + | { outcome: "error"; error: string }; + +type RunWorkflowStepsLoopParams = { + definition: WorkflowDefinition; + cwd?: string; + pageExcerpt?: string; + workflowSignal: AbortSignal; + createStepAbort: () => AbortController; + startStepIndex: number; + stepOutputs: string[]; + resumePartialForCurrentStep?: string; + onProgress: (p: WorkflowRunProgress) => void; + onNoteMarkdown: (fullMarkdown: string) => void; + baseContentBeforeWorkflow: string; +}; + +/** + * Runs or resumes a workflow: streams each step into the note via `onNoteMarkdown`. + * ワークフローを実行または再開し、各ステップを `onNoteMarkdown` 経由でノートへストリームする。 + */ +export async function runWorkflowExecution(options: { + definition: WorkflowDefinition; + cwd?: string; + pageExcerpt?: string; + /** Aborts the whole run (Stop). / 実行全体を中止(停止) */ + workflowSignal: AbortSignal; + /** Fresh controller per step; UI aborts the current one on Pause. / ステップごとに新規。UI が一時停止で現在のみ abort */ + createStepAbort: () => AbortController; + /** First step index to execute (0 on fresh run). / 最初に実行するステップ index(新規は 0) */ + startStepIndex: number; + /** Completed outputs for steps before `startStepIndex`. / `startStepIndex` より前の完了出力 */ + stepOutputs: string[]; + /** When resuming the same step after pause, partial assistant text. / 一時停止後に同じステップを再開するときの部分テキスト */ + resumePartialForCurrentStep?: string; + onProgress: (p: WorkflowRunProgress) => void; + /** Full note body = base snapshot + formatted workflow block. / ベーススナップショット + 整形済みブロック */ + onNoteMarkdown: (fullMarkdown: string) => void; + /** Editor content before the workflow block was inserted. / ワークフローブロック挿入前のエディタ内容 */ + baseContentBeforeWorkflow: string; +}): Promise { + const { + definition, + cwd, + pageExcerpt, + workflowSignal, + createStepAbort, + startStepIndex, + stepOutputs: initialOutputs, + resumePartialForCurrentStep, + onProgress, + onNoteMarkdown, + baseContentBeforeWorkflow, + } = options; + + const steps = definition.steps; + if (steps.length === 0) { + return { outcome: "error", error: "Workflow has no steps." }; + } + + const stepOutputs = normalizeStepOutputs(initialOutputs, steps.length); + + return runWorkflowStepsLoop({ + definition, + cwd, + pageExcerpt, + workflowSignal, + createStepAbort, + startStepIndex, + stepOutputs, + resumePartialForCurrentStep, + onProgress, + onNoteMarkdown, + baseContentBeforeWorkflow, + }); +} + +/** + * Pads or trims `stepOutputs` to match `stepsLength`. + * `stepOutputs` を `stepsLength` に合わせて埋めたり切り詰めたりする。 + */ +function normalizeStepOutputs(initialOutputs: string[], stepsLength: number): string[] { + const stepOutputs = [...initialOutputs]; + while (stepOutputs.length < stepsLength) { + stepOutputs.push(""); + } + if (stepOutputs.length > stepsLength) { + stepOutputs.length = stepsLength; + } + return stepOutputs; +} + +/** + * Main step loop: streams each step, updates note and progress. + * メインのステップループ:各ステップをストリームし、ノートと進捗を更新する。 + */ +async function runWorkflowStepsLoop( + params: RunWorkflowStepsLoopParams, +): Promise { + const { + definition, + cwd, + pageExcerpt, + workflowSignal, + createStepAbort, + startStepIndex, + stepOutputs, + resumePartialForCurrentStep, + onProgress, + onNoteMarkdown, + baseContentBeforeWorkflow, + } = params; + + const steps = definition.steps; + const stepTitles = steps.map((s) => s.title); + + const pushProgress = ( + phase: WorkflowRunProgress["phase"], + currentStepIndex: number, + statuses: WorkflowStepRunStatus[], + streaming: string, + lastError?: string, + ): void => { + onProgress({ + phase, + currentStepIndex, + stepStatuses: statuses, + stepOutputs: [...stepOutputs], + currentStepStreaming: streaming, + lastError, + }); + }; + + const emitNote = ( + currentStepIndex: number, + statuses: WorkflowStepRunStatus[], + streamingStepIndex: number | null, + streamingText: string, + ): void => { + const block = formatWorkflowNoteMarkdown({ + title: definition.name, + stepTitles, + stepStatuses: statuses, + stepOutputs, + streamingStepIndex, + streamingText, + }); + const base = baseContentBeforeWorkflow.trimEnd(); + const full = base.length > 0 ? `${base}\n\n${block}` : block; + onNoteMarkdown(full); + }; + + for (let i = startStepIndex; i < steps.length; i += 1) { + if (workflowSignal.aborted) { + pushProgress("aborted", i, buildStatuses(steps.length, i, "pending"), "", undefined); + return { outcome: "stopped" }; + } + + const step = steps[i]; + const statusesBefore = buildStatuses(steps.length, i, "running"); + const initialStreaming = + resumePartialForCurrentStep && i === startStepIndex ? resumePartialForCurrentStep : ""; + pushProgress("running", i, statusesBefore, initialStreaming, undefined); + emitNote(i, statusesBefore, i, initialStreaming); + + const prior = stepOutputs.slice(0, i); + + const prompt = buildWorkflowStepPrompt({ + workflowName: definition.name, + step, + stepIndex: i, + totalSteps: steps.length, + pageExcerpt, + priorOutputs: prior, + resumeFromPartial: i === startStepIndex ? resumePartialForCurrentStep : undefined, + }); + + const stepController = createStepAbort(); + const merged = AbortSignal.any([workflowSignal, stepController.signal]); + + let streaming = initialStreaming; + + const result = await streamClaudeQuery( + prompt, + { + cwd, + maxTurns: defaultWorkflowStepMaxTurns(step), + allowedTools: step.allowedTools, + }, + merged, + { + onChunk: (chunk) => { + streaming += chunk; + const statuses = buildStatuses(steps.length, i, "running"); + pushProgress("running", i, statuses, streaming, undefined); + emitNote(i, statuses, i, streaming); + }, + }, + ); + + if (!result.ok) { + if (result.error === "Aborted") { + if (workflowSignal.aborted) { + pushProgress("aborted", i, buildStatuses(steps.length, i, "error"), "", undefined); + return { outcome: "stopped" }; + } + pushProgress("paused", i, buildStatuses(steps.length, i, "running"), streaming, undefined); + emitNote(i, buildStatuses(steps.length, i, "running"), i, streaming); + const stepOutputsById: Record = {}; + for (let k = 0; k < i; k += 1) { + stepOutputsById[steps[k].id] = stepOutputs[k]; + } + return { + outcome: "paused", + pausedAtStepIndex: i, + pausedStepId: steps[i].id, + stepOutputsById, + stepOutputs: [...stepOutputs], + partialForStep: streaming, + }; + } + stepOutputs[i] = ""; + const errStatuses = buildStatuses(steps.length, i, "error"); + pushProgress("running", i, errStatuses, streaming, result.error); + emitNote(i, errStatuses, null, ""); + return { outcome: "error", error: result.error }; + } + + stepOutputs[i] = result.content; + const doneStatuses = buildStatuses(steps.length, i, "done"); + pushProgress("running", i, doneStatuses, "", undefined); + emitNote(i, doneStatuses, null, ""); + } + + pushProgress( + "completed", + steps.length - 1, + steps.map(() => "done"), + "", + undefined, + ); + emitNote( + steps.length - 1, + steps.map(() => "done"), + null, + "", + ); + + return { outcome: "completed" }; +} + +function buildStatuses( + total: number, + runningIndex: number, + runningKind: WorkflowStepRunStatus, +): WorkflowStepRunStatus[] { + const out: WorkflowStepRunStatus[] = []; + for (let j = 0; j < total; j += 1) { + if (j < runningIndex) out.push("done"); + else if (j === runningIndex) out.push(runningKind); + else out.push("pending"); + } + return out; +} diff --git a/src/lib/workflow/templates.ts b/src/lib/workflow/templates.ts new file mode 100644 index 00000000..5ce1bbf5 --- /dev/null +++ b/src/lib/workflow/templates.ts @@ -0,0 +1,148 @@ +/** + * Built-in workflow templates (Issue #462). + * 組み込みワークフローテンプレート(Issue #462)。 + */ + +import type { WorkflowDefinition, WorkflowStepDefinition } from "./types"; +import { newWorkflowId } from "./newWorkflowId"; + +/** + * Known template ids for selection UI. + * 選択 UI 用のテンプレート ID。 + */ +export const WORKFLOW_TEMPLATE_IDS = [ + "code-investigate-design", + "test-analyze-improve", + "repo-analyze-docs", + "web-research-note", +] as const; + +/** Template id union. / テンプレート ID ユニオン */ +export type WorkflowTemplateId = (typeof WORKFLOW_TEMPLATE_IDS)[number]; + +/** + * i18n key for the template title (see `aiChat.workflow.templates.*`). + * テンプレートタイトル用 i18n キー(`aiChat.workflow.templates.*`)。 + */ +export const WORKFLOW_TEMPLATE_NAME_KEYS: Record = { + "code-investigate-design": "aiChat.workflow.templates.codeInvestigateDesign", + "test-analyze-improve": "aiChat.workflow.templates.testAnalyzeImprove", + "repo-analyze-docs": "aiChat.workflow.templates.repoAnalyzeDocs", + "web-research-note": "aiChat.workflow.templates.webResearchNote", +}; + +type StepSeed = Omit; + +function stepsForTemplate(id: WorkflowTemplateId): StepSeed[] { + switch (id) { + case "code-investigate-design": + return [ + { + title: "Investigate code patterns", + instruction: + "Explore the linked workspace: identify API or routing patterns, naming conventions, and error-handling style. Summarize findings as bullet points.", + maxTurns: 20, + allowedTools: ["Read"], + }, + { + title: "Draft design memo", + instruction: + "Based on the investigation, propose a design for the feature described in the note context. Output structured Markdown (goal, options, recommendation, risks).", + maxTurns: 16, + allowedTools: ["Read"], + }, + ]; + case "test-analyze-improve": + return [ + { + title: "Run tests", + instruction: + "Run the project's test command in the linked workspace (e.g. `bun run test:run` or the standard command you detect). Capture failing vs passing summary.", + maxTurns: 12, + allowedTools: ["Bash", "Read"], + }, + { + title: "Analyze results", + instruction: + "Analyze the test output: categorize failures, likely root causes, and flaky vs deterministic issues.", + maxTurns: 14, + allowedTools: ["Read"], + }, + { + title: "Suggest improvements", + instruction: + "Propose concrete next steps: code changes, test fixes, and follow-up commands. Use Markdown with numbered actions.", + maxTurns: 14, + allowedTools: ["Read"], + }, + ]; + case "repo-analyze-docs": + return [ + { + title: "Repository analysis", + instruction: + "Scan the repository structure (top-level dirs, packages, build entrypoints). Summarize architecture and main technologies.", + maxTurns: 20, + allowedTools: ["Read", "Bash"], + }, + { + title: "Generate documentation draft", + instruction: + "Produce a documentation outline: overview, setup, development, testing, deployment. Fill with what you can infer from the repo.", + maxTurns: 18, + allowedTools: ["Read"], + }, + ]; + case "web-research-note": + return [ + { + title: "Web research", + instruction: + "Research the topic implied by the note title/context using web search. Collect key facts, sources, and conflicting viewpoints.", + maxTurns: 20, + allowedTools: ["WebSearch", "Read"], + }, + { + title: "Organize information", + instruction: + "Structure the findings: summary table or bullets, source list, and open questions.", + maxTurns: 12, + allowedTools: [], + }, + { + title: "Draft note content", + instruction: + "Write polished Markdown suitable for the note: clear headings, links to sources, and a short conclusion.", + maxTurns: 16, + allowedTools: [], + }, + ]; + default: { + const _exhaustive: never = id; + throw new Error(`Unknown template: ${String(_exhaustive)}`); + } + } +} + +/** + * Creates a new {@link WorkflowDefinition} from a built-in template. + * 組み込みテンプレートから新しい {@link WorkflowDefinition} を作る。 + */ +export function instantiateWorkflowTemplate( + id: WorkflowTemplateId, + displayName: string, +): WorkflowDefinition { + const now = Date.now(); + const seeds = stepsForTemplate(id); + const steps: WorkflowStepDefinition[] = seeds.map((s) => ({ + ...s, + id: newWorkflowId(), + })); + return { + id: newWorkflowId(), + name: displayName, + steps, + createdAt: now, + updatedAt: now, + }; +} diff --git a/src/lib/workflow/types.ts b/src/lib/workflow/types.ts new file mode 100644 index 00000000..7e110476 --- /dev/null +++ b/src/lib/workflow/types.ts @@ -0,0 +1,67 @@ +/** + * Multi-step Claude Code workflow (Issue #462). + * Claude Code マルチステップワークフロー(Issue #462)。 + */ + +/** + * One step in a workflow definition. + * ワークフロー定義の 1 ステップ。 + */ +export interface WorkflowStepDefinition { + /** Stable id for React keys and persistence. / React キーと永続化用の安定 ID */ + id: string; + /** Short label shown in UI and note. / UI とノートに表示する短いラベル */ + title: string; + /** Instruction sent to Claude Code for this step. / このステップで Claude Code に渡す指示 */ + instruction: string; + /** + * Max agent turns for this step (Claude Agent SDK `maxTurns`). + * このステップのエージェント最大ターン数(SDK `maxTurns`)。 + */ + maxTurns?: number; + /** + * Allowed tools for this step; omit for default sidecar tool set. + * このステップで許可するツール。省略時は sidecar 既定ツール。 + */ + allowedTools?: string[]; +} + +/** + * A saved or template-derived workflow. + * 保存済みまたはテンプレート由来のワークフロー。 + */ +export interface WorkflowDefinition { + id: string; + name: string; + steps: WorkflowStepDefinition[]; + createdAt: number; + updatedAt: number; +} + +/** + * Lifecycle of a workflow run in the UI engine. + * UI エンジン上のワークフロー実行ライフサイクル。 + */ +export type WorkflowRunPhase = "idle" | "running" | "paused" | "completed" | "aborted"; + +/** + * Status of each step during a run. + * 実行中の各ステップの状態。 + */ +export type WorkflowStepRunStatus = "pending" | "running" | "done" | "error"; + +/** + * Snapshot emitted to UI while executing. + * 実行中に UI へ送るスナップショット。 + */ +export interface WorkflowRunProgress { + phase: WorkflowRunPhase; + currentStepIndex: number; + stepStatuses: WorkflowStepRunStatus[]; + /** Final assistant text per completed step. / 完了ステップごとの最終テキスト */ + stepOutputs: string[]; + /** Streaming buffer for the active step. / 実行中ステップのストリームバッファ */ + currentStepStreaming: string; + /** Error message when a step fails. / ステップ失敗時のメッセージ */ + lastError?: string; +} diff --git a/src/lib/workspace/bridge.ts b/src/lib/workspace/bridge.ts new file mode 100644 index 00000000..b5bee1fd --- /dev/null +++ b/src/lib/workspace/bridge.ts @@ -0,0 +1,24 @@ +/** + * Tauri workspace helpers (directory listing for slash path completion). + * スラッシュのパス補完用ディレクトリ一覧(Tauri)。 + */ + +import { invoke } from "@tauri-apps/api/core"; +import { isTauriDesktop } from "@/lib/platform"; + +/** + * Lists names in a directory relative to the app process cwd (repo root in dev). + * Directories are returned with a trailing `/`. + * + * プロセス cwd 基準の相対ディレクトリ内の名前を返す。ディレクトリは末尾 `/`。 + */ +export async function listWorkspaceDirectoryEntries(relativeDir: string): Promise { + if (!isTauriDesktop()) return []; + try { + return await invoke("list_workspace_directory_entries", { + relativeDir: relativeDir.replace(/\\/g, "/"), + }); + } catch { + return []; + } +} diff --git a/src/pages/NotePageView.tsx b/src/pages/NotePageView.tsx index 87674cb8..cab87f61 100644 --- a/src/pages/NotePageView.tsx +++ b/src/pages/NotePageView.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ArrowLeft } from "lucide-react"; import { AppLayout } from "@/components/layout/AppLayout"; @@ -9,7 +9,10 @@ import { useNote, useNotePage } from "@/hooks/useNoteQueries"; import { useAuth } from "@/hooks/useAuth"; import { useCollaboration } from "@/hooks/useCollaboration"; import { ContentWithAIChat } from "@/components/ai-chat/ContentWithAIChat"; +import { NoteWorkspaceProvider, useNoteWorkspaceOptional } from "@/contexts/NoteWorkspaceContext"; import { useAIChatContext } from "@/contexts/AIChatContext"; +import { NoteWorkspaceToolbar } from "@/components/note/NoteWorkspaceToolbar"; +import { convertMarkdownToTiptapContent } from "@/lib/markdownToTiptap"; import type { UseCollaborationReturn } from "@/lib/collaboration/types"; import type { Page } from "@/types/page"; @@ -23,28 +26,38 @@ function canEditPage( return Boolean(userId && page?.ownerUserId && page.ownerUserId === userId); } -/** key={page.id} でページ切替時にリセット。editorContent の初期値を page.content から取得。 */ +/** + * Uses `key` on the parent so page switches reset local editor state. + * `editorContent` の初期値は `page.content` から。 + */ function NotePageEditorEditable({ page, + noteId, collaboration, isCollaborationEnabled, }: { page: Page; + noteId: string; collaboration: UseCollaborationReturn; isCollaborationEnabled: boolean; -}) { +}): React.JSX.Element { const [editorContent, setEditorContent] = useState(page.content ?? ""); - const { setPageContext, contentAppendHandlerRef } = useAIChatContext(); + const { setPageContext, contentAppendHandlerRef, insertAtCursorRef } = useAIChatContext(); + const noteWorkspace = useNoteWorkspaceOptional(); + const workspaceRoot = noteWorkspace?.workspaceRoot ?? null; + const editorInsertRef = useRef<((content: unknown) => boolean) | null>(null); useEffect(() => { setPageContext({ type: "editor", pageId: page.id, + noteId, + claudeWorkspaceRoot: workspaceRoot ?? undefined, pageTitle: page.title, pageContent: editorContent.slice(0, 3000), pageFullContent: editorContent, }); - }, [page.id, page.title, editorContent, setPageContext]); + }, [page.id, page.title, editorContent, setPageContext, noteId, workspaceRoot]); useEffect(() => { return () => setPageContext(null); @@ -57,8 +70,25 @@ function NotePageEditorEditable({ }; }, [contentAppendHandlerRef]); + useEffect(() => { + insertAtCursorRef.current = (markdown: string) => { + if (!editorInsertRef.current) return false; + try { + const docJson = convertMarkdownToTiptapContent(markdown); + const doc = JSON.parse(docJson) as { content: unknown[] }; + return editorInsertRef.current(doc.content); + } catch { + return false; + } + }; + return () => { + insertAtCursorRef.current = null; + }; + }, [insertAtCursorRef]); + return ( + undefined} collaboration={isCollaborationEnabled ? collaboration : undefined} + insertAtCursorRef={editorInsertRef} /> ); } /** - * + * Single page inside a note (collaboration, AI chat, optional linked workspace). + * ノート内の 1 ページ(コラボ・AI チャット・任意のワークスペース連携)。 */ const NotePageView: React.FC = () => { const { noteId, pageId } = useParams<{ noteId: string; pageId: string }>(); @@ -157,29 +189,32 @@ const NotePageView: React.FC = () => {
- {canEdit ? ( - - ) : ( - undefined} - onContentError={() => undefined} - /> - )} + + {canEdit ? ( + + ) : ( + undefined} + onContentError={() => undefined} + /> + )} +
diff --git a/src/stores/mcpConfigStore.test.ts b/src/stores/mcpConfigStore.test.ts new file mode 100644 index 00000000..0c706d6b --- /dev/null +++ b/src/stores/mcpConfigStore.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from "vitest"; +import { stripSensitiveConfigForPersist } from "./mcpConfigStore"; + +describe("stripSensitiveConfigForPersist", () => { + it("drops env for stdio", () => { + const c = stripSensitiveConfigForPersist({ + type: "stdio", + command: "npx", + args: ["a"], + env: { TOKEN: "secret" }, + }); + expect(c).toEqual({ type: "stdio", command: "npx", args: ["a"] }); + }); + + it("drops headers for http", () => { + const c = stripSensitiveConfigForPersist({ + type: "http", + url: "https://x/mcp", + headers: { h: "v" }, + }); + expect(c).toEqual({ type: "http", url: "https://x/mcp" }); + }); +}); diff --git a/src/stores/mcpConfigStore.ts b/src/stores/mcpConfigStore.ts new file mode 100644 index 00000000..4cb07eec --- /dev/null +++ b/src/stores/mcpConfigStore.ts @@ -0,0 +1,247 @@ +/** + * MCP サーバー設定の永続化ストア(Issue #463)。 + * Zustand store for persisting MCP server configurations (Issue #463). + * + * 有効なサーバーを SDK 形式に変換する `getMcpServersForQuery()` を提供する。 + * Provides `getMcpServersForQuery()` to convert enabled servers to SDK format. + * + * 機密(env / HTTP headers)は localStorage に保存しない(partialize で除外)。 + * Secrets (env / HTTP headers) are not persisted to localStorage (stripped in partialize). + */ + +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import type { + McpServerEntry, + McpServerConfig, + McpServersRecord, + McpConnectionStatus, + McpServerTool, +} from "../types/mcp"; + +interface McpConfigState { + /** 登録済み MCP サーバー一覧 / Registered MCP servers */ + servers: McpServerEntry[]; + + // ── Actions ── + + /** + * サーバーを追加する。同名がある場合は上書き。 + * Add a server. Overwrites if a server with the same name exists. + */ + addServer: (name: string, config: McpServerConfig) => void; + + /** + * サーバーを削除する。 + * Remove a server by name. + */ + removeServer: (name: string) => void; + + /** + * サーバー設定を更新する。 + * Update a server's config. + */ + updateServer: (name: string, config: McpServerConfig) => void; + + /** + * サーバーの有効/無効を切り替える。 + * Toggle a server's enabled state. + */ + toggleServer: (name: string, enabled: boolean) => void; + + /** + * サーバーの接続ステータスを更新する。 + * Update a server's connection status. + */ + setServerStatus: ( + name: string, + status: McpConnectionStatus, + error?: string, + tools?: McpServerTool[], + ) => void; + + /** + * 複数のサーバーのステータスを一括更新する。 + * Batch update statuses for multiple servers. + */ + updateStatuses: ( + statuses: Array<{ + name: string; + status: McpConnectionStatus; + error?: string; + tools?: McpServerTool[]; + }>, + ) => void; + + /** + * 外部設定(Claude Code の claude_desktop_config.json 等)からインポートする。 + * Import servers from external config (e.g. Claude Code's claude_desktop_config.json). + * + * 既存のサーバーと名前が重複するものはスキップする。 + * Skips servers whose names already exist. + */ + importServers: (entries: Array<{ name: string; config: McpServerConfig }>) => void; + + /** + * 全サーバーをクリアする。 + * Clear all servers. + */ + clearAll: () => void; +} + +/** partialize / migrate で保存するスライスの型。Type for persisted slice. */ +type McpPersistedSlice = Pick; + +/** + * 永続化時に env / headers を除いた設定を返す(localStorage に機密を載せない)。 + * Returns config without env/headers for persistence (no secrets in localStorage). + */ +export function stripSensitiveConfigForPersist(config: McpServerConfig): McpServerConfig { + if (config.type === "stdio") { + return { + type: "stdio", + command: config.command, + args: config.args, + }; + } + if (config.type === "http") { + return { + type: "http", + url: config.url, + }; + } + return { + type: "sse", + url: config.url, + }; +} + +/** + * 有効なサーバーを SDK の `options.mcpServers` に渡す Record 形式に変換する。 + * Converts enabled servers to the Record format expected by SDK's `options.mcpServers`. + */ +export function getMcpServersForQuery(servers: McpServerEntry[]): McpServersRecord | undefined { + const enabled = servers.filter((s) => s.enabled); + if (enabled.length === 0) return undefined; + + const record: McpServersRecord = {}; + for (const entry of enabled) { + record[entry.name] = entry.config; + } + return record; +} + +/** + * MCP サーバー設定の Zustand ストア(永続化あり)。 + * Zustand store for MCP server settings (persisted). + */ +export const useMcpConfigStore = create()( + persist( + (set) => ({ + servers: [], + + addServer: (name, config) => + set((state) => { + const existing = state.servers.findIndex((s) => s.name === name); + const entry: McpServerEntry = { + name, + config, + enabled: true, + status: "unknown", + }; + if (existing >= 0) { + const updated = [...state.servers]; + updated[existing] = entry; + return { servers: updated }; + } + return { servers: [...state.servers, entry] }; + }), + + removeServer: (name) => + set((state) => ({ + servers: state.servers.filter((s) => s.name !== name), + })), + + updateServer: (name, config) => + set((state) => ({ + servers: state.servers.map((s) => + s.name === name ? { ...s, config, status: "unknown" as const } : s, + ), + })), + + toggleServer: (name, enabled) => + set((state) => ({ + servers: state.servers.map((s) => (s.name === name ? { ...s, enabled } : s)), + })), + + setServerStatus: (name, status, error, tools) => + set((state) => ({ + servers: state.servers.map((s) => + s.name === name ? { ...s, status, error, tools: tools ?? s.tools } : s, + ), + })), + + updateStatuses: (statuses) => + set((state) => { + const statusMap = new Map(statuses.map((s) => [s.name, s])); + return { + servers: state.servers.map((s) => { + const update = statusMap.get(s.name); + if (!update) return s; + return { + ...s, + status: update.status, + error: update.error, + tools: update.tools ?? s.tools, + }; + }), + }; + }), + + importServers: (entries) => + set((state) => { + const existingNames = new Set(state.servers.map((s) => s.name)); + const newEntries: McpServerEntry[] = entries + .filter((e) => !existingNames.has(e.name)) + .map((e) => ({ + name: e.name, + config: e.config, + enabled: true, + status: "unknown" as const, + })); + return { servers: [...state.servers, ...newEntries] }; + }), + + clearAll: () => set({ servers: [] }), + }), + { + name: "mcp-config-storage", + version: 2, + migrate: (persistedState, fromVersion) => { + if ( + fromVersion < 2 && + persistedState && + typeof persistedState === "object" && + "servers" in persistedState + ) { + const ps = persistedState as { servers: McpServerEntry[] }; + return { + servers: ps.servers.map((s) => ({ + ...s, + config: stripSensitiveConfigForPersist(s.config), + })), + } satisfies McpPersistedSlice; + } + return persistedState as McpPersistedSlice; + }, + partialize: (state) => ({ + servers: state.servers.map((s) => ({ + name: s.name, + config: stripSensitiveConfigForPersist(s.config), + enabled: s.enabled, + status: "unknown" as McpConnectionStatus, + })), + }), + }, + ), +); diff --git a/src/stores/workflowDefinitionsStore.ts b/src/stores/workflowDefinitionsStore.ts new file mode 100644 index 00000000..1aac0cef --- /dev/null +++ b/src/stores/workflowDefinitionsStore.ts @@ -0,0 +1,39 @@ +/** + * Persisted custom workflow definitions (Issue #462). + * 永続化するカスタムワークフロー定義(Issue #462)。 + */ + +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import type { WorkflowDefinition } from "@/lib/workflow/types"; + +interface WorkflowDefinitionsState { + definitions: WorkflowDefinition[]; + /** Insert or replace by id. / id で挿入または置換 */ + upsertDefinition: (definition: WorkflowDefinition) => void; + removeDefinition: (id: string) => void; +} + +/** + * Local persisted store for user-defined workflows. + * ユーザー定義ワークフローをローカル永続化するストア。 + */ +export const useWorkflowDefinitionsStore = create()( + persist( + (set, get) => ({ + definitions: [], + upsertDefinition: (definition) => { + const rest = get().definitions.filter((d) => d.id !== definition.id); + const next = [...rest, definition].sort((a, b) => b.updatedAt - a.updatedAt); + set({ definitions: next }); + }, + removeDefinition: (id) => { + set({ definitions: get().definitions.filter((d) => d.id !== id) }); + }, + }), + { + name: "zedi-workflow-definitions", + version: 1, + }, + ), +); diff --git a/src/types/ai.ts b/src/types/ai.ts index 6801f1d4..00d1d01e 100644 --- a/src/types/ai.ts +++ b/src/types/ai.ts @@ -1,11 +1,24 @@ -// AI関連の型定義 +// AI関連の型定義 / AI-related type definitions -export type AIProviderType = "openai" | "anthropic" | "google"; +/** + * AI プロバイダー種別。API 直接呼び出し 3 種 + Claude Code (Tauri desktop only)。 + * AI provider type. Three direct-API providers plus Claude Code (Tauri desktop only). + */ +export type AIProviderType = "openai" | "anthropic" | "google" | "claude-code"; +/** User API key vs Zedi server API. */ export type APIMode = "user_api_key" | "api_server"; +/** + * 利用モードの論理的な 3 分類。`AISettings` の `provider` + `apiMode` から導出する。 + * Logical interaction mode derived from `AISettings.provider` + `apiMode`. + */ +export type AIInteractionMode = "default" | "user_api_key" | "claude_code"; + +/** Subscription tier for model access. */ export type UserTier = "free" | "pro"; +/** Persisted AI preferences (provider, model, mode). */ export interface AISettings { provider: AIProviderType; apiKey: string; // ユーザーAPIキーモード時のみ使用 @@ -15,18 +28,48 @@ export interface AISettings { isConfigured: boolean; } +/** + * AI プロバイダーが提供する機能セット(Issue #457)。 + * Capability matrix for an AI provider (Issue #457). + */ +export interface AICapabilities { + /** テキスト生成 / Text generation */ + textGeneration: boolean; + /** ローカルファイルアクセス(Claude Code のみ) / Local file access (Claude Code only) */ + fileAccess: boolean; + /** コマンド実行(Claude Code のみ) / Command execution (Claude Code only) */ + commandExecution: boolean; + /** Web 検索 / Web search */ + webSearch: boolean; + /** MCP 統合(Claude Code のみ) / MCP integration (Claude Code only) */ + mcpIntegration: boolean; + /** エージェントループ(Claude Code のみ) / Agent loop (Claude Code only) */ + agentLoop: boolean; +} + +/** + * AI プロバイダーの静的メタデータ。UI 表示・設定バリデーションに使う。 + * Static metadata for an AI provider. Used for UI rendering and settings validation. + */ export interface AIProvider { id: AIProviderType; name: string; - defaultModels: string[]; // フォールバック用のデフォルトモデル + defaultModels: string[]; apiKeyPrefix: string; apiKeyHelpUrl: string; placeholder: string; requiresApiKey: boolean; description?: string; + capabilities: AICapabilities; + /** + * デスクトップ環境(Tauri)でのみ利用可能か。 + * Whether this provider requires a desktop (Tauri) environment. + */ + desktopOnly?: boolean; } // サーバーから取得するモデル情報 +/** Model row returned from the server models API. */ export interface AIModel { id: string; // e.g. "openai:gpt-4o-mini" provider: AIProviderType; @@ -39,6 +82,7 @@ export interface AIModel { } // AI使用量情報 +/** Usage quota snapshot for the current billing period. */ export interface AIUsage { usagePercent: number; consumedUnits: number; @@ -49,6 +93,7 @@ export interface AIUsage { } // AIレスポンスに含まれるusage情報 +/** Token/cost usage attached to a completion response. */ export interface AIResponseUsage { inputTokens: number; outputTokens: number; @@ -57,6 +102,7 @@ export interface AIResponseUsage { } // キャッシュされたモデル一覧(レガシー:ユーザーAPIキーモード用) +/** Legacy cache of model id strings for user-key mode. */ export interface CachedModels { provider: AIProviderType; models: string[]; @@ -64,13 +110,35 @@ export interface CachedModels { } // サーバーから取得したモデル一覧のキャッシュ +/** Cached server model list + tier from `/api/ai/models`. */ export interface CachedServerModels { models: AIModel[]; tier: UserTier; cachedAt: number; } +/** API 経由のプロバイダー共通ケーパビリティ / Common capabilities for direct-API providers */ +const API_PROVIDER_CAPABILITIES: AICapabilities = { + textGeneration: true, + fileAccess: false, + commandExecution: false, + webSearch: true, + mcpIntegration: false, + agentLoop: false, +}; + +/** Claude Code (Sidecar) のケーパビリティ / Claude Code sidecar capabilities */ +const CLAUDE_CODE_CAPABILITIES: AICapabilities = { + textGeneration: true, + fileAccess: true, + commandExecution: true, + webSearch: true, + mcpIntegration: true, + agentLoop: true, +}; + // モデル方針: Gemini 3.x / GPT-5 / Claude 4 以上のみ +/** Static registry of AI providers for settings UI. */ export const AI_PROVIDERS: AIProvider[] = [ { id: "google", @@ -81,6 +149,7 @@ export const AI_PROVIDERS: AIProvider[] = [ placeholder: "AIza...", requiresApiKey: true, description: "Google Gemini 3 モデル", + capabilities: API_PROVIDER_CAPABILITIES, }, { id: "openai", @@ -91,6 +160,7 @@ export const AI_PROVIDERS: AIProvider[] = [ placeholder: "sk-...", requiresApiKey: true, description: "OpenAI GPT-5 モデル", + capabilities: API_PROVIDER_CAPABILITIES, }, { id: "anthropic", @@ -101,9 +171,23 @@ export const AI_PROVIDERS: AIProvider[] = [ placeholder: "sk-ant-...", requiresApiKey: true, description: "Anthropic Claude 4 モデル", + capabilities: API_PROVIDER_CAPABILITIES, + }, + { + id: "claude-code", + name: "Claude Code", + defaultModels: [], + apiKeyPrefix: "", + apiKeyHelpUrl: "https://docs.anthropic.com/en/docs/claude-code/overview", + placeholder: "", + requiresApiKey: false, + description: "Claude Code (デスクトップ専用 / Desktop only)", + capabilities: CLAUDE_CODE_CAPABILITIES, + desktopOnly: true, }, ]; +/** Default settings when none are stored yet. */ export const DEFAULT_AI_SETTINGS: AISettings = { provider: "google", apiKey: "", @@ -113,16 +197,49 @@ export const DEFAULT_AI_SETTINGS: AISettings = { isConfigured: false, }; +/** + * サーバー API で使えるプロバイダーのみ(claude-code 除外)。 + * Providers usable via server API (excludes claude-code). + */ +export type APIProviderType = Exclude; + +/** + * API 直接呼び出し用プロバイダーのみのリスト。 + * List of direct-API providers only (no desktop-only entries). + */ +export const API_ONLY_PROVIDERS: AIProvider[] = AI_PROVIDERS.filter((p) => !p.desktopOnly); + +/** Look up provider metadata by id. */ export function getProviderById(id: AIProviderType): AIProvider | undefined { return AI_PROVIDERS.find((p) => p.id === id); } +/** + * プロバイダーが API キー方式(直接 API)のプロバイダーかどうか。 + * Whether the provider type is a direct-API provider (not claude-code). + */ +export function isAPIProvider(id: AIProviderType): id is APIProviderType { + return id !== "claude-code"; +} + +/** First default model id for a provider. */ export function getDefaultModel(provider: AIProviderType): string { const providerInfo = getProviderById(provider); return providerInfo?.defaultModels[0] ?? ""; } +/** Default model list for a provider. */ export function getDefaultModels(provider: AIProviderType): string[] { const providerInfo = getProviderById(provider); return providerInfo?.defaultModels ?? []; } + +/** + * `AISettings` から論理的な利用モードを導出する。 + * Derives the logical interaction mode from settings. + */ +export function getInteractionMode(settings: AISettings): AIInteractionMode { + if (settings.provider === "claude-code") return "claude_code"; + if (settings.apiMode === "user_api_key") return "user_api_key"; + return "default"; +} diff --git a/src/types/aiChat.ts b/src/types/aiChat.ts index 8dc23907..6cba11e7 100644 --- a/src/types/aiChat.ts +++ b/src/types/aiChat.ts @@ -21,6 +21,15 @@ export interface Conversation { updatedAt: number; } +/** + * ツール実行状況の 1 エントリ。 + * A single tool execution status entry. + */ +export interface ToolExecution { + toolName: string; + status: "running" | "completed"; +} + /** メッセージ */ export interface ChatMessage { id: string; @@ -30,6 +39,11 @@ export interface ChatMessage { referencedPages?: ReferencedPage[]; // このメッセージに添付された参照ページ /** このメッセージの生成に使用されたモデル表示名 */ modelDisplayName?: string; + /** + * ストリーミング中のツール実行状況リスト(Claude Code のみ)。 + * Tool execution status list during streaming (Claude Code only). + */ + toolExecutions?: ToolExecution[]; timestamp: number; isStreaming?: boolean; error?: string; @@ -146,6 +160,16 @@ export const MAX_REFERENCED_PAGES = 5; export interface PageContext { type: "editor" | "home" | "search" | "other"; pageId?: string; + /** + * Parent note id when editing a page inside a note (local metadata only). + * ノート内ページ編集中の親ノート ID(ローカルメタデータのみ)。 + */ + noteId?: string; + /** + * Linked local workspace root for Claude Code cwd (desktop, not sent to API server). + * Claude Code cwd 用のローカルワークスペース(デスクトップ、API サーバには送らない)。 + */ + claudeWorkspaceRoot?: string; pageTitle?: string; pageContent?: string; /** Full editor content for local actions such as AI-driven page updates. */ diff --git a/src/types/generalSettings.ts b/src/types/generalSettings.ts index 5546d1f3..1fad17f7 100644 --- a/src/types/generalSettings.ts +++ b/src/types/generalSettings.ts @@ -1,23 +1,34 @@ // 一般設定の型定義 +/** UI テーマ(システム追従またはライト/ダーク)。 / UI theme (system, light, or dark). */ export type ThemeMode = "system" | "light" | "dark"; +/** エディタのプリセットまたはカスタム px。 / Editor font size preset or custom px. */ export type EditorFontSize = "small" | "medium" | "large" | "custom"; +/** UI 表示言語。 / UI display language. */ export type UILocale = "ja" | "en"; +/** 一般設定の永続化フィールド。 / Persisted general settings fields. */ export interface GeneralSettings { theme: ThemeMode; editorFontSize: EditorFontSize; /** カスタムフォントサイズ(editorFontSize が "custom" のときのみ使用) */ customFontSizePx?: number; locale: UILocale; + /** + * When true, show a confirmation dialog before running executable code blocks (Claude Code). + * true のとき、実行可能コードブロック実行前に確認ダイアログを表示する(Claude Code)。 + */ + executableCodeConfirmBeforeRun?: boolean; } +/** 初回・リセット時の既定値。 / Defaults for first load and reset. */ export const DEFAULT_GENERAL_SETTINGS: GeneralSettings = { theme: "system", editorFontSize: "medium", locale: "ja", + executableCodeConfirmBeforeRun: true, }; /** テーマの表示名 */ diff --git a/src/types/mcp.ts b/src/types/mcp.ts new file mode 100644 index 00000000..5b5c4558 --- /dev/null +++ b/src/types/mcp.ts @@ -0,0 +1,115 @@ +/** + * MCP (Model Context Protocol) 関連の型定義(Issue #463)。 + * MCP-related type definitions (Issue #463). + * + * SDK の McpStdioServerConfig / McpSSEServerConfig / McpHttpServerConfig に対応する + * シリアライズ可能な設定型を定義する。 + * Defines serializable config types matching SDK's Mcp*ServerConfig. + */ + +/** + * MCP サーバーのトランスポート種別。 + * Transport type for an MCP server. + */ +export type McpServerTransport = "stdio" | "http" | "sse"; + +/** + * stdio トランスポート設定。 + * stdio transport configuration. + */ +export interface McpStdioConfig { + type: "stdio"; + /** 実行するコマンド / Command to execute */ + command: string; + /** コマンド引数 / Command arguments */ + args?: string[]; + /** 環境変数 / Environment variables */ + env?: Record; +} + +/** + * HTTP (streamable) トランスポート設定。 + * HTTP (streamable) transport configuration. + */ +export interface McpHttpConfig { + type: "http"; + /** サーバー URL / Server URL */ + url: string; + /** HTTP ヘッダー / HTTP headers */ + headers?: Record; +} + +/** + * SSE (Server-Sent Events) トランスポート設定。 + * SSE (Server-Sent Events) transport configuration. + */ +export interface McpSseConfig { + type: "sse"; + /** サーバー URL / Server URL */ + url: string; + /** HTTP ヘッダー / HTTP headers */ + headers?: Record; +} + +/** + * MCP サーバー設定の共用体。SDK の McpServerConfigForProcessTransport に対応。 + * Union of MCP server configs. Maps to SDK's McpServerConfigForProcessTransport. + */ +export type McpServerConfig = McpStdioConfig | McpHttpConfig | McpSseConfig; + +/** + * MCP サーバーの接続ステータス。SDK の McpServerStatus.status に対応。 + * Connection status of an MCP server. Maps to SDK's McpServerStatus.status. + */ +export type McpConnectionStatus = + | "connected" + | "failed" + | "needs-auth" + | "pending" + | "disabled" + | "unknown"; + +/** + * MCP サーバーが提供するツール情報。 + * Tool information provided by an MCP server. + */ +export interface McpServerTool { + name: string; + description?: string; +} + +/** + * ストアで管理する MCP サーバーエントリ(設定 + ランタイム状態)。 + * MCP server entry managed in the store (config + runtime state). + */ +export interface McpServerEntry { + /** 一意なサーバー名(SDK に渡すキー) / Unique server name (key passed to SDK) */ + name: string; + /** トランスポート設定 / Transport configuration */ + config: McpServerConfig; + /** 有効/無効フラグ / Enabled/disabled flag */ + enabled: boolean; + /** 最新の接続ステータス / Latest connection status */ + status: McpConnectionStatus; + /** エラーメッセージ(status が failed 時) / Error message when status is failed */ + error?: string; + /** サーバーが提供するツール一覧 / Tools provided by the server */ + tools?: McpServerTool[]; +} + +/** + * SDK の query() options.mcpServers に渡す形式へ変換するための Record 型。 + * Record type for conversion to SDK's query() options.mcpServers format. + */ +export type McpServersRecord = Record; + +/** + * SDK から返る MCP サーバーステータス情報。 + * MCP server status info returned from the SDK. + */ +export interface McpServerStatusInfo { + name: string; + status: McpConnectionStatus; + error?: string; + tools?: McpServerTool[]; +} diff --git a/vite.config.ts b/vite.config.ts index a2b26925..15f127e3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,25 +12,43 @@ const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, "package.jso }; const appVersion = packageJson.version ?? "0.0.0"; +/** + * Parse a truthy env string (e.g. `TAURI_ENV_DEBUG`). Non-empty strings like `"false"` are not treated as true. + * 環境変数の真偽フラグを解釈する。`"false"` などは true にしない。 + */ +function envFlagIsTrue(value: string | undefined): boolean { + return /^(1|true|yes)$/i.test(String(value ?? "").trim()); +} + // https://vitejs.dev/config/ export default defineConfig(({ mode }) => { // Load all env vars (including non-VITE_ prefixed) for vite.config use const env = loadEnv(mode, process.cwd(), ""); - // 環境変数からポートを取得、デフォルトは30000(被りにくい開発用ポート) - // ポートが使用中の場合は自動的に次の利用可能なポートを使用 const port = parseInt(env.VITE_PORT || env.PORT || "30000", 10); // API Gateway URL for dev proxy (avoids CORS in local development). - // Uses ZEDI_API_PROXY_TARGET (no VITE_ prefix) so client code doesn't see it; - // client requests go to same-origin /api/* which Vite proxies to the real API. + // Uses ZEDI_API_PROXY_TARGET (no VITE_ prefix) so client code doesn't see it. const apiTarget = env.ZEDI_API_PROXY_TARGET || ""; + // Tauri CLI は子プロセスの process.env に TAURI_* を注入する。loadEnv は .env 由来のみのため + // process.env とマージして参照する(strictPort / build.target の分岐を確実にする)。 + // Tauri CLI injects TAURI_* into the dev server process; merge with loadEnv for reliable detection. + const tauriPlatform = process.env.TAURI_ENV_PLATFORM ?? env.TAURI_ENV_PLATFORM; + const tauriDevHost = process.env.TAURI_DEV_HOST ?? env.TAURI_DEV_HOST; + const tauriEnvDebug = envFlagIsTrue(process.env.TAURI_ENV_DEBUG ?? env.TAURI_ENV_DEBUG); + const isTauri = !!tauriPlatform; + return { + clearScreen: false, server: { - host: "::", + host: tauriDevHost || "::", port, - strictPort: false, // ポートが使用中の場合は自動的に次のポートを使用 + strictPort: isTauri, // Tauri はポート固定を期待 / Tauri expects a fixed port + hmr: tauriDevHost ? { protocol: "ws", host: tauriDevHost, port: 1421 } : undefined, + watch: { + ignored: ["**/src-tauri/**"], + }, ...(apiTarget ? { proxy: { @@ -43,9 +61,12 @@ export default defineConfig(({ mode }) => { } : {}), }, - // Enable top-level await for sql.js + // Vite はプレフィックスの前方一致のみ(ワイルドカードなし)。TAURI_ENV_* は使えない。 + envPrefix: ["VITE_", "TAURI_ENV_"], build: { - target: "esnext", + target: isTauri ? (tauriPlatform === "windows" ? "chrome105" : "safari13") : "esnext", + minify: tauriEnvDebug ? false : "esbuild", + sourcemap: tauriEnvDebug, }, define: { "import.meta.env.VITE_APP_VERSION": JSON.stringify(appVersion),
A1