diff --git a/README.ja.md b/README.ja.md
new file mode 100644
index 000000000..6e3f7a992
--- /dev/null
+++ b/README.ja.md
@@ -0,0 +1,172 @@
+
+
+
+
+Paseo
+
+
+ English ·
+ 简体中文 ·
+ 日本語
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Claude Code、Codex、Copilot、OpenCode、Pi のエージェントを、ひとつのインターフェースで。
+
+
+
+
+
+
+
+
+
+> [!NOTE]
+> 私はひとりでメンテナンスしているため、GitHub Issues を毎日確認できるとは限りません。
+> 急ぎの問題や作業がブロックされている場合は、[Discord](https://discord.gg/jz8T2uahpH) から連絡するのが一番早いです。
+
+---
+
+自分のマシンでエージェントを並列実行。スマートフォンからでもデスクからでも、開発を進めてリリースできます。
+
+- **セルフホスト:** エージェントはあなたのマシン上で動作し、完全な開発環境を使用します。自分のツール・設定・スキルをそのまま活用できます。
+- **マルチプロバイダー:** Claude Code、Codex、Copilot、OpenCode、Pi を同一のインターフェースで利用。タスクに合ったモデルを選べます。
+- **音声コントロール:** 音声モードでタスクを口述したり問題を話し合ったりできます。ハンズフリーが必要なときに便利です。
+- **クロスデバイス:** iOS、Android、デスクトップ、Web、CLI に対応。机で作業を始め、スマートフォンで確認し、ターミナルから自動化できます。
+- **プライバシー優先:** Paseo にはテレメトリー・トラッキング・強制ログインは一切ありません。
+
+## はじめかた
+
+Paseo はコーディングエージェントを管理するローカルサーバー(デーモン)を起動します。デスクトップアプリ・モバイルアプリ・Web アプリ・CLI などのクライアントがこのデーモンに接続します。
+
+### 前提条件
+
+エージェント CLI をひとつ以上インストールし、認証情報を設定しておく必要があります。
+
+- [Claude Code](https://docs.anthropic.com/en/docs/claude-code)
+- [Codex](https://github.com/openai/codex)
+- [GitHub Copilot](https://github.com/features/copilot/cli/)
+- [OpenCode](https://github.com/anomalyco/opencode)
+- [Pi](https://pi.dev)
+
+### デスクトップアプリ(推奨)
+
+[paseo.sh/download](https://paseo.sh/download) または [GitHub のリリースページ](https://github.com/getpaseo/paseo/releases)からダウンロードしてください。アプリを開くとデーモンが自動的に起動します。追加のインストールは不要です。
+
+スマートフォンから接続するには、Settings 画面に表示される QR コードをスキャンしてください。
+
+### CLI / ヘッドレス
+
+CLI をインストールして Paseo を起動します。
+
+```bash
+npm install -g @getpaseo/cli
+paseo
+```
+
+ターミナルに QR コードが表示されます。どのクライアントからでも接続できます。サーバーやリモートマシンでの利用に適しています。
+
+詳しいセットアップと設定については以下を参照してください。
+
+- [ドキュメント](https://paseo.sh/docs)
+- [設定リファレンス](https://paseo.sh/docs/configuration)
+
+## CLI
+
+アプリでできることはすべてターミナルからも実行できます。
+
+```bash
+paseo run --provider claude/opus-4.6 "implement user authentication"
+paseo run --provider codex/gpt-5.4 --worktree feature-x "implement feature X"
+
+paseo ls # 実行中のエージェントを一覧表示
+paseo attach abc123 # ライブ出力をストリーミング
+paseo send abc123 "also add tests" # 追加タスクを送信
+
+# リモートデーモンで実行
+paseo --host workstation.local:6767 run "run the full test suite"
+```
+
+詳細は[完全な CLI リファレンス](https://paseo.sh/docs/cli)を参照してください。
+
+## スキル
+
+スキルはエージェントに Paseo を使って他のエージェントをオーケストレーションする方法を教えます。
+
+```bash
+npx skills add getpaseo/paseo
+```
+
+どのエージェントとの会話でも使用できます。
+
+- `/paseo-handoff` — エージェント間で作業を引き継ぎます。私はこれを使って Claude で計画し、Codex に実装を引き継いでいます。
+- `/paseo-loop` — 明確な受け入れ基準に沿ってエージェントをループさせます(Ralph loops とも呼ばれます)。検証役を追加することもできます。
+- `/paseo-advisor` — 単一のエージェントをアドバイザーとして起動し、作業を委任せずにセカンドオピニオンを得ます。
+- `/paseo-committee` — 対照的な2つのエージェントで委員会を構成し、一歩引いた視点で根本原因を分析して計画を作成します。
+
+## 開発
+
+モノレポのパッケージ構成:
+
+- `packages/server`: Paseo デーモン(エージェントプロセスのオーケストレーション、WebSocket API、MCP サーバー)
+- `packages/app`: Expo クライアント(iOS、Android、Web)
+- `packages/cli`: デーモンおよびエージェントワークフロー向け `paseo` CLI
+- `packages/desktop`: Electron デスクトップアプリ
+- `packages/relay`: リモート接続用リレーパッケージ
+- `packages/website`: マーケティングサイトとドキュメント(`paseo.sh`)
+
+よく使うコマンド:
+
+```bash
+# すべてのローカル開発サービスを起動
+npm run dev
+
+# 個別のサービスを起動
+npm run dev:server
+npm run dev:app
+npm run dev:desktop
+npm run dev:website
+
+# サーバースタックをビルド
+npm run build:server
+
+# リポジトリ全体のチェック
+npm run typecheck
+```
+
+## コミュニティ
+
+- [paseo-relay](https://github.com/zenghongtu/paseo-relay) — Go 実装のセルフホスト型リレー
+
+---
+
+
+
+
+
+
+
+
+
+
+
+## ライセンス
+
+AGPL-3.0
diff --git a/README.md b/README.md
index 907f13a34..7bacd87ac 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,8 @@
English ·
- 简体中文
+ 简体中文 ·
+ 日本語
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 07fc4f6cb..385464471 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -6,7 +6,8 @@
English ·
- 简体中文
+ 简体中文 ·
+ 日本語
diff --git a/packages/app/e2e/helpers/workspace.ts b/packages/app/e2e/helpers/workspace.ts
index 275f36a0a..0ab5a9559 100644
--- a/packages/app/e2e/helpers/workspace.ts
+++ b/packages/app/e2e/helpers/workspace.ts
@@ -3,6 +3,9 @@ import { mkdtemp, writeFile, rm, mkdir, realpath } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
+const TEMP_CLEANUP_RETRIES = 5;
+const TEMP_CLEANUP_RETRY_DELAY_MS = 100;
+
interface TempRepo {
path: string;
branchHeads: Record;
@@ -126,7 +129,12 @@ export const createTempGitRepo = async (
path: repoPath,
branchHeads,
cleanup: async () => {
- await rm(repoPath, { recursive: true, force: true });
+ await rm(repoPath, {
+ recursive: true,
+ force: true,
+ maxRetries: TEMP_CLEANUP_RETRIES,
+ retryDelay: TEMP_CLEANUP_RETRY_DELAY_MS,
+ });
},
};
};
@@ -141,7 +149,12 @@ export async function createTempDirectory(prefix = "paseo-e2e-dir-"): Promise {
- await rm(dirPath, { recursive: true, force: true });
+ await rm(dirPath, {
+ recursive: true,
+ force: true,
+ maxRetries: TEMP_CLEANUP_RETRIES,
+ retryDelay: TEMP_CLEANUP_RETRY_DELAY_MS,
+ });
},
};
}
diff --git a/packages/app/src/i18n/i18next.ts b/packages/app/src/i18n/i18next.ts
index 9ad28632b..168f02ade 100644
--- a/packages/app/src/i18n/i18next.ts
+++ b/packages/app/src/i18n/i18next.ts
@@ -5,6 +5,7 @@ import { ar } from "./resources/ar";
import { en } from "./resources/en";
import { es } from "./resources/es";
import { fr } from "./resources/fr";
+import { ja } from "./resources/ja";
import { ru } from "./resources/ru";
import { zhCN } from "./resources/zh-CN";
@@ -20,6 +21,7 @@ observeI18nInit(
en: { translation: en },
es: { translation: es },
fr: { translation: fr },
+ ja: { translation: ja },
ru: { translation: ru },
"zh-CN": { translation: zhCN },
},
diff --git a/packages/app/src/i18n/locales.test.ts b/packages/app/src/i18n/locales.test.ts
index e6c4fd357..b85d9ae76 100644
--- a/packages/app/src/i18n/locales.test.ts
+++ b/packages/app/src/i18n/locales.test.ts
@@ -7,13 +7,14 @@ import {
} from "./locales";
describe("parseAppLanguage", () => {
- it("accepts system and all UN official language locales", () => {
- expect(["system", "ar", "en", "es", "fr", "ru", "zh-CN"].map(parseAppLanguage)).toEqual([
+ it("accepts system and all supported language locales", () => {
+ expect(["system", "ar", "en", "es", "fr", "ja", "ru", "zh-CN"].map(parseAppLanguage)).toEqual([
"system",
"ar",
"en",
"es",
"fr",
+ "ja",
"ru",
"zh-CN",
]);
@@ -24,13 +25,14 @@ describe("parseAppLanguage", () => {
expect(parseAppLanguage(null)).toBeNull();
});
- it("offers system plus the six UN official languages", () => {
+ it("offers system plus all supported languages", () => {
expect(LANGUAGE_OPTIONS.map((option) => option.value)).toEqual([
"system",
"ar",
"en",
"es",
"fr",
+ "ja",
"ru",
"zh-CN",
]);
@@ -81,15 +83,17 @@ describe("resolveSupportedLocale", () => {
expect(resolveSupportedLocale("en", ["zh-CN"])).toBe("en");
expect(resolveSupportedLocale("es", ["en-US"])).toBe("es");
expect(resolveSupportedLocale("fr", ["en-US"])).toBe("fr");
+ expect(resolveSupportedLocale("ja", ["en-US"])).toBe("ja");
expect(resolveSupportedLocale("ru", ["en-US"])).toBe("ru");
expect(resolveSupportedLocale("zh-CN", ["en-US"])).toBe("zh-CN");
});
- it("maps UN official system locales", () => {
+ it("maps supported system locales", () => {
expect(resolveSupportedLocale("system", ["ar-EG"])).toBe("ar");
expect(resolveSupportedLocale("system", ["en-US"])).toBe("en");
expect(resolveSupportedLocale("system", ["es-MX"])).toBe("es");
expect(resolveSupportedLocale("system", ["fr-CA"])).toBe("fr");
+ expect(resolveSupportedLocale("system", ["ja-JP"])).toBe("ja");
expect(resolveSupportedLocale("system", ["ru-RU"])).toBe("ru");
});
diff --git a/packages/app/src/i18n/locales.ts b/packages/app/src/i18n/locales.ts
index decf04ee2..12e3454d1 100644
--- a/packages/app/src/i18n/locales.ts
+++ b/packages/app/src/i18n/locales.ts
@@ -1,4 +1,4 @@
-export type SupportedLocale = "ar" | "en" | "es" | "fr" | "ru" | "zh-CN";
+export type SupportedLocale = "ar" | "en" | "es" | "fr" | "ja" | "ru" | "zh-CN";
export type AppLanguage = "system" | SupportedLocale;
export interface LanguageOption {
@@ -14,16 +14,27 @@ export const LANGUAGE_OPTIONS: LanguageOption[] = [
{ value: "en", labelKey: "settings.general.language.options.en" },
{ value: "es", labelKey: "settings.general.language.options.es" },
{ value: "fr", labelKey: "settings.general.language.options.fr" },
+ { value: "ja", labelKey: "settings.general.language.options.ja" },
{ value: "ru", labelKey: "settings.general.language.options.ru" },
{ value: "zh-CN", labelKey: "settings.general.language.options.zhCN" },
];
-const SUPPORTED_LANGUAGES = new Set(["system", "ar", "en", "es", "fr", "ru", "zh-CN"]);
+const SUPPORTED_LANGUAGES = new Set([
+ "system",
+ "ar",
+ "en",
+ "es",
+ "fr",
+ "ja",
+ "ru",
+ "zh-CN",
+]);
const LANGUAGE_NATIVE_NAMES: Record = {
ar: "العربية",
en: "English",
es: "Español",
fr: "Français",
+ ja: "日本語",
ru: "Русский",
"zh-CN": "简体中文",
};
@@ -33,6 +44,7 @@ const LANGUAGE_NAMES_BY_LOCALE: Record {
- it("keeps UN official language keys in sync with English", () => {
+ it("keeps all supported language keys in sync with English", () => {
const englishKeys = flattenKeys(en).sort();
expect(flattenKeys(ar).sort()).toEqual(englishKeys);
expect(flattenKeys(es).sort()).toEqual(englishKeys);
expect(flattenKeys(fr).sort()).toEqual(englishKeys);
+ expect(flattenKeys(ja).sort()).toEqual(englishKeys);
expect(flattenKeys(ru).sort()).toEqual(englishKeys);
expect(flattenKeys(zhCN).sort()).toEqual(englishKeys);
});
- it("keeps non-English UN official languages translated beyond fallback labels", () => {
+ it("keeps non-English supported languages translated beyond fallback labels", () => {
const totalStrings = Object.keys(flattenStrings(en)).length;
const maxFallbackStrings = Math.floor(totalStrings * 0.25);
expect(countMatchingEnglishStrings(ar)).toBeLessThan(maxFallbackStrings);
expect(countMatchingEnglishStrings(es)).toBeLessThan(maxFallbackStrings);
expect(countMatchingEnglishStrings(fr)).toBeLessThan(maxFallbackStrings);
+ expect(countMatchingEnglishStrings(ja)).toBeLessThan(maxFallbackStrings);
expect(countMatchingEnglishStrings(ru)).toBeLessThan(maxFallbackStrings);
});
@@ -123,6 +126,7 @@ describe("translation resources", () => {
expect(findInterpolationMismatches(ar)).toEqual([]);
expect(findInterpolationMismatches(es)).toEqual([]);
expect(findInterpolationMismatches(fr)).toEqual([]);
+ expect(findInterpolationMismatches(ja)).toEqual([]);
expect(findInterpolationMismatches(ru)).toEqual([]);
expect(findInterpolationMismatches(zhCN)).toEqual([]);
});
@@ -137,11 +141,13 @@ describe("translation resources", () => {
expect(ar.modelSelector.modelCountPlural).toBe("{{count}} نماذج");
expect(es.modelSelector.modelCountPlural).toBe("{{count}} modelos");
expect(fr.modelSelector.modelCountPlural).toBe("{{count}} modèles");
+ expect(ja.modelSelector.modelCountPlural).toBe("{{count}}つのモデル");
expect(ru.modelSelector.modelCountPlural).toBe("{{count}} моделей");
expect(zhCN.modelSelector.modelCountPlural).toBe("{{count}} 个模型");
expect(ar.settings.providers.models.many).toBe("{{count}} نماذج");
expect(es.settings.providers.models.many).toBe("{{count}} modelos");
expect(fr.settings.providers.models.many).toBe("{{count}} modèles");
+ expect(ja.settings.providers.models.many).toBe("{{count}}つのモデル");
expect(ru.settings.providers.models.many).toBe("{{count}} моделей");
expect(zhCN.settings.providers.models.many).toBe("{{count}} 个 Model");
});
diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts
index 604f69708..3366f3469 100644
--- a/packages/app/src/i18n/resources/ar.ts
+++ b/packages/app/src/i18n/resources/ar.ts
@@ -1438,6 +1438,7 @@ export const ar: TranslationResources = {
en: "English",
es: "Español",
fr: "Français",
+ ja: "日本語",
ru: "Русский",
zhCN: "中文",
},
diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts
index 4b1dd6508..da31267a4 100644
--- a/packages/app/src/i18n/resources/en.ts
+++ b/packages/app/src/i18n/resources/en.ts
@@ -1444,6 +1444,7 @@ export const en = {
en: "English",
es: "Spanish",
fr: "French",
+ ja: "Japanese",
ru: "Russian",
zhCN: "Simplified Chinese",
},
diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts
index f100813de..b129b73b6 100644
--- a/packages/app/src/i18n/resources/es.ts
+++ b/packages/app/src/i18n/resources/es.ts
@@ -1475,6 +1475,7 @@ export const es: TranslationResources = {
en: "English",
es: "Español",
fr: "Français",
+ ja: "日本語",
ru: "Русский",
zhCN: "中文",
},
diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts
index 74676b573..922375a23 100644
--- a/packages/app/src/i18n/resources/fr.ts
+++ b/packages/app/src/i18n/resources/fr.ts
@@ -1478,6 +1478,7 @@ export const fr: TranslationResources = {
en: "English",
es: "Español",
fr: "Français",
+ ja: "日本語",
ru: "Русский",
zhCN: "中文",
},
diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts
new file mode 100644
index 000000000..b7522d87a
--- /dev/null
+++ b/packages/app/src/i18n/resources/ja.ts
@@ -0,0 +1,1909 @@
+import type { TranslationResources } from "./en";
+
+export const ja: TranslationResources = {
+ common: {
+ back: "戻る",
+ loading: "読み込み中...",
+ actions: {
+ back: "戻る",
+ cancel: "キャンセル",
+ close: "閉じる",
+ copy: "コピー",
+ dismiss: "閉じる",
+ retry: "再試行",
+ search: "検索",
+ select: "選択",
+ },
+ placeholders: {
+ search: "検索...",
+ },
+ empty: {
+ noResults: "結果が見つかりません",
+ noOptionsMatchSearch: "検索に一致するオプションがありません。",
+ },
+ states: {
+ loading: "読み込み中...",
+ starting: "起動中...",
+ copied: "コピーしました",
+ copiedLabel: "{{label}}をコピーしました",
+ downloadComplete: "ダウンロード完了",
+ downloadFailed: "ダウンロード失敗",
+ },
+ errors: {
+ error: "エラー",
+ unableToSave: "保存できません",
+ nameRequired: "名前は必須です",
+ daemonUnavailable: "デーモンが利用できません",
+ daemonClientUnavailable: "デーモンクライアントが利用できません",
+ daemonClientDisconnected: "デーモンクライアントが切断されています",
+ noFileFound: "{{token}}のファイルが見つかりません",
+ unexpectedDictationError: "音声入力処理中に予期しないエラーが発生しました。",
+ },
+ connectionStatus: {
+ online: "オンライン",
+ connecting: "接続中",
+ offline: "オフライン",
+ error: "エラー",
+ idle: "アイドル",
+ },
+ },
+ shell: {
+ menu: {
+ toggleSidebar: "サイドバーを切り替え",
+ open: "メニューを開く",
+ close: "メニューを閉じる",
+ },
+ commandCenter: {
+ placeholder: "コマンドを入力またはエージェントを検索...",
+ noMatches: "一致なし",
+ actions: "アクション",
+ agents: "エージェント",
+ newAgent: "新しいエージェント",
+ openProject: "プロジェクトを開く",
+ home: "ホーム",
+ },
+ },
+ composer: {
+ placeholders: {
+ desktop: "エージェントにメッセージ、@ファイル、/コマンドや/スキルを入力",
+ mobile: "メッセージ、@ファイル、/コマンド",
+ fallback: "メッセージ...",
+ },
+ input: {
+ accessibilityLabel: "エージェントにメッセージ...",
+ focusHint: "{{shortcut}}でフォーカス",
+ addAttachment: "添付ファイルを追加",
+ interruptAgent: "エージェントを中断",
+ queueMessage: "メッセージをキューに追加",
+ sendAndInterrupt: "送信して中断",
+ sendMessage: "メッセージを送信",
+ queue: "キュー",
+ send: "送信",
+ },
+ cancel: {
+ cancelingAgent: "エージェントをキャンセル中",
+ stopAgent: "エージェントを停止",
+ interrupt: "中断",
+ },
+ voice: {
+ enableVoiceMode: "音声モードを有効にする",
+ voiceMode: "音声モード",
+ unmuteVoiceMode: "音声モードのミュートを解除",
+ muteVoiceMode: "音声モードをミュート",
+ stopDictation: "音声入力を停止",
+ startDictation: "音声入力を開始",
+ unmuteVoice: "音声のミュートを解除",
+ muteVoice: "音声をミュート",
+ dictation: "音声入力",
+ interruptBeforeVoice: "音声モードを開始する前にエージェントを中断してください",
+ },
+ attachments: {
+ addImage: "画像を追加",
+ addFile: "ファイルをアップロード",
+ addIssueOrPr: "イシューまたはPRを追加",
+ dropImagesHere: "ここに画像をドロップ",
+ dropFilesHere: "ここにファイルをドロップ",
+ editQueuedMessage: "キューに入れたメッセージを編集",
+ sendQueuedMessageNow: "キューに入れたメッセージを今すぐ送信",
+ openImage: "画像添付ファイルを開く",
+ removeImage: "画像添付ファイルを削除",
+ removeFile: "ファイル添付ファイルを削除",
+ openGithub: "{{kind}} #{{number}}を開く",
+ removeGithub: "{{kind}} #{{number}}を削除",
+ element: "要素",
+ openBrowserElement: "ブラウザ要素の添付ファイルを開く",
+ removeBrowserElement: "ブラウザ要素の添付ファイルを削除",
+ openReview: "レビュー添付ファイルを開く",
+ removeReview: "レビュー添付ファイルを削除",
+ },
+ errors: {
+ failedToSend: "メッセージの送信に失敗しました",
+ failedToCreateAgent: "エージェントの作成に失敗しました",
+ noHostSelected: "ホストが選択されていません",
+ initialPromptRequired: "初期プロンプトが必要です",
+ alreadyLoading: "すでに読み込み中です",
+ uploadFailed: "ファイルのアップロードに失敗しました",
+ fileTooLarge: "{{fileName}}が大きすぎます(最大{{size}})",
+ },
+ clientCommands: {
+ archiveAgent: "現在のエージェントをアーカイブ",
+ freshDraft: "このエージェントをアーカイブして新しい下書きを開始",
+ },
+ github: {
+ searching: "検索中...",
+ noResults: "結果が見つかりません。",
+ searchPlaceholder: "イシューとPRを検索...",
+ title: "イシューまたはPRを添付",
+ },
+ },
+ agentControls: {
+ provider: {
+ fallback: "プロバイダー",
+ select: "エージェントプロバイダーを選択",
+ },
+ thinking: {
+ title: "思考",
+ unknown: "不明",
+ extraHigh: "非常に高い",
+ select: "思考オプションを選択",
+ selectWithValue: "思考オプションを選択({{value}})",
+ },
+ model: {
+ unknown: "不明なモデル",
+ },
+ features: {
+ title: "機能",
+ open: "エージェント機能を開く",
+ on: "オン",
+ off: "オフ",
+ },
+ mode: {
+ title: "モード",
+ searchPlaceholder: "モードを検索...",
+ selectWithValue: "エージェントモードを選択({{value}})",
+ },
+ hints: {
+ thinking: "思考モード",
+ model: "モデルを変更",
+ mode: "権限モードを変更",
+ },
+ },
+ agentStream: {
+ empty: "このエージェントとチャットを始めましょう...",
+ scrollToBottom: "下にスクロール",
+ permission: {
+ plan: "プラン",
+ required: "権限が必要です",
+ deny: "拒否",
+ accept: "承認",
+ implement: "実装",
+ question: "どのように続けますか?",
+ proposedPlan: "提案されたプラン",
+ },
+ },
+ agentPanel: {
+ states: {
+ notFound: "エージェントが見つかりません",
+ failedToLoad: "エージェントの読み込みに失敗しました",
+ reconnecting: "再接続中...",
+ archivingTitle: "エージェントをアーカイブ中...",
+ archivingSubtitle: "このエージェントをアーカイブするまでお待ちください。",
+ },
+ unavailable: {
+ selectedHost: "選択中のホスト",
+ unknownHost:
+ "{{serverLabel}}がこのデバイスで設定されていないため、このエージェントを開けません。",
+ addHost:
+ "続行するには設定でホストを追加するか、設定済みのサーバーでエージェントを開いてください。",
+ preparingSession: "{{serverLabel}}セッションを準備中...",
+ connecting: "{{serverLabel}}に接続中...",
+ showSoon: "このエージェントはすぐに表示されます。",
+ showWhenOnline: "ホストがオンラインになったらこのエージェントを表示します。",
+ reconnectingTo: "{{serverLabel}}に再接続中...",
+ showAgainWhenReachable:
+ "ホストに到達できるようになり次第、このエージェントを再び表示します。",
+ },
+ archived: {
+ callout: "このエージェントはアーカイブされています",
+ unarchive: "アーカイブ解除",
+ },
+ },
+ sessions: {
+ title: "履歴",
+ empty: "セッションがまだありません",
+ actions: {
+ loadMore: "さらに読み込む",
+ },
+ },
+ agentList: {
+ fallbackTitle: "新しいセッション",
+ dateSections: {
+ recent: "最近",
+ today: "今日",
+ yesterday: "昨日",
+ thisWeek: "今週",
+ thisMonth: "今月",
+ older: "以前",
+ },
+ status: {
+ initializing: "起動中",
+ idle: "アイドル",
+ running: "実行中",
+ error: "エラー",
+ closed: "クローズ",
+ },
+ badges: {
+ archived: "アーカイブ済み",
+ pending: "{{count}}件保留中",
+ attention: "注意",
+ },
+ archiveSheet: {
+ hostOffline: "ホストオフライン",
+ runningAgent: "このエージェントはまだ実行中です。アーカイブするとエージェントが停止します。",
+ archive: "アーカイブ",
+ },
+ },
+ message: {
+ actions: {
+ copyCode: "コードをコピー",
+ copyTurn: "ターンをコピー",
+ copyMessage: "メッセージをコピー",
+ openFile: "ファイルを開く",
+ copied: "コピーしました",
+ },
+ attachments: {
+ dismissImage: "画像を閉じる",
+ closeImage: "画像を閉じる",
+ imageLoadFailed: "画像を読み込めませんでした",
+ imageUnavailable: "画像が利用できません",
+ imagePreviewUnavailable: "画像プレビューは利用できません。",
+ imagePreviewLoadFailed: "画像プレビューを読み込めません。",
+ review: "レビュー",
+ commentsOne: "1件のコメント",
+ commentsMany: "{{count}}件のコメント",
+ textAttachment: "テキスト添付ファイル",
+ text: "テキスト",
+ file: "ファイル",
+ },
+ speak: {
+ header: "読み上げ済み",
+ },
+ activity: {
+ details: "詳細",
+ },
+ dictation: {
+ start: "音声入力を開始",
+ cancel: "音声入力をキャンセル",
+ retry: "音声入力を再試行",
+ insert: "文字起こしを挿入",
+ insertAndSend: "文字起こしを挿入して送信",
+ failed: "音声入力に失敗しました: {{error}}",
+ failedRetry: "音声入力に失敗しました。再試行をタップしてください。",
+ },
+ question: {
+ submit: "送信",
+ next: "次へ",
+ answerPlaceholder: "回答を入力...",
+ otherPlaceholder: "その他...",
+ },
+ todo: {
+ title: "タスク",
+ empty: "タスクがまだありません。",
+ },
+ compaction: {
+ loading: "コンテキストを圧縮中...",
+ auto: "コンテキストが自動的に圧縮されました",
+ manual: "コンテキストが手動で圧縮されました",
+ withTokens: "コンテキストを圧縮しました({{tokens}}Kトークン)",
+ completed: "コンテキストを圧縮しました",
+ },
+ },
+ importSession: {
+ title: "セッションをインポート",
+ filters: {
+ all: "すべて",
+ },
+ status: {
+ connectHost: "セッションをインポートするにはホストに接続してください",
+ updateHost: "セッションをインポートするにはホストを更新してください。",
+ noProviders: "インポート可能なプロバイダーが有効になっていません。",
+ loading: "最近のセッションを読み込み中...",
+ failedAll: "最近のセッションを読み込めませんでした。",
+ failedProviders: "{{providers}}のセッションを読み込めませんでした。",
+ failedImport: "選択したセッションをインポートできませんでした。",
+ },
+ actions: {
+ refresh: "セッションを更新",
+ },
+ preview: {
+ untitledSession: "無題のセッション",
+ noPrompt: "プロンプトのプレビューなし",
+ },
+ empty: {
+ noRecent: "インポートする最近のセッションがありません。",
+ alreadyImported: "最近のセッションはすでにすべてインポートされています。",
+ noProviderSessions: "{{provider}}のセッションが見つかりません。",
+ },
+ row: {
+ importing: "インポート中...",
+ },
+ },
+ workspace: {
+ route: {
+ loading: "ワークスペースを読み込み中",
+ restoring: "ワークスペースを復元中",
+ restoreFailed:
+ "このワークスペースを復元できませんでした。ディレクトリが移動または削除された可能性があります。",
+ connecting: "接続中",
+ hostOffline: "{{hostName}}はオフラインです",
+ cannotReachHost: "{{hostName}}に到達できません",
+ hostStatus: "ホストの状態: {{status}}",
+ missing: "ワークスペースが見つかりません",
+ needsHostUpgrade: "このワークスペースを復元するにはホストを更新してください",
+ manageHost: "ホストを管理",
+ },
+ hoverCard: {
+ scriptsAccessibility: "ワークスペーススクリプト",
+ copyPath: "パスをコピー",
+ copyBranchName: "ブランチ名をコピー",
+ copied: "コピーしました",
+ },
+ fileExplorer: {
+ sort: {
+ name: "名前",
+ modified: "更新日時",
+ size: "サイズ",
+ },
+ context: {
+ size: "サイズ",
+ modified: "更新日時",
+ copyPath: "パスをコピー",
+ download: "ダウンロード",
+ },
+ actions: {
+ back: "戻る",
+ retry: "再試行",
+ refresh: "ファイルを更新",
+ refreshing: "ファイルを更新中",
+ hideHiddenFiles: "隠しファイルを非表示",
+ showHiddenFiles: "隠しファイルを表示",
+ },
+ empty: {
+ noFiles: "ファイルなし",
+ noVisibleFiles: "表示可能なファイルなし",
+ },
+ states: {
+ unavailable: "ワークスペースが利用できません",
+ loading: "ファイルを読み込み中...",
+ },
+ errors: {
+ failedToListDirectory: "ディレクトリの一覧取得に失敗しました",
+ },
+ },
+ setup: {
+ descriptor: {
+ label: "セットアップ",
+ completed: "セットアップ完了",
+ failed: "セットアップ失敗",
+ workspace: "ワークスペースセットアップ",
+ },
+ status: {
+ running: "実行中",
+ completed: "完了",
+ failed: "失敗",
+ waiting: "セットアップ出力を待機中",
+ },
+ waiting: "ワークスペースをセットアップ中...",
+ empty: {
+ noCommands: "このワークスペースでセットアップコマンドは実行されませんでした。",
+ },
+ accessibility: {
+ noCommands: "このワークスペースでセットアップコマンドは実行されませんでした",
+ log: "ワークスペースセットアップログ",
+ },
+ log: {
+ noOutput: "出力なし",
+ },
+ },
+ browser: {
+ unavailable: {
+ title: "ブラウザはデスクトップ専用です",
+ subtitle: "組み込みブラウザを使用するには、このワークスペースをElectronで開いてください。",
+ },
+ session: "ブラウザセッション{{browserId}}",
+ controls: {
+ back: "戻る",
+ forward: "進む",
+ stopLoading: "読み込みを停止",
+ refresh: "更新",
+ browserUrl: "ブラウザURL",
+ enterUrl: "URLを入力",
+ openDevTools: "ブラウザ開発ツールを開く",
+ cancelSelector: "要素セレクターをキャンセル",
+ selectElement: "要素を選択",
+ },
+ errors: {
+ failedToLoad: "ページの読み込みに失敗しました",
+ invalidUrl: "無効なブラウザURL",
+ unsupportedProtocol: "サポートされていないブラウザURLをブロック: {{protocol}}",
+ },
+ },
+ terminal: {
+ hostDisconnected: "ホストが接続されていません",
+ unableToSubscribe: "ターミナルに接続できません",
+ },
+ tabs: {
+ loading: "読み込み中...",
+ loadingAgentTitle: "エージェントタイトルを読み込み中",
+ emptyPane: "このペインにタブがありません。",
+ fallback: {
+ newAgent: "新しいエージェント",
+ setup: "セットアップ",
+ workspaceSetup: "ワークスペースセットアップ",
+ terminal: "ターミナル",
+ browser: "ブラウザ",
+ agent: "エージェント",
+ workspace: "ワークスペース",
+ },
+ switcher: {
+ trigger: "タブを切り替え({{count}}件開いています)",
+ title: "タブを切り替え",
+ searchPlaceholder: "タブを検索",
+ },
+ menu: {
+ openFor: "{{label}}のメニューを開く",
+ copyResumeCommand: "再開コマンドをコピー",
+ copyAgentId: "エージェントIDをコピー",
+ copyFilePath: "ファイルパスをコピー",
+ rename: "名前を変更",
+ closeAbove: "上のタブを閉じる",
+ closeBelow: "下のタブを閉じる",
+ closeLeft: "左のタブを閉じる",
+ closeRight: "右のタブを閉じる",
+ closeOthers: "他のタブを閉じる",
+ reloadAgent: "エージェントを再読み込み",
+ reloadAgentTooltip:
+ "スキル、MCP、ログイン状態を更新するためにエージェントを再読み込みします。",
+ close: "閉じる",
+ renameTerminal: "ターミナルの名前を変更",
+ renameAgent: "エージェントの名前を変更",
+ },
+ actions: {
+ newAgent: "新しいエージェント",
+ newTerminal: "新しいターミナル",
+ preparingTerminal: "ターミナルタブを準備中",
+ preparingTerminalTooltip: "ターミナルを準備中...",
+ newBrowser: "新しいブラウザ",
+ splitRight: "右にペインを分割",
+ splitDown: "下にペインを分割",
+ terminalProfilesMenu: "ターミナルプロファイル",
+ editTerminalProfiles: "プロファイルを編集…",
+ pinTarget: "ピン留め",
+ unpinTarget: "ピン留めを解除",
+ },
+ explorer: {
+ open: "エクスプローラーを開く",
+ close: "エクスプローラーを閉じる",
+ toggle: "エクスプローラーを切り替え",
+ changes: "変更",
+ files: "ファイル",
+ },
+ toasts: {
+ copyFailed: "コピーに失敗しました",
+ agentIdCopiedLabel: "エージェントID",
+ resumeCommandCopiedLabel: "再開コマンド",
+ filePathCopiedLabel: "ファイルパス",
+ resumeIdUnavailable: "再開IDが利用できません",
+ resumeCommandUnavailable: "再開コマンドが利用できません",
+ reloadingAgent: "エージェントを再読み込み中...",
+ reloadedAgent: "エージェントを再読み込みしました",
+ failedToReloadAgent: "エージェントの再読み込みに失敗しました",
+ },
+ confirmations: {
+ close: "閉じる",
+ cancel: "キャンセル",
+ archive: "アーカイブ",
+ closeTerminalTitle: "ターミナルを閉じますか?",
+ closeTerminalMessage: "このターミナルで実行中のプロセスはすぐに停止されます。",
+ archiveRunningAgentTitle: "実行中のエージェントをアーカイブしますか?",
+ archiveRunningAgentMessage:
+ "このエージェントはまだ実行中です。アーカイブするとエージェントが停止してタブが閉じられます。",
+ closeTabsLeftTitle: "左のタブを閉じますか?",
+ closeTabsRightTitle: "右のタブを閉じますか?",
+ closeOtherTabsTitle: "他のタブを閉じますか?",
+ bulk: {
+ all: "{{agents}}件のエージェントをアーカイブし、{{terminals}}件のターミナルを閉じ、{{tabs}}件のタブを閉じます。閉じたターミナルで実行中のプロセスはすぐに停止されます。",
+ agentsAndTerminals:
+ "{{agents}}件のエージェントをアーカイブし、{{terminals}}件のターミナルを閉じます。閉じたターミナルで実行中のプロセスはすぐに停止されます。",
+ terminalsAndTabs:
+ "{{terminals}}件のターミナルを閉じ、{{tabs}}件のタブを閉じます。閉じたターミナルで実行中のプロセスはすぐに停止されます。",
+ agentsAndTabs: "{{agents}}件のエージェントをアーカイブし、{{tabs}}件のタブを閉じます。",
+ terminals:
+ "{{terminals}}件のターミナルを閉じます。閉じたターミナルで実行中のプロセスはすぐに停止されます。",
+ tabs: "{{tabs}}件のタブを閉じます。",
+ agents: "{{agents}}件のエージェントをアーカイブします。",
+ },
+ },
+ },
+ header: {
+ actions: {
+ workspaceActions: "ワークスペースアクション",
+ newAgent: "新しいエージェント",
+ newTerminal: "新しいターミナル",
+ newBrowser: "新しいブラウザタブ",
+ importSession: "セッションをインポート",
+ copyPath: "ワークスペースパスをコピー",
+ copyBranchName: "ブランチ名をコピー",
+ showSetup: "セットアップを表示",
+ },
+ toasts: {
+ workspacePathUnavailable: "ワークスペースパスはまだ利用できません",
+ branchNameUnavailable: "ブランチ名が利用できません",
+ terminalQueued: "ワークスペースを準備中、準備ができたらターミナルを開きます...",
+ workspacePathCopiedLabel: "ワークスペースパス",
+ branchNameCopiedLabel: "ブランチ名",
+ },
+ },
+ scripts: {
+ title: "スクリプト",
+ actions: {
+ run: "実行",
+ view: "表示",
+ },
+ accessibility: {
+ trigger: "ワークスペーススクリプト",
+ openAt: "{{label}}で{{scriptName}}を開く",
+ viewTerminal: "{{scriptName}}ターミナルを表示",
+ runScript: "{{scriptName}}スクリプトを実行",
+ script: "{{scriptName}}スクリプト",
+ },
+ states: {
+ exitCode: "終了コード: {{code}}",
+ startFailed: "{{scriptName}}の起動に失敗しました",
+ },
+ },
+ git: {
+ actions: {
+ moreOptions: "その他のオプション",
+ moreActions: "その他のアクション",
+ commit: {
+ label: "コミット",
+ pending: "コミット中...",
+ success: "コミットしました",
+ },
+ pull: {
+ label: "プル",
+ pending: "プル中...",
+ success: "プルしました",
+ },
+ push: {
+ label: "プッシュ",
+ pending: "プッシュ中...",
+ success: "プッシュしました",
+ },
+ pullAndPush: {
+ label: "プルしてプッシュ",
+ pending: "プルしてプッシュ中...",
+ success: "プルしてプッシュしました",
+ },
+ viewPr: "PRを表示",
+ createPr: {
+ label: "PRを作成",
+ pending: "PRを作成中...",
+ success: "PRが作成されました",
+ },
+ mergeBranch: {
+ label: "ローカルでマージ",
+ pending: "マージ中...",
+ success: "マージしました",
+ },
+ mergeFromBase: {
+ label: "{{baseRef}}から更新",
+ pending: "更新中...",
+ success: "更新しました",
+ },
+ archive: {
+ label: "ワークツリーをアーカイブ",
+ pending: "アーカイブ中...",
+ success: "アーカイブしました",
+ },
+ mergePr: {
+ squash: "PRをマージ(スカッシュ)",
+ merge: "PRをマージ(マージ)",
+ rebase: "PRをマージ(リベース)",
+ pending: "PRをマージ中...",
+ success: "PRがマージされました",
+ },
+ autoMerge: {
+ enableSquash: "自動マージ(スカッシュ)",
+ enableMerge: "自動マージ(マージ)",
+ enableRebase: "自動マージ(リベース)",
+ enabled: "自動マージが有効になりました",
+ enabling: "自動マージを有効にしています...",
+ disabling: "自動マージを無効にしています...",
+ disabled: "自動マージが無効になりました",
+ },
+ unavailable: {
+ viewPrNoGithub: "GitHubが接続されていないため、PRの表示は現在利用できません",
+ pullNoRemote:
+ "このブランチはまだリモートに接続されていないため、プルはここでは利用できません",
+ pullDirty:
+ "ローカルに変更があるためプルは利用できません。先にコミットまたはスタッシュしてください",
+ pullUpToDate: "このブランチはすでに最新のため、プルは利用できません",
+ pushNoRemote:
+ "このブランチはまだリモートに接続されていないため、プッシュはここでは利用できません",
+ pushBehind: "取り込む必要がある新しい変更があるため、まだプッシュは利用できません",
+ pushNothing: "プッシュする変更がないため、プッシュは利用できません",
+ pullAndPushNoRemote:
+ "このブランチはまだリモートに接続されていないため、プル&プッシュはここでは利用できません",
+ pullAndPushDirty:
+ "ローカルに変更があるためプル&プッシュは利用できません。先にコミットまたはスタッシュしてください",
+ pullAndPushInSync:
+ "このブランチはすでに同期されているため、プル&プッシュは利用できません",
+ createPrNoGithub: "GitHubが接続されていないため、PRの作成は現在利用できません",
+ createPrNoCommits: "このブランチにまだ新しいコミットがないため、PRの作成は利用できません",
+ mergeNoBase: "ベースブランチを特定できなかったため、マージは利用できません",
+ mergeDirty:
+ "ローカルに変更があるためマージは利用できません。先にコミットまたはスタッシュしてください",
+ mergeNothing: "このブランチにまだマージするものがないため、マージは利用できません",
+ updateNoBase: "ベースブランチを特定できなかったため、更新は利用できません",
+ updateDirty:
+ "ローカルに変更があるため更新は利用できません。先にコミットまたはスタッシュしてください",
+ updateCurrent: "このブランチはすでに{{baseRef}}と最新の状態のため、更新は利用できません",
+ archiveNotWorktree:
+ "このワークスペースはPaseoワークツリーとして作成されていないため、アーカイブはここでは利用できません",
+ mergePrNoGithub: "GitHubが接続されていないため、PRのマージは現在利用できません",
+ mergePrMissing: "プルリクエストがまだないため、PRのマージは利用できません",
+ mergePrDraft: "プルリクエストがまだドラフトのため、PRのマージは利用できません",
+ mergePrMerged: "プルリクエストはすでにマージされているため、PRのマージは利用できません",
+ mergePrClosed: "プルリクエストがクローズされているため、PRのマージは利用できません",
+ mergePrConflicts: "プルリクエストにコンフリクトがあるため、PRのマージは利用できません",
+ mergePrQueue:
+ "このリポジトリはマージキューを使用しているため、PRのマージはここでは利用できません",
+ mergePrNotReady:
+ "GitHub上でプルリクエストがマージ可能になるまで、PRのマージは利用できません",
+ autoMergeCannotDisable:
+ "自動マージは有効になっていますが、このアカウントでは無効にできません",
+ },
+ toasts: {
+ failedCommit: "コミットに失敗しました",
+ failedPull: "プルに失敗しました",
+ failedPush: "プッシュに失敗しました",
+ failedPullAndPush: "プル&プッシュに失敗しました",
+ failedCreatePr: "PRの作成に失敗しました",
+ failedMergePr: "PRのマージに失敗しました",
+ failedEnableAutoMerge: "自動マージの有効化に失敗しました",
+ failedDisableAutoMerge: "自動マージの無効化に失敗しました",
+ baseRefUnavailable: "ベースRefが利用できません",
+ failedMerge: "マージに失敗しました",
+ failedMergeFromBase: "ベースからのマージに失敗しました",
+ worktreePathUnavailable: "ワークツリーパスが利用できません",
+ failedArchive: "ワークツリーのアーカイブに失敗しました",
+ },
+ archiveWarning: {
+ title: '"{{worktreeName}}"をアーカイブしますか?',
+ confirm: "アーカイブ",
+ cancel: "キャンセル",
+ uncommittedChanges: "未コミットの変更",
+ uncommittedChangesWithDiff: "未コミットの変更({{diffStat}})",
+ addedLine: "{{count}}行追加",
+ addedLines: "{{count}}行追加",
+ deletedLine: "{{count}}行削除",
+ deletedLines: "{{count}}行削除",
+ unpushedCommit: "{{count}}件の未プッシュコミット",
+ unpushedCommits: "{{count}}件の未プッシュコミット",
+ },
+ },
+ diff: {
+ binaryFile: "バイナリファイル",
+ tooLarge: "差分が大きすぎて表示できません",
+ unified: "ユニファイド差分",
+ split: "左右比較",
+ hideWhitespace: "空白を非表示",
+ scrollLongLines: "長い行をスクロール",
+ wrapLongLines: "長い行を折り返す",
+ collapseAll: "すべて折りたたむ",
+ expandAll: "すべて展開",
+ refreshing: "更新中",
+ refresh: "更新",
+ refreshState: "gitとGitHubの状態を更新",
+ failedRefresh: "gitの状態の更新に失敗しました。",
+ emptyHiddenWhitespace: "空白を非表示にすると変更は表示されません",
+ emptyUncommitted: "未コミットの変更なし",
+ emptyAgainstBase: "{{baseRef}}との差分なし",
+ checkingRepository: "リポジトリを確認中...",
+ notRepository: "gitリポジトリではありません",
+ diffMode: "差分モード",
+ uncommitted: "未コミット",
+ committed: "コミット済み",
+ branchUnknown: "不明",
+ base: "ベース",
+ newFile: "新規",
+ deletedFile: "削除済み",
+ },
+ openInEditor: {
+ open: "開く",
+ chooseEditor: "エディタを選択",
+ openIn: "{{target}}でワークスペースを開く",
+ openFileIn: "{{target}}で{{fileName}}を開く",
+ failedOpen: "ワークスペースを開けませんでした",
+ },
+ pr: {
+ actions: {
+ viewPullRequest: "表示",
+ },
+ sections: {
+ checks: "チェック",
+ reviews: "レビュー",
+ },
+ accessibility: {
+ pullRequest: "プルリクエスト#{{number}}",
+ },
+ states: {
+ draft: "ドラフト",
+ merged: "マージ済み",
+ closed: "クローズ済み",
+ open: "オープン",
+ },
+ activity: {
+ commented: "コメント済み",
+ approved: "承認済み",
+ requestedChanges: "変更をリクエスト",
+ reviewed: "レビュー済み",
+ },
+ time: {
+ justNow: "たった今",
+ },
+ errors: {
+ statusLoadFailed: "プルリクエストのステータスを読み込めません",
+ activityLoadFailed: "プルリクエストのアクティビティを読み込めません",
+ },
+ },
+ },
+ },
+ sidebar: {
+ host: {
+ noHost: "ホストなし",
+ switchTitle: "ホストを切り替え",
+ searchPlaceholder: "ホストを検索...",
+ },
+ actions: {
+ addProject: "プロジェクトを追加",
+ newWorkspace: "新しいワークスペース",
+ home: "ホーム",
+ settings: "設定",
+ closeSidebar: "サイドバーを閉じる",
+ },
+ sections: {
+ sessions: "履歴",
+ },
+ worktreeSetup: {
+ title: "ワークツリースクリプトを設定",
+ description:
+ "新しいワークツリーが依存関係をインストールして自動的に準備できるようにセットアップコマンドを追加してください。",
+ openProjectSettings: "プロジェクト設定を開く",
+ },
+ project: {
+ actions: {
+ menu: "プロジェクトアクション",
+ openSettings: "プロジェクト設定を開く",
+ openNewWindow: "新しいウィンドウで開く",
+ openNewWindowFailed: "新しいウィンドウを開けませんでした",
+ remove: "プロジェクトを削除",
+ removing: "削除中...",
+ },
+ confirmations: {
+ removeTitle: "プロジェクトを削除しますか?",
+ removeMessage:
+ '"{{projectName}}"をサイドバーから削除しますか?\n\nディスク上のファイルは変更されません。',
+ removeConfirm: "削除",
+ cancel: "キャンセル",
+ },
+ toasts: {
+ hostDisconnected: "ホストが接続されていません",
+ removeFailed: "一部のワークスペースの削除に失敗しました",
+ updateHostToRemove: "プロジェクトを削除するにはホストを更新してください。",
+ },
+ empty: {
+ title: "プロジェクトがまだありません",
+ description: "始めるにはプロジェクトを追加してください",
+ },
+ },
+ workspace: {
+ status: {
+ scriptsAvailable: "スクリプトが利用可能",
+ creating: "作成中...",
+ },
+ actions: {
+ menu: "ワークスペースアクション",
+ newWorkspace: "新しいワークスペース",
+ createWorkspaceFor: "{{projectName}}の新しいワークスペースを作成",
+ copyPath: "パスをコピー",
+ copyBranchName: "ブランチ名をコピー",
+ rename: "ワークスペースの名前を変更",
+ archive: "アーカイブ",
+ archiveWorktree: "ワークツリーをアーカイブ",
+ hideFromSidebar: "サイドバーから非表示",
+ archiving: "アーカイブ中...",
+ hiding: "非表示にしています...",
+ },
+ confirmations: {
+ hideTitle: "ワークスペースを非表示にしますか?",
+ hideMessage:
+ '"{{workspaceName}}"をサイドバーから非表示にしますか?\n\nディスク上のファイルは変更されません。',
+ hideConfirm: "非表示",
+ cancel: "キャンセル",
+ },
+ rename: {
+ title: "ワークスペースの名前を変更",
+ submit: "名前を変更",
+ invalidBranchName: "無効なブランチ名",
+ },
+ toasts: {
+ workspacePathUnavailable: "ワークスペースパスが利用できません",
+ pathCopied: "パスをコピーしました",
+ branchNameCopied: "ブランチ名をコピーしました",
+ hostDisconnected: "ホストが接続されていません",
+ hideFailed: "ワークスペースの非表示に失敗しました",
+ archiveFailed: "ワークツリーのアーカイブに失敗しました",
+ },
+ },
+ },
+ newWorkspace: {
+ title: "新しいワークスペース",
+ create: "作成",
+ isolation: {
+ local: "ローカル",
+ worktree: "新しいワークツリー",
+ label: "分離方法",
+ },
+ fields: {
+ project: "プロジェクト",
+ base: "ベース",
+ baseNotApplicable: "該当なし",
+ },
+ titlePlaceholder: "タイトル(任意)",
+ errors: {
+ hostDisconnected: "ホストが接続されていません",
+ createWorktreeFailed: "ワークツリーの作成に失敗しました",
+ composerStateRequired: "コンポーザーの状態が必要です",
+ selectModel: "モデルを選択してください",
+ },
+ refPicker: {
+ startingRef: "開始Ref",
+ chooseStart: "開始点を選択",
+ checkoutHint: "PR #{{number}}をチェックアウトしますか?",
+ checkoutPr: "PR #{{number}}をチェックアウト",
+ dismissCheckoutHint: "PR #{{number}}のチェックアウトヒントを閉じる",
+ intoBase: "{{baseRef}}に",
+ searching: "検索中...",
+ noMatchingRefs: "一致するRefがありません。",
+ searchPlaceholder: "ブランチとPRを検索",
+ title: "開始点",
+ },
+ },
+ desktop: {
+ quitting: {
+ title: "Paseoを終了中...",
+ detail: "ローカルデーモンを停止中。",
+ },
+ daemon: {
+ title: "デーモン",
+ status: {
+ title: "ステータス",
+ builtInOnly: "組み込みデスクトップデーモンのみここに表示されます",
+ running: "実行中",
+ notRunning: "実行していません",
+ pid: "PID {{pid}}",
+ },
+ management: {
+ title: "組み込みデーモンを管理",
+ hint: "Paseoが組み込みデーモンを起動・停止できるようにする",
+ pauseTitle: "組み込みデーモンを一時停止",
+ pauseMessage:
+ "これにより組み込みデーモンが即座に停止します。組み込みデーモンに接続されている実行中のエージェントとターミナルが停止されます。",
+ pauseAndStop: "一時停止して停止",
+ registrationFailed:
+ "組み込みデーモンは起動しましたが、Paseoがlocalhostの接続を保存できませんでした。デーモン管理をオフにしてから再度オンにするか、localhostを手動で追加してください。",
+ pausedStopFailed:
+ "組み込みデーモン管理は一時停止されましたが、Paseoがデーモンを停止できませんでした。",
+ updateFailed: "組み込みデーモン管理を更新できません。",
+ },
+ keepRunning: {
+ title: "終了後もデーモンを実行し続ける",
+ hint: "Paseoを終了してもデーモンは実行し続けます",
+ },
+ logs: {
+ title: "ログファイル",
+ modalTitle: "デーモンログ",
+ unavailable: "ログパスが利用できません",
+ empty: "(ログファイルは空です)",
+ copied: "ログパスをコピーしました。",
+ copyFailed: "ログパスをコピーできません。",
+ open: "ログを開く",
+ copyPath: "パスをコピー",
+ },
+ fullStatus: {
+ title: "詳細ステータス",
+ modalTitle: "デーモンのステータス",
+ hint: "`paseo daemon status`を実行して出力を表示します",
+ view: "ステータスを表示",
+ copied: "ステータスをクリップボードにコピーしました。",
+ fetchFailed: "デーモンのステータスの取得に失敗しました: {{message}}",
+ },
+ advancedSettings: "詳細設定",
+ openAdvancedSettings: "高度なデーモン設定を開く",
+ versionMismatch:
+ "アプリとデーモンのバージョンが一致しません。最良の体験のために両方を同じバージョンに更新してください。",
+ loadFailed: "デスクトップデーモンのステータスを読み込めません。",
+ },
+ updates: {
+ status: {
+ checking: "アプリの更新を確認中...",
+ installing: "アプリの更新をインストール中...",
+ upToDate: "アプリは最新です。",
+ upToDateWithLastChecked: "最新の状態です。最終確認: {{time}}。",
+ pending: "更新の準備ができたらお知らせします。",
+ availableWithVersion: "更新の準備ができました: {{version}}",
+ available: "アプリの更新をインストールできます。",
+ installed: "アプリの更新がインストールされました。再起動が必要です。",
+ failed: "アプリの更新に失敗しました。",
+ idle: "更新ステータスはまだ確認されていません。",
+ },
+ installError: "デスクトップアプリの更新をインストールできません。",
+ callout: {
+ installingTitle: "更新をインストール中",
+ failedTitle: "更新に失敗しました",
+ availableTitle: "更新が利用可能",
+ genericError: "問題が発生しました。",
+ whatsNew: "新機能",
+ installingAction: "インストール中...",
+ installAndRestart: "インストールして再起動",
+ installingDescription: "インストールして再起動中...",
+ versionReady: "{{version}}のインストール準備ができました。",
+ newVersionReady: "新しいバージョンのインストール準備ができました。",
+ restartWarning:
+ "アプリを更新すると、実行中のエージェントが停止しターミナルセッションが閉じられます。",
+ },
+ },
+ settings: {
+ loadFailed: "デスクトップ設定を読み込めません。",
+ saveFailed: "デスクトップ設定を保存できません。",
+ },
+ rosetta: {
+ title: "Apple Siliconビルドをダウンロード",
+ runningIntel: "Apple Silicon上のRosettaでPaseoのIntelビルドを実行しています。",
+ highCpu:
+ "これにより高いCPU使用率が発生します。修正するにはApple Siliconビルドをダウンロードしてください。",
+ download: "ダウンロード",
+ },
+ permissions: {
+ notifications: {
+ allowed: "通知はOSによって許可されています。",
+ denied: "通知はシステム設定で拒否されています。",
+ notGranted: "通知はまだ許可されていません。",
+ webOnly: "デスクトップ通知のステータスはWebランタイムでのみ利用できます。",
+ supported: "デスクトップ通知はサポートされています。",
+ unsupported: "デスクトップ通知はこのプラットフォームではサポートされていません。",
+ apiUnavailable: "この環境ではWeb Notification APIは利用できません。",
+ requestsWebOnly: "デスクトップ通知のリクエストはWebランタイムでのみ利用できます。",
+ requestUnavailable: "Web Notification API の requestPermission() は利用できません。",
+ requestFailed: "通知の権限リクエストに失敗しました: {{message}}",
+ unexpectedState: "予期しない通知の権限状態: {{state}}",
+ },
+ microphone: {
+ webOnly: "デスクトップマイクのステータスはWebランタイムでのみ利用できます。",
+ navigatorUnavailable: "この環境ではNavigatorは利用できません。",
+ granted: "マイクへのアクセスが許可されています。",
+ denied: "マイクへのアクセスはシステム設定で拒否されています。",
+ notGranted: "マイクの権限はまだ許可されていません。",
+ unexpectedState: "予期しないマイクの権限状態: {{state}}",
+ statusApiUnavailable:
+ "マイクステータスAPIはこのランタイムでは利用できません。アクセス確認は[許可を求める]から行ってください。",
+ queryFailed: "マイクのステータス確認に失敗しました: {{message}}",
+ captureUnavailable: "この環境ではマイクのキャプチャは利用できません。",
+ permissionApiUnavailable:
+ "権限ステータスAPIは利用できません。アクセス確認は[許可を求める]から行ってください。",
+ requestsWebOnly: "デスクトップマイクのリクエストはWebランタイムでのみ利用できます。",
+ captureApiUnavailable: "この環境ではマイクキャプチャAPIは利用できません。",
+ requestDenied: "マイクの権限はユーザーまたはシステムによって拒否されました。",
+ noDevice: "マイクデバイスが見つかりませんでした。",
+ requestFailed: "マイクの権限リクエストに失敗しました: {{message}}",
+ },
+ empty: {
+ notifications: "通知のステータスはまだ確認されていません。",
+ microphone: "マイクのステータスはまだ確認されていません。",
+ },
+ testNotification: {
+ title: "Paseo通知テスト",
+ body: "これが見えれば、デスクトップ通知は機能しています。",
+ notDelivered: "通知が届きませんでした。システム設定 > 通知を確認してください。",
+ failed: "通知の送信に失敗しました。",
+ },
+ },
+ integrations: {
+ cli: {
+ statusFailed: "CLIのインストール状態を確認できません。",
+ installFailed: "Paseo CLIをインストールできません。",
+ },
+ skills: {
+ statusFailed: "オーケストレーションスキルのステータスを確認できません。",
+ installFailed: "オーケストレーションスキルをインストールできません。",
+ updateFailed: "オーケストレーションスキルを更新できません。",
+ uninstallFailed: "オーケストレーションスキルをアンインストールできません。",
+ },
+ },
+ },
+ startup: {
+ errorTitle: "問題が発生しました",
+ errorDescription:
+ "ローカルサーバーの起動に失敗しました。この問題が続く場合は、以下のログを添えてGitHubでIssueを作成してください。",
+ logs: {
+ loading: "デーモンログを読み込み中...",
+ unavailable: "利用可能なデーモンログがありません。",
+ loadFailed: "デーモンログの読み込みに失敗しました: {{message}}",
+ },
+ },
+ openProject: {
+ tiles: {
+ addProject: {
+ title: "プロジェクトを追加",
+ description: "マシン上のフォルダを開く",
+ },
+ importSession: {
+ title: "セッションをインポート",
+ description: "最近の外部CLIセッションを取り込む",
+ },
+ setupProviders: {
+ title: "プロバイダーをセットアップ",
+ description: "Claude Code、Codexなどを設定",
+ },
+ pairDevice: {
+ title: "デバイスをペアリング",
+ description: "このデーモンにスマートフォンを接続",
+ },
+ },
+ },
+ projectPicker: {
+ placeholder: "ディレクトリパスを入力...",
+ opening: "プロジェクトを開いています...",
+ empty: "パスを入力してください",
+ errors: {
+ directory_not_found: "ディレクトリが見つかりません。",
+ open_failed: "プロジェクトを開けませんでした。",
+ },
+ openPath: "パスを開く",
+ },
+ branchSwitcher: {
+ currentBranch: "現在のブランチ: {{branchName}}。押してブランチを切り替えてください。",
+ placeholder: "ブランチを切り替え...",
+ searchPlaceholder: "ブランチをフィルタ...",
+ empty: "ブランチが見つかりません。",
+ title: "ブランチを切り替え",
+ uncommittedTitle: "未コミットの変更",
+ uncommittedMessage: "未コミットの変更があります。ブランチを切り替える前にスタッシュしますか?",
+ stashAndSwitch: "スタッシュして切り替え",
+ failedToStash: "変更のスタッシュに失敗しました",
+ failedToSwitch: "ブランチの切り替えに失敗しました",
+ restoreStashTitle: "スタッシュした変更を復元しますか?",
+ restoreStashMessage:
+ "このブランチには前のセッションからスタッシュした変更があります。復元しますか?",
+ restore: "復元",
+ later: "後で",
+ stashRestored: "スタッシュした変更を復元しました",
+ },
+ agentAutocomplete: {
+ searchingWorkspace: "ワークスペースを検索中...",
+ loadingCommands: "コマンドを読み込み中...",
+ noFiles: "ファイルまたはディレクトリが見つかりません",
+ noCommands: "コマンドが見つかりません",
+ failedToLoad: "読み込みに失敗しました",
+ },
+ loadOlderHistory: {
+ failed: "古い履歴を読み込めませんでした",
+ },
+ imageAttachmentPicker: {
+ permissionTitle: "権限が必要です",
+ permissionMessage: "画像を添付するにはフォトライブラリへのアクセスを許可してください。",
+ errorTitle: "エラー",
+ failedToSelect: "画像の選択に失敗しました",
+ dialogTitle: "画像を添付",
+ dialogFilterName: "画像",
+ },
+ workspaceSetup: {
+ title: "ワークスペースを作成",
+ errors: {
+ failedCreateWorktree: "ワークツリーの作成に失敗しました",
+ failedOpenProject: "プロジェクトを開けませんでした",
+ selectModel: "モデルを選択してください",
+ hostDisconnected: "ホストが接続されていません",
+ pendingRequired: "保留中のワークスペースセットアップがありません",
+ composerStateRequired: "ワークスペースセットアップのコンポーザー状態が必要です",
+ },
+ },
+ onboarding: {
+ title: "Paseoへようこそ",
+ subtitle: "始めるにはコンピューターに接続してください",
+ actions: {
+ settings: "設定",
+ },
+ },
+ modelSelector: {
+ title: "プロバイダーを選択",
+ selectModel: "モデルを選択",
+ selectedModel: "モデルを選択({{model}})",
+ loading: "読み込み中...",
+ loadingShort: "読み込み中",
+ loadingSelector: "モデルセレクターを読み込み中...",
+ error: "エラー",
+ defaultModel: "デフォルト",
+ favorites: "お気に入り",
+ favoriteModel: "お気に入りモデル",
+ unfavoriteModel: "お気に入りを解除",
+ modelCount: "{{count}}つのモデル",
+ modelCountPlural: "{{count}}つのモデル",
+ retry: "再試行",
+ retrying: "再試行中...",
+ noMatches: "検索に一致するモデルがありません",
+ searchPlaceholder: "モデルを検索...",
+ openProviderSettings: "{{provider}}の設定を開く",
+ },
+ providerCatalog: {
+ title: "プロバイダーを追加",
+ search: "プロバイダーを検索",
+ noProviders: "プロバイダーが見つかりません",
+ actions: {
+ add: "追加",
+ adding: "追加中",
+ installed: "インストール済み",
+ cancel: "キャンセル",
+ installInstructions: "インストール手順",
+ installInstructionsFor: "{{provider}}のインストール手順",
+ },
+ errors: {
+ unableToInstall: "プロバイダーをインストールできません",
+ },
+ },
+ providerSelection: {
+ defaultModel: "デフォルト",
+ selectModel: "モデルを選択",
+ loading: "読み込み中...",
+ error: "エラー",
+ unavailable: "利用不可",
+ unknownError: "不明なエラー",
+ readiness: {
+ initialPromptRequired: "初期プロンプトが必要です",
+ noProviders: "選択したホストで利用可能なプロバイダーがありません",
+ modelDefaultsLoading: "モデルのデフォルトをまだ読み込んでいます",
+ noModelAvailable: "選択したプロバイダーで利用可能なモデルがありません",
+ workspaceDirectoryNotFound: "ワークスペースディレクトリが見つかりません",
+ hostDisconnected: "ホストが接続されていません",
+ },
+ },
+ pairing: {
+ connectionMethods: {
+ title: "接続を追加",
+ direct: {
+ title: "直接接続",
+ description: "ローカルネットワークまたはVPN。",
+ },
+ scanQr: {
+ title: "QRコードをスキャン",
+ description: "暗号化されたリレー接続。",
+ },
+ pasteLink: {
+ title: "ペアリングリンクを貼り付け",
+ description: "暗号化されたリレー接続。",
+ },
+ },
+ direct: {
+ title: "直接接続",
+ helper: "Paseoサーバーのアドレスを入力してください。",
+ fields: {
+ host: "ホスト",
+ port: "ポート",
+ password: "パスワード",
+ optional: "任意",
+ useSsl: "SSLを使用",
+ connectionUri: "接続URI",
+ },
+ advanced: {
+ label: "詳細設定",
+ show: "詳細設定を表示",
+ hide: "詳細設定を非表示",
+ },
+ passwordVisibility: {
+ show: "パスワードを表示",
+ hide: "パスワードを非表示",
+ },
+ actions: {
+ cancel: "キャンセル",
+ connect: "接続",
+ connecting: "接続中...",
+ },
+ errors: {
+ hostRequired: "ホストは必須です",
+ invalidPort: "ポートは1から65535の間である必要があります",
+ invalidConnection: "無効な接続",
+ failedTitle: "接続に失敗しました",
+ failedToConnect: "{{endpoint}}への接続に失敗しました。",
+ noAdditionalDetails: "{{detail}}(追加の詳細は提供されていません)",
+ timedOut: "接続がタイムアウトしました。ホスト/ポートとネットワークを確認してください。",
+ refused: "接続が拒否されました。このアドレスでサーバーが実行されていますか?",
+ hostNotFound: "ホストが見つかりません。ホスト名を確認してもう一度試してください。",
+ hostUnreachable:
+ "ホストに到達できません。ネットワークとファイアウォールを確認してください。",
+ tlsError: "TLSエラー。直接接続は、デーモンの前にTLS終端がある場合のみSSLを使用します。",
+ unableToConnect: "接続できません。ホスト/ポートとデーモンが到達可能かを確認してください。",
+ details: "詳細: {{detail}}",
+ },
+ },
+ link: {
+ title: "ペアリングリンクを貼り付け",
+ helper: "サーバーからのペアリングリンクを貼り付けてください。",
+ label: "ペアリングリンク",
+ errors: {
+ required: "ペアリングリンクを貼り付けてください(.../#offer=...)",
+ missingOffer: "リンクには#offer=...が必要です",
+ emptyOffer: "オファーのペイロードが空です",
+ invalid: "無効なペアリングリンク",
+ unableToPair: "ホストをペアリングできません",
+ },
+ alert: {
+ failedTitle: "ペアリングに失敗しました",
+ },
+ actions: {
+ cancel: "キャンセル",
+ pair: "ペアリング",
+ pairing: "ペアリング中...",
+ },
+ },
+ scan: {
+ title: "QRをスキャン",
+ webUnavailableTitle: "Webでは利用できません",
+ webUnavailableBody:
+ "QRスキャンはWebビルドではサポートされていません。代わりに「リンクを貼り付け」を使用してください。",
+ backToSettings: "設定に戻る",
+ cameraPermissionTitle: "カメラの権限",
+ cameraPermissionBody:
+ "デーモンからのペアリングQRコードをスキャンするためにカメラへのアクセスを許可してください。",
+ grantPermission: "権限を許可",
+ pairing: "ペアリング中...",
+ unableToPair: "ホストをペアリングできません",
+ errorTitle: "エラー",
+ },
+ device: {
+ loadingOffer: "ペアリングオファーを読み込み中...",
+ failedToLoadOffer: "ペアリングオファーの読み込みに失敗しました。",
+ relayDisabled:
+ "リレーが有効になっていません。デバイスをペアリングするにはリレーを有効にしてください。",
+ unavailable: "ペアリングオファーが利用できません。",
+ hint: "スマートフォンのPaseoでこのQRコードをスキャンするか、以下のリンクをコピーしてください。",
+ qrUnavailable: "QRコードが利用できません。",
+ retry: "再試行",
+ copy: "コピー",
+ copied: "コピーしました",
+ },
+ },
+ realtimeVoice: {
+ actions: {
+ mute: "リアルタイム音声をミュート",
+ unmute: "リアルタイム音声のミュートを解除",
+ stop: "リアルタイム音声を停止してターンを中断",
+ },
+ },
+ rewind: {
+ tooltip: "このメッセージに巻き戻す",
+ warning: "この操作は元に戻せません",
+ actions: {
+ conversation: "会話を巻き戻す",
+ files: "ファイルを巻き戻す",
+ both: "会話とファイルを巻き戻す",
+ },
+ errors: {
+ failed: "エージェントの巻き戻しに失敗しました",
+ },
+ },
+ diffViewer: {
+ empty: "表示する変更がありません",
+ },
+ serviceUrl: {
+ title: "サービスURLを開く",
+ message: "{{url}}を開きますか?",
+ inPaseo: "Paseoで",
+ externalBrowser: "外部ブラウザ",
+ dontAskAgain: "次回から確認しない",
+ },
+ downloads: {
+ requestTokenFailed: "ダウンロードトークンのリクエストに失敗しました。",
+ hostUnavailable: "ダウンロードホストが利用できません。",
+ cancelled: "ダウンロードがキャンセルされました。",
+ failed: "ファイルのダウンロードに失敗しました。",
+ shareFile: "ファイルを共有",
+ shareFileNamed: "{{fileName}}を共有",
+ },
+ menu: {
+ backdrop: "メニューの背景",
+ },
+ subagents: {
+ detachAction: "{{label}}を切り離す",
+ detachTooltip: "サブエージェントを切り離す",
+ archiveAction: "{{label}}をアーカイブ",
+ archiveTooltip: "サブエージェントをアーカイブ",
+ },
+ panels: {
+ draft: {
+ newAgent: "新しいエージェント",
+ creatingAgent: "エージェントを作成中",
+ },
+ file: {
+ directoryMissing: "ワークスペースディレクトリが見つかりません。",
+ loading: "ファイルを読み込み中...",
+ noPreview: "プレビューが利用できません",
+ binaryPreviewUnavailable: "バイナリプレビューが利用できません",
+ failedToLoad: "ファイルの読み込みに失敗しました",
+ failedToLoadPreview: "ファイルプレビューの読み込みに失敗しました",
+ },
+ },
+ toolCallDetails: {
+ error: "エラー",
+ empty: "追加の詳細はありません",
+ subAgentActivity: "サブエージェントアクティビティ",
+ input: "入力",
+ output: "出力",
+ },
+ renameModal: {
+ rename: "名前を変更",
+ saving: "保存中...",
+ },
+ sidebarCallout: {
+ dismiss: "閉じる",
+ },
+ contextWindow: {
+ title: "コンテキストウィンドウ",
+ used: "{{percentage}}%使用",
+ tokens: "{{used}} / {{max}}トークン",
+ sessionCost: "セッションコスト: {{cost}}",
+ accessibility: "コンテキストウィンドウ{{percentage}}%使用",
+ },
+ review: {
+ comment: {
+ add: "レビューコメントを追加",
+ edit: "レビューコメントを編集",
+ delete: "レビューコメントを削除",
+ label: "レビューコメント",
+ placeholder: "コメントを入力",
+ cancel: "キャンセル",
+ cancelAccessibility: "レビューコメントをキャンセル",
+ save: "コメント",
+ saveAccessibility: "レビューコメントを保存",
+ },
+ },
+ settings: {
+ title: "設定",
+ loading: "設定を読み込み中...",
+ groups: {
+ app: "アプリ",
+ host: "ホスト",
+ },
+ hostPicker: {
+ switchHost: "ホストを切り替え",
+ local: "ローカル",
+ },
+ backToWorkspace: "戻る",
+ addHost: "ホストを追加",
+ projects: "プロジェクト",
+ projectList: {
+ hostLoadFailed: "ホスト{{hostName}}からプロジェクトを読み込めませんでした: {{message}}",
+ editProject: "{{projectName}}を編集",
+ },
+ groupInfo: "{{title}}について",
+ sections: {
+ general: "一般",
+ daemon: "デーモン",
+ appearance: "外観",
+ shortcuts: "ショートカット",
+ integrations: "連携",
+ permissions: "権限",
+ diagnostics: "診断",
+ about: "アプリ情報",
+ },
+ hostSections: {
+ connections: "接続",
+ agents: "エージェント",
+ workspaces: "ワークスペース",
+ providers: "プロバイダー",
+ usage: "使用状況",
+ terminals: "ターミナル",
+ host: "ホスト",
+ },
+ general: {
+ title: "一般",
+ defaultSend: {
+ label: "デフォルトの送信",
+ descriptions: {
+ interrupt: "エージェント実行中、Enterで中断します。Command/Ctrl+Enterでキューに追加。",
+ queue: "エージェント実行中、Enterでキューに追加します。Command/Ctrl+Enterで送信。",
+ },
+ options: {
+ interrupt: "中断",
+ queue: "キュー",
+ },
+ },
+ serviceUrls: {
+ label: "サービスURL",
+ description: "実行中のスクリプトからURLを開く場所",
+ options: {
+ ask: "確認する",
+ inApp: "Paseoで",
+ external: "外部ブラウザ",
+ },
+ },
+ terminalScrollback: {
+ label: "ターミナルスクロールバック",
+ description: "組み込みターミナルバッファに保持する行数",
+ accessibilityLabel: "ターミナルスクロールバック行数",
+ },
+ language: {
+ label: "言語",
+ description: "アプリの言語",
+ options: {
+ system: "システム",
+ ar: "アラビア語",
+ en: "英語",
+ es: "スペイン語",
+ fr: "フランス語",
+ ja: "日本語",
+ ru: "ロシア語",
+ zhCN: "簡体字中国語",
+ },
+ },
+ },
+ diagnostics: {
+ title: "診断",
+ testAudio: "音声をテスト",
+ playTest: "テスト再生",
+ playing: "再生中...",
+ playbackFailed: "再生に失敗しました: {{message}}",
+ },
+ about: {
+ title: "アプリ情報",
+ appVersion: "アプリバージョン",
+ thisDevice: "このデバイス",
+ connectedHosts: "接続されているホスト",
+ offline: "オフライン",
+ versionDiffers: "このデバイスとバージョンが異なります",
+ releaseChannel: {
+ label: "リリースチャンネル",
+ description: "ベータに切り替えると早期に更新を取得してフィードバックを提供できます",
+ stable: "安定版",
+ beta: "ベータ",
+ },
+ updates: {
+ label: "アプリの更新",
+ readyToInstall: "インストール準備完了: {{version}}",
+ installTitle: "デスクトップの更新をインストール",
+ installMessage: "このコンピューターのPaseoを更新します",
+ installConfirm: "更新をインストール",
+ update: "更新",
+ updateTo: "{{version}}に更新",
+ installing: "インストール中...",
+ check: "確認",
+ checking: "確認中...",
+ alertTitle: "エラー",
+ alertMessage: "更新確認ダイアログを開けません。",
+ },
+ },
+ appearance: {
+ theme: {
+ title: "テーマ",
+ accessibilityLabel: "テーマ: {{value}}",
+ options: {
+ light: "ライト",
+ dark: "ダーク",
+ zinc: "Zinc",
+ midnight: "Midnight",
+ claude: "Claude",
+ ghostty: "Ghostty",
+ auto: "システム",
+ },
+ },
+ fonts: {
+ title: "フォント",
+ systemDefault: "システムデフォルト",
+ interfaceFont: "インターフェースフォント",
+ interfaceFontHint:
+ "アプリ全体で使用されます。システムデフォルトにするには空のままにしてください",
+ interfaceFontAccessibility: "インターフェースフォントファミリー",
+ interfaceSize: "インターフェースサイズ",
+ interfaceSizeAccessibility: "インターフェースフォントサイズ",
+ codeFont: "コードフォント",
+ codeFontHint:
+ "コード、差分、ターミナル出力で使用されます。システムデフォルトにするには空のままにしてください",
+ codeFontAccessibility: "コードフォントファミリー",
+ codeSize: "コードサイズ",
+ codeSizeAccessibility: "コードフォントサイズ",
+ },
+ syntax: {
+ title: "構文ハイライト",
+ highlightTheme: "ハイライトテーマ",
+ highlightThemeHint: "コード用の色、アプリのテーマとは独立しています",
+ highlightThemeAccessibility: "ハイライトテーマ: {{value}}",
+ previewAccessibility: "構文ハイライトテーマとコードフォントのライブプレビュー",
+ },
+ },
+ shortcuts: {
+ dialogTitle: "ショートカット",
+ unavailableOnMobile: "キーボードショートカットはデスクトップでのみ利用できます",
+ capturePrompt: "ショートカットを押してください...",
+ actions: {
+ done: "完了",
+ cancel: "キャンセル",
+ rebind: "再割り当て",
+ reset: "リセット",
+ resetAll: "すべてリセット",
+ },
+ sections: {
+ navigation: "ナビゲーション",
+ tabsPanes: "タブ&ペイン",
+ projects: "プロジェクト",
+ panels: "パネル",
+ agentInput: "エージェント入力",
+ },
+ help: {
+ openProject: "プロジェクトを開く",
+ newWorkspace: "新しいワークスペース",
+ newWorktree: "新しいワークツリー",
+ archiveWorktree: "ワークツリーをアーカイブ",
+ newTab: "新しいタブ",
+ closeCurrentTab: "現在のタブを閉じる",
+ jumpToWorkspace: "ワークスペースにジャンプ",
+ jumpToTab: "タブにジャンプ",
+ previousWorkspace: "前のワークスペース",
+ nextWorkspace: "次のワークスペース",
+ previousTab: "前のタブ",
+ nextTab: "次のタブ",
+ splitPaneRight: "右にペインを分割",
+ splitPaneDown: "下にペインを分割",
+ focusPaneLeft: "左のペインにフォーカス",
+ focusPaneRight: "右のペインにフォーカス",
+ focusPaneUp: "上のペインにフォーカス",
+ focusPaneDown: "下のペインにフォーカス",
+ moveTabLeft: "タブを左に移動",
+ moveTabRight: "タブを右に移動",
+ moveTabUp: "タブを上に移動",
+ moveTabDown: "タブを下に移動",
+ closePane: "ペインを閉じる",
+ newTerminal: "新しいターミナル",
+ toggleCommandCenter: "コマンドセンターを切り替え",
+ showKeyboardShortcuts: "キーボードショートカットを表示",
+ toggleLeftSidebar: "左サイドバーを切り替え",
+ toggleRightSidebar: "右サイドバーを切り替え",
+ toggleBothSidebars: "両方のサイドバーを切り替え",
+ toggleSettings: "設定を切り替え",
+ toggleFocusMode: "フォーカスモードを切り替え",
+ cycleTheme: "テーマを順に切り替え",
+ focusMessageInput: "メッセージ入力にフォーカス",
+ cycleAgentMode: "エージェントモードを順に切り替え",
+ toggleVoiceMode: "音声モードを切り替え",
+ startStopDictation: "音声入力を開始/停止",
+ interruptAgent: "エージェントを中断",
+ sendMessage: "メッセージを送信",
+ queueMessage: "メッセージをキューに追加",
+ muteUnmuteVoiceMode: "音声モードのミュートを切り替え",
+ },
+ helpNotes: {
+ showKeyboardShortcuts:
+ "テキストフィールドまたはターミナルにフォーカスがない場合に利用できます。",
+ },
+ },
+ integrations: {
+ title: "連携",
+ docs: {
+ cli: "CLIドキュメント",
+ skills: "スキルドキュメント",
+ openCli: "CLIドキュメントを開く",
+ openSkills: "スキルドキュメントを開く",
+ },
+ commandLine: {
+ title: "コマンドライン",
+ description: "ターミナルからエージェントを制御し、スクリプトで操作",
+ },
+ skills: {
+ title: "オーケストレーションスキル",
+ description: "エージェントがCLI経由でオーケストレーションできるようにします。",
+ updateAvailable: "更新が利用可能",
+ updateTitle: "Paseoスキルを更新しますか?",
+ updateFallback: "バンドルされたスキルをマシンに同期します。",
+ uninstallTitle: "Paseoスキルをアンインストールしますか?",
+ uninstallMessage:
+ "~/.agents、~/.claude、~/.codexからすべてのPaseoオーケストレーションスキルを削除します。",
+ },
+ actions: {
+ install: "インストール",
+ installing: "インストール中...",
+ installed: "インストール済み",
+ update: "更新",
+ working: "処理中...",
+ uninstall: "アンインストール",
+ },
+ operations: {
+ add: "スキルを追加",
+ update: "スキルを更新",
+ delete: "スキルを削除",
+ },
+ },
+ permissions: {
+ title: "権限",
+ notifications: "通知",
+ microphone: "マイク",
+ refresh: "更新",
+ refreshing: "更新中...",
+ refreshAccessibility: "デスクトップの権限を更新",
+ test: "テスト",
+ actions: {
+ granted: "許可済み",
+ request: "許可を求める",
+ requesting: "許可を求めています...",
+ busySuffix: "{{label}}...",
+ },
+ },
+ host: {
+ notFound: "ホストが見つかりません",
+ badges: {
+ relay: "リレー",
+ local: "ローカル",
+ },
+ connections: {
+ title: "接続",
+ removeTitle: "接続を削除",
+ removeMessage: "{{name}}を削除しますか?この操作は元に戻せません。",
+ removeAction: "削除",
+ removeErrorTitle: "エラー",
+ removeErrorMessage: "接続を削除できません",
+ timeout: "タイムアウト",
+ },
+ pairDevices: {
+ title: "デバイスをペアリング",
+ rowTitle: "デバイスをペアリング",
+ rowHint: "QRコードをスキャンするかリンクをコピーしてスマートフォンをこのホストに接続",
+ },
+ orchestration: {
+ title: "オーケストレーション",
+ unavailable: "オーケストレーションを管理するにはこのホストに接続してください",
+ enableTools: {
+ title: "Paseoツールを有効にする",
+ hint: "エージェントがワークツリー、エージェント、スケジュールを管理できるようになります",
+ accessibilityLabel: "Paseoツールを有効にする",
+ },
+ systemPrompt: {
+ title: "システムプロンプト",
+ hint: "すべてのエージェントにシステムプロンプトを追加します",
+ sheetTitle: "システムプロンプトを追加",
+ accessibilityLabel: "システムプロンプトを追加",
+ placeholder: "常に返答を簡潔にしてください。",
+ edit: "編集",
+ reset: "リセット",
+ save: "保存",
+ saving: "保存中...",
+ },
+ },
+ agents: {
+ unavailable: "エージェントを管理するにはこのホストに接続してください",
+ },
+ workspaces: {
+ unavailable: "ワークスペースを管理するにはこのホストに接続してください",
+ },
+ terminalProfiles: {
+ unavailable: "ターミナルプロファイルを管理するにはこのホストに接続してください",
+ sectionTitle: "ターミナルプロファイル",
+ editProfile: "プロファイルを編集",
+ addProfileTitle: "ターミナルプロファイルを追加",
+ editProfileTitle: "ターミナルプロファイルを編集",
+ namePlaceholder: "Claude Code",
+ commandPlaceholder: "claude",
+ argsPlaceholder: "--dangerously-skip-permissions",
+ nameLabel: "名前",
+ commandLabel: "コマンド",
+ argsLabel: "引数",
+ nameRequired: "名前は必須です",
+ commandRequired: "コマンドは必須です",
+ argsHint: "コマンドに渡すスペース区切りの引数",
+ saving: "保存中...",
+ remove: "削除",
+ removeConfirmTitle: "プロファイルを削除しますか?",
+ removeConfirmMessage: '"{{name}}"を削除しますか?',
+ moveUp: "上に移動",
+ moveDown: "下に移動",
+ save: "保存",
+ emptyState:
+ "プロファイルがまだありません。特定のコマンドでターミナルを起動するために追加してください。",
+ },
+ daemon: {
+ rename: {
+ editLabel: "ラベルを編集",
+ title: "ホストの名前を変更",
+ placeholder: "マイホスト",
+ submit: "名前を変更",
+ },
+ restart: {
+ title: "デーモンを再起動",
+ hint: "デーモンプロセスを再起動します。アプリは自動的に再接続します",
+ confirm: "再起動",
+ confirmTitle: "{{name}}を再起動",
+ confirmMessage:
+ "これによりデーモンが再起動されます。実行中のエージェントは継続し、アプリは自動的に再接続します。",
+ restarting: "再起動中...",
+ unableToReconnectTitle: "再接続できません",
+ unableToReconnectMessage:
+ "{{name}}がオンラインに戻りませんでした。再起動されたことを確認してください。",
+ unavailableTitle: "ホストが利用できません",
+ unavailableMessage:
+ "このホストは接続されていません。再起動する前にオンラインになるまでお待ちください。",
+ offlineTitle: "ホストオフライン",
+ offlineMessage:
+ "このホストはオフラインです。Paseoが自動再接続します。再起動は、ホストがオンラインに戻ってから行ってください。",
+ requestFailedTitle: "エラー",
+ requestFailedMessage:
+ "再起動リクエストの送信に失敗しました。Paseoは自動的に再接続します。ホストがオンラインになったら再試行してください。",
+ dialogFailedMessage: "再起動確認ダイアログを開けませんでした。",
+ },
+ dangerZone: "危険ゾーン",
+ remove: {
+ title: "ホストを削除",
+ localTitle: "localhost接続を削除",
+ hint: "このホストと保存された接続をこのデバイスから削除します",
+ localHint: "このデバイスからlocalhostを削除して組み込みデーモンを停止します",
+ localConfirmTitle: "localhost接続を削除してデーモンを停止しますか?",
+ confirmMessage: "{{name}}を削除しますか?保存された接続が削除されます。",
+ localConfirmMessage:
+ "localhost接続を削除し、組み込みデーモン管理をオフにして、管理されているデーモンを停止します。リモートホストは接続されたままです。",
+ errorTitle: "エラー",
+ errorMessage: "ホストを削除できません",
+ localErrorMessage: "localhost接続を削除できません",
+ },
+ },
+ },
+ providers: {
+ title: "プロバイダー",
+ addProvider: "プロバイダーを追加",
+ providerDetails: "{{name}}プロバイダーの詳細",
+ enableProvider: "{{name}}を有効にする",
+ unavailable: "プロバイダーを見るにはこのホストに接続してください",
+ loading: "読み込み中...",
+ addErrorTitle: "プロバイダーを追加できません",
+ updateErrorTitle: "プロバイダーを更新できません",
+ statuses: {
+ disabled: "無効",
+ loading: "読み込み中",
+ error: "エラー",
+ available: "利用可能",
+ notInstalled: "未インストール",
+ },
+ models: {
+ one: "1つのモデル",
+ many: "{{count}}つのモデル",
+ addModel: "モデルを追加",
+ addCustomTitle: "カスタムモデルを追加",
+ modelId: "モデルID",
+ modelIdPlaceholder: "例: openai/gpt-5",
+ add: "追加",
+ adding: "追加中...",
+ failedToSave: "モデルの保存に失敗しました",
+ removeModel: "{{id}}を削除",
+ searchPlaceholder: "モデルを検索",
+ loading: "モデルを読み込み中...",
+ retry: "再試行",
+ retrying: "再試行中...",
+ noSearchMatches: "検索に一致するモデルがありません",
+ noneDetected: "モデルが検出されませんでした",
+ discovered: "検出済み",
+ custom: "カスタムモデル",
+ updated: "{{time}}に更新",
+ },
+ diagnostic: {
+ title: "診断",
+ button: "診断",
+ refresh: "更新",
+ refreshing: "更新中...",
+ copyLabel: "診断",
+ copyAccessibility: "診断をコピー",
+ copyFailed: "診断のコピーに失敗しました",
+ refreshAccessibility: "診断を更新",
+ refreshingAccessibility: "診断を更新中",
+ running: "診断を実行中...",
+ none: "利用可能な診断がありません",
+ failedToFetch: "診断の取得に失敗しました",
+ unknownError: "不明なエラー",
+ },
+ },
+ project: {
+ noEditableTarget:
+ "接続されているホストのどれにもこのプロジェクトの編集可能なコピーがありません。",
+ backToProjects: "プロジェクトに戻る",
+ switchHost: "ホストを切り替え",
+ rename: {
+ renamedToast: "プロジェクト名を変更しました",
+ errorFallback: "プロジェクト名を変更できませんでした",
+ renameLabel: "プロジェクトの名前を変更",
+ resetLabel: "プロジェクト名をデフォルトにリセット",
+ projectNameLabel: "プロジェクト名",
+ saveLabel: "プロジェクト名を保存",
+ cancelLabel: "名前変更をキャンセル",
+ reset: "リセット",
+ },
+ readFailures: {
+ invalidTitle: "paseo.jsonを解析できませんでした",
+ invalidDescription: "ディスク上のファイルを修正してから再読み込みしてください。",
+ missingTitle: "このホストにはこのプロジェクトがありません",
+ missingWithHosts: "上で別のホストに切り替えるか、再読み込みしてください。",
+ missingSingleHost: "選択したホストにはこのプロジェクトの記録がありません。",
+ transportTitle: "paseo.jsonを読み込めませんでした",
+ transportFallback: "ホストが応答しませんでした。",
+ failedTitle: "paseo.jsonを読み込めませんでした",
+ failedDescription: "再読み込みして再試行してください。",
+ },
+ worktree: {
+ title: "ワークツリーライフサイクルフック",
+ info: "このプロジェクトのワークツリーが作成または削除されたときに実行されるコマンド",
+ docs: "ドキュメント",
+ docsTooltip:
+ "これらのコマンドで使用可能な詳細と環境変数についてはドキュメントを参照してください",
+ setup: "セットアップ",
+ setupAccessibility: "ワークツリーセットアップコマンド",
+ teardown: "削除時",
+ teardownAccessibility: "ワークツリー削除時のコマンド",
+ },
+ scripts: {
+ title: "スクリプト",
+ info: "このプロジェクトのどのエージェントからでも起動できる、長時間実行サービスと単発コマンド",
+ empty: "スクリプトがまだありません。",
+ untitled: "無題のスクリプト",
+ port: "ポート{{port}}",
+ menuAccessibility: "スクリプトメニューを開く",
+ removeTitle: "スクリプトを削除しますか?",
+ removeMessage: "{{name}}を削除しますか?",
+ removeFallbackName: "このスクリプト",
+ name: "名前",
+ command: "コマンド",
+ nameAccessibility: "スクリプト名",
+ commandAccessibility: "スクリプトコマンド",
+ nameRequired: "名前は必須です",
+ commandRequired: "コマンドは必須です",
+ newScript: "新しいスクリプト",
+ editScript: "{{name}}を編集",
+ runAsService: "サービスとして実行",
+ serviceHint: "Paseoがプロセスを監督し、$PASEO_PORTを通じてポートを割り当てます",
+ actions: {
+ add: "スクリプトを追加",
+ edit: "編集",
+ remove: "削除",
+ },
+ },
+ metadata: {
+ title: "メタデータ生成",
+ info: "Paseoがメタデータ生成に使うAIプロンプトへ追加する、プロジェクト固有の指示です。ブランチ名、コミット形式、PR形式など、チームの規約を反映するために使います。",
+ branchName: "ブランチ名",
+ branchNamePlaceholder: "ブランチ名は feat/ または fix/ で始め、個人ブランチは mb/ にする",
+ commitMessage: "コミットメッセージ",
+ commitMessagePlaceholder: "スコープ付きのConventional Commitsを使用",
+ pullRequest: "プルリクエスト",
+ pullRequestPlaceholder: "1段落の要約で始め、テスト計画セクションを含める",
+ },
+ writeFailures: {
+ staleTitle: "設定がディスク上で変更されました",
+ staleDescription: "保存する前に最新のpaseo.jsonを取得するために再読み込みしてください。",
+ failedTitle: "paseo.jsonを保存できませんでした",
+ failedDescription: "再試行するか、ディスクから最新バージョンを再読み込みしてください。",
+ },
+ actions: {
+ reload: "再読み込み",
+ tryAgain: "再試行",
+ save: "保存",
+ saved: "プロジェクトを保存しました",
+ saving: "保存中...",
+ cancel: "キャンセル",
+ },
+ },
+ },
+} as const;
diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts
index 5d7943970..a9b433d43 100644
--- a/packages/app/src/i18n/resources/ru.ts
+++ b/packages/app/src/i18n/resources/ru.ts
@@ -1465,6 +1465,7 @@ export const ru: TranslationResources = {
en: "English",
es: "Español",
fr: "Français",
+ ja: "日本語",
ru: "Русский",
zhCN: "中文",
},
diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts
index 6f3716a2d..8d1e51810 100644
--- a/packages/app/src/i18n/resources/zh-CN.ts
+++ b/packages/app/src/i18n/resources/zh-CN.ts
@@ -1420,6 +1420,7 @@ export const zhCN: TranslationResources = {
en: "English",
es: "Español",
fr: "Français",
+ ja: "日本語",
ru: "Русский",
zhCN: "简体中文",
},
diff --git a/packages/server/src/server/agent/prompt-attachments.test.ts b/packages/server/src/server/agent/prompt-attachments.test.ts
index 535056ffd..d187415c0 100644
--- a/packages/server/src/server/agent/prompt-attachments.test.ts
+++ b/packages/server/src/server/agent/prompt-attachments.test.ts
@@ -123,7 +123,7 @@ describe("prompt attachments", () => {
expect(buildAgentBranchNameSeed({ attachments: [] })).toBeUndefined();
});
- it("joins prompt and rendered attachments into a single seed", () => {
+ it("wraps prompt and rendered attachments as tagged naming input", () => {
expect(
buildAgentBranchNameSeed({
prompt: "Investigate flaky test",
@@ -140,7 +140,7 @@ describe("prompt attachments", () => {
],
}),
).toBe(
- "Investigate flaky test\n\nGitHub PR #123: Fix worktree naming\nhttps://github.com/getpaseo/paseo/pull/123\nBase: main\nHead: fix/worktree-naming",
+ "\nInvestigate flaky test\n\n\n\nGitHub PR #123: Fix worktree naming\nhttps://github.com/getpaseo/paseo/pull/123\nBase: main\nHead: fix/worktree-naming\n",
);
});
});
diff --git a/packages/server/src/server/agent/prompt-attachments.ts b/packages/server/src/server/agent/prompt-attachments.ts
index da7137ed9..129018f2f 100644
--- a/packages/server/src/server/agent/prompt-attachments.ts
+++ b/packages/server/src/server/agent/prompt-attachments.ts
@@ -80,13 +80,17 @@ export function buildAgentBranchNameSeed(
const parts: string[] = [];
const prompt = firstAgentContext.prompt?.trim();
if (prompt) {
- parts.push(prompt);
+ parts.push(["", prompt, ""].join("\n"));
}
+ const renderedAttachments: string[] = [];
for (const attachment of firstAgentContext.attachments ?? []) {
const rendered = renderPromptAttachmentAsText(attachment).trim();
if (rendered) {
- parts.push(rendered);
+ renderedAttachments.push(rendered);
}
}
+ if (renderedAttachments.length > 0) {
+ parts.push(["", renderedAttachments.join("\n\n"), ""].join("\n"));
+ }
return parts.length > 0 ? parts.join("\n\n") : undefined;
}
diff --git a/packages/server/src/server/agent/providers/claude/agent.image-rendering.test.ts b/packages/server/src/server/agent/providers/claude/agent.image-rendering.test.ts
new file mode 100644
index 000000000..194b51e5c
--- /dev/null
+++ b/packages/server/src/server/agent/providers/claude/agent.image-rendering.test.ts
@@ -0,0 +1,225 @@
+import { existsSync, rmSync } from "node:fs";
+import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
+import { describe, expect, test } from "vitest";
+
+import { createTestLogger } from "../../../../test-utils/test-logger.js";
+import type { AgentStreamEvent, AgentTimelineItem } from "../../agent-sdk-types.js";
+import { ClaudeAgentClient } from "./agent.js";
+
+const ONE_BY_ONE_PNG_BASE64 =
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+X1r0AAAAASUVORK5CYII=";
+
+interface ClaudeImageTestSession {
+ translateMessageToEvents(message: SDKMessage): AgentStreamEvent[];
+ convertHistoryEntry(entry: unknown): AgentTimelineItem[];
+}
+
+async function createSession(): Promise {
+ const client = new ClaudeAgentClient({
+ logger: createTestLogger(),
+ resolveBinary: async () => "/test/claude/bin",
+ });
+ const session = await client.createSession({ provider: "claude", cwd: process.cwd() });
+ return session as unknown as ClaudeImageTestSession;
+}
+
+function imageToolResultUserMessage(): SDKMessage {
+ return {
+ type: "user",
+ parent_tool_use_id: null,
+ message: {
+ role: "user",
+ content: [
+ {
+ type: "tool_result",
+ tool_use_id: "toolu_read_png",
+ tool_name: "Read",
+ content: [
+ {
+ type: "image",
+ source: {
+ type: "base64",
+ media_type: "image/png",
+ data: ONE_BY_ONE_PNG_BASE64,
+ },
+ },
+ ],
+ },
+ ],
+ },
+ uuid: "user-image-result-1",
+ session_id: "session-1",
+ } as unknown as SDKMessage;
+}
+
+function imageToolResultHistoryEntry(): unknown {
+ return {
+ type: "user",
+ uuid: "user-image-result-1",
+ message: {
+ role: "user",
+ content: [
+ {
+ type: "tool_result",
+ tool_use_id: "toolu_read_png",
+ tool_name: "Read",
+ content: [
+ {
+ type: "image",
+ source: {
+ type: "base64",
+ media_type: "image/png",
+ data: ONE_BY_ONE_PNG_BASE64,
+ },
+ },
+ ],
+ },
+ ],
+ },
+ };
+}
+
+function erroredImageToolResultUserMessage(): SDKMessage {
+ return {
+ type: "user",
+ parent_tool_use_id: null,
+ message: {
+ role: "user",
+ content: [
+ {
+ type: "tool_result",
+ tool_use_id: "toolu_read_png",
+ tool_name: "Read",
+ is_error: true,
+ content: [
+ {
+ type: "image",
+ source: {
+ type: "base64",
+ media_type: "image/png",
+ data: ONE_BY_ONE_PNG_BASE64,
+ },
+ },
+ ],
+ },
+ ],
+ },
+ uuid: "user-image-error-1",
+ session_id: "session-1",
+ } as unknown as SDKMessage;
+}
+
+function multiImageToolResultUserMessage(): SDKMessage {
+ const imageBlock = {
+ type: "image",
+ source: { type: "base64", media_type: "image/png", data: ONE_BY_ONE_PNG_BASE64 },
+ };
+ return {
+ type: "user",
+ parent_tool_use_id: null,
+ message: {
+ role: "user",
+ content: [
+ {
+ type: "tool_result",
+ tool_use_id: "toolu_read_png",
+ tool_name: "Read",
+ content: [imageBlock, imageBlock],
+ },
+ ],
+ },
+ uuid: "user-image-result-multi",
+ session_id: "session-1",
+ } as unknown as SDKMessage;
+}
+
+function imageMessages(items: AgentTimelineItem[]): string[] {
+ return items
+ .filter((item) => item.type === "assistant_message")
+ .map((item) => (item as { text: string }).text)
+ .filter((text) => text.startsWith("!["));
+}
+
+function markdownImageSource(markdown: string): string {
+ const match = markdown.match(/^!\[[^\]]*]\((.*)\)$/);
+ if (!match) {
+ throw new Error(`Expected markdown image, got: ${markdown}`);
+ }
+ // Reverse escapeMarkdownImageSource: "\\" -> "\" and "\)" -> ")" (Windows paths are escaped).
+ return match[1].replace(/\\(.)/g, "$1");
+}
+
+describe("Claude tool_result image rendering", () => {
+ test("emits the image as assistant markdown and keeps base64 out of the live tool output", async () => {
+ const session = await createSession();
+
+ const events = session.translateMessageToEvents(imageToolResultUserMessage());
+
+ const timelineItems = events
+ .filter((event) => event.type === "timeline")
+ .map((event) => (event as { item: AgentTimelineItem }).item);
+ const [imageMessage, ...extraImages] = imageMessages(timelineItems);
+ expect(extraImages).toEqual([]);
+
+ const source = markdownImageSource(imageMessage);
+ expect(source).toMatch(/paseo-attachments[\\/][0-9a-f]{64}\.png$/);
+ expect(existsSync(source)).toBe(true);
+ expect(JSON.stringify(events)).not.toContain(ONE_BY_ONE_PNG_BASE64);
+
+ rmSync(source, { force: true });
+ });
+
+ test("replays the image as assistant markdown through history conversion", async () => {
+ const session = await createSession();
+
+ const items = session.convertHistoryEntry(imageToolResultHistoryEntry());
+
+ const [imageMessage, ...extraImages] = imageMessages(items);
+ expect(extraImages).toEqual([]);
+
+ const source = markdownImageSource(imageMessage);
+ expect(source).toMatch(/paseo-attachments[\\/][0-9a-f]{64}\.png$/);
+ expect(existsSync(source)).toBe(true);
+ expect(JSON.stringify(items)).not.toContain(ONE_BY_ONE_PNG_BASE64);
+
+ rmSync(source, { force: true });
+ });
+
+ test("keeps base64 out of an errored tool_result that carries an image", async () => {
+ const session = await createSession();
+
+ const events = session.translateMessageToEvents(erroredImageToolResultUserMessage());
+
+ const timelineItems = events
+ .filter((event) => event.type === "timeline")
+ .map((event) => (event as { item: AgentTimelineItem }).item);
+ const [imageMessage, ...extraImages] = imageMessages(timelineItems);
+ expect(extraImages).toEqual([]);
+
+ const source = markdownImageSource(imageMessage);
+ expect(existsSync(source)).toBe(true);
+ expect(JSON.stringify(events)).not.toContain(ONE_BY_ONE_PNG_BASE64);
+ expect(JSON.stringify(events)).toContain("[image]");
+
+ rmSync(source, { force: true });
+ });
+
+ test("emits one image message per image block in a multi-image tool_result", async () => {
+ const session = await createSession();
+
+ const events = session.translateMessageToEvents(multiImageToolResultUserMessage());
+
+ const timelineItems = events
+ .filter((event) => event.type === "timeline")
+ .map((event) => (event as { item: AgentTimelineItem }).item);
+ const sources = imageMessages(timelineItems).map(markdownImageSource);
+
+ expect(sources).toHaveLength(2);
+ // Identical bytes materialize to one content-hashed file (idempotent), one message per block.
+ expect(new Set(sources).size).toBe(1);
+ expect(existsSync(sources[0])).toBe(true);
+ expect(JSON.stringify(events)).not.toContain(ONE_BY_ONE_PNG_BASE64);
+
+ rmSync(sources[0], { force: true });
+ });
+});
diff --git a/packages/server/src/server/agent/providers/claude/agent.redesign.test.ts b/packages/server/src/server/agent/providers/claude/agent.redesign.test.ts
index 83be9f5ba..f43bbacf7 100644
--- a/packages/server/src/server/agent/providers/claude/agent.redesign.test.ts
+++ b/packages/server/src/server/agent/providers/claude/agent.redesign.test.ts
@@ -677,6 +677,7 @@ test("maps tool_result content shapes into deterministic string output", async (
const session = await createSession();
const internal: {
buildToolOutput: (
+ content: unknown,
block: Record,
entry: Record | undefined,
) => Record | undefined;
@@ -724,6 +725,7 @@ test("maps tool_result content shapes into deterministic string output", async (
try {
for (const fixture of fixtures) {
const output = internal.buildToolOutput(
+ fixture.content,
{
type: "tool_result",
tool_use_id: "tool-1",
@@ -750,6 +752,7 @@ test("Grep tool_result string content flows to a search detail with content", as
const session = await createSession();
const internal: {
buildToolOutput: (
+ content: unknown,
block: Record,
entry: Record | undefined,
) => Record | undefined;
@@ -765,12 +768,14 @@ test("Grep tool_result string content flows to a search detail with content", as
};
try {
+ const grepContent = "Found 2 files\nsrc/foo.tsx\nsrc/bar.tsx";
const output = internal.buildToolOutput(
+ grepContent,
{
type: "tool_result",
tool_use_id: "tool-grep-1",
tool_name: "Grep",
- content: "Found 2 files\nsrc/foo.tsx\nsrc/bar.tsx",
+ content: grepContent,
is_error: false,
},
grepEntry,
diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts
index dbfeb9186..62813bcec 100644
--- a/packages/server/src/server/agent/providers/claude/agent.ts
+++ b/packages/server/src/server/agent/providers/claude/agent.ts
@@ -45,6 +45,12 @@ import { realClaudeRewindSdk, revertClaudeConversation, revertClaudeFiles } from
import { normalizeProviderReplayTimestamp } from "../../provider-history-timestamps.js";
import { claudeProjectDirSync } from "./project-dir.js";
import { SETTING_APPLIES_NEXT_TURN_NOTICE } from "../../provider-notices.js";
+import {
+ isProviderImageMarkdown,
+ materializeProviderImage,
+ renderProviderImageOutputAsAssistantMarkdown,
+ type ProviderImageOutput,
+} from "../provider-image-output.js";
import {
getAgentStreamEventTurnId,
@@ -590,6 +596,44 @@ function coerceToolResultContentToString(content: unknown): string {
return deterministicStringify(content);
}
+function toBase64ImageOutput(block: unknown): ProviderImageOutput | null {
+ const record = toObjectRecord(block);
+ if (!record || record.type !== "image") {
+ return null;
+ }
+ const source = toObjectRecord(record.source);
+ if (!source || source.type !== "base64" || typeof source.data !== "string") {
+ return null;
+ }
+ return {
+ data: source.data,
+ mimeType: typeof source.media_type === "string" ? source.media_type : null,
+ };
+}
+
+// Claude returns images inside tool_result content as base64 Anthropic blocks. Left in place they
+// reach coerceToolResultContentToString, which JSON.stringifies the whole array — dumping base64
+// into the tool output. We pull those blocks out to render them as image markdown and leave a
+// "[image]" placeholder so image-only results still produce non-empty output.
+function splitClaudeToolResultImages(content: unknown): {
+ images: ProviderImageOutput[];
+ text: unknown;
+} {
+ if (!Array.isArray(content)) {
+ return { images: [], text: content };
+ }
+ const images: ProviderImageOutput[] = [];
+ const text = content.map((block) => {
+ const image = toBase64ImageOutput(block);
+ if (image) {
+ images.push(image);
+ return { type: "text", text: "[image]" };
+ }
+ return block;
+ });
+ return { images, text };
+}
+
function normalizeClaudeTranscriptText(value: unknown): string | null {
if (typeof value !== "string") {
return null;
@@ -4353,8 +4397,10 @@ class ClaudeAgentSession implements AgentSession {
? block.tool_use_id
: (entry?.id ?? null);
- // Extract output from block.content (SDK always returns content in string form)
- const output = this.buildToolOutput(block, entry);
+ // Pull image blocks out of the result so base64 never reaches the tool output, and render each
+ // one as an assistant_message markdown image after the tool_call (matching how Codex emits).
+ const { images, text } = splitClaudeToolResultImages(block.content);
+ const output = this.buildToolOutput(text, block, entry);
if (block.is_error) {
this.pushToolCall(
@@ -4363,7 +4409,7 @@ class ClaudeAgentSession implements AgentSession {
callId,
input: entry?.input ?? null,
output: output ?? null,
- error: block,
+ error: { ...block, content: text },
}),
items,
);
@@ -4379,6 +4425,15 @@ class ClaudeAgentSession implements AgentSession {
);
}
+ for (const image of images) {
+ const imageItem = renderProviderImageOutputAsAssistantMarkdown(image, {
+ materialize: materializeProviderImage,
+ });
+ if (imageItem) {
+ items.push(imageItem);
+ }
+ }
+
if (typeof block.tool_use_id === "string") {
this.toolUseCache.delete(block.tool_use_id);
this.sidechainTracker.delete(block.tool_use_id);
@@ -4386,6 +4441,7 @@ class ClaudeAgentSession implements AgentSession {
}
private buildToolOutput(
+ content: unknown,
block: ClaudeContentChunk,
entry: ToolUseCacheEntry | undefined,
): AgentMetadata | undefined {
@@ -4397,11 +4453,11 @@ class ClaudeAgentSession implements AgentSession {
const blockToolName = typeof block.tool_name === "string" ? block.tool_name : undefined;
const server = entry?.server ?? blockServer ?? "tool";
const tool = entry?.name ?? blockToolName ?? "tool";
- const content = coerceToolResultContentToString(block.content);
+ const coercedContent = coerceToolResultContentToString(content);
const input = entry?.input;
// Build structured result based on tool type
- const structured = this.buildStructuredToolResult(server, tool, content, input);
+ const structured = this.buildStructuredToolResult(server, tool, coercedContent, input);
if (structured) {
return structured;
@@ -4410,13 +4466,13 @@ class ClaudeAgentSession implements AgentSession {
// Fallback format - try to parse JSON first
const result: AgentMetadata = {};
- if (content.length > 0) {
+ if (coercedContent.length > 0) {
try {
// If content is a JSON string, parse it
- result.output = JSON.parse(content);
+ result.output = JSON.parse(coercedContent);
} catch {
// If not JSON, return unchanged (no extra wrapping)
- result.output = content;
+ result.output = coercedContent;
}
}
@@ -4936,6 +4992,10 @@ function convertClaudeHistoryEntryPreamble(
return { proceed: { content } };
}
+function isProviderImageMessage(item: AgentTimelineItem): boolean {
+ return item.type === "assistant_message" && isProviderImageMarkdown(item.text);
+}
+
export function convertClaudeHistoryEntry(
entry: ClaudeHistoryEntry,
mapBlocks: (content: string | ClaudeContentChunk[]) => AgentTimelineItem[],
@@ -4979,7 +5039,12 @@ export function convertClaudeHistoryEntry(
if (hasToolBlock && normalizedBlocks) {
const mapped = mapBlocks(normalizedBlocks);
if (entry.type === "user") {
- const toolItems = mapped.filter((item) => item.type === "tool_call");
+ // tool_result handling (handleToolResult) emits image markdown as an assistant_message
+ // alongside the tool_call. User-entry text blocks also map to assistant_message in this path
+ // and must stay suppressed, so keep tool_calls plus only the image assistant_messages.
+ const toolItems = mapped.filter(
+ (item) => item.type === "tool_call" || isProviderImageMessage(item),
+ );
return timeline.length ? [...timeline, ...toolItems] : toolItems;
}
return mapped;
diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts
index 634044b30..41d91f7d2 100644
--- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts
+++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts
@@ -39,7 +39,6 @@ import type { Logger } from "pino";
import type { ChildProcess, ChildProcessWithoutNullStreams } from "node:child_process";
import { randomUUID } from "node:crypto";
import { Dirent } from "node:fs";
-import * as fsSync from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@@ -79,6 +78,7 @@ import {
} from "./codex/app-server-transport.js";
import { type CodexUserMessageTurnIndex, revertCodexConversation } from "./codex/rewind.js";
import {
+ materializeProviderImage,
renderProviderImageOutputAsAssistantMarkdown,
type ProviderImageOutput,
} from "./provider-image-output.js";
@@ -114,7 +114,6 @@ function isCodexAlreadyUnarchivedError(error: unknown, threadId: string): boolea
const TURN_START_TIMEOUT_MS = 90 * 1000;
const INTERRUPT_TIMEOUT_MS = 2_000;
const CODEX_PROVIDER = "codex" as const;
-const CODEX_IMAGE_ATTACHMENT_DIR = "paseo-attachments";
// Codex treats most app-server client names as the model-request originator.
// This reserved Codex name is non-originating, so requests keep Codex's default
// CLI identity instead of showing up as Paseo in provider usage logs.
@@ -1626,25 +1625,6 @@ function codexImageOutputFromResult(result: unknown): ProviderImageOutput | null
};
}
-function writeImageAttachmentSync(mimeType: string, data: string): string {
- const attachmentsDir = path.join(os.tmpdir(), CODEX_IMAGE_ATTACHMENT_DIR);
- fsSync.mkdirSync(attachmentsDir, { recursive: true });
- const normalized = normalizeImageData(mimeType, data);
- const extension = getImageExtension(normalized.mimeType);
- const filename = `${randomUUID()}.${extension}`;
- const filePath = path.join(attachmentsDir, filename);
- fsSync.writeFileSync(filePath, Buffer.from(normalized.data, "base64"));
- return filePath;
-}
-
-function materializeCodexImageOutput(image: { data: string; mimeType: string | null }): {
- path: string;
-} {
- return {
- path: writeImageAttachmentSync(image.mimeType ?? "image/png", image.data),
- };
-}
-
function mapCodexThreadImageItem(
normalizedType: string,
normalizedItem: Record,
@@ -1664,7 +1644,7 @@ function mapCodexThreadImageItem(
data: result?.data ?? null,
mimeType: result?.mimeType ?? null,
},
- { materialize: materializeCodexImageOutput },
+ { materialize: materializeProviderImage },
);
}
@@ -1809,40 +1789,6 @@ function toSandboxPolicy(type: string, networkAccess?: boolean): Record {
- const attachmentsDir = path.join(os.tmpdir(), CODEX_IMAGE_ATTACHMENT_DIR);
- await fs.mkdir(attachmentsDir, { recursive: true });
- const normalized = normalizeImageData(mimeType, data);
- const extension = getImageExtension(normalized.mimeType);
- const filename = `${randomUUID()}.${extension}`;
- const filePath = path.join(attachmentsDir, filename);
- await fs.writeFile(filePath, Buffer.from(normalized.data, "base64"));
- return filePath;
-}
-
async function readCodexConfiguredDefaults(
client: CodexAppServerClient,
logger: Logger,
@@ -2794,7 +2729,10 @@ export async function codexAppServerTurnInputFromPrompt(
}
if (block.type === "image") {
try {
- const filePath = await writeImageAttachment(block.mimeType, block.data);
+ const filePath = materializeProviderImage({
+ data: block.data,
+ mimeType: block.mimeType,
+ }).path;
output.push({ type: "localImage", path: filePath });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
diff --git a/packages/server/src/server/agent/providers/mock-load-test-agent.test.ts b/packages/server/src/server/agent/providers/mock-load-test-agent.test.ts
index 1ae4f5502..ce0d81a6b 100644
--- a/packages/server/src/server/agent/providers/mock-load-test-agent.test.ts
+++ b/packages/server/src/server/agent/providers/mock-load-test-agent.test.ts
@@ -56,13 +56,14 @@ describe("MockLoadTestAgentClient", () => {
const resultPromise = session.run(
[
- "Generate a git branch name for a coding agent based on the user prompt and attachments.",
+ "Generate a title and a git branch name for a coding agent from the user prompt and attachments.",
"Title: a short human-readable sentence-case label for the task (no slug rules, max 80 characters).",
"Branch: concise lowercase slug using letters, numbers, hyphens, and slashes only.",
"Return JSON only with fields 'title' and 'branch'.",
"",
- "User context:",
+ "",
"Fix login bug",
+ "",
].join("\n"),
);
await vi.advanceTimersByTimeAsync(0);
diff --git a/packages/server/src/server/agent/providers/mock-load-test-agent.ts b/packages/server/src/server/agent/providers/mock-load-test-agent.ts
index 2a5954101..439628d2d 100644
--- a/packages/server/src/server/agent/providers/mock-load-test-agent.ts
+++ b/packages/server/src/server/agent/providers/mock-load-test-agent.ts
@@ -275,7 +275,7 @@ function parseStructuredBranchNamePrompt(
): { title: string; branch: string } | null {
const text = promptToText(prompt);
const hasBranchNamePrompt =
- text.includes("Generate a git branch name for a coding agent") &&
+ text.includes("Generate a title and a git branch name for a coding agent") &&
(text.includes("Return JSON only with fields 'title' and 'branch'.") ||
text.includes('"title"') ||
text.includes('"branch"'));
@@ -290,7 +290,10 @@ function parseStructuredBranchNamePrompt(
return null;
}
- const seed = text.split("User context:\n").at(-1)?.trim() ?? "";
+ const seed =
+ text.match(/\n([\s\S]*?)\n<\/user-prompt>/)?.[1]?.trim() ??
+ text.match(/\n([\s\S]*?)\n<\/attachments>/)?.[1]?.trim() ??
+ "";
const firstLine =
seed
.split("\n")
diff --git a/packages/server/src/server/agent/providers/provider-image-output.test.ts b/packages/server/src/server/agent/providers/provider-image-output.test.ts
new file mode 100644
index 000000000..8f5410cb1
--- /dev/null
+++ b/packages/server/src/server/agent/providers/provider-image-output.test.ts
@@ -0,0 +1,28 @@
+import { describe, expect, test } from "vitest";
+
+import { isProviderImageMarkdown } from "./provider-image-output.js";
+
+const HASH = "a".repeat(64);
+
+describe("isProviderImageMarkdown", () => {
+ test("matches the markdown emitted for a materialized attachment", () => {
+ expect(isProviderImageMarkdown(``)).toBe(true);
+ expect(isProviderImageMarkdown(``)).toBe(
+ true,
+ );
+ // Windows: backslash path separators are doubled by escapeMarkdownImageSource.
+ expect(
+ isProviderImageMarkdown(
+ ``,
+ ),
+ ).toBe(true);
+ });
+
+ test("rejects user-authored markdown that is not a materialized attachment", () => {
+ // No content hash — a hand-written path, not something the writer produced.
+ expect(isProviderImageMarkdown("")).toBe(false);
+ expect(isProviderImageMarkdown("")).toBe(false);
+ // Image markdown that does not start the text.
+ expect(isProviderImageMarkdown("see the chart: ")).toBe(false);
+ });
+});
diff --git a/packages/server/src/server/agent/providers/provider-image-output.ts b/packages/server/src/server/agent/providers/provider-image-output.ts
index bc726081c..0a4a154f7 100644
--- a/packages/server/src/server/agent/providers/provider-image-output.ts
+++ b/packages/server/src/server/agent/providers/provider-image-output.ts
@@ -1,3 +1,8 @@
+import { createHash } from "node:crypto";
+import * as fsSync from "node:fs";
+import os from "node:os";
+import path from "node:path";
+
import type { AgentTimelineItem } from "../agent-sdk-types.js";
export interface ProviderImageOutput {
@@ -12,6 +17,68 @@ export interface MaterializedProviderImage {
path: string;
}
+const PROVIDER_IMAGE_ATTACHMENT_DIR = "paseo-attachments";
+
+function getImageExtension(mimeType: string): string {
+ switch (mimeType) {
+ case "image/jpeg":
+ return "jpg";
+ case "image/png":
+ return "png";
+ case "image/webp":
+ return "webp";
+ case "image/gif":
+ return "gif";
+ case "image/bmp":
+ return "bmp";
+ case "image/tiff":
+ return "tiff";
+ default:
+ return "bin";
+ }
+}
+
+function normalizeImageData(mimeType: string, data: string): { mimeType: string; data: string } {
+ if (data.startsWith("data:")) {
+ const match = data.match(/^data:([^;]+);base64,(.*)$/);
+ if (match) {
+ return { mimeType: match[1], data: match[2] };
+ }
+ }
+ return { mimeType, data };
+}
+
+// Filenames are a content hash of the bytes so re-materializing the same image
+// is idempotent: history replay reuses the existing temp file instead of leaking
+// a fresh one on every load.
+export function materializeProviderImage(image: {
+ data: string;
+ mimeType: string | null;
+}): MaterializedProviderImage {
+ const attachmentsDir = path.join(os.tmpdir(), PROVIDER_IMAGE_ATTACHMENT_DIR);
+ fsSync.mkdirSync(attachmentsDir, { recursive: true });
+ const normalized = normalizeImageData(image.mimeType ?? "image/png", image.data);
+ const bytes = Buffer.from(normalized.data, "base64");
+ const extension = getImageExtension(normalized.mimeType);
+ const hash = createHash("sha256").update(bytes).digest("hex");
+ const filePath = path.join(attachmentsDir, `${hash}.${extension}`);
+ fsSync.writeFileSync(filePath, bytes);
+ return { path: filePath };
+}
+
+// Recognizes the markdown renderProviderImageOutputAsAssistantMarkdown emits for a materialized
+// provider image: its source is a content-hashed file in the attachments dir. Matching the full
+// . shape (not just a leading "![") keeps user-authored text from being mistaken for a
+// provider image when it reaches the history-replay filter. The separator class allows one-or-more
+// because on Windows the path uses "\\" and escapeMarkdownImageSource doubles each backslash.
+const PROVIDER_IMAGE_MARKDOWN = new RegExp(
+ `^!\\[[^\\]]*\\]\\([^)]*${PROVIDER_IMAGE_ATTACHMENT_DIR}[/\\\\]+[0-9a-f]{64}\\.[a-z0-9]+\\)`,
+);
+
+export function isProviderImageMarkdown(text: string): boolean {
+ return PROVIDER_IMAGE_MARKDOWN.test(text);
+}
+
interface RenderProviderImageOutputOptions {
materialize?: (image: { data: string; mimeType: string | null }) => MaterializedProviderImage;
}
diff --git a/packages/server/src/server/session.test.ts b/packages/server/src/server/session.test.ts
index e3de3e24f..7a1410921 100644
--- a/packages/server/src/server/session.test.ts
+++ b/packages/server/src/server/session.test.ts
@@ -65,7 +65,6 @@ interface SessionHandlerInternals {
describeWorkspaceRecord(...args: unknown[]): Promise;
describeWorkspaceRecordWithGitData(...args: unknown[]): Promise;
handleValidateBranchRequest(params: unknown): Promise;
- createBranchFromBase(params: unknown): Promise;
handleCheckoutSwitchBranchRequest(params: unknown): Promise;
handleBranchSuggestionsRequest(params: unknown): Promise;
handleStashListRequest(params: unknown): Promise;
@@ -3253,101 +3252,6 @@ describe("session branch validation", () => {
});
});
-describe("session branch creation handling", () => {
- test("validates the base branch through the workspace git service", async () => {
- const workspaceGitService = {
- getSnapshot: vi.fn(),
- validateBranchRef: vi.fn().mockResolvedValue({ kind: "not-found" }),
- hasLocalBranch: vi.fn(),
- };
- const session = createSessionForTest({ workspaceGitService });
-
- await expect(
- asSessionInternals(session).createBranchFromBase({
- cwd: "/tmp/repo",
- baseBranch: "missing-base",
- newBranchName: "feature/new-work",
- }),
- ).rejects.toThrow("Base branch not found: missing-base");
-
- expect(workspaceGitService.validateBranchRef).toHaveBeenCalledTimes(1);
- expect(workspaceGitService.validateBranchRef).toHaveBeenCalledWith("/tmp/repo", "missing-base");
- expect(workspaceGitService.hasLocalBranch).not.toHaveBeenCalled();
- expect(spawnMocks.execCommand).not.toHaveBeenCalledWith(
- "git",
- ["rev-parse", "--verify", "missing-base"],
- { cwd: "/tmp/repo" },
- );
- });
-
- test("checks local branch existence through the workspace git service", async () => {
- const workspaceGitService = {
- getSnapshot: vi.fn(),
- validateBranchRef: vi.fn().mockResolvedValue({ kind: "local", name: "main" }),
- hasLocalBranch: vi.fn().mockResolvedValue(true),
- };
- const session = createSessionForTest({ workspaceGitService });
-
- await expect(
- asSessionInternals(session).createBranchFromBase({
- cwd: "/tmp/repo",
- baseBranch: "main",
- newBranchName: "feature/existing",
- }),
- ).rejects.toThrow("Branch already exists: feature/existing");
-
- expect(workspaceGitService.validateBranchRef).toHaveBeenCalledWith("/tmp/repo", "main");
- expect(workspaceGitService.hasLocalBranch).toHaveBeenCalledTimes(1);
- expect(workspaceGitService.hasLocalBranch).toHaveBeenCalledWith(
- "/tmp/repo",
- "feature/existing",
- );
- expect(spawnMocks.execCommand).not.toHaveBeenCalledWith(
- "git",
- ["show-ref", "--verify", "--quiet", "refs/heads/feature/existing"],
- { cwd: "/tmp/repo" },
- );
- });
-
- test("forces a workspace git snapshot refresh after creating a branch", async () => {
- const workspaceGitService = {
- getSnapshot: vi.fn().mockResolvedValue(
- createWorkspaceGitSnapshot("/tmp/repo", {
- git: {
- isDirty: false,
- },
- }),
- ),
- validateBranchRef: vi.fn().mockResolvedValue({ kind: "local", name: "main" }),
- hasLocalBranch: vi.fn().mockResolvedValue(false),
- };
- const session = createSessionForTest({ workspaceGitService });
- spawnMocks.execCommand.mockResolvedValue({
- stdout: "",
- stderr: "",
- exitCode: 0,
- signal: null,
- truncated: false,
- });
-
- await asSessionInternals(session).createBranchFromBase({
- cwd: "/tmp/repo",
- baseBranch: "main",
- newBranchName: "feature/new-work",
- });
-
- expect(spawnMocks.execCommand).toHaveBeenCalledWith(
- "git",
- ["checkout", "-b", "feature/new-work", "main"],
- { cwd: "/tmp/repo" },
- );
- expect(workspaceGitService.getSnapshot).toHaveBeenCalledWith("/tmp/repo", {
- force: true,
- reason: "create-branch",
- });
- });
-});
-
describe("session checkout switch branch handling", () => {
test("forces a workspace git snapshot refresh after switching branches", async () => {
const messages: unknown[] = [];
diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts
index 5a4f0fffe..799377c5d 100644
--- a/packages/server/src/server/session.ts
+++ b/packages/server/src/server/session.ts
@@ -1,10 +1,8 @@
import equal from "fast-deep-equal";
import { v4 as uuidv4 } from "uuid";
-import type { FSWatcher } from "node:fs";
import { stat } from "node:fs/promises";
import { basename, normalize, resolve, sep } from "path";
import { homedir } from "node:os";
-import { z } from "zod";
import type { ToolSet } from "ai";
import { CLIENT_CAPS, type ClientCapability } from "@getpaseo/protocol/client-capabilities";
import {
@@ -54,14 +52,13 @@ import { respondToAgentPermission } from "./agent/permission-response.js";
import { experimental_createMCPClient } from "ai";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { VoiceCallerContext, VoiceSpeakHandler } from "./voice-types.js";
-import {
- buildWorkspaceScriptPayloads,
- readPaseoConfigForProjection,
-} from "./script-status-projection.js";
-import { deriveProjectSlug } from "./workspace-git-metadata.js";
import type { ScriptHealthState } from "./script-health-monitor.js";
import { spawnWorkspaceScript } from "./worktree-bootstrap.js";
import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
+import {
+ createWorkspaceScriptsService,
+ type WorkspaceScriptsService,
+} from "./session/workspace-scripts/workspace-scripts-service.js";
import type { DaemonConfigStore } from "./daemon-config-store.js";
import { getErrorMessage, getErrorMessageOr } from "@getpaseo/protocol/error-utils";
import { getAgentStatusPriority } from "@getpaseo/protocol/agent-state-bucket";
@@ -88,7 +85,6 @@ import {
} from "./agent/lifecycle-command.js";
import {
buildStoredAgentPayload,
- resolveEffectiveThinkingOptionId,
resolveStoredAgentPayloadUpdatedAt,
toAgentPayload,
} from "./agent/agent-projections.js";
@@ -102,15 +98,7 @@ import {
type TimelineProjectionEntry,
type TimelineProjectionMode,
} from "./agent/timeline-projection.js";
-import {
- StructuredAgentFallbackError,
- StructuredAgentResponseError,
- generateStructuredAgentResponseWithFallback,
-} from "./agent/agent-response-loop.js";
-import {
- resolveStructuredGenerationProviders,
- type StructuredGenerationDaemonConfig,
-} from "./agent/structured-generation-providers.js";
+import type { StructuredGenerationDaemonConfig } from "./agent/structured-generation-providers.js";
import {
getAgentStreamEventTurnId,
type AgentPersistenceHandle,
@@ -130,14 +118,10 @@ import {
} from "./agent/import-sessions.js";
import {
checkoutLiteFromGitSnapshot,
- classifyDirectoryForProjectMembership,
deriveWorkspaceDisplayName,
- generateWorkspaceId,
} from "./workspace-registry-model.js";
import { resolveWorkspaceIdForPath } from "./resolve-workspace-id-for-path.js";
import {
- createPersistedProjectRecord,
- createPersistedWorkspaceRecord,
resolveProjectDisplayName,
resolveWorkspaceDisplayName,
resolveWorkspaceName,
@@ -150,6 +134,14 @@ import { wrapSpokenInput } from "./voice-config.js";
import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
import { VoiceSession } from "./session/voice/voice-session.js";
import { CheckoutSession } from "./session/checkout/checkout-session.js";
+import {
+ createWorkspaceGitObserverService,
+ type WorkspaceGitObserverService,
+} from "./session/workspace-git-observer/workspace-git-observer-service.js";
+import {
+ createAgentStructuredTextGeneration,
+ createGitMetadataGenerator,
+} from "./session/checkout/git-metadata-generator.js";
import { ChatScheduleLoopSession } from "./session/chat/chat-schedule-loop-session.js";
import { ProviderCatalogSession } from "./session/provider/provider-catalog-session.js";
import { WorkspaceFilesSession } from "./session/files/workspace-files-session.js";
@@ -158,19 +150,26 @@ import { ProjectConfigSession } from "./session/project-config/project-config-se
import { DaemonSession, type DaemonRuntimeConfig } from "./session/daemon/daemon-session.js";
import { DownloadTokenStore } from "./file-download/token-store.js";
import { PushTokenStore } from "./push/token-store.js";
-import { buildMetadataPrompt } from "../utils/build-metadata-prompt.js";
import {
archivePersistedWorkspaceRecord,
archiveWorkspaceContents,
} from "./workspace-archive-service.js";
import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
import type { ServiceProxySubsystem } from "./service-proxy.js";
+import { renameCurrentBranch as renameCurrentBranchDefault } from "../utils/checkout-git.js";
import {
- checkoutResolvedBranch,
- type CheckoutExistingBranchResult,
- type GitMutationRefreshReason,
- renameCurrentBranch as renameCurrentBranchDefault,
-} from "../utils/checkout-git.js";
+ createGitMutationService,
+ type GitMutationService,
+} from "./session/git-mutation/git-mutation-service.js";
+import {
+ createWorkspaceProvisioningService,
+ type WorkspaceProvisioningService,
+} from "./session/workspace-provisioning/workspace-provisioning-service.js";
+import {
+ createAgentUpdatesService,
+ matchesAgentUpdatesFilter,
+ type AgentUpdatesService,
+} from "./session/agent-updates/agent-updates-service.js";
import { expandTilde } from "../utils/path.js";
import { searchHomeDirectories, searchWorkspaceEntries } from "../utils/directory-suggestions.js";
import type { CheckoutDiffManager } from "./checkout-diff-manager.js";
@@ -180,7 +179,6 @@ import type pino from "pino";
import { FileBackedChatService } from "./chat/chat-service.js";
import { LoopService } from "./loop-service.js";
import { ScheduleService } from "./schedule/service.js";
-import { execCommand } from "../utils/spawn.js";
import { createGitHubService, type GitHubService } from "../services/github-service.js";
import type { ProviderUsageService } from "../services/quota-fetcher/service.js";
import {
@@ -202,7 +200,6 @@ import {
type GeneratedWorkspaceName,
} from "./worktree-branch-name-generator.js";
import {
- assertSafeGitRef as assertWorktreeSafeGitRef,
buildAgentSessionConfig as buildWorktreeAgentSessionConfig,
createPaseoWorktreeWorkflow as createWorktreeWorkflow,
type CreatePaseoWorktreeSetupContinuationInput,
@@ -222,8 +219,6 @@ import { type WorktreeConfig, createWorktree } from "../utils/worktree.js";
import { runGitCommand } from "../utils/run-git-command.js";
import { CreateAgentLifecycleDispatch } from "./agent/create-agent-lifecycle-dispatch.js";
-const WORKSPACE_GIT_WATCH_REMOVED_STATE_KEY = "__removed__";
-
// TODO: Remove once all app store clients are on >=0.1.45 and understand arbitrary provider strings.
// Clients before 0.1.45 validate providers with z.enum(["claude", "codex", "opencode"]) and reject
// the entire session message if they encounter an unknown provider.
@@ -247,12 +242,6 @@ function resolveSubscriptionId(
return uuidv4();
}
-function diffChangeTypeFor(file: { isNew?: boolean; isDeleted?: boolean }): "A" | "D" | "M" {
- if (file.isNew) return "A";
- if (file.isDeleted) return "D";
- return "M";
-}
-
function buildWorkspaceCheckout(
workspace: PersistedWorkspaceRecord,
project: PersistedProjectRecord,
@@ -338,17 +327,6 @@ export function resolveWaitForFinishError(options: {
return typeof message === "string" && message.trim().length > 0 ? message : "Agent failed";
}
-interface WorkspaceGitWatchTarget {
- cwd: string;
- workspaceId: string;
- watchers: FSWatcher[];
- debounceTimer: ReturnType | null;
- refreshPromise: Promise | null;
- refreshQueued: boolean;
- latestDescriptorStateKey: string | null;
- lastBranchName: string | null;
-}
-
export interface SessionRuntimeMetrics {
terminalDirectorySubscriptionCount: number;
terminalSubscriptionCount: number;
@@ -370,14 +348,7 @@ type FetchAgentsResponsePayload = Extract<
>["payload"];
type FetchAgentsResponseEntry = FetchAgentsResponsePayload["entries"][number];
type FetchAgentsResponsePageInfo = FetchAgentsResponsePayload["pageInfo"];
-type AgentUpdatePayload = Extract["payload"];
type AgentUpdatesFilter = FetchAgentsRequestFilter;
-interface AgentUpdatesSubscriptionState {
- subscriptionId: string;
- filter?: AgentUpdatesFilter;
- isBootstrapping: boolean;
- pendingUpdatesByAgentId: Map;
-}
type FetchWorkspacesRequestMessage = Extract<
SessionInboundMessage,
{ type: "fetch_workspaces_request" }
@@ -578,12 +549,14 @@ export class Session {
private readonly renameCurrentBranch: typeof renameCurrentBranchDefault;
private readonly generateWorkspaceName: typeof generateBranchNameFromFirstAgentContext;
private readonly workspaceGitService: WorkspaceGitService;
+ private readonly gitMutation: GitMutationService;
+ private readonly workspaceProvisioning: WorkspaceProvisioningService;
private readonly daemonConfigStore: DaemonConfigStore;
private readonly mcpBaseUrl: string | null;
private readonly pushTokenStore: PushTokenStore;
private unsubscribeAgentEvents: (() => void) | null = null;
private unsubscribeTerminalWorkspaceContributionEvents: (() => void) | null = null;
- private agentUpdatesSubscription: AgentUpdatesSubscriptionState | null = null;
+ private readonly agentUpdates: AgentUpdatesService;
private workspaceUpdatesSubscription: WorkspaceUpdatesSubscriptionState | null = null;
private clientActivity: {
deviceType: "web" | "mobile";
@@ -597,11 +570,6 @@ export class Session {
private readonly providerSnapshotManager: ProviderSnapshotManager;
private readonly serviceProxy: ServiceProxySubsystem | null;
private readonly scriptRuntimeStore: WorkspaceScriptRuntimeStore | null;
- private readonly onBranchChanged?: (
- workspaceId: string,
- oldBranch: string | null,
- newBranch: string | null,
- ) => void;
private readonly getDaemonTcpPort: (() => number | null) | null;
private readonly getDaemonTcpHost: (() => string | null) | null;
private readonly serviceProxyPublicBaseUrl: string | null;
@@ -609,10 +577,8 @@ export class Session {
private readonly terminalController: TerminalSessionController;
private inflightRequests = 0;
private peakInflightRequests = 0;
- private readonly workspaceGitWatchTargets = new Map();
private readonly workspaceSetupSnapshots: Map;
- private readonly workspaceGitFetchSubscriptions = new Map void>();
- private readonly workspaceGitSubscriptions = new Map void>();
+ private readonly workspaceGitObserver: WorkspaceGitObserverService;
private readonly workspaceDirectory: WorkspaceDirectory;
private readonly voiceSession: VoiceSession;
private readonly checkoutSession: CheckoutSession;
@@ -622,6 +588,7 @@ export class Session {
private readonly agentConfigSession: AgentConfigSession;
private readonly projectConfigSession: ProjectConfigSession;
private readonly daemonSession: DaemonSession;
+ private readonly workspaceScripts: WorkspaceScriptsService;
private readonly createAgentLifecycleDispatch: CreateAgentLifecycleDispatch;
constructor(options: SessionOptions) {
@@ -709,26 +676,52 @@ export class Session {
this.renameCurrentBranch = renameCurrentBranch ?? renameCurrentBranchDefault;
this.generateWorkspaceName = generateWorkspaceName ?? generateBranchNameFromFirstAgentContext;
this.workspaceGitService = workspaceGitService;
+ this.gitMutation = createGitMutationService({
+ workspaceGitService: this.workspaceGitService,
+ github: this.github,
+ logger: this.sessionLogger,
+ });
+ this.workspaceProvisioning = createWorkspaceProvisioningService({
+ workspaceRegistry: this.workspaceRegistry,
+ projectRegistry: this.projectRegistry,
+ workspaceGitService: this.workspaceGitService,
+ });
this.checkoutSession = new CheckoutSession({
host: {
emit: (msg) => this.emit(msg),
- notifyGitMutation: (cwd, reason, mutationOptions) =>
- this.notifyGitMutation(cwd, reason, mutationOptions),
emitWorkspaceUpdateForCwd: (cwd) => this.emitWorkspaceUpdateForCwd(cwd),
handleWorkspaceGitBranchSnapshot: (cwd, branchName) =>
- this.handleWorkspaceGitBranchSnapshot(cwd, branchName),
+ this.workspaceGitObserver.handleBranchSnapshot(cwd, branchName),
renameCurrentBranch: (cwd, branch) => this.renameCurrentBranch(cwd, branch),
- checkoutExistingBranch: (cwd, branch) => this.checkoutExistingBranch(cwd, branch),
- generateCommitMessage: (cwd) => this.generateCommitMessage(cwd),
- generatePullRequestText: (cwd, baseRef) => this.generatePullRequestText(cwd, baseRef),
},
+ gitMutation: this.gitMutation,
workspaceGitService: this.workspaceGitService,
github: this.github,
checkoutDiffManager,
+ gitMetadataGenerator: createGitMetadataGenerator({
+ workspaceGitService: this.workspaceGitService,
+ generation: createAgentStructuredTextGeneration({
+ agentManager: this.agentManager,
+ providerSnapshotManager,
+ readDaemonConfig: () => this.readStructuredGenerationDaemonConfig(),
+ getFocusedSelection: (cwd) => this.getFocusedAgentSelectionForCwd(cwd),
+ }),
+ }),
paseoHome: this.paseoHome,
worktreesRoot: this.worktreesRoot,
logger: this.sessionLogger,
});
+ this.workspaceGitObserver = createWorkspaceGitObserverService({
+ workspaceGitService: this.workspaceGitService,
+ describeWorkspaceRecordWithGitData: (workspace) =>
+ this.describeWorkspaceRecordWithGitData(workspace),
+ emitWorkspaceUpdateForCwd: (cwd) => this.emitWorkspaceUpdateForCwd(cwd),
+ emitWorkspaceUpdateForWorkspaceId: (workspaceId) =>
+ this.emitWorkspaceUpdateForWorkspaceId(workspaceId),
+ emitStatusUpdate: (cwd, snapshot) => this.checkoutSession.emitStatusUpdate(cwd, snapshot),
+ onBranchChanged,
+ logger: this.sessionLogger,
+ });
this.chatScheduleLoopSession = new ChatScheduleLoopSession({
host: {
emit: (msg) => this.emit(msg),
@@ -812,6 +805,17 @@ export class Session {
this.clientCapabilities.has(CLIENT_CAPS.terminalReflowableSnapshot),
getClientBufferedAmount: () => this.getTransportBufferedAmount(),
});
+ this.agentUpdates = createAgentUpdatesService({
+ emit: (message) => this.emit(message),
+ buildAgentPayload: (agent) => this.buildAgentPayload(agent),
+ buildStoredAgentPayload: (record) => this.buildStoredAgentPayload(record),
+ isProviderVisibleToClient: (provider) => this.isProviderVisibleToClient(provider),
+ buildProjectPlacementForWorkspaceId: (workspaceId) =>
+ this.buildProjectPlacementForWorkspaceId(workspaceId),
+ emitWorkspaceUpdateForWorkspaceId: (workspaceId) =>
+ this.emitWorkspaceUpdateForWorkspaceId(workspaceId),
+ logger: this.sessionLogger,
+ });
this.createAgentLifecycleDispatch = new CreateAgentLifecycleDispatch({
paseoHome: this.paseoHome,
worktreesRoot: this.worktreesRoot,
@@ -826,14 +830,7 @@ export class Session {
listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
emit: (message) => this.emit(message),
- emitAgentRemove: (agentId) => {
- if (this.agentUpdatesSubscription) {
- this.bufferOrEmitAgentUpdate(this.agentUpdatesSubscription, {
- kind: "remove",
- agentId,
- });
- }
- },
+ emitAgentRemove: (agentId) => this.agentUpdates.removeAgent(agentId),
emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) =>
this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds),
markWorkspaceArchiving: (workspaceIds, archivingAt) =>
@@ -847,11 +844,24 @@ export class Session {
this.serviceProxy = serviceProxy ?? null;
this.scriptRuntimeStore = scriptRuntimeStore ?? null;
this.workspaceSetupSnapshots = workspaceSetupSnapshots ?? new Map();
- this.onBranchChanged = onBranchChanged;
this.getDaemonTcpPort = getDaemonTcpPort ?? null;
this.getDaemonTcpHost = getDaemonTcpHost ?? null;
this.serviceProxyPublicBaseUrl = serviceProxyPublicBaseUrl ?? null;
this.resolveScriptHealth = resolveScriptHealth ?? null;
+ this.workspaceScripts = createWorkspaceScriptsService({
+ serviceProxy: this.serviceProxy,
+ scriptRuntimeStore: this.scriptRuntimeStore,
+ terminalManager: this.terminalManager,
+ workspaceRegistry: this.workspaceRegistry,
+ workspaceGitService: this.workspaceGitService,
+ getDaemonTcpPort: this.getDaemonTcpPort,
+ getDaemonTcpHost: this.getDaemonTcpHost,
+ serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
+ resolveScriptHealth: this.resolveScriptHealth,
+ logger: this.sessionLogger,
+ emit: (message) => this.emit(message),
+ spawnWorkspaceScript,
+ });
this.subscribeToOptionalManagers();
this.workspaceDirectory = new WorkspaceDirectory({
logger: this.sessionLogger,
@@ -920,8 +930,7 @@ export class Session {
}
async syncWorkspaceGitObserverForWorkspace(workspace: PersistedWorkspaceRecord): Promise {
- const descriptor = await this.describeWorkspaceRecordWithGitData(workspace);
- this.syncWorkspaceGitObservers([descriptor]);
+ await this.workspaceGitObserver.syncObserverForWorkspace(workspace);
}
async emitWorkspaceUpdateForWorkspaceId(workspaceId: string): Promise {
@@ -948,8 +957,7 @@ export class Session {
}
async warmWorkspaceGitDataForWorkspace(workspace: PersistedWorkspaceRecord): Promise {
- await this.syncWorkspaceGitObserverForWorkspace(workspace);
- await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
+ await this.workspaceGitObserver.warmGitData(workspace);
}
/**
@@ -1178,7 +1186,7 @@ export class Session {
},
"agent.session.forward_update",
);
- void this.forwardAgentUpdate(event.agent);
+ void this.agentUpdates.forwardLiveAgent(event.agent);
return;
}
@@ -1293,199 +1301,6 @@ export class Session {
return LEGACY_PROVIDER_IDS.has(provider);
}
- private agentThinkingOptionMatchesFilter(
- agent: AgentSnapshotPayload,
- filter: AgentUpdatesFilter,
- ): boolean {
- if (filter.thinkingOptionId === undefined) {
- return true;
- }
- const expectedThinkingOptionId = resolveEffectiveThinkingOptionId({
- configuredThinkingOptionId: filter.thinkingOptionId ?? null,
- });
- const resolvedThinkingOptionId =
- agent.effectiveThinkingOptionId ??
- resolveEffectiveThinkingOptionId({
- runtimeInfo: agent.runtimeInfo,
- configuredThinkingOptionId: agent.thinkingOptionId ?? null,
- });
- return resolvedThinkingOptionId === expectedThinkingOptionId;
- }
-
- private matchesAgentStructuralFilter(
- agent: AgentSnapshotPayload,
- project: ProjectPlacementPayload,
- filter: AgentUpdatesFilter,
- ): boolean {
- if (filter.statuses && filter.statuses.length > 0) {
- const statuses = new Set(filter.statuses);
- if (!statuses.has(agent.status)) {
- return false;
- }
- }
-
- if (typeof filter.requiresAttention === "boolean") {
- const requiresAttention = agent.requiresAttention ?? false;
- if (requiresAttention !== filter.requiresAttention) {
- return false;
- }
- }
-
- if (filter.projectKeys && filter.projectKeys.length > 0) {
- const projectKeys = new Set(filter.projectKeys.filter((item) => item.trim().length > 0));
- if (projectKeys.size > 0 && !projectKeys.has(project.projectKey)) {
- return false;
- }
- }
- return true;
- }
-
- private matchesAgentFilter(options: {
- agent: AgentSnapshotPayload;
- project: ProjectPlacementPayload;
- filter?: AgentUpdatesFilter;
- }): boolean {
- const { agent, project, filter } = options;
-
- if (filter?.labels) {
- const matchesLabels = Object.entries(filter.labels).every(
- ([key, value]) => agent.labels[key] === value,
- );
- if (!matchesLabels) {
- return false;
- }
- }
-
- const includeArchived = filter?.includeArchived ?? false;
- if (!includeArchived && agent.archivedAt) {
- return false;
- }
-
- if (filter && !this.agentThinkingOptionMatchesFilter(agent, filter)) {
- return false;
- }
-
- if (filter && !this.matchesAgentStructuralFilter(agent, project, filter)) {
- return false;
- }
-
- return true;
- }
-
- private getAgentUpdateTargetId(update: AgentUpdatePayload): string {
- return update.kind === "remove" ? update.agentId : update.agent.id;
- }
-
- private bufferOrEmitAgentUpdate(
- subscription: AgentUpdatesSubscriptionState,
- payload: AgentUpdatePayload,
- ): void {
- if (payload.kind === "upsert" && !this.isProviderVisibleToClient(payload.agent.provider)) {
- return;
- }
- if (subscription.isBootstrapping) {
- subscription.pendingUpdatesByAgentId.set(this.getAgentUpdateTargetId(payload), payload);
- return;
- }
-
- this.emit({
- type: "agent_update",
- payload,
- });
- }
-
- private async emitStoredAgentUpdate(record: StoredAgentRecord): Promise {
- const payload = this.buildStoredAgentPayload(record);
- const subscription = this.agentUpdatesSubscription;
- if (!subscription) {
- return payload;
- }
-
- const project = payload.workspaceId
- ? await this.buildProjectPlacementForWorkspaceId(payload.workspaceId)
- : null;
- if (!project) {
- this.bufferOrEmitAgentUpdate(subscription, {
- kind: "remove",
- agentId: payload.id,
- });
- return payload;
- }
-
- const matches = this.matchesAgentFilter({
- agent: payload,
- project,
- filter: subscription.filter,
- });
- this.bufferOrEmitAgentUpdate(
- subscription,
- matches
- ? {
- kind: "upsert",
- agent: payload,
- project,
- }
- : {
- kind: "remove",
- agentId: payload.id,
- },
- );
- return payload;
- }
-
- private flushBootstrappedAgentUpdates(options?: {
- snapshotUpdatedAtByAgentId?: Map;
- }): void {
- const subscription = this.agentUpdatesSubscription;
- if (!subscription || !subscription.isBootstrapping) {
- return;
- }
-
- subscription.isBootstrapping = false;
- const pending = Array.from(subscription.pendingUpdatesByAgentId.values());
- subscription.pendingUpdatesByAgentId.clear();
-
- for (const payload of pending) {
- if (payload.kind === "upsert") {
- const snapshotUpdatedAt = options?.snapshotUpdatedAtByAgentId?.get(payload.agent.id);
- if (typeof snapshotUpdatedAt === "number") {
- const updateUpdatedAt = Date.parse(payload.agent.updatedAt);
- if (!Number.isNaN(updateUpdatedAt) && updateUpdatedAt <= snapshotUpdatedAt) {
- continue;
- }
- }
- }
-
- this.emit({
- type: "agent_update",
- payload,
- });
- }
- }
-
- private async findExactWorkspaceByDirectory(
- cwd: string,
- options?: { refreshGit?: boolean },
- ): Promise {
- const normalizedCwd = await this.resolveWorkspaceDirectory(cwd, options);
- const workspaces = await this.workspaceRegistry.list();
- return workspaces.find((workspace) => workspace.cwd === normalizedCwd) ?? null;
- }
-
- private async resolveWorkspaceDirectory(
- cwd: string,
- options?: { refreshGit?: boolean },
- ): Promise {
- const normalizedCwd = resolve(cwd);
- if (options?.refreshGit === false) {
- const snapshot = this.workspaceGitService.peekSnapshot(normalizedCwd);
- return resolve(snapshot?.git.repoRoot ?? normalizedCwd);
- }
-
- const checkout = await this.workspaceGitService.getCheckout(normalizedCwd);
- return resolve(checkout.worktreeRoot ?? normalizedCwd);
- }
-
private async buildProjectPlacementForWorkspace(
workspace: PersistedWorkspaceRecord,
projectRecord?: PersistedProjectRecord | null,
@@ -1524,51 +1339,6 @@ export class Session {
return this.buildProjectPlacementForWorkspace(workspace, project);
}
- private async forwardAgentUpdate(agent: ManagedAgent): Promise {
- try {
- const subscription = this.agentUpdatesSubscription;
- const payload = await this.buildAgentPayload(agent);
- if (subscription) {
- const project = payload.workspaceId
- ? await this.buildProjectPlacementForWorkspaceId(payload.workspaceId)
- : null;
- if (!project) {
- this.bufferOrEmitAgentUpdate(subscription, {
- kind: "remove",
- agentId: payload.id,
- });
- } else {
- const matches = this.matchesAgentFilter({
- agent: payload,
- project,
- filter: subscription.filter,
- });
-
- if (matches) {
- this.bufferOrEmitAgentUpdate(subscription, {
- kind: "upsert",
- agent: payload,
- project,
- });
- } else {
- this.bufferOrEmitAgentUpdate(subscription, {
- kind: "remove",
- agentId: payload.id,
- });
- }
- }
- }
-
- // A lifecycle change updates exactly the agent's owning workspace, never
- // every workspace sharing its cwd. Ownership is the agent's workspaceId.
- if (payload.workspaceId) {
- await this.emitWorkspaceUpdateForWorkspaceId(payload.workspaceId);
- }
- } catch (error) {
- this.sessionLogger.error({ err: error }, "Failed to emit agent update");
- }
- }
-
/**
* Main entry point for processing session messages
*/
@@ -2090,12 +1860,7 @@ export class Session {
},
});
- if (this.agentUpdatesSubscription) {
- this.bufferOrEmitAgentUpdate(this.agentUpdatesSubscription, {
- kind: "remove",
- agentId,
- });
- }
+ this.agentUpdates.removeAgent(agentId);
if (knownWorkspaceId) {
await this.emitWorkspaceUpdateForWorkspaceId(knownWorkspaceId);
@@ -2129,8 +1894,8 @@ export class Session {
agentId,
);
- if (this.agentUpdatesSubscription) {
- const payload = await this.emitStoredAgentUpdate(archivedRecord);
+ if (this.agentUpdates.hasSubscription()) {
+ const payload = await this.agentUpdates.emitStoredRecord(archivedRecord);
if (payload.workspaceId) {
await this.emitWorkspaceUpdateForWorkspaceId(payload.workspaceId);
}
@@ -2147,7 +1912,7 @@ export class Session {
const affectedWorkspaceIds = new Set();
if (!result.live) {
- const payload = await this.emitStoredAgentUpdate(result.record);
+ const payload = await this.agentUpdates.emitStoredRecord(result.record);
if (payload.workspaceId) {
affectedWorkspaceIds.add(payload.workspaceId);
}
@@ -2667,12 +2432,14 @@ export class Session {
const createAgentConfig: AgentSessionConfig = createdWorktree
? { ...config, cwd: createdWorktree.worktree.worktreePath }
: config;
- const workspaceId = await this.resolveOrCreateWorkspaceIdForCreateAgent({
- createdWorktree,
- requestedWorkspaceId: msg.workspaceId,
- cwd: createAgentConfig.cwd,
- initialTitle: workspacePromptTitle,
- });
+ const workspaceId = await this.workspaceProvisioning.resolveOrCreateWorkspaceIdForCreateAgent(
+ {
+ createdWorktree,
+ requestedWorkspaceId: msg.workspaceId,
+ cwd: createAgentConfig.cwd,
+ initialTitle: workspacePromptTitle,
+ },
+ );
const { snapshot, liveSnapshot } = await createAgentCommand(
{
@@ -2706,7 +2473,7 @@ export class Session {
if (!createdWorktree && msg.workspaceId) {
await this.writeInitialWorkspaceTitleIfUntitled(workspaceId, workspacePromptTitle);
}
- await this.forwardAgentUpdate(snapshot);
+ await this.agentUpdates.forwardLiveAgent(snapshot);
if (!createdWorktree && trimmedPrompt) {
await this.scheduleAutoNameLocalWorkspaceTitleForFirstAgent({
workspaceId,
@@ -2803,7 +2570,7 @@ export class Session {
const snapshot = await this.agentManager.resumeAgentFromPersistence(handle, overrides);
await unarchiveAgentState(this.agentStorage, this.agentManager, snapshot.id);
await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
- await this.forwardAgentUpdate(snapshot);
+ await this.agentUpdates.forwardLiveAgent(snapshot);
const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
if (requestId) {
const agentPayload = await this.buildAgentPayload(snapshot);
@@ -2871,7 +2638,9 @@ export class Session {
}
// An imported agent mints its own workspace; ownership is its workspaceId,
// never an existing same-cwd workspace resolved by path.
- const workspace = await this.createWorkspaceForDirectory(normalized.cwd);
+ const workspace = await this.workspaceProvisioning.createWorkspaceForDirectory(
+ normalized.cwd,
+ );
const { snapshot, timelineSize } = await importProviderSession({
request: normalized,
workspaceId: workspace.workspaceId,
@@ -2951,7 +2720,7 @@ export class Session {
);
}
await this.agentManager.hydrateTimelineFromProvider(agentId);
- await this.forwardAgentUpdate(snapshot);
+ await this.agentUpdates.forwardLiveAgent(snapshot);
const timelineSize = this.agentManager.getTimeline(agentId).length;
if (requestId) {
this.emit({
@@ -3079,8 +2848,9 @@ export class Session {
logger: this.sessionLogger,
},
}),
- checkoutExistingBranch: (cwd, branch) => this.checkoutExistingBranch(cwd, branch),
- createBranchFromBase: (params) => this.createBranchFromBase(params),
+ checkoutExistingBranch: (cwd, branch) =>
+ this.gitMutation.checkoutExistingBranch(cwd, branch),
+ createBranchFromBase: (params) => this.gitMutation.createBranchFromBase(params),
github: this.github,
},
config,
@@ -3139,7 +2909,7 @@ export class Session {
branch: result.branchName,
promptTitle: resolveFirstAgentPromptTitle(input.firstAgentContext),
});
- await this.notifyGitMutation(input.workspace.cwd, "rename-branch");
+ await this.gitMutation.notifyGitMutation(input.workspace.cwd, "rename-branch");
await this.emitWorkspaceUpdateForCwd(input.workspace.cwd);
}
@@ -3242,13 +3012,6 @@ export class Session {
);
}
- private assertSafeGitRef(ref: string, label: string): void {
- if (!/^[A-Za-z0-9._/-]+$/.test(ref)) {
- throw new Error(`Invalid ${label}: ${ref}`);
- }
- assertWorktreeSafeGitRef(ref, label);
- }
-
private isPathWithinRoot(rootPath: string, candidatePath: string): boolean {
const resolvedRoot = resolve(rootPath);
const resolvedCandidate = resolve(candidatePath);
@@ -3258,257 +3021,6 @@ export class Session {
return resolvedCandidate.startsWith(resolvedRoot + sep);
}
- private async generateCommitMessage(cwd: string): Promise {
- const diff = await this.workspaceGitService.getCheckoutDiff(cwd, {
- mode: "uncommitted",
- includeStructured: true,
- });
- const schema = z.object({
- message: z
- .string()
- .min(1)
- .max(72)
- .describe("Concise git commit message, imperative mood, no trailing period."),
- });
- const fileList =
- diff.structured && diff.structured.length > 0
- ? [
- "Files changed:",
- ...diff.structured.map((file) => {
- const changeType = diffChangeTypeFor(file);
- const status = file.status && file.status !== "ok" ? ` [${file.status}]` : "";
- return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
- }),
- ].join("\n")
- : "Files changed: (unknown)";
- const maxPatchChars = 120_000;
- const patch =
- diff.diff.length > maxPatchChars
- ? `${diff.diff.slice(0, maxPatchChars)}\n\n... (diff truncated to ${maxPatchChars} chars)\n`
- : diff.diff;
- const prompt = await buildMetadataPrompt({
- cwd,
- workspaceGitService: this.workspaceGitService,
- contract: "Write a concise git commit message for the changes below.",
- styles: [
- {
- configKey: "commitMessage",
- default: "Concise, imperative mood, no trailing period.",
- },
- ],
- after: [
- "Return JSON only with a single field 'message'.",
- "",
- fileList,
- "",
- patch.length > 0 ? patch : "(No diff available)",
- ].join("\n"),
- });
- const providers = await resolveStructuredGenerationProviders({
- cwd,
- providerSnapshotManager: this.providerSnapshotManager,
- daemonConfig: this.readStructuredGenerationDaemonConfig(),
- currentSelection: this.getFocusedAgentSelectionForCwd(cwd),
- });
- try {
- const result = await generateStructuredAgentResponseWithFallback({
- manager: this.agentManager,
- cwd,
- prompt,
- schema,
- schemaName: "CommitMessage",
- maxRetries: 2,
- providers,
- persistSession: false,
- agentConfigOverrides: {
- title: "Commit generator",
- internal: true,
- },
- });
- return result.message;
- } catch (error) {
- if (
- error instanceof StructuredAgentResponseError ||
- error instanceof StructuredAgentFallbackError
- ) {
- return "Update files";
- }
- throw error;
- }
- }
-
- private async generatePullRequestText(
- cwd: string,
- baseRef?: string,
- ): Promise<{
- title: string;
- body: string;
- }> {
- const diff = await this.workspaceGitService.getCheckoutDiff(cwd, {
- mode: "base",
- baseRef,
- includeStructured: true,
- });
- const schema = z.object({
- title: z.string().min(1).max(72),
- body: z.string().min(1),
- });
- const fileList =
- diff.structured && diff.structured.length > 0
- ? [
- "Files changed:",
- ...diff.structured.map((file) => {
- const changeType = diffChangeTypeFor(file);
- const status = file.status && file.status !== "ok" ? ` [${file.status}]` : "";
- return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
- }),
- ].join("\n")
- : "Files changed: (unknown)";
- const maxPatchChars = 200_000;
- const patch =
- diff.diff.length > maxPatchChars
- ? `${diff.diff.slice(0, maxPatchChars)}\n\n... (diff truncated to ${maxPatchChars} chars)\n`
- : diff.diff;
- const prompt = await buildMetadataPrompt({
- cwd,
- workspaceGitService: this.workspaceGitService,
- contract: "Write a pull request title and body for the changes below.",
- styles: [
- {
- configKey: "pullRequest",
- default: "Clear, descriptive title; body explaining what changed and why.",
- },
- ],
- after: [
- "Return JSON only with fields 'title' and 'body'.",
- "",
- fileList,
- "",
- patch.length > 0 ? patch : "(No diff available)",
- ].join("\n"),
- });
- const providers = await resolveStructuredGenerationProviders({
- cwd,
- providerSnapshotManager: this.providerSnapshotManager,
- daemonConfig: this.readStructuredGenerationDaemonConfig(),
- currentSelection: this.getFocusedAgentSelectionForCwd(cwd),
- });
- try {
- return await generateStructuredAgentResponseWithFallback({
- manager: this.agentManager,
- cwd,
- prompt,
- schema,
- schemaName: "PullRequest",
- maxRetries: 2,
- providers,
- persistSession: false,
- agentConfigOverrides: {
- title: "PR generator",
- internal: true,
- },
- });
- } catch (error) {
- if (
- error instanceof StructuredAgentResponseError ||
- error instanceof StructuredAgentFallbackError
- ) {
- return {
- title: "Update changes",
- body: "Automated PR generated by Paseo.",
- };
- }
- throw error;
- }
- }
-
- private async ensureCleanWorkingTree(cwd: string): Promise {
- const dirty = await this.isWorkingTreeDirty(cwd);
- if (dirty) {
- throw new Error(
- "Working directory has uncommitted changes. Commit or stash before switching branches.",
- );
- }
- }
-
- private async isWorkingTreeDirty(cwd: string): Promise {
- try {
- const snapshot = await this.workspaceGitService.getSnapshot(cwd);
- return snapshot.git.isDirty === true;
- } catch (error) {
- throw new Error(`Unable to inspect git status for ${cwd}: ${getErrorMessage(error)}`, {
- cause: error,
- });
- }
- }
-
- private async checkoutExistingBranch(
- cwd: string,
- branch: string,
- ): Promise {
- this.assertSafeGitRef(branch, "branch");
- const resolution = await this.workspaceGitService.validateBranchRef(cwd, branch);
- if (resolution.kind === "not-found") {
- throw new Error(`Branch not found: ${branch}`);
- }
- await this.ensureCleanWorkingTree(cwd);
- const result = await checkoutResolvedBranch({
- cwd,
- resolution,
- });
- await this.notifyGitMutation(cwd, "switch-branch", { invalidateGithub: true });
- return result;
- }
-
- private async createBranchFromBase(params: {
- cwd: string;
- baseBranch: string;
- newBranchName: string;
- }): Promise {
- const { cwd, baseBranch, newBranchName } = params;
- this.assertSafeGitRef(baseBranch, "base branch");
- this.assertSafeGitRef(newBranchName, "new branch");
-
- const baseResolution = await this.workspaceGitService.validateBranchRef(cwd, baseBranch);
- if (baseResolution.kind === "not-found") {
- throw new Error(`Base branch not found: ${baseBranch}`);
- }
-
- const exists = await this.doesLocalBranchExist(cwd, newBranchName);
- if (exists) {
- throw new Error(`Branch already exists: ${newBranchName}`);
- }
-
- await this.ensureCleanWorkingTree(cwd);
- await execCommand("git", ["checkout", "-b", newBranchName, baseBranch], {
- cwd,
- });
- await this.notifyGitMutation(cwd, "create-branch");
- }
-
- private async doesLocalBranchExist(cwd: string, branch: string): Promise {
- this.assertSafeGitRef(branch, "branch");
- return this.workspaceGitService.hasLocalBranch(cwd, branch);
- }
-
- private async notifyGitMutation(
- cwd: string,
- reason: GitMutationRefreshReason,
- options?: { invalidateGithub?: boolean },
- ): Promise {
- if (options?.invalidateGithub) {
- this.github.invalidate({ cwd });
- }
- try {
- await this.workspaceGitService.getSnapshot(cwd, { force: true, reason });
- } catch (error) {
- this.sessionLogger.warn(
- { err: error, cwd, reason },
- "Failed to force-refresh workspace git snapshot after mutation",
- );
- }
- }
-
/**
* Handle clearing agent attention flag
*/
@@ -3749,158 +3261,6 @@ export class Session {
}
}
- private closeWorkspaceGitWatchTarget(target: WorkspaceGitWatchTarget): void {
- if (target.debounceTimer) {
- clearTimeout(target.debounceTimer);
- target.debounceTimer = null;
- }
- for (const watcher of target.watchers) {
- try {
- watcher.close();
- } catch {
- // Ignore watcher close errors
- }
- }
- target.watchers.length = 0;
- }
-
- private async removeWorkspaceGitWatchTarget(cwd: string): Promise {
- const normalizedCwd = resolve(cwd);
- const target = this.workspaceGitWatchTargets.get(normalizedCwd);
- if (target) {
- this.closeWorkspaceGitWatchTarget(target);
- this.workspaceGitWatchTargets.delete(normalizedCwd);
- }
- }
-
- private removeWorkspaceGitSubscription(cwd: string): void {
- const normalizedCwd = resolve(cwd);
- const target = this.workspaceGitWatchTargets.get(normalizedCwd);
- if (target) {
- const unsubscribeFetch = this.workspaceGitFetchSubscriptions.get(normalizedCwd);
- unsubscribeFetch?.();
- this.workspaceGitFetchSubscriptions.delete(normalizedCwd);
- this.closeWorkspaceGitWatchTarget(target);
- this.workspaceGitWatchTargets.delete(normalizedCwd);
- }
- this.workspaceGitSubscriptions.get(normalizedCwd)?.();
- this.workspaceGitSubscriptions.delete(normalizedCwd);
- }
-
- private workspaceGitDescriptorStateKey(workspace: WorkspaceDescriptorPayload | null): string {
- if (!workspace) {
- return WORKSPACE_GIT_WATCH_REMOVED_STATE_KEY;
- }
- return JSON.stringify([
- workspace.name,
- workspace.diffStat ? [workspace.diffStat.additions, workspace.diffStat.deletions] : null,
- ]);
- }
-
- private resolveWorkspaceGitWatchTarget(workspaceId: string): WorkspaceGitWatchTarget | null {
- for (const target of this.workspaceGitWatchTargets.values()) {
- if (target.workspaceId === workspaceId) {
- return target;
- }
- }
- return null;
- }
-
- private shouldSkipWorkspaceGitWatchUpdate(
- workspaceId: string,
- workspace: WorkspaceDescriptorPayload | null,
- ): boolean {
- const target = this.resolveWorkspaceGitWatchTarget(workspaceId);
- if (!target) {
- return false;
- }
- const nextStateKey = this.workspaceGitDescriptorStateKey(workspace);
- if (target.latestDescriptorStateKey === nextStateKey) {
- return true;
- }
- target.latestDescriptorStateKey = nextStateKey;
- return false;
- }
-
- private rememberWorkspaceGitDescriptorState(
- workspaceId: string,
- workspace: WorkspaceDescriptorPayload | null,
- ): void {
- const target = this.resolveWorkspaceGitWatchTarget(workspaceId);
- if (!target) {
- return;
- }
- target.latestDescriptorStateKey = this.workspaceGitDescriptorStateKey(workspace);
- target.lastBranchName = workspace?.name ?? null;
- }
-
- private handleWorkspaceGitBranchSnapshot(cwd: string, branchName: string | null): void {
- const target = this.workspaceGitWatchTargets.get(resolve(cwd));
- if (!target) {
- return;
- }
-
- const previousBranchName = target.lastBranchName;
- if (branchName === previousBranchName) {
- return;
- }
-
- target.lastBranchName = branchName;
- this.onBranchChanged?.(target.workspaceId, previousBranchName, branchName);
- }
-
- private syncWorkspaceGitObservers(workspaces: Iterable): void {
- for (const workspace of workspaces) {
- this.syncWorkspaceGitObserver(workspace.workspaceDirectory, {
- isGit: workspace.projectKind === "git",
- workspaceId: workspace.id,
- });
- this.rememberWorkspaceGitDescriptorState(workspace.workspaceDirectory, workspace);
- }
- }
-
- private syncWorkspaceGitObserver(
- cwd: string,
- options: { isGit: boolean; workspaceId: string },
- ): void {
- const normalizedCwd = resolve(cwd);
- if (!options.isGit) {
- this.removeWorkspaceGitSubscription(normalizedCwd);
- return;
- }
-
- if (this.workspaceGitSubscriptions.has(normalizedCwd)) {
- return;
- }
-
- const target: WorkspaceGitWatchTarget = {
- cwd: normalizedCwd,
- workspaceId: options.workspaceId,
- watchers: [],
- debounceTimer: null,
- refreshPromise: null,
- refreshQueued: false,
- latestDescriptorStateKey: null,
- lastBranchName: null,
- };
- this.workspaceGitWatchTargets.set(normalizedCwd, target);
-
- const subscription = this.workspaceGitService.registerWorkspace(
- { cwd: normalizedCwd },
- (snapshot) => {
- this.handleWorkspaceGitBranchSnapshot(normalizedCwd, snapshot.git.currentBranch ?? null);
- void this.emitWorkspaceUpdateForCwd(normalizedCwd).catch((error) => {
- this.sessionLogger.warn(
- { err: error, cwd: normalizedCwd },
- "Failed to emit workspace update after git branch snapshot",
- );
- });
- this.checkoutSession.emitStatusUpdate(normalizedCwd, snapshot);
- },
- );
- this.workspaceGitSubscriptions.set(normalizedCwd, subscription.unsubscribe);
- }
-
private async handlePaseoWorktreeListRequest(
msg: Extract,
): Promise {
@@ -4170,7 +3530,7 @@ export class Session {
continue;
}
if (
- !this.matchesAgentFilter({
+ !matchesAgentUpdatesFilter({
agent: entry.agent,
project: entry.project,
filter,
@@ -4342,20 +3702,7 @@ export class Session {
statusEnteredAt: null,
activityAt: null,
diffStat,
- scripts:
- this.serviceProxy && this.scriptRuntimeStore
- ? buildWorkspaceScriptPayloads({
- workspaceId: workspace.workspaceId,
- workspaceDirectory: workspace.cwd,
- paseoConfig: readPaseoConfigForProjection(workspace.cwd, this.sessionLogger),
- serviceProxy: this.serviceProxy,
- runtimeStore: this.scriptRuntimeStore,
- daemonPort: this.getDaemonTcpPort?.() ?? null,
- serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
- gitMetadata: this.resolveWorkspaceScriptGitMetadata(workspace.cwd),
- resolveHealth: this.resolveScriptHealth ?? undefined,
- })
- : [],
+ scripts: this.buildWorkspaceScriptPayloadSnapshot(workspace.workspaceId, workspace.cwd),
...(resolvedProjectRecord
? {
project: await this.buildProjectPlacementForWorkspace(workspace, resolvedProjectRecord),
@@ -4575,108 +3922,6 @@ export class Session {
}
}
- private async findOrCreateWorkspaceForDirectory(cwd: string): Promise {
- const inputCwd = resolve(cwd);
- const normalizedCwd = await this.resolveWorkspaceDirectory(cwd);
- const existingWorkspace = await this.findExactWorkspaceByDirectory(normalizedCwd, {
- refreshGit: false,
- });
- if (existingWorkspace) {
- if (existingWorkspace.archivedAt && inputCwd !== normalizedCwd) {
- const timestamp = new Date().toISOString();
- const checkout = checkoutLiteFromGitSnapshot(inputCwd, {
- isGit: false,
- currentBranch: null,
- remoteUrl: null,
- repoRoot: null,
- isPaseoOwnedWorktree: false,
- mainRepoRoot: null,
- });
- const membership = classifyDirectoryForProjectMembership({ cwd: inputCwd, checkout });
- const projectRecord = await this.resolveProjectRecordForPlacement({
- membership,
- timestamp,
- });
- await this.projectRegistry.upsert(projectRecord);
- const workspaceRecord = createPersistedWorkspaceRecord({
- workspaceId: generateWorkspaceId(),
- projectId: projectRecord.projectId,
- cwd: inputCwd,
- kind: membership.workspaceKind,
- displayName: membership.workspaceDisplayName,
- createdAt: timestamp,
- updatedAt: timestamp,
- });
- await this.workspaceRegistry.upsert(workspaceRecord);
- return workspaceRecord;
- }
- return this.reclassifyOrUnarchiveWorkspaceForDirectory({
- workspace: existingWorkspace,
- project: await this.projectRegistry.get(existingWorkspace.projectId),
- cwd: normalizedCwd,
- });
- }
-
- return this.createWorkspaceForDirectory(normalizedCwd);
- }
-
- private async resolveOrCreateWorkspaceIdForCreateAgent(input: {
- createdWorktree: CreatePaseoWorktreeWorkflowResult | null;
- requestedWorkspaceId?: string;
- cwd: string;
- initialTitle: string | null;
- }): Promise {
- if (input.createdWorktree) {
- return input.createdWorktree.workspace.workspaceId;
- }
-
- if (input.requestedWorkspaceId) {
- return input.requestedWorkspaceId;
- }
-
- return (await this.createWorkspaceForDirectory(input.cwd, input.initialTitle)).workspaceId;
- }
-
- private async createWorkspaceForDirectory(
- cwd: string,
- title?: string | null,
- ): Promise {
- const checkout = await this.workspaceGitService.getCheckout(cwd);
- const membership = classifyDirectoryForProjectMembership({ cwd, checkout });
- const timestamp = new Date().toISOString();
-
- const projectRecord = await this.resolveProjectRecordForPlacement({
- membership,
- timestamp,
- });
- await this.projectRegistry.upsert(projectRecord);
-
- const workspaceRecord = createPersistedWorkspaceRecord({
- workspaceId: generateWorkspaceId(),
- projectId: projectRecord.projectId,
- cwd,
- kind: membership.workspaceKind,
- displayName: membership.workspaceDisplayName,
- title: title ?? null,
- createdAt: timestamp,
- updatedAt: timestamp,
- });
- await this.workspaceRegistry.upsert(workspaceRecord);
- return workspaceRecord;
- }
-
- private async findOrCreateProjectForDirectory(cwd: string): Promise {
- const normalizedCwd = resolve(cwd);
- const checkout = await this.workspaceGitService.getCheckout(normalizedCwd);
- const membership = classifyDirectoryForProjectMembership({ cwd: normalizedCwd, checkout });
- const projectRecord = await this.resolveProjectRecordForPlacement({
- membership,
- timestamp: new Date().toISOString(),
- });
- await this.projectRegistry.upsert(projectRecord);
- return projectRecord;
- }
-
private buildProjectDescriptor(
project: PersistedProjectRecord,
): WorkspaceProjectDescriptorPayload {
@@ -4689,81 +3934,6 @@ export class Session {
};
}
- private async reclassifyOrUnarchiveWorkspaceForDirectory(input: {
- workspace: PersistedWorkspaceRecord;
- project: PersistedProjectRecord | null;
- cwd: string;
- }): Promise {
- const checkout = await this.workspaceGitService.getCheckout(input.cwd);
- const membership = classifyDirectoryForProjectMembership({ cwd: input.cwd, checkout });
- const timestamp = new Date().toISOString();
- const projectRecord = await this.resolveProjectRecordForPlacement({
- membership,
- timestamp,
- });
- const projectId = projectRecord.projectId;
- const kind = membership.workspaceKind;
- const displayName = membership.workspaceDisplayName;
-
- if (
- input.workspace.projectId === projectId &&
- input.workspace.kind === kind &&
- input.workspace.displayName === displayName
- ) {
- if (!input.project) {
- await this.projectRegistry.upsert(projectRecord);
- }
- return this.ensureWorkspaceRecordUnarchived(input.workspace);
- }
-
- await this.projectRegistry.upsert(projectRecord);
-
- const nextWorkspace = {
- ...input.workspace,
- workspaceId: input.workspace.workspaceId,
- projectId,
- cwd: input.cwd,
- kind,
- displayName,
- archivedAt: null,
- updatedAt: timestamp,
- };
- await this.workspaceRegistry.upsert(nextWorkspace);
- return nextWorkspace;
- }
-
- private async resolveProjectRecordForPlacement(input: {
- membership: ReturnType;
- timestamp: string;
- }): Promise {
- const rootPath = input.membership.projectRootPath;
- const kind = input.membership.projectKind;
- const projects = await this.projectRegistry.list();
- const existingProject =
- projects.find((project) => !project.archivedAt && project.rootPath === rootPath) ??
- projects.find((project) => project.rootPath === rootPath) ??
- null;
-
- if (!existingProject) {
- return createPersistedProjectRecord({
- projectId: input.membership.projectKey,
- rootPath,
- kind,
- displayName: input.membership.projectName,
- createdAt: input.timestamp,
- updatedAt: input.timestamp,
- });
- }
-
- return {
- ...existingProject,
- rootPath,
- kind,
- archivedAt: null,
- updatedAt: input.timestamp,
- };
- }
-
private async unarchiveOwningWorkspaceForAgent(agentId: string): Promise {
const record = await this.agentStorage.get(agentId);
if (!record?.workspaceId) {
@@ -4785,7 +3955,7 @@ export class Session {
await this.recreateOwningWorktreeForRestore(workspace, workspace.branch);
}
- await this.ensureWorkspaceRecordUnarchived(workspace);
+ await this.workspaceProvisioning.ensureWorkspaceRecordUnarchived(workspace);
await this.emitWorkspaceUpdatesForWorkspaceIds([workspace.workspaceId]);
}
@@ -4844,30 +4014,6 @@ export class Session {
}
}
- private async ensureWorkspaceRecordUnarchived(
- workspace: PersistedWorkspaceRecord,
- ): Promise {
- const project = await this.projectRegistry.get(workspace.projectId);
- if (!workspace.archivedAt && (!project || !project.archivedAt)) {
- return workspace;
- }
-
- const timestamp = new Date().toISOString();
- let unarchivedWorkspace = workspace;
- if (workspace.archivedAt) {
- unarchivedWorkspace = { ...workspace, archivedAt: null, updatedAt: timestamp };
- await this.workspaceRegistry.upsert(unarchivedWorkspace);
- }
- if (project?.archivedAt) {
- await this.projectRegistry.upsert({
- ...project,
- archivedAt: null,
- updatedAt: timestamp,
- });
- }
- return unarchivedWorkspace;
- }
-
private async createPaseoWorktree(
input: CreatePaseoWorktreeInput,
options?: {
@@ -4884,8 +4030,8 @@ export class Session {
workspaceGitService: this.workspaceGitService,
});
void Promise.all([
- this.notifyGitMutation(input.cwd, "create-worktree"),
- this.notifyGitMutation(result.worktree.worktreePath, "create-worktree"),
+ this.gitMutation.notifyGitMutation(input.cwd, "create-worktree"),
+ this.gitMutation.notifyGitMutation(result.worktree.worktreePath, "create-worktree"),
]).catch((error) => {
this.sessionLogger.warn(
{ err: error, cwd: input.cwd, worktreePath: result.worktree.worktreePath },
@@ -4914,10 +4060,7 @@ export class Session {
workspaceRegistry: this.workspaceRegistry,
});
if (!existingWorkspace) {
- const watchTarget = this.resolveWorkspaceGitWatchTarget(workspaceId);
- if (watchTarget) {
- this.removeWorkspaceGitSubscription(watchTarget.cwd);
- }
+ this.workspaceGitObserver.removeForWorkspaceId(workspaceId);
return;
}
@@ -4950,9 +4093,8 @@ export class Session {
workspaceId: string;
cwd: string;
}): Promise {
- await this.removeWorkspaceGitWatchTarget(input.cwd);
+ this.workspaceGitObserver.removeForCwd(input.cwd);
this.scriptRuntimeStore?.removeForWorkspace(input.workspaceId);
- this.removeWorkspaceGitSubscription(input.cwd);
}
private async reconcileAndEmitWorkspaceUpdates(): Promise {
@@ -5042,11 +4184,11 @@ export class Session {
: null;
if (
options?.dedupeGitState &&
- this.shouldSkipWorkspaceGitWatchUpdate(workspaceId, nextWorkspace)
+ this.workspaceGitObserver.shouldSkipUpdate(workspaceId, nextWorkspace)
) {
continue;
}
- this.recordWorkspaceGitDescriptorState(workspaceId, nextWorkspace);
+ this.workspaceGitObserver.recordDescriptorState(workspaceId, nextWorkspace);
if (!nextWorkspace) {
subscription.lastEmittedByWorkspaceId.delete(workspaceId);
@@ -5079,20 +4221,6 @@ export class Session {
}
}
- private recordWorkspaceGitDescriptorState(
- workspaceId: string,
- nextWorkspace: WorkspaceDescriptorPayload | null,
- ): void {
- const watchTarget = this.resolveWorkspaceGitWatchTarget(workspaceId);
- if (watchTarget && this.onBranchChanged) {
- const newBranchName = nextWorkspace?.name ?? null;
- if (newBranchName !== watchTarget.lastBranchName) {
- this.onBranchChanged(workspaceId, watchTarget.lastBranchName, newBranchName);
- }
- }
- this.rememberWorkspaceGitDescriptorState(workspaceId, nextWorkspace);
- }
-
private async buildWorkspaceRemoveUpdatePayload(
workspaceId: string,
removedProjectId?: string,
@@ -5163,12 +4291,10 @@ export class Session {
try {
if (subscriptionId) {
- this.agentUpdatesSubscription = {
+ this.agentUpdates.beginSubscription({
subscriptionId,
filter: request.filter,
- isBootstrapping: true,
- pendingUpdatesByAgentId: new Map(),
- };
+ });
}
const payload = await this.listFetchAgentsEntries(request);
@@ -5189,12 +4315,12 @@ export class Session {
},
});
- if (subscriptionId && this.agentUpdatesSubscription?.subscriptionId === subscriptionId) {
- this.flushBootstrappedAgentUpdates({ snapshotUpdatedAtByAgentId });
+ if (subscriptionId) {
+ this.agentUpdates.flushBootstrapped(subscriptionId, { snapshotUpdatedAtByAgentId });
}
} catch (error) {
- if (subscriptionId && this.agentUpdatesSubscription?.subscriptionId === subscriptionId) {
- this.agentUpdatesSubscription = null;
+ if (subscriptionId) {
+ this.agentUpdates.clearSubscription(subscriptionId);
}
const code = error instanceof SessionRequestError ? error.code : "fetch_agents_failed";
const message = error instanceof Error ? error.message : "Failed to fetch agents";
@@ -5310,7 +4436,7 @@ export class Session {
}
const payload = await this.listFetchWorkspacesEntries(request);
- this.syncWorkspaceGitObservers(payload.entries);
+ this.workspaceGitObserver.syncObservers(payload.entries);
this.sessionLogger.debug(
{
requestId: request.requestId,
@@ -5617,7 +4743,7 @@ export class Session {
for (const workspaceRecord of await this.workspaceRegistry.list()) {
workspacesBefore.set(workspaceRecord.workspaceId, workspaceRecord);
}
- const workspace = await this.findOrCreateWorkspaceForDirectory(cwd);
+ const workspace = await this.workspaceProvisioning.findOrCreateWorkspaceForDirectory(cwd);
const project = await this.projectRegistry.get(workspace.projectId);
await this.syncWorkspaceGitObserverForWorkspace(workspace);
const descriptor = await this.describeWorkspaceRecord(workspace);
@@ -5702,7 +4828,7 @@ export class Session {
for (const project of await this.projectRegistry.list()) {
projectsBefore.set(project.projectId, project);
}
- const project = await this.findOrCreateProjectForDirectory(cwd);
+ const project = await this.workspaceProvisioning.findOrCreateProjectForDirectory(cwd);
this.sessionLogger.info(
{
requestedCwd,
@@ -5737,116 +4863,17 @@ export class Session {
}
}
+ // Named accessor: the workspace descriptor builder and the git-watch test both read a workspace's
+ // scripts snapshot through here; the workspace-scripts module owns the payload assembly.
private buildWorkspaceScriptPayloadSnapshot(
workspaceId: string,
workspaceDirectory: string,
): WorkspaceDescriptorPayload["scripts"] {
- if (!this.serviceProxy || !this.scriptRuntimeStore) {
- return [];
- }
- return buildWorkspaceScriptPayloads({
- workspaceId,
- workspaceDirectory,
- paseoConfig: readPaseoConfigForProjection(workspaceDirectory, this.sessionLogger),
- serviceProxy: this.serviceProxy,
- runtimeStore: this.scriptRuntimeStore,
- daemonPort: this.getDaemonTcpPort?.() ?? null,
- serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
- gitMetadata: this.resolveWorkspaceScriptGitMetadata(workspaceDirectory),
- resolveHealth: this.resolveScriptHealth ?? undefined,
- });
+ return this.workspaceScripts.buildSnapshot(workspaceId, workspaceDirectory);
}
- private resolveWorkspaceScriptGitMetadata(
- workspaceDirectory: string,
- ): { projectSlug: string; currentBranch: string | null } | undefined {
- const snapshot = this.workspaceGitService.peekSnapshot(workspaceDirectory);
- if (!snapshot) {
- return undefined;
- }
- return {
- projectSlug: deriveProjectSlug(
- workspaceDirectory,
- snapshot.git.isGit ? snapshot.git.remoteUrl : null,
- ),
- currentBranch: snapshot.git.currentBranch,
- };
- }
-
- private emitWorkspaceScriptStatusUpdate(workspaceId: string, workspaceDirectory: string): void {
- this.emit({
- type: "script_status_update",
- payload: {
- workspaceId,
- scripts: this.buildWorkspaceScriptPayloadSnapshot(workspaceId, workspaceDirectory),
- },
- });
- }
-
- private async handleStartWorkspaceScriptRequest(
- request: StartWorkspaceScriptRequest,
- ): Promise {
- try {
- if (!this.terminalManager || !this.serviceProxy || !this.scriptRuntimeStore) {
- throw new Error("Workspace scripts are not available on this daemon");
- }
-
- const workspace = await this.workspaceRegistry.get(request.workspaceId);
- if (!workspace) {
- throw new Error(`Workspace not found: ${request.workspaceId}`);
- }
- const gitMetadata = await this.workspaceGitService.getWorkspaceGitMetadata(workspace.cwd);
-
- const serviceResult = await spawnWorkspaceScript({
- repoRoot: workspace.cwd,
- workspaceId: workspace.workspaceId,
- projectSlug: gitMetadata.projectSlug,
- branchName: gitMetadata.currentBranch,
- scriptName: request.scriptName,
- daemonPort: this.getDaemonTcpPort?.() ?? null,
- daemonListenHost: this.getDaemonTcpHost?.() ?? null,
- serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
- serviceProxy: this.serviceProxy,
- runtimeStore: this.scriptRuntimeStore,
- terminalManager: this.terminalManager,
- logger: this.sessionLogger,
- onLifecycleChanged: () => {
- this.emitWorkspaceScriptStatusUpdate(workspace.workspaceId, workspace.cwd);
- },
- });
-
- this.emitWorkspaceScriptStatusUpdate(workspace.workspaceId, workspace.cwd);
- this.emit({
- type: "start_workspace_script_response",
- payload: {
- requestId: request.requestId,
- workspaceId: request.workspaceId,
- scriptName: request.scriptName,
- terminalId: serviceResult.terminalId,
- error: null,
- },
- });
- } catch (error) {
- const message = error instanceof Error ? error.message : "Failed to start workspace script";
- this.sessionLogger.error(
- {
- err: error,
- workspaceId: request.workspaceId,
- scriptName: request.scriptName,
- },
- "Failed to start workspace script",
- );
- this.emit({
- type: "start_workspace_script_response",
- payload: {
- requestId: request.requestId,
- workspaceId: request.workspaceId,
- scriptName: request.scriptName,
- terminalId: null,
- error: message,
- },
- });
- }
+ private handleStartWorkspaceScriptRequest(request: StartWorkspaceScriptRequest): Promise {
+ return this.workspaceScripts.start(request);
}
// COMPAT(desktopEditorBridge): added in v0.1.88, remove after 2026-12-03 once old clients no longer call daemon editor RPCs.
@@ -5922,7 +4949,7 @@ export class Session {
getDaemonTcpHost: this.getDaemonTcpHost,
serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl,
onScriptsChanged: (workspaceId, workspaceDirectory) => {
- this.emitWorkspaceScriptStatusUpdate(workspaceId, workspaceDirectory);
+ this.workspaceScripts.emitStatusUpdate(workspaceId, workspaceDirectory);
},
},
input,
@@ -6631,6 +5658,7 @@ export class Session {
this.unsubscribeAgentEvents();
this.unsubscribeAgentEvents = null;
}
+ this.agentUpdates.dispose();
if (this.unsubscribeTerminalWorkspaceContributionEvents) {
this.unsubscribeTerminalWorkspaceContributionEvents();
this.unsubscribeTerminalWorkspaceContributionEvents = null;
@@ -6654,9 +5682,6 @@ export class Session {
this.checkoutSession.cleanup();
- for (const unsubscribe of this.workspaceGitSubscriptions.values()) {
- unsubscribe();
- }
- this.workspaceGitSubscriptions.clear();
+ this.workspaceGitObserver.dispose();
}
}
diff --git a/packages/server/src/server/session.workspace-git-watch.test.ts b/packages/server/src/server/session.workspace-git-watch.test.ts
index 1690a42b1..38a590476 100644
--- a/packages/server/src/server/session.workspace-git-watch.test.ts
+++ b/packages/server/src/server/session.workspace-git-watch.test.ts
@@ -13,12 +13,13 @@ import type {
WorkspaceGitRuntimeSnapshot,
WorkspaceGitService,
} from "./workspace-git-service.js";
-import type { SessionOutboundMessage } from "./messages.js";
+import type { SessionOutboundMessage, WorkspaceDescriptorPayload } from "./messages.js";
import {
createPersistedProjectRecord,
createPersistedWorkspaceRecord,
} from "./workspace-registry.js";
import { createNoopWorkspaceGitService } from "./test-utils/workspace-git-service-stub.js";
+import type { WorkspaceGitObserverService } from "./session/workspace-git-observer/workspace-git-observer-service.js";
interface SessionInternals {
workspaceUpdatesSubscription: {
@@ -29,10 +30,23 @@ interface SessionInternals {
lastEmittedByWorkspaceId: Map;
};
buildWorkspaceDescriptorMap: () => Promise