diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
index 9cbf4b8f..36e8fe2c 100644
--- a/.github/workflows/dependency-review.yml
+++ b/.github/workflows/dependency-review.yml
@@ -3,10 +3,10 @@ name: Dependency Review
on:
pull_request:
paths:
- - "**/package.json"
- - "**/package-lock.json"
- - "**/pnpm-lock.yaml"
- - "pnpm-workspace.yaml"
+ - '**/package.json'
+ - '**/package-lock.json'
+ - '**/pnpm-lock.yaml'
+ - 'pnpm-workspace.yaml'
permissions:
contents: read
@@ -24,5 +24,7 @@ jobs:
with:
fail-on-severity: high
fail-on-scopes: runtime, development, unknown
+ # Vitest is used via `vitest run`, not Vitest UI/API/browser mode.
+ allow-ghsas: GHSA-5xrq-8626-4rwp
license-check: false
show-patched-versions: true
diff --git a/docs/articles/agent-teams-opus-4-8.en.md b/docs/articles/agent-teams-opus-4-8.en.md
new file mode 100644
index 00000000..b4942a79
--- /dev/null
+++ b/docs/articles/agent-teams-opus-4-8.en.md
@@ -0,0 +1,46 @@
+# Agent Teams with Claude Opus 4.8
+
+Claude Opus 4.8 made a single agent noticeably stronger. But the real productivity jump comes when this model powers **a whole team of agents** that talk to each other, coordinate, and autonomously carry a task through to the result.
+
+Here's what that looks like in practice.
+
+## A team of agents instead of just one
+
+You assemble a team of several Opus 4.8 agents and assign roles: lead, backend, frontend, reviewer — whatever fits your task. From there they work in parallel, each in their own area.
+
+If you're not ready for a full team yet, there's a solo mode with a single agent that manages its own task list. You can grow it into a full team later.
+
+## They talk and coordinate
+
+This isn't a bunch of independent chats. The agents:
+- message each other inside the team
+- hand off results and ask each other for clarification
+- code-review each other's work
+- create and close tasks on a shared kanban board on their own
+
+And here's something you usually don't see anywhere else: **you can run multiple teams at once, and they coordinate between themselves**. Spin up parallel teams for different tracks (say, backend and frontend, or two features at once) — their leads will talk to each other, sync progress, and pass results down the chain.
+
+You set the goal at a high level — breakdown, distribution, and execution happen without you.
+
+## Everything is visible in the UI
+
+In real time:
+- **Kanban** with tasks moving across statuses
+- **Diff viewer** for every task: accept / reject / comment
+- **Agent-to-agent chat** plus direct messages with any of them
+- **Detailed logs** for every agent — what it did, which commands it ran, which decisions it made
+- **Per-task view**: open a card on the kanban and see everything tied to it — code changes, agent conversations, comments, logs. No confusion about what belongs where
+- **Active agent sessions** with open links
+- **Notifications** when the team is done or needs your input
+
+## Not just Claude
+
+Beyond Opus 4.8, you can plug **Codex** and **OpenCode** agents into the same team _(200+ models, 70+ LLM providers)_. Different runtimes coexist within a single team — pick the strengths of each where they fit best, without locking yourself into one vendor.
+
+## Under the hood
+
+Local, no cloud, free, open source. It works through the Claude / Codex / OpenCode CLIs you already have installed — no separate app-level API keys required.
+
+---
+
+> Screenshots and video below.
diff --git a/docs/articles/agent-teams-opus-4-8.ru.md b/docs/articles/agent-teams-opus-4-8.ru.md
new file mode 100644
index 00000000..bf84b7b1
--- /dev/null
+++ b/docs/articles/agent-teams-opus-4-8.ru.md
@@ -0,0 +1,46 @@
+# Agent Teams with Claude Opus 4.8
+
+С выходом Claude Opus 4.8 одиночный агент стал заметно сильнее. Но настоящий скачок продуктивности — когда на этой модели работает **целая команда агентов**, которые общаются между собой, координируются и автономно доводят задачу до результата.
+
+Ниже — как это выглядит на практике.
+
+## Команда агентов вместо одного
+
+Вы собираете команду из нескольких Opus 4.8-агентов и распределяете роли: лид, бэкенд, фронтенд, ревьюер — что угодно под вашу задачу. Дальше они работают параллельно, каждый в своей зоне.
+
+Если не хочется сразу команду — есть solo-режим с одним агентом, который сам ведёт свои задачи. Дальше его можно развернуть в полноценную команду.
+
+## Они общаются и координируются
+
+Это не несколько независимых чатов. Агенты:
+- пишут друг другу внутри команды
+- передают результаты, спрашивают уточнения
+- делают код-ревью работы коллег
+- сами создают и закрывают задачи на общем канбане
+
+И ещё одна штука, которой обычно нет нигде: **команд может быть несколько, и они координируются между собой**. Можно поднять параллельно несколько команд под разные направления (например, бэкенд и фронтенд, или две фичи сразу) — и их лиды будут общаться друг с другом, синхронизировать работу, передавать результаты по цепочке.
+
+Вы ставите цель на высоком уровне — декомпозиция, распределение и исполнение происходят без вашего участия.
+
+## Всё видно в UI
+
+В реальном времени:
+- **Канбан** с движением задач по статусам
+- **Diff-вьювер** на каждую задачу: accept / reject / комментарий
+- **Переписка** агентов между собой и личные DM с любым из них
+- **Подробные логи** каждого агента — что он делал, какие команды запускал, какие решения принимал
+- **Срез по задаче**: открываете карточку на канбане и видите всё, что к ней относится — изменения в коде, переписку агентов, комментарии, логи. Никакой путаницы, что к чему привязано
+- **Активные сессии агентов** с открытыми ссылками
+- **Уведомления**, когда команда закончила или ей нужно ваше решение
+
+## Не только Claude
+
+Помимо Opus 4.8, в команду можно подключать агентов на **Codex** и **OpenCode** _(200+ моделей, 70+ LLM-провайдеров)_. В одной команде спокойно уживаются разные рантаймы — берёте сильные стороны каждого там, где они уместны, без привязки к одному вендору.
+
+## Под капотом
+
+Локально, без облака, бесплатно, open source. Работает через уже установленные у вас CLI Claude / Codex / OpenCode — отдельные API-ключи на уровне приложения не нужны.
+
+---
+
+> Скриншоты и видео — ниже.
diff --git a/docs/team-management/opencode-native-semantic-messaging-plan.md b/docs/team-management/opencode-native-semantic-messaging-plan.md
index 59b4348e..935b1b1f 100644
--- a/docs/team-management/opencode-native-semantic-messaging-plan.md
+++ b/docs/team-management/opencode-native-semantic-messaging-plan.md
@@ -1827,9 +1827,9 @@ OpenCode lead rule:
- Mixed team with native Codex/Claude/Gemini lead: keep the existing `relayLeadInboxMessages()` path.
- OpenCode teammate or secondary lane: use `relayOpenCodeMemberInboxMessages()`.
-- Pure OpenCode lead inbox in v1: do not mark messages read and do not report delivery success unless a real stored OpenCode `team-lead` session exists. Return a diagnostic like `opencode_lead_runtime_session_missing`.
+- Pure OpenCode lead inbox: launch and store a real OpenCode `team-lead` runtime session, then relay through `relayOpenCodeMemberInboxMessages()`. Do not mark messages read unless that delivery is accepted.
- Do not fake lead delivery by sending to a random teammate session. That would make messages appear delivered while the actual recipient never saw them.
-- A future explicit OpenCode lead lane can reuse this selector by teaching the bridge to create/store a `team-lead` session and by passing `agent: "team-lead"` where the bridge supports it. That is not part of this v1 seam.
+- If the stored `team-lead` session is missing, keep the row retryable instead of falling back to another teammate.
FileWatcher change:
@@ -2967,13 +2967,12 @@ it('routes native lead inbox relay through the legacy stdin path', async () => {
```
```ts
-it('does not silently consume pure OpenCode lead inbox when no lead session exists', async () => {
- // Configure a pure OpenCode runtime-adapter team where isTeamAlive() is true via runtimeAdapterRunByTeam.
- // Ensure there is no stored OpenCode session record for the canonical lead name.
+it('relays pure OpenCode lead inbox through the stored lead session', async () => {
+ // Configure a pure OpenCode runtime-adapter team with a stored team-lead session.
// Seed inboxes/.json with one unread message.
// Call relayInboxFileToLiveRecipient(teamName, leadName).
- // Assert diagnostics include opencode_lead_runtime_session_missing.
- // Assert the inbox row remains unread and no teammate session received the prompt.
+ // Assert the relay kind is opencode_member and the prompt targets team-lead.
+ // Assert the inbox row is marked read only after accepted runtime delivery.
});
```
@@ -3464,8 +3463,8 @@ Avoid heavy E2E until targeted tests pass.
- UI direct sends to live OpenCode teammates either confirm runtime delivery or show a visible warning; there is no log-only post-send delivery failure.
- Persisted inbox messages addressed to OpenCode teammates are live-relayed to their runtime lanes, while native teammates keep file-watch behavior and lead keeps lead relay behavior.
- OpenCode inbox relay is direct-to-runtime and does not reuse native `relayMemberInboxMessages()` / `SendMessage` forwarding.
-- Pure OpenCode lead inbox delivery is not silently consumed: without a real OpenCode lead session, rows remain unread and diagnostics say `opencode_lead_runtime_session_missing` or equivalent.
+- Pure OpenCode lead inbox delivery uses the stored `team-lead` runtime session and does not silently fall back to another teammate.
- Renderer send-message actions return `SendMessageResult` on success and reject on real send failure, so pending-reply cleanup is not dependent on dead `.catch()` paths.
- `message_send` cannot create `from: "user", to: "user"` rows; user-directed MCP replies require a configured teammate sender.
- OpenCode replies appear in Messages UI without frontend fake state.
-- Tests cover native default, OpenCode override, assignment protocol, tool alias canonicalization, tool proof, taskRefs persistence, user-directed sender guard, local recipient canonicalization, direct-message runtime delivery result visibility, OpenCode reply feed projection, OpenCode-targeted inbox relay/dedupe, unsupported OpenCode lead diagnostics, launch identity injection, lane-scoped manifest activeRunId recovery, and runtime delivery team-change event shape.
+- Tests cover native default, OpenCode override, assignment protocol, tool alias canonicalization, tool proof, taskRefs persistence, user-directed sender guard, local recipient canonicalization, direct-message runtime delivery result visibility, OpenCode reply feed projection, OpenCode-targeted inbox relay/dedupe, pure OpenCode lead relay, launch identity injection, lane-scoped manifest activeRunId recovery, and runtime delivery team-change event shape.
diff --git a/landing/components/hero/CyberHeroFeatureStrip.vue b/landing/components/hero/CyberHeroFeatureStrip.vue
index 2e7eab41..7f1dc4a4 100644
--- a/landing/components/hero/CyberHeroFeatureStrip.vue
+++ b/landing/components/hero/CyberHeroFeatureStrip.vue
@@ -20,10 +20,10 @@ const props = defineProps<{
reducedMotion?: boolean;
}>();
-const { locale } = useI18n();
+const { t, locale } = useI18n();
const localizedHeroFeatureRail = computed(() => getLocalizedHeroFeatureRail(locale.value));
const localizedHeroReviewerFeatureCard = computed(() => getLocalizedHeroReviewerFeatureCard(locale.value));
-const statusLabel = computed(() => locale.value === "ru" ? "Статус:" : "Status:");
+const statusLabel = computed(() => t("common.statusLabel"));
const icons = [
mdiRobotOutline,
diff --git a/landing/components/hero/CyberHeroRobot.vue b/landing/components/hero/CyberHeroRobot.vue
index d6547404..0490bd18 100644
--- a/landing/components/hero/CyberHeroRobot.vue
+++ b/landing/components/hero/CyberHeroRobot.vue
@@ -7,12 +7,12 @@ const props = defineProps<{
activeReceiver?: HeroAgentRole | "video" | null;
}>();
-const { locale } = useI18n();
+const { t } = useI18n();
const isSender = computed(() => props.activeSender === props.agent.id);
const isReceiver = computed(() => props.activeReceiver === props.agent.id);
const imageLoading = computed(() => (props.agent.priority ? "eager" : "lazy"));
const imageFetchPriority = computed(() => (props.agent.priority ? "high" : "auto"));
-const statusLabel = computed(() => locale.value === "ru" ? "Статус:" : "Status:");
+const statusLabel = computed(() => t("common.statusLabel"));
const rootStyle = computed(() => ({
"--agent-x": String(props.agent.desktop.x),
diff --git a/landing/components/hero/CyberHeroVideoFrame.vue b/landing/components/hero/CyberHeroVideoFrame.vue
index 89724f91..68cbb030 100644
--- a/landing/components/hero/CyberHeroVideoFrame.vue
+++ b/landing/components/hero/CyberHeroVideoFrame.vue
@@ -1,6 +1,5 @@
@@ -8,12 +7,12 @@ const isRu = computed(() => locale.value === "ru");
id="hero-demo"
class="cyber-video-frame"
role="region"
- :aria-label="isRu ? 'Смотреть демо Agent Teams' : 'Watch Agent Teams demo'"
+ :aria-label="t('hero.videoFrameLabel')"
>
- {{ isRu ? 'Командная лента' : 'Team command feed' }}
- {{ isRu ? 'Живое демо' : 'Live demo' }}
+ {{ t('hero.commandFeed') }}
+ {{ t('hero.liveDemo') }}
diff --git a/landing/components/sections/DownloadSection.vue b/landing/components/sections/DownloadSection.vue
index 82c200db..5f82618f 100644
--- a/landing/components/sections/DownloadSection.vue
+++ b/landing/components/sections/DownloadSection.vue
@@ -251,7 +251,7 @@ const releaseDate = computed(() => {
day: 'numeric',
});
});
-const linuxRobotBubble = computed(() => locale.value === 'ru' ? 'Готов начать!' : 'Ready to start!');
+const linuxRobotBubble = computed(() => t('download.readyToStart'));
diff --git a/landing/components/sections/HeroSection.vue b/landing/components/sections/HeroSection.vue
index 55f61b77..b016b140 100644
--- a/landing/components/sections/HeroSection.vue
+++ b/landing/components/sections/HeroSection.vue
@@ -55,14 +55,10 @@ const supportedProviders = [
},
] as const;
const supportedProvidersLabel = computed(() => (
- locale.value === "ru"
- ? "Поддерживаем AI-провайдеры"
- : "Supported AI providers"
+ t("hero.supportedProviders")
));
const heroSlogan = computed(() => (
- locale.value === "ru"
- ? "Делайте много, почти ничего не делая"
- : "Get a lot done by doing very little"
+ t("hero.slogan")
));
const heroDownloadUrl = computed(() => {
@@ -79,17 +75,13 @@ const docsHref = computed(() => buildDocsHref({
}));
const downloadActionSubtitle = computed(() => {
if (!selectedDownloadAsset.value) {
- return locale.value === "ru"
- ? "Для вашей платформы"
- : "For your platform";
+ return t("hero.platformDefault");
}
return selectedDownloadAsset.value.actionSubtitle;
});
const docsActionSubtitle = computed(() => (
- locale.value === "ru"
- ? "Гайды и настройка"
- : "Guides and setup"
+ t("hero.guidesSetup")
));
function clearHeroMessageTimers() {
diff --git a/landing/components/sections/ScreenshotsSection.vue b/landing/components/sections/ScreenshotsSection.vue
index acfc349d..76ac9090 100644
--- a/landing/components/sections/ScreenshotsSection.vue
+++ b/landing/components/sections/ScreenshotsSection.vue
@@ -27,8 +27,8 @@ const screenshots = computed(() => screenshotData.map((s) => ({
width: s.width,
height: s.height,
})));
-const prevLabel = computed(() => locale.value === 'ru' ? 'Предыдущий' : 'Previous');
-const nextLabel = computed(() => locale.value === 'ru' ? 'Следующий' : 'Next');
+const prevLabel = computed(() => t('common.previous'));
+const nextLabel = computed(() => t('common.next'));
const swiperRef = ref
(null);
const swiperReady = ref(false);
diff --git a/landing/components/ui/HeroDemo.vue b/landing/components/ui/HeroDemo.vue
index ee4033d4..2815271e 100644
--- a/landing/components/ui/HeroDemo.vue
+++ b/landing/components/ui/HeroDemo.vue
@@ -1,7 +1,9 @@
-
+
@@ -225,7 +238,7 @@ function statusDotColor(status: string) {
- {{ col === 'progress' ? 'IN PROGRESS' : col.toUpperCase() }}
+ {{ colLabel(col) }}
@@ -249,7 +262,7 @@ function statusDotColor(status: string) {
{{ currentTask || 'Waiting for tasks...' }}
+ >{{ currentTask || t('hero.demo.waiting') }}
diff --git a/landing/components/ui/HeroDemoVideo.vue b/landing/components/ui/HeroDemoVideo.vue
index 3a6f84a7..0c1f8abf 100644
--- a/landing/components/ui/HeroDemoVideo.vue
+++ b/landing/components/ui/HeroDemoVideo.vue
@@ -2,19 +2,15 @@
import { computed, onMounted, onUnmounted, ref } from "vue";
import { mdiPlay } from "@mdi/js";
-const { t, locale } = useI18n();
+const { t } = useI18n();
const config = useRuntimeConfig();
const muxAccentColor = "#00f0ff";
const muxPrimaryColor = "#e6fbff";
const muxSecondaryColor = "#020617";
const muxPlaybackId = computed(() => String(config.public.muxPlaybackId || "").trim());
-const videoTitle = computed(() => (
- locale.value === "ru" ? "Демо-видео Agent Teams" : "Agent Teams demo video"
-));
-const muxVideoTitle = computed(() => (
- locale.value === "ru" ? "Демо Agent Teams" : "Agent Teams demo"
-));
+const videoTitle = computed(() => t("hero.demoVideoTitle"));
+const muxVideoTitle = computed(() => t("hero.demoTitle"));
const muxPlayerUrl = computed(() => {
if (!muxPlaybackId.value) return "";
diff --git a/landing/locales/ar.json b/landing/locales/ar.json
index 61e3f987..9b584773 100644
--- a/landing/locales/ar.json
+++ b/landing/locales/ar.json
@@ -7,7 +7,16 @@
"download": "تحميل",
"pricing": "مجاني",
"faq": "الأسئلة الشائعة",
- "viewOnGithub": "View on GitHub"
+ "viewOnGithub": "View on GitHub",
+ "openMenu": "فتح القائمة",
+ "closeMenu": "إغلاق القائمة",
+ "short": {
+ "screenshots": "صور",
+ "docs": "Docs",
+ "download": "تحميل",
+ "comparison": "مقارنة",
+ "pricing": "مجاني"
+ }
},
"hero": {
"badge": "Agent Teams",
@@ -22,13 +31,46 @@
"openSource": "مفتوح المصدر"
},
"watchDemo": "شاهد العرض",
- "videoUnavailable": "الفيديو غير متوفر"
+ "videoUnavailable": "الفيديو غير متوفر",
+ "supportedProviders": "مزودو AI المدعومون",
+ "slogan": "أنجز الكثير بجهد قليل جدًا",
+ "platformDefault": "لمنصتك",
+ "guidesSetup": "الأدلة والإعداد",
+ "videoFrameLabel": "شاهد عرض Agent Teams",
+ "commandFeed": "تدفق أوامر الفريق",
+ "liveDemo": "عرض مباشر",
+ "demoVideoTitle": "فيديو عرض Agent Teams",
+ "demoTitle": "عرض Agent Teams",
+ "demo": {
+ "ariaLabel": "عرض فريق الوكلاء",
+ "live": "LIVE",
+ "waiting": "بانتظار المهام...",
+ "activity": {
+ "authMiddleware": "تنفيذ middleware للمصادقة...",
+ "unitTests": "كتابة اختبارات وحدة للـ API...",
+ "reviewPr": "مراجعة تغييرات PR #42...",
+ "ciPipeline": "إعداد pipeline CI/CD...",
+ "refactorDatabase": "إعادة هيكلة طبقة قاعدة البيانات..."
+ },
+ "tasks": {
+ "authApi": "Auth API",
+ "unitTests": "اختبارات وحدة",
+ "ciSetup": "إعداد CI"
+ },
+ "columns": {
+ "todo": "TODO",
+ "progress": "قيد التنفيذ",
+ "review": "مراجعة",
+ "done": "تم"
+ }
+ }
},
"download": {
"title": "تحميل",
"detected": "تم الكشف",
"systemRequirements": "متطلبات النظام",
- "version": "الإصدار {version}"
+ "version": "الإصدار {version}",
+ "readyToStart": "جاهز للبدء!"
},
"theme": {
"dark": "داكن",
@@ -100,7 +142,10 @@
"sectionSubtitle": "لقطات شاشة حقيقية من التطبيق — لوحة كانبان، مراجعة الكود، فرق الوكلاء، والمزيد."
},
"common": {
- "learnMore": "اعرف المزيد"
+ "learnMore": "اعرف المزيد",
+ "statusLabel": "الحالة:",
+ "previous": "السابق",
+ "next": "التالي"
},
"footer": {
"copyright": "© {year} Agent Teams",
@@ -108,6 +153,7 @@
"robotBubble": "أنا أنتظر",
"links": {
"github": "GitHub",
+ "author": "المؤلف",
"docs": "التوثيق"
}
},
diff --git a/landing/locales/bn.json b/landing/locales/bn.json
index b181e2b4..2f8e4e70 100644
--- a/landing/locales/bn.json
+++ b/landing/locales/bn.json
@@ -7,7 +7,16 @@
"download": "ডাউনলোড করা হয়েছে",
"pricing": "মুক্ত",
"faq": "FAQ",
- "viewOnGithub": "প্রদর্শন GitHub"
+ "viewOnGithub": "প্রদর্শন GitHub",
+ "openMenu": "মেনু খুলুন",
+ "closeMenu": "মেনু বন্ধ করুন",
+ "short": {
+ "screenshots": "স্ক্রিন",
+ "docs": "Docs",
+ "download": "ডাউনলোড",
+ "comparison": "তুলনা",
+ "pricing": "ফ্রি"
+ }
},
"hero": {
"badge": "Agent Teams",
@@ -22,13 +31,46 @@
"openSource": "উৎস খুলুন"
},
"watchDemo": "নমুনা",
- "videoUnavailable": "ভিডিও উপলব্ধ নয়"
+ "videoUnavailable": "ভিডিও উপলব্ধ নয়",
+ "supportedProviders": "সমর্থিত AI providers",
+ "slogan": "খুব কম কাজ করে অনেক কিছু করুন",
+ "platformDefault": "আপনার platform-এর জন্য",
+ "guidesSetup": "Guides এবং setup",
+ "videoFrameLabel": "Agent Teams demo দেখুন",
+ "commandFeed": "Team command feed",
+ "liveDemo": "Live demo",
+ "demoVideoTitle": "Agent Teams demo video",
+ "demoTitle": "Agent Teams demo",
+ "demo": {
+ "ariaLabel": "Agent team demo",
+ "live": "LIVE",
+ "waiting": "Tasks এর অপেক্ষা...",
+ "activity": {
+ "authMiddleware": "Auth middleware implement হচ্ছে...",
+ "unitTests": "API এর জন্য unit tests লেখা হচ্ছে...",
+ "reviewPr": "PR #42 changes review হচ্ছে...",
+ "ciPipeline": "CI/CD pipeline setup হচ্ছে...",
+ "refactorDatabase": "Database layer refactor হচ্ছে..."
+ },
+ "tasks": {
+ "authApi": "Auth API",
+ "unitTests": "Unit tests",
+ "ciSetup": "CI setup"
+ },
+ "columns": {
+ "todo": "TODO",
+ "progress": "IN PROGRESS",
+ "review": "REVIEW",
+ "done": "DONE"
+ }
+ }
},
"download": {
"title": "ডাউনলোড করা হয়েছে",
"detected": "সনাক্ত",
"systemRequirements": "সিস্টেম প্রয়োজন",
- "version": "সংস্করণ {version}"
+ "version": "সংস্করণ {version}",
+ "readyToStart": "শুরু করার জন্য প্রস্তুত!"
},
"theme": {
"dark": "কালো",
@@ -100,7 +142,10 @@
"sectionSubtitle": "অ্যাপটির বাস্তব স্ক্রিনশট —কানবান বোর্ড, কোড পর্যালোচনা, এজেন্ট দল এবং আরও অনেক কিছু ।"
},
"common": {
- "learnMore": "অারো জানুন"
+ "learnMore": "অারো জানুন",
+ "statusLabel": "স্ট্যাটাস:",
+ "previous": "আগের",
+ "next": "পরের"
},
"footer": {
"copyright": "© {year} Agent Teams",
@@ -108,6 +153,7 @@
"robotBubble": "আমি অপেক্ষা করছি",
"links": {
"github": "GitHub",
+ "author": "লেখক",
"docs": "নথিপত্র"
}
},
diff --git a/landing/locales/de.json b/landing/locales/de.json
index f407c51b..611bfacd 100644
--- a/landing/locales/de.json
+++ b/landing/locales/de.json
@@ -7,7 +7,16 @@
"download": "Download",
"pricing": "Kostenlos",
"faq": "FAQ",
- "viewOnGithub": "View on GitHub"
+ "viewOnGithub": "View on GitHub",
+ "openMenu": "Menü öffnen",
+ "closeMenu": "Menü schließen",
+ "short": {
+ "screenshots": "Bilder",
+ "docs": "Docs",
+ "download": "Laden",
+ "comparison": "Vergleich",
+ "pricing": "Gratis"
+ }
},
"hero": {
"badge": "Agent Teams",
@@ -22,13 +31,46 @@
"openSource": "Open Source"
},
"watchDemo": "Demo ansehen",
- "videoUnavailable": "Video nicht verfügbar"
+ "videoUnavailable": "Video nicht verfügbar",
+ "supportedProviders": "Unterstützte KI-Anbieter",
+ "slogan": "Viel erledigen mit sehr wenig Aufwand",
+ "platformDefault": "Für Ihre Plattform",
+ "guidesSetup": "Anleitungen und Einrichtung",
+ "videoFrameLabel": "Agent Teams Demo ansehen",
+ "commandFeed": "Team-Befehlsfeed",
+ "liveDemo": "Live-Demo",
+ "demoVideoTitle": "Agent Teams Demo-Video",
+ "demoTitle": "Agent Teams Demo",
+ "demo": {
+ "ariaLabel": "Agententeam-Demo",
+ "live": "LIVE",
+ "waiting": "Warte auf Aufgaben...",
+ "activity": {
+ "authMiddleware": "Auth-Middleware wird implementiert...",
+ "unitTests": "Unit-Tests für API werden geschrieben...",
+ "reviewPr": "PR #42 Änderungen werden geprüft...",
+ "ciPipeline": "CI/CD-Pipeline wird eingerichtet...",
+ "refactorDatabase": "Datenbankschicht wird refaktoriert..."
+ },
+ "tasks": {
+ "authApi": "Auth API",
+ "unitTests": "Unit-Tests",
+ "ciSetup": "CI Setup"
+ },
+ "columns": {
+ "todo": "TODO",
+ "progress": "IN ARBEIT",
+ "review": "REVIEW",
+ "done": "FERTIG"
+ }
+ }
},
"download": {
"title": "Herunterladen",
"detected": "Erkannt",
"systemRequirements": "Systemanforderungen",
- "version": "Version {version}"
+ "version": "Version {version}",
+ "readyToStart": "Bereit zum Start!"
},
"theme": {
"dark": "Dunkel",
@@ -100,7 +142,10 @@
"sectionSubtitle": "Echte Screenshots der App — Kanban-Board, Code-Review, Agenten-Teams und mehr."
},
"common": {
- "learnMore": "Mehr erfahren"
+ "learnMore": "Mehr erfahren",
+ "statusLabel": "Status:",
+ "previous": "Zurück",
+ "next": "Weiter"
},
"footer": {
"copyright": "© {year} Agent Teams",
@@ -108,6 +153,7 @@
"robotBubble": "Ich warte",
"links": {
"github": "GitHub",
+ "author": "Autor",
"docs": "Dokumentation"
}
},
diff --git a/landing/locales/en.json b/landing/locales/en.json
index fc0f0476..23d4ee54 100644
--- a/landing/locales/en.json
+++ b/landing/locales/en.json
@@ -7,7 +7,16 @@
"download": "Download",
"pricing": "Free",
"faq": "FAQ",
- "viewOnGithub": "View on GitHub"
+ "viewOnGithub": "View on GitHub",
+ "openMenu": "Open menu",
+ "closeMenu": "Close menu",
+ "short": {
+ "screenshots": "Shots",
+ "docs": "Docs",
+ "download": "Get",
+ "comparison": "Compare",
+ "pricing": "Free"
+ }
},
"hero": {
"badge": "Agent Teams",
@@ -22,13 +31,46 @@
"openSource": "Open Source"
},
"watchDemo": "Watch Demo",
- "videoUnavailable": "Video unavailable"
+ "videoUnavailable": "Video unavailable",
+ "supportedProviders": "Supported AI providers",
+ "slogan": "Get a lot done by doing very little",
+ "platformDefault": "For your platform",
+ "guidesSetup": "Guides and setup",
+ "videoFrameLabel": "Watch Agent Teams demo",
+ "commandFeed": "Team command feed",
+ "liveDemo": "Live demo",
+ "demoVideoTitle": "Agent Teams demo video",
+ "demoTitle": "Agent Teams demo",
+ "demo": {
+ "ariaLabel": "Agent team demo",
+ "live": "LIVE",
+ "waiting": "Waiting for tasks...",
+ "activity": {
+ "authMiddleware": "Implementing auth middleware...",
+ "unitTests": "Writing unit tests for API...",
+ "reviewPr": "Reviewing PR #42 changes...",
+ "ciPipeline": "Setting up CI/CD pipeline...",
+ "refactorDatabase": "Refactoring database layer..."
+ },
+ "tasks": {
+ "authApi": "Auth API",
+ "unitTests": "Unit tests",
+ "ciSetup": "CI setup"
+ },
+ "columns": {
+ "todo": "TODO",
+ "progress": "IN PROGRESS",
+ "review": "REVIEW",
+ "done": "DONE"
+ }
+ }
},
"download": {
"title": "Download",
"detected": "Detected",
"systemRequirements": "System requirements",
- "version": "Version {version}"
+ "version": "Version {version}",
+ "readyToStart": "Ready to start!"
},
"theme": {
"dark": "Dark",
@@ -100,7 +142,10 @@
"sectionSubtitle": "Real screenshots from the app — kanban board, code review, agent teams, and more."
},
"common": {
- "learnMore": "Learn more"
+ "learnMore": "Learn more",
+ "statusLabel": "Status:",
+ "previous": "Previous",
+ "next": "Next"
},
"footer": {
"copyright": "© {year} Agent Teams",
@@ -108,6 +153,7 @@
"robotBubble": "I'm waiting",
"links": {
"github": "GitHub",
+ "author": "Author",
"docs": "Documentation"
}
},
diff --git a/landing/locales/es.json b/landing/locales/es.json
index 579f122f..3da96d24 100644
--- a/landing/locales/es.json
+++ b/landing/locales/es.json
@@ -7,7 +7,16 @@
"download": "Descargar",
"pricing": "Gratis",
"faq": "FAQ",
- "viewOnGithub": "View on GitHub"
+ "viewOnGithub": "View on GitHub",
+ "openMenu": "Abrir menú",
+ "closeMenu": "Cerrar menú",
+ "short": {
+ "screenshots": "Capturas",
+ "docs": "Docs",
+ "download": "Bajar",
+ "comparison": "Comparar",
+ "pricing": "Gratis"
+ }
},
"hero": {
"badge": "Agent Teams",
@@ -22,13 +31,46 @@
"openSource": "Open Source"
},
"watchDemo": "Ver demo",
- "videoUnavailable": "Vídeo no disponible"
+ "videoUnavailable": "Vídeo no disponible",
+ "supportedProviders": "Proveedores de IA compatibles",
+ "slogan": "Haz mucho haciendo muy poco",
+ "platformDefault": "Para tu plataforma",
+ "guidesSetup": "Guías y configuración",
+ "videoFrameLabel": "Ver demo de Agent Teams",
+ "commandFeed": "Feed de comandos del equipo",
+ "liveDemo": "Demo en vivo",
+ "demoVideoTitle": "Vídeo demo de Agent Teams",
+ "demoTitle": "Demo de Agent Teams",
+ "demo": {
+ "ariaLabel": "Demo del equipo de agentes",
+ "live": "LIVE",
+ "waiting": "Esperando tareas...",
+ "activity": {
+ "authMiddleware": "Implementando middleware de autenticación...",
+ "unitTests": "Escribiendo pruebas unitarias para API...",
+ "reviewPr": "Revisando cambios del PR #42...",
+ "ciPipeline": "Configurando pipeline CI/CD...",
+ "refactorDatabase": "Refactorizando capa de base de datos..."
+ },
+ "tasks": {
+ "authApi": "API de auth",
+ "unitTests": "Pruebas unitarias",
+ "ciSetup": "Config CI"
+ },
+ "columns": {
+ "todo": "TODO",
+ "progress": "EN PROGRESO",
+ "review": "REVISIÓN",
+ "done": "LISTO"
+ }
+ }
},
"download": {
"title": "Descargar",
"detected": "Detectado",
"systemRequirements": "Requisitos del sistema",
- "version": "Versión {version}"
+ "version": "Versión {version}",
+ "readyToStart": "Listo para empezar!"
},
"theme": {
"dark": "Oscuro",
@@ -100,7 +142,10 @@
"sectionSubtitle": "Capturas reales de la aplicación — tablero kanban, revisión de código, equipos de agentes y más."
},
"common": {
- "learnMore": "Más información"
+ "learnMore": "Más información",
+ "statusLabel": "Estado:",
+ "previous": "Anterior",
+ "next": "Siguiente"
},
"footer": {
"copyright": "© {year} Agent Teams",
@@ -108,6 +153,7 @@
"robotBubble": "Estoy esperando",
"links": {
"github": "GitHub",
+ "author": "Autor",
"docs": "Documentación"
}
},
diff --git a/landing/locales/fr.json b/landing/locales/fr.json
index 675c6833..253de5ac 100644
--- a/landing/locales/fr.json
+++ b/landing/locales/fr.json
@@ -7,7 +7,16 @@
"download": "Télécharger",
"pricing": "Gratuit",
"faq": "FAQ",
- "viewOnGithub": "View on GitHub"
+ "viewOnGithub": "View on GitHub",
+ "openMenu": "Ouvrir le menu",
+ "closeMenu": "Fermer le menu",
+ "short": {
+ "screenshots": "Captures",
+ "docs": "Docs",
+ "download": "Télécharger",
+ "comparison": "Comparer",
+ "pricing": "Gratuit"
+ }
},
"hero": {
"badge": "Agent Teams",
@@ -22,13 +31,46 @@
"openSource": "Open Source"
},
"watchDemo": "Voir la démo",
- "videoUnavailable": "Vidéo indisponible"
+ "videoUnavailable": "Vidéo indisponible",
+ "supportedProviders": "Fournisseurs IA compatibles",
+ "slogan": "Faites beaucoup en faisant très peu",
+ "platformDefault": "Pour votre plateforme",
+ "guidesSetup": "Guides et configuration",
+ "videoFrameLabel": "Regarder la démo Agent Teams",
+ "commandFeed": "Flux de commandes d'équipe",
+ "liveDemo": "Démo en direct",
+ "demoVideoTitle": "Vidéo démo Agent Teams",
+ "demoTitle": "Démo Agent Teams",
+ "demo": {
+ "ariaLabel": "Démo d'équipe d'agents",
+ "live": "LIVE",
+ "waiting": "En attente de tâches...",
+ "activity": {
+ "authMiddleware": "Implémentation du middleware d'auth...",
+ "unitTests": "Écriture de tests unitaires pour l'API...",
+ "reviewPr": "Revue des changements PR #42...",
+ "ciPipeline": "Configuration du pipeline CI/CD...",
+ "refactorDatabase": "Refactorisation de la couche base de données..."
+ },
+ "tasks": {
+ "authApi": "API auth",
+ "unitTests": "Tests unitaires",
+ "ciSetup": "Config CI"
+ },
+ "columns": {
+ "todo": "TODO",
+ "progress": "EN COURS",
+ "review": "REVUE",
+ "done": "TERMINÉ"
+ }
+ }
},
"download": {
"title": "Télécharger",
"detected": "Détecté",
"systemRequirements": "Configuration requise",
- "version": "Version {version}"
+ "version": "Version {version}",
+ "readyToStart": "Prêt à commencer!"
},
"theme": {
"dark": "Sombre",
@@ -100,7 +142,10 @@
"sectionSubtitle": "Captures d'écran réelles — tableau kanban, revue de code, équipes d'agents et plus."
},
"common": {
- "learnMore": "En savoir plus"
+ "learnMore": "En savoir plus",
+ "statusLabel": "Statut:",
+ "previous": "Précédent",
+ "next": "Suivant"
},
"footer": {
"copyright": "© {year} Agent Teams",
@@ -108,6 +153,7 @@
"robotBubble": "J'attends",
"links": {
"github": "GitHub",
+ "author": "Auteur",
"docs": "Documentation"
}
},
diff --git a/landing/locales/hi.json b/landing/locales/hi.json
index db802dcf..01fd572d 100644
--- a/landing/locales/hi.json
+++ b/landing/locales/hi.json
@@ -7,7 +7,16 @@
"download": "डाउनलोड",
"pricing": "मुफ़्त",
"faq": "FAQ",
- "viewOnGithub": "View on GitHub"
+ "viewOnGithub": "View on GitHub",
+ "openMenu": "मेनू खोलें",
+ "closeMenu": "मेनू बंद करें",
+ "short": {
+ "screenshots": "शॉट्स",
+ "docs": "Docs",
+ "download": "डाउनलोड",
+ "comparison": "तुलना",
+ "pricing": "मुफ्त"
+ }
},
"hero": {
"badge": "Agent Teams",
@@ -22,13 +31,46 @@
"openSource": "ओपन सोर्स"
},
"watchDemo": "डेमो देखें",
- "videoUnavailable": "वीडियो उपलब्ध नहीं"
+ "videoUnavailable": "वीडियो उपलब्ध नहीं",
+ "supportedProviders": "समर्थित AI providers",
+ "slogan": "बहुत कम करके बहुत कुछ करें",
+ "platformDefault": "आपके platform के लिए",
+ "guidesSetup": "Guides और setup",
+ "videoFrameLabel": "Agent Teams demo देखें",
+ "commandFeed": "Team command feed",
+ "liveDemo": "Live demo",
+ "demoVideoTitle": "Agent Teams demo video",
+ "demoTitle": "Agent Teams demo",
+ "demo": {
+ "ariaLabel": "Agent team demo",
+ "live": "LIVE",
+ "waiting": "Tasks का इंतज़ार...",
+ "activity": {
+ "authMiddleware": "Auth middleware implement हो रहा है...",
+ "unitTests": "API के लिए unit tests लिखे जा रहे हैं...",
+ "reviewPr": "PR #42 changes review हो रहे हैं...",
+ "ciPipeline": "CI/CD pipeline setup हो रही है...",
+ "refactorDatabase": "Database layer refactor हो रही है..."
+ },
+ "tasks": {
+ "authApi": "Auth API",
+ "unitTests": "Unit tests",
+ "ciSetup": "CI setup"
+ },
+ "columns": {
+ "todo": "TODO",
+ "progress": "IN PROGRESS",
+ "review": "REVIEW",
+ "done": "DONE"
+ }
+ }
},
"download": {
"title": "डाउनलोड",
"detected": "पहचाना गया",
"systemRequirements": "सिस्टम आवश्यकताएँ",
- "version": "संस्करण {version}"
+ "version": "संस्करण {version}",
+ "readyToStart": "शुरू करने के लिए तैयार!"
},
"theme": {
"dark": "डार्क",
@@ -100,7 +142,10 @@
"sectionSubtitle": "ऐप के असली स्क्रीनशॉट — कानबन बोर्ड, कोड रिव्यू, एजेंट टीमें, और बहुत कुछ।"
},
"common": {
- "learnMore": "और जानें"
+ "learnMore": "और जानें",
+ "statusLabel": "स्थिति:",
+ "previous": "पिछला",
+ "next": "अगला"
},
"footer": {
"copyright": "© {year} Agent Teams",
@@ -108,6 +153,7 @@
"robotBubble": "मैं इंतज़ार कर रहा हूँ",
"links": {
"github": "GitHub",
+ "author": "लेखक",
"docs": "दस्तावेज़"
}
},
diff --git a/landing/locales/id.json b/landing/locales/id.json
index b736966f..adb69a40 100644
--- a/landing/locales/id.json
+++ b/landing/locales/id.json
@@ -7,7 +7,16 @@
"download": "Unduh",
"pricing": "Bebas",
"faq": "FAQ",
- "viewOnGithub": "Tilik pada GitHub"
+ "viewOnGithub": "Tilik pada GitHub",
+ "openMenu": "Buka menu",
+ "closeMenu": "Tutup menu",
+ "short": {
+ "screenshots": "Gambar",
+ "docs": "Docs",
+ "download": "Unduh",
+ "comparison": "Banding",
+ "pricing": "Gratis"
+ }
},
"hero": {
"badge": "Agent Teams",
@@ -22,13 +31,46 @@
"openSource": "Buka Sumber"
},
"watchDemo": "Watch Demo",
- "videoUnavailable": "Video tidak tersedia"
+ "videoUnavailable": "Video tidak tersedia",
+ "supportedProviders": "Penyedia AI yang didukung",
+ "slogan": "Selesaikan banyak hal dengan sangat sedikit aksi",
+ "platformDefault": "Untuk platform Anda",
+ "guidesSetup": "Panduan dan penyiapan",
+ "videoFrameLabel": "Tonton demo Agent Teams",
+ "commandFeed": "Feed perintah tim",
+ "liveDemo": "Demo langsung",
+ "demoVideoTitle": "Video demo Agent Teams",
+ "demoTitle": "Demo Agent Teams",
+ "demo": {
+ "ariaLabel": "Demo tim agen",
+ "live": "LIVE",
+ "waiting": "Menunggu tugas...",
+ "activity": {
+ "authMiddleware": "Menerapkan middleware auth...",
+ "unitTests": "Menulis unit test untuk API...",
+ "reviewPr": "Meninjau perubahan PR #42...",
+ "ciPipeline": "Menyiapkan pipeline CI/CD...",
+ "refactorDatabase": "Merefactor layer database..."
+ },
+ "tasks": {
+ "authApi": "Auth API",
+ "unitTests": "Unit test",
+ "ciSetup": "Setup CI"
+ },
+ "columns": {
+ "todo": "TODO",
+ "progress": "BERJALAN",
+ "review": "REVIEW",
+ "done": "SELESAI"
+ }
+ }
},
"download": {
"title": "Unduh",
"detected": "Terdeteksi",
"systemRequirements": "Kebutuhan sistem",
- "version": "Versi {version}"
+ "version": "Versi {version}",
+ "readyToStart": "Siap mulai!"
},
"theme": {
"dark": "Gelap",
@@ -100,7 +142,10 @@
"sectionSubtitle": "Gambar layar nyata dari aplikasi - papan kanban, ulasan kode, tim agen, dan banyak lagi."
},
"common": {
- "learnMore": "Pelajari lagi"
+ "learnMore": "Pelajari lagi",
+ "statusLabel": "Status:",
+ "previous": "Sebelumnya",
+ "next": "Berikutnya"
},
"footer": {
"copyright": "© {year} Agent Teams",
@@ -108,6 +153,7 @@
"robotBubble": "Aku menunggu",
"links": {
"github": "GitHub",
+ "author": "Penulis",
"docs": "Dokumentasi"
}
},
diff --git a/landing/locales/ja.json b/landing/locales/ja.json
index 4f957f5f..fd4ba32b 100644
--- a/landing/locales/ja.json
+++ b/landing/locales/ja.json
@@ -7,7 +7,16 @@
"download": "ダウンロード",
"pricing": "無料",
"faq": "FAQ",
- "viewOnGithub": "View on GitHub"
+ "viewOnGithub": "View on GitHub",
+ "openMenu": "メニューを開く",
+ "closeMenu": "メニューを閉じる",
+ "short": {
+ "screenshots": "画像",
+ "docs": "Docs",
+ "download": "入手",
+ "comparison": "比較",
+ "pricing": "無料"
+ }
},
"hero": {
"badge": "Agent Teams",
@@ -22,13 +31,46 @@
"openSource": "オープンソース"
},
"watchDemo": "デモを見る",
- "videoUnavailable": "動画は利用できません"
+ "videoUnavailable": "動画は利用できません",
+ "supportedProviders": "対応 AI プロバイダー",
+ "slogan": "少ない操作で多くをこなす",
+ "platformDefault": "お使いのプラットフォーム向け",
+ "guidesSetup": "ガイドとセットアップ",
+ "videoFrameLabel": "Agent Teams のデモを見る",
+ "commandFeed": "チームコマンドフィード",
+ "liveDemo": "ライブデモ",
+ "demoVideoTitle": "Agent Teams デモ動画",
+ "demoTitle": "Agent Teams デモ",
+ "demo": {
+ "ariaLabel": "エージェントチームのデモ",
+ "live": "LIVE",
+ "waiting": "タスクを待機中...",
+ "activity": {
+ "authMiddleware": "認証ミドルウェアを実装中...",
+ "unitTests": "API のユニットテストを作成中...",
+ "reviewPr": "PR #42 の変更をレビュー中...",
+ "ciPipeline": "CI/CD パイプラインを設定中...",
+ "refactorDatabase": "データベース層をリファクタリング中..."
+ },
+ "tasks": {
+ "authApi": "認証 API",
+ "unitTests": "ユニットテスト",
+ "ciSetup": "CI 設定"
+ },
+ "columns": {
+ "todo": "TODO",
+ "progress": "進行中",
+ "review": "レビュー",
+ "done": "完了"
+ }
+ }
},
"download": {
"title": "ダウンロード",
"detected": "検出済み",
"systemRequirements": "動作環境",
- "version": "バージョン {version}"
+ "version": "バージョン {version}",
+ "readyToStart": "開始できます!"
},
"theme": {
"dark": "ダーク",
@@ -100,7 +142,10 @@
"sectionSubtitle": "アプリの実際のスクリーンショット — カンバンボード、コードレビュー、エージェントチームなど。"
},
"common": {
- "learnMore": "詳細"
+ "learnMore": "詳細",
+ "statusLabel": "ステータス:",
+ "previous": "前へ",
+ "next": "次へ"
},
"footer": {
"copyright": "© {year} Agent Teams",
@@ -108,6 +153,7 @@
"robotBubble": "待ってるよ",
"links": {
"github": "GitHub",
+ "author": "作者",
"docs": "ドキュメント"
}
},
diff --git a/landing/locales/ko.json b/landing/locales/ko.json
index 60435aab..91e0e4b4 100644
--- a/landing/locales/ko.json
+++ b/landing/locales/ko.json
@@ -7,7 +7,16 @@
"download": "다운로드",
"pricing": "무료",
"faq": "FAQ",
- "viewOnGithub": "GitHub에서 보기"
+ "viewOnGithub": "GitHub에서 보기",
+ "openMenu": "메뉴 열기",
+ "closeMenu": "메뉴 닫기",
+ "short": {
+ "screenshots": "샷",
+ "docs": "문서",
+ "download": "받기",
+ "comparison": "비교",
+ "pricing": "무료"
+ }
},
"hero": {
"badge": "Agent Teams",
@@ -22,13 +31,46 @@
"openSource": "오픈 소스"
},
"watchDemo": "데모 보기",
- "videoUnavailable": "동영상을 사용할 수 없습니다"
+ "videoUnavailable": "동영상을 사용할 수 없습니다",
+ "supportedProviders": "지원되는 AI 제공자",
+ "slogan": "아주 적은 조작으로 많은 일을 처리하세요",
+ "platformDefault": "현재 플랫폼용",
+ "guidesSetup": "가이드 및 설정",
+ "videoFrameLabel": "Agent Teams 데모 보기",
+ "commandFeed": "팀 명령 피드",
+ "liveDemo": "라이브 데모",
+ "demoVideoTitle": "Agent Teams 데모 동영상",
+ "demoTitle": "Agent Teams 데모",
+ "demo": {
+ "ariaLabel": "에이전트 팀 데모",
+ "live": "LIVE",
+ "waiting": "작업 대기 중...",
+ "activity": {
+ "authMiddleware": "인증 미들웨어 구현 중...",
+ "unitTests": "API 단위 테스트 작성 중...",
+ "reviewPr": "PR #42 변경 사항 검토 중...",
+ "ciPipeline": "CI/CD 파이프라인 설정 중...",
+ "refactorDatabase": "데이터베이스 계층 리팩터링 중..."
+ },
+ "tasks": {
+ "authApi": "Auth API",
+ "unitTests": "단위 테스트",
+ "ciSetup": "CI 설정"
+ },
+ "columns": {
+ "todo": "TODO",
+ "progress": "진행 중",
+ "review": "리뷰",
+ "done": "완료"
+ }
+ }
},
"download": {
"title": "다운로드",
"detected": "감지됨",
"systemRequirements": "시스템 요구 사항",
- "version": "버전 {version}"
+ "version": "버전 {version}",
+ "readyToStart": "시작할 준비 완료!"
},
"theme": {
"dark": "다크",
@@ -100,7 +142,10 @@
"sectionSubtitle": "칸반 보드, 코드 리뷰, 에이전트 팀 등 앱의 실제 스크린샷입니다."
},
"common": {
- "learnMore": "자세히 알아보기"
+ "learnMore": "자세히 알아보기",
+ "statusLabel": "상태:",
+ "previous": "이전",
+ "next": "다음"
},
"footer": {
"copyright": "© {year} Agent Teams",
@@ -108,6 +153,7 @@
"robotBubble": "기다리고 있어요",
"links": {
"github": "GitHub",
+ "author": "작성자",
"docs": "문서"
}
},
diff --git a/landing/locales/pt.json b/landing/locales/pt.json
index f5b7960c..2f786ee5 100644
--- a/landing/locales/pt.json
+++ b/landing/locales/pt.json
@@ -7,7 +7,16 @@
"download": "Baixar",
"pricing": "Grátis",
"faq": "FAQ",
- "viewOnGithub": "View on GitHub"
+ "viewOnGithub": "View on GitHub",
+ "openMenu": "Abrir menu",
+ "closeMenu": "Fechar menu",
+ "short": {
+ "screenshots": "Imagens",
+ "docs": "Docs",
+ "download": "Baixar",
+ "comparison": "Comparar",
+ "pricing": "Grátis"
+ }
},
"hero": {
"badge": "Agent Teams",
@@ -22,13 +31,46 @@
"openSource": "Open Source"
},
"watchDemo": "Ver demo",
- "videoUnavailable": "Vídeo indisponível"
+ "videoUnavailable": "Vídeo indisponível",
+ "supportedProviders": "Provedores de IA compatíveis",
+ "slogan": "Faça muito fazendo muito pouco",
+ "platformDefault": "Para sua plataforma",
+ "guidesSetup": "Guias e configuração",
+ "videoFrameLabel": "Assistir demo do Agent Teams",
+ "commandFeed": "Feed de comandos da equipe",
+ "liveDemo": "Demo ao vivo",
+ "demoVideoTitle": "Vídeo demo do Agent Teams",
+ "demoTitle": "Demo do Agent Teams",
+ "demo": {
+ "ariaLabel": "Demo da equipe de agentes",
+ "live": "LIVE",
+ "waiting": "Aguardando tarefas...",
+ "activity": {
+ "authMiddleware": "Implementando middleware de autenticação...",
+ "unitTests": "Escrevendo testes unitários para API...",
+ "reviewPr": "Revisando mudanças do PR #42...",
+ "ciPipeline": "Configurando pipeline CI/CD...",
+ "refactorDatabase": "Refatorando camada de banco de dados..."
+ },
+ "tasks": {
+ "authApi": "API auth",
+ "unitTests": "Testes unitários",
+ "ciSetup": "Setup CI"
+ },
+ "columns": {
+ "todo": "TODO",
+ "progress": "EM PROGRESSO",
+ "review": "REVISÃO",
+ "done": "PRONTO"
+ }
+ }
},
"download": {
"title": "Baixar",
"detected": "Detectado",
"systemRequirements": "Requisitos do sistema",
- "version": "Versão {version}"
+ "version": "Versão {version}",
+ "readyToStart": "Pronto para começar!"
},
"theme": {
"dark": "Escuro",
@@ -100,7 +142,10 @@
"sectionSubtitle": "Capturas reais do app — quadro kanban, revisão de código, equipes de agentes e mais."
},
"common": {
- "learnMore": "Saiba mais"
+ "learnMore": "Saiba mais",
+ "statusLabel": "Status:",
+ "previous": "Anterior",
+ "next": "Próximo"
},
"footer": {
"copyright": "© {year} Agent Teams",
@@ -108,6 +153,7 @@
"robotBubble": "Estou esperando",
"links": {
"github": "GitHub",
+ "author": "Autor",
"docs": "Documentação"
}
},
diff --git a/landing/locales/ru.json b/landing/locales/ru.json
index 4cb982cc..05967f5c 100644
--- a/landing/locales/ru.json
+++ b/landing/locales/ru.json
@@ -7,7 +7,16 @@
"download": "Скачать",
"pricing": "Бесплатно",
"faq": "FAQ",
- "viewOnGithub": "GitHub"
+ "viewOnGithub": "GitHub",
+ "openMenu": "Открыть меню",
+ "closeMenu": "Закрыть меню",
+ "short": {
+ "screenshots": "Скрины",
+ "docs": "Док",
+ "download": "Скачать",
+ "comparison": "Сравн.",
+ "pricing": "Беспл."
+ }
},
"hero": {
"badge": "Agent Teams",
@@ -22,13 +31,46 @@
"openSource": "Open Source"
},
"watchDemo": "Смотреть демо",
- "videoUnavailable": "Видео недоступно"
+ "videoUnavailable": "Видео недоступно",
+ "supportedProviders": "Поддерживаем AI-провайдеры",
+ "slogan": "Делайте много, почти ничего не делая",
+ "platformDefault": "Для вашей платформы",
+ "guidesSetup": "Гайды и настройка",
+ "videoFrameLabel": "Смотреть демо Agent Teams",
+ "commandFeed": "Командная лента",
+ "liveDemo": "Живое демо",
+ "demoVideoTitle": "Демо-видео Agent Teams",
+ "demoTitle": "Демо Agent Teams",
+ "demo": {
+ "ariaLabel": "Демо команды агентов",
+ "live": "LIVE",
+ "waiting": "Ожидание задач...",
+ "activity": {
+ "authMiddleware": "Реализация auth middleware...",
+ "unitTests": "Написание unit-тестов для API...",
+ "reviewPr": "Ревью изменений PR #42...",
+ "ciPipeline": "Настройка CI/CD pipeline...",
+ "refactorDatabase": "Рефакторинг слоя базы данных..."
+ },
+ "tasks": {
+ "authApi": "Auth API",
+ "unitTests": "Unit-тесты",
+ "ciSetup": "CI setup"
+ },
+ "columns": {
+ "todo": "TODO",
+ "progress": "В РАБОТЕ",
+ "review": "РЕВЬЮ",
+ "done": "ГОТОВО"
+ }
+ }
},
"download": {
"title": "Скачать",
"detected": "Определено",
"systemRequirements": "Системные требования",
- "version": "Версия {version}"
+ "version": "Версия {version}",
+ "readyToStart": "Готов начать!"
},
"theme": {
"dark": "Тёмная",
@@ -100,7 +142,10 @@
"sectionSubtitle": "Реальные скриншоты приложения — канбан-доска, код-ревью, команды агентов и многое другое."
},
"common": {
- "learnMore": "Подробнее"
+ "learnMore": "Подробнее",
+ "statusLabel": "Статус:",
+ "previous": "Предыдущий",
+ "next": "Следующий"
},
"footer": {
"copyright": "© {year} Agent Teams",
@@ -108,6 +153,7 @@
"robotBubble": "Я жду",
"links": {
"github": "GitHub",
+ "author": "Автор",
"docs": "Документация"
}
},
diff --git a/landing/locales/ur.json b/landing/locales/ur.json
index d14425bf..90a5cc51 100644
--- a/landing/locales/ur.json
+++ b/landing/locales/ur.json
@@ -7,7 +7,16 @@
"download": "گر تے ہوئے",
"pricing": "مفت",
"faq": "FAQ",
- "viewOnGithub": "دیکھیں GitHub"
+ "viewOnGithub": "دیکھیں GitHub",
+ "openMenu": "مینو کھولیں",
+ "closeMenu": "مینو بند کریں",
+ "short": {
+ "screenshots": "تصاویر",
+ "docs": "Docs",
+ "download": "ڈاؤن لوڈ",
+ "comparison": "موازنہ",
+ "pricing": "مفت"
+ }
},
"hero": {
"badge": "Agent Teams",
@@ -22,13 +31,46 @@
"openSource": "کھولیں"
},
"watchDemo": "دیمو",
- "videoUnavailable": "ویڈیو غیر درج شدہ"
+ "videoUnavailable": "ویڈیو غیر درج شدہ",
+ "supportedProviders": "حمایت یافتہ AI providers",
+ "slogan": "بہت کم کام کر کے بہت کچھ کریں",
+ "platformDefault": "آپ کے platform کے لیے",
+ "guidesSetup": "Guides اور setup",
+ "videoFrameLabel": "Agent Teams demo دیکھیں",
+ "commandFeed": "Team command feed",
+ "liveDemo": "Live demo",
+ "demoVideoTitle": "Agent Teams demo video",
+ "demoTitle": "Agent Teams demo",
+ "demo": {
+ "ariaLabel": "Agent team demo",
+ "live": "LIVE",
+ "waiting": "Tasks کا انتظار...",
+ "activity": {
+ "authMiddleware": "Auth middleware implement ہو رہا ہے...",
+ "unitTests": "API کے لیے unit tests لکھے جا رہے ہیں...",
+ "reviewPr": "PR #42 changes review ہو رہے ہیں...",
+ "ciPipeline": "CI/CD pipeline setup ہو رہی ہے...",
+ "refactorDatabase": "Database layer refactor ہو رہی ہے..."
+ },
+ "tasks": {
+ "authApi": "Auth API",
+ "unitTests": "Unit tests",
+ "ciSetup": "CI setup"
+ },
+ "columns": {
+ "todo": "TODO",
+ "progress": "IN PROGRESS",
+ "review": "REVIEW",
+ "done": "DONE"
+ }
+ }
},
"download": {
"title": "گر تے ہوئے",
"detected": "غیر متصل",
"systemRequirements": "سسٹم تقاضوں",
- "version": "ورژن {version}"
+ "version": "ورژن {version}",
+ "readyToStart": "شروع کرنے کے لیے تیار!"
},
"theme": {
"dark": "اندھیرا",
@@ -100,7 +142,10 @@
"sectionSubtitle": "ایپ کی طرف سے حقیقی اسکرین — کابینہ بورڈ ، کوڈ جائزہ ، ایجنٹ ٹیموں اور زیادہ سے زیادہ"
},
"common": {
- "learnMore": "مزید سیکھیں"
+ "learnMore": "مزید سیکھیں",
+ "statusLabel": "اسٹیٹس:",
+ "previous": "پچھلا",
+ "next": "اگلا"
},
"footer": {
"copyright": "© {year} Agent Teams",
@@ -108,6 +153,7 @@
"robotBubble": "میں انتظار کر رہا ہوں",
"links": {
"github": "GitHub",
+ "author": "مصنف",
"docs": "دستاویز"
}
},
diff --git a/landing/locales/zh.json b/landing/locales/zh.json
index 66cd073e..4c95f12b 100644
--- a/landing/locales/zh.json
+++ b/landing/locales/zh.json
@@ -7,7 +7,16 @@
"download": "下载",
"pricing": "免费",
"faq": "常见问题",
- "viewOnGithub": "View on GitHub"
+ "viewOnGithub": "View on GitHub",
+ "openMenu": "打开菜单",
+ "closeMenu": "关闭菜单",
+ "short": {
+ "screenshots": "截图",
+ "docs": "文档",
+ "download": "下载",
+ "comparison": "比较",
+ "pricing": "免费"
+ }
},
"hero": {
"badge": "Agent Teams",
@@ -22,13 +31,46 @@
"openSource": "开源"
},
"watchDemo": "观看演示",
- "videoUnavailable": "视频不可用"
+ "videoUnavailable": "视频不可用",
+ "supportedProviders": "支持的 AI 提供商",
+ "slogan": "用很少操作完成大量工作",
+ "platformDefault": "适用于你的平台",
+ "guidesSetup": "指南和设置",
+ "videoFrameLabel": "观看 Agent Teams 演示",
+ "commandFeed": "团队命令流",
+ "liveDemo": "实时演示",
+ "demoVideoTitle": "Agent Teams 演示视频",
+ "demoTitle": "Agent Teams 演示",
+ "demo": {
+ "ariaLabel": "代理团队演示",
+ "live": "LIVE",
+ "waiting": "等待任务...",
+ "activity": {
+ "authMiddleware": "正在实现身份验证中间件...",
+ "unitTests": "正在为 API 编写单元测试...",
+ "reviewPr": "正在审查 PR #42 更改...",
+ "ciPipeline": "正在设置 CI/CD 流水线...",
+ "refactorDatabase": "正在重构数据库层..."
+ },
+ "tasks": {
+ "authApi": "认证 API",
+ "unitTests": "单元测试",
+ "ciSetup": "CI 设置"
+ },
+ "columns": {
+ "todo": "待办",
+ "progress": "进行中",
+ "review": "审查",
+ "done": "完成"
+ }
+ }
},
"download": {
"title": "下载",
"detected": "已检测",
"systemRequirements": "系统要求",
- "version": "版本 {version}"
+ "version": "版本 {version}",
+ "readyToStart": "准备开始!"
},
"theme": {
"dark": "深色",
@@ -100,7 +142,10 @@
"sectionSubtitle": "应用的真实截图——看板、代码审查、智能体团队等等。"
},
"common": {
- "learnMore": "了解更多"
+ "learnMore": "了解更多",
+ "statusLabel": "状态:",
+ "previous": "上一个",
+ "next": "下一个"
},
"footer": {
"copyright": "© {year} Agent Teams",
@@ -108,6 +153,7 @@
"robotBubble": "我在等你",
"links": {
"github": "GitHub",
+ "author": "作者",
"docs": "文档"
}
},
diff --git a/mcp-server/package.json b/mcp-server/package.json
index 988eda5a..ae4a02e2 100644
--- a/mcp-server/package.json
+++ b/mcp-server/package.json
@@ -45,7 +45,7 @@
"tsup": "^8.5.1",
"tsx": "^4.21.0",
"typescript": "^5.8.2",
- "vitest": "^3.1.4"
+ "vitest": "^3.2.5"
},
"engines": {
"node": ">=24.15.0 <25"
diff --git a/package.json b/package.json
index e44bcb8f..d8ae6db0 100644
--- a/package.json
+++ b/package.json
@@ -222,7 +222,7 @@
"@types/react-dom": "^19.0.0",
"@types/ssh2": "^1.15.5",
"@vitejs/plugin-react": "^4.3.1",
- "@vitest/coverage-v8": "^3.1.4",
+ "@vitest/coverage-v8": "^3.2.5",
"autoprefixer": "^10.4.17",
"electron": "^40.10.0",
"electron-builder": "^26.8.1",
@@ -254,7 +254,7 @@
"typescript": "^5.9.3",
"typescript-eslint": "^8.54.0",
"vite": "^6.4.2",
- "vitest": "^3.1.4"
+ "vitest": "^3.2.5"
},
"build": {
"appId": "com.agent-teams.app",
@@ -413,7 +413,7 @@
"flatted": "3.4.2",
"follow-redirects": "1.16.0",
"handlebars": "4.7.9",
- "hono": "4.12.18",
+ "hono": "4.12.23",
"ip-address": "10.1.1",
"lodash": "^4.18.1",
"lodash-es": "^4.18.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 96bfbe35..7fc98b40 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -18,7 +18,7 @@ overrides:
flatted: 3.4.2
follow-redirects: 1.16.0
handlebars: 4.7.9
- hono: 4.12.18
+ hono: 4.12.23
ip-address: 10.1.1
lodash: ^4.18.1
lodash-es: ^4.18.1
@@ -443,8 +443,8 @@ importers:
specifier: ^4.3.1
version: 4.7.0(vite@6.4.2(@types/node@24.12.4)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
'@vitest/coverage-v8':
- specifier: ^3.1.4
- version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
+ specifier: ^3.2.5
+ version: 3.2.6(vitest@3.2.6(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
autoprefixer:
specifier: ^10.4.17
version: 10.4.23(postcss@8.5.10)
@@ -539,8 +539,8 @@ importers:
specifier: ^6.4.2
version: 6.4.2(@types/node@24.12.4)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
vitest:
- specifier: ^3.1.4
- version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
+ specifier: ^3.2.5
+ version: 3.2.6(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
agent-teams-controller: {}
@@ -654,8 +654,8 @@ importers:
specifier: ^5.8.2
version: 5.9.3
vitest:
- specifier: ^3.1.4
- version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
+ specifier: ^3.2.5
+ version: 3.2.6(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
packages/agent-graph:
dependencies:
@@ -1886,7 +1886,7 @@ packages:
resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==}
engines: {node: '>=18.14.1'}
peerDependencies:
- hono: 4.12.18
+ hono: 4.12.23
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
@@ -2149,8 +2149,8 @@ packages:
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
- '@istanbuljs/schema@0.1.3':
- resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
+ '@istanbuljs/schema@0.1.6':
+ resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==}
engines: {node: '>=8'}
'@jridgewell/gen-mapping@0.3.13':
@@ -5061,20 +5061,20 @@ packages:
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
vue: ^3.2.25
- '@vitest/coverage-v8@3.2.4':
- resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==}
+ '@vitest/coverage-v8@3.2.6':
+ resolution: {integrity: sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw==}
peerDependencies:
- '@vitest/browser': 3.2.4
- vitest: 3.2.4
+ '@vitest/browser': 3.2.6
+ vitest: 3.2.6
peerDependenciesMeta:
'@vitest/browser':
optional: true
- '@vitest/expect@3.2.4':
- resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
+ '@vitest/expect@3.2.6':
+ resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==}
- '@vitest/mocker@3.2.4':
- resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
+ '@vitest/mocker@3.2.6':
+ resolution: {integrity: sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==}
peerDependencies:
msw: ^2.4.9
vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
@@ -5084,20 +5084,20 @@ packages:
vite:
optional: true
- '@vitest/pretty-format@3.2.4':
- resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
+ '@vitest/pretty-format@3.2.6':
+ resolution: {integrity: sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==}
- '@vitest/runner@3.2.4':
- resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
+ '@vitest/runner@3.2.6':
+ resolution: {integrity: sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==}
- '@vitest/snapshot@3.2.4':
- resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
+ '@vitest/snapshot@3.2.6':
+ resolution: {integrity: sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==}
- '@vitest/spy@3.2.4':
- resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
+ '@vitest/spy@3.2.6':
+ resolution: {integrity: sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==}
- '@vitest/utils@3.2.4':
- resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
+ '@vitest/utils@3.2.6':
+ resolution: {integrity: sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==}
'@volar/language-core@2.4.28':
resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
@@ -5506,8 +5506,8 @@ packages:
ast-types-flow@0.0.8:
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
- ast-v8-to-istanbul@0.3.10:
- resolution: {integrity: sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==}
+ ast-v8-to-istanbul@0.3.12:
+ resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==}
ast-walker-scope@0.6.2:
resolution: {integrity: sha512-1UWOyC50xI3QZkRuDj6PqDtpm1oHWtYs+NQGwqL/2R11eN3Q81PHAHPM0SWW3BNQm53UDwS//Jv8L4CCVLM1bQ==}
@@ -7548,8 +7548,8 @@ packages:
hls.js@1.6.16:
resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==}
- hono@4.12.18:
- resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==}
+ hono@4.12.23:
+ resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==}
engines: {node: '>=16.9.0'}
hookable@5.5.3:
@@ -8030,6 +8030,9 @@ packages:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
+ js-tokens@10.0.0:
+ resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -10476,8 +10479,8 @@ packages:
engines: {node: '>=10'}
hasBin: true
- test-exclude@7.0.1:
- resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
+ test-exclude@7.0.2:
+ resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==}
engines: {node: '>=18'}
text-decoder@1.2.7:
@@ -10513,10 +10516,6 @@ packages:
tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
- tinyexec@1.0.2:
- resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
- engines: {node: '>=18'}
-
tinyexec@1.1.2:
resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==}
engines: {node: '>=18'}
@@ -11221,16 +11220,16 @@ packages:
postcss:
optional: true
- vitest@3.2.4:
- resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
+ vitest@3.2.6:
+ resolution: {integrity: sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/debug': ^4.1.12
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
- '@vitest/browser': 3.2.4
- '@vitest/ui': 3.2.4
+ '@vitest/browser': 3.2.6
+ '@vitest/ui': 3.2.6
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
@@ -11591,7 +11590,7 @@ snapshots:
'@antfu/install-pkg@1.1.0':
dependencies:
package-manager-detector: 1.6.0
- tinyexec: 1.0.2
+ tinyexec: 1.1.2
'@apidevtools/json-schema-ref-parser@14.2.1(@types/json-schema@7.0.15)':
dependencies:
@@ -11654,15 +11653,15 @@ snapshots:
'@babel/generator@7.28.6':
dependencies:
- '@babel/parser': 7.28.6
- '@babel/types': 7.28.6
+ '@babel/parser': 7.29.3
+ '@babel/types': 7.29.0
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
'@babel/generator@7.29.1':
dependencies:
- '@babel/parser': 7.29.2
+ '@babel/parser': 7.29.3
'@babel/types': 7.29.0
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
@@ -11813,7 +11812,7 @@ snapshots:
'@babel/template@7.28.6':
dependencies:
'@babel/code-frame': 7.29.0
- '@babel/parser': 7.29.2
+ '@babel/parser': 7.29.3
'@babel/types': 7.29.0
'@babel/traverse@7.28.6':
@@ -11821,9 +11820,9 @@ snapshots:
'@babel/code-frame': 7.28.6
'@babel/generator': 7.29.1
'@babel/helper-globals': 7.28.0
- '@babel/parser': 7.28.6
+ '@babel/parser': 7.29.3
'@babel/template': 7.28.6
- '@babel/types': 7.28.6
+ '@babel/types': 7.29.0
debug: 4.4.3
transitivePeerDependencies:
- supports-color
@@ -11833,7 +11832,7 @@ snapshots:
'@babel/code-frame': 7.29.0
'@babel/generator': 7.29.1
'@babel/helper-globals': 7.28.0
- '@babel/parser': 7.29.2
+ '@babel/parser': 7.29.3
'@babel/template': 7.28.6
'@babel/types': 7.29.0
debug: 4.4.3
@@ -12832,9 +12831,9 @@ snapshots:
'@floating-ui/utils@0.2.11': {}
- '@hono/node-server@1.19.13(hono@4.12.18)':
+ '@hono/node-server@1.19.13(hono@4.12.23)':
dependencies:
- hono: 4.12.18
+ hono: 4.12.23
'@humanfs/core@0.19.1': {}
@@ -13072,7 +13071,7 @@ snapshots:
'@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.3.0)(@vue/compiler-dom@3.5.34)(vue-i18n@10.0.8(vue@3.5.30(typescript@5.9.3)))(vue@3.5.30(typescript@5.9.3))':
dependencies:
- '@babel/parser': 7.29.2
+ '@babel/parser': 7.29.3
optionalDependencies:
'@intlify/shared': 11.3.0
'@vue/compiler-dom': 3.5.34
@@ -13094,7 +13093,7 @@ snapshots:
dependencies:
minipass: 7.1.3
- '@istanbuljs/schema@0.1.3': {}
+ '@istanbuljs/schema@0.1.6': {}
'@jridgewell/gen-mapping@0.3.13':
dependencies:
@@ -13267,7 +13266,7 @@ snapshots:
'@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)':
dependencies:
- '@hono/node-server': 1.19.13(hono@4.12.18)
+ '@hono/node-server': 1.19.13(hono@4.12.23)
ajv: 8.18.0
ajv-formats: 3.0.1(ajv@8.18.0)
content-type: 1.0.5
@@ -13277,7 +13276,7 @@ snapshots:
eventsource-parser: 3.0.6
express: 5.2.1
express-rate-limit: 8.3.0(express@5.2.1)
- hono: 4.12.18
+ hono: 4.12.23
jose: 6.2.0
json-schema-typed: 8.0.2
pkce-challenge: 5.0.1
@@ -13532,8 +13531,8 @@ snapshots:
pkg-types: 2.3.0
rc9: 3.0.0
scule: 1.3.0
- semver: 7.7.4
- tinyglobby: 0.2.15
+ semver: 7.8.0
+ tinyglobby: 0.2.16
ufo: 1.6.3
unctx: 2.5.0
untyped: 2.0.0
@@ -15662,16 +15661,16 @@ snapshots:
'@types/babel__generator@7.27.0':
dependencies:
- '@babel/types': 7.28.6
+ '@babel/types': 7.29.0
'@types/babel__template@7.4.4':
dependencies:
- '@babel/parser': 7.28.6
- '@babel/types': 7.28.6
+ '@babel/parser': 7.29.3
+ '@babel/types': 7.29.0
'@types/babel__traverse@7.28.0':
dependencies:
- '@babel/types': 7.28.6
+ '@babel/types': 7.29.0
'@types/cacheable-request@6.0.3':
dependencies:
@@ -16080,7 +16079,7 @@ snapshots:
'@typescript-eslint/visitor-keys': 8.57.1
debug: 4.4.3
minimatch: 10.2.3
- semver: 7.7.4
+ semver: 7.8.0
tinyglobby: 0.2.16
ts-api-utils: 2.4.0(typescript@5.9.3)
typescript: 5.9.3
@@ -16258,11 +16257,11 @@ snapshots:
vite: 7.3.3(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
vue: 3.5.34(typescript@5.9.3)
- '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))':
+ '@vitest/coverage-v8@3.2.6(vitest@3.2.6(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
- ast-v8-to-istanbul: 0.3.10
+ ast-v8-to-istanbul: 0.3.12
debug: 4.4.3
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
@@ -16271,59 +16270,59 @@ snapshots:
magic-string: 0.30.21
magicast: 0.3.5
std-env: 3.10.0
- test-exclude: 7.0.1
+ test-exclude: 7.0.2
tinyrainbow: 2.0.0
- vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
+ vitest: 3.2.6(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
transitivePeerDependencies:
- supports-color
- '@vitest/expect@3.2.4':
+ '@vitest/expect@3.2.6':
dependencies:
'@types/chai': 5.2.3
- '@vitest/spy': 3.2.4
- '@vitest/utils': 3.2.4
+ '@vitest/spy': 3.2.6
+ '@vitest/utils': 3.2.6
chai: 5.3.3
tinyrainbow: 2.0.0
- '@vitest/mocker@3.2.4(vite@7.3.2(@types/node@24.12.4)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))':
+ '@vitest/mocker@3.2.6(vite@6.4.2(@types/node@24.12.4)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))':
dependencies:
- '@vitest/spy': 3.2.4
+ '@vitest/spy': 3.2.6
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 7.3.2(@types/node@24.12.4)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
+ vite: 6.4.2(@types/node@24.12.4)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
- '@vitest/mocker@3.2.4(vite@7.3.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))':
+ '@vitest/mocker@3.2.6(vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))':
dependencies:
- '@vitest/spy': 3.2.4
+ '@vitest/spy': 3.2.6
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 7.3.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
+ vite: 6.4.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
- '@vitest/pretty-format@3.2.4':
+ '@vitest/pretty-format@3.2.6':
dependencies:
tinyrainbow: 2.0.0
- '@vitest/runner@3.2.4':
+ '@vitest/runner@3.2.6':
dependencies:
- '@vitest/utils': 3.2.4
+ '@vitest/utils': 3.2.6
pathe: 2.0.3
strip-literal: 3.1.0
- '@vitest/snapshot@3.2.4':
+ '@vitest/snapshot@3.2.6':
dependencies:
- '@vitest/pretty-format': 3.2.4
+ '@vitest/pretty-format': 3.2.6
magic-string: 0.30.21
pathe: 2.0.3
- '@vitest/spy@3.2.4':
+ '@vitest/spy@3.2.6':
dependencies:
tinyspy: 4.0.4
- '@vitest/utils@3.2.4':
+ '@vitest/utils@3.2.6':
dependencies:
- '@vitest/pretty-format': 3.2.4
+ '@vitest/pretty-format': 3.2.6
loupe: 3.2.1
tinyrainbow: 2.0.0
@@ -16378,14 +16377,14 @@ snapshots:
'@babel/core': 7.29.0
'@babel/helper-module-imports': 7.28.6
'@babel/helper-plugin-utils': 7.28.6
- '@babel/parser': 7.29.2
+ '@babel/parser': 7.29.3
'@vue/compiler-sfc': 3.5.30
transitivePeerDependencies:
- supports-color
'@vue/compiler-core@3.5.30':
dependencies:
- '@babel/parser': 7.29.2
+ '@babel/parser': 7.29.3
'@vue/shared': 3.5.30
entities: 7.0.1
estree-walker: 2.0.2
@@ -16411,7 +16410,7 @@ snapshots:
'@vue/compiler-sfc@3.5.30':
dependencies:
- '@babel/parser': 7.29.2
+ '@babel/parser': 7.29.3
'@vue/compiler-core': 3.5.30
'@vue/compiler-dom': 3.5.30
'@vue/compiler-ssr': 3.5.30
@@ -16859,30 +16858,30 @@ snapshots:
ast-kit@1.4.3:
dependencies:
- '@babel/parser': 7.29.2
+ '@babel/parser': 7.29.3
pathe: 2.0.3
ast-kit@2.2.0:
dependencies:
- '@babel/parser': 7.29.2
+ '@babel/parser': 7.29.3
pathe: 2.0.3
ast-types-flow@0.0.8: {}
- ast-v8-to-istanbul@0.3.10:
+ ast-v8-to-istanbul@0.3.12:
dependencies:
'@jridgewell/trace-mapping': 0.3.31
estree-walker: 3.0.3
- js-tokens: 9.0.1
+ js-tokens: 10.0.0
ast-walker-scope@0.6.2:
dependencies:
- '@babel/parser': 7.29.2
+ '@babel/parser': 7.29.3
ast-kit: 1.4.3
ast-walker-scope@0.8.3:
dependencies:
- '@babel/parser': 7.29.2
+ '@babel/parser': 7.29.3
ast-kit: 2.2.0
astral-regex@2.0.0:
@@ -18400,7 +18399,7 @@ snapshots:
eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
is-glob: 4.0.3
minimatch: 9.0.7
- semver: 7.7.4
+ semver: 7.8.0
stable-hash-x: 0.2.0
unrs-resolver: 1.11.1
optionalDependencies:
@@ -18420,7 +18419,7 @@ snapshots:
eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
is-glob: 4.0.3
minimatch: 9.0.7
- semver: 7.7.4
+ semver: 7.8.0
stable-hash-x: 0.2.0
unrs-resolver: 1.11.1
optionalDependencies:
@@ -18472,7 +18471,7 @@ snapshots:
html-entities: 2.6.0
object-deep-merge: 2.0.0
parse-imports-exports: 0.2.4
- semver: 7.7.4
+ semver: 7.8.0
spdx-expression-parse: 4.0.0
to-valid-identifier: 1.0.0
transitivePeerDependencies:
@@ -18590,7 +18589,7 @@ snapshots:
pluralize: 8.0.0
regexp-tree: 0.1.27
regjsparser: 0.13.0
- semver: 7.7.4
+ semver: 7.8.0
strip-indent: 4.1.1
eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4(jiti@2.7.0)))(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.7.0))):
@@ -18600,7 +18599,7 @@ snapshots:
natural-compare: 1.4.0
nth-check: 2.1.1
postcss-selector-parser: 7.1.1
- semver: 7.7.4
+ semver: 7.8.0
vue-eslint-parser: 10.4.0(eslint@9.39.4(jiti@2.7.0))
xml-name-validator: 4.0.0
optionalDependencies:
@@ -18956,7 +18955,7 @@ snapshots:
execa: 9.6.1
file-type: 21.3.2
fuse.js: 7.3.0
- hono: 4.12.18
+ hono: 4.12.23
mcp-proxy: 6.4.1
strict-event-emitter-types: 2.0.0
undici: 7.24.0
@@ -19269,7 +19268,7 @@ snapshots:
es6-error: 4.1.1
matcher: 3.0.0
roarr: 2.15.4
- semver: 7.7.4
+ semver: 7.8.0
serialize-error: 7.0.1
optional: true
@@ -19513,7 +19512,7 @@ snapshots:
hls.js@1.6.16: {}
- hono@4.12.18: {}
+ hono@4.12.23: {}
hookable@5.5.3: {}
@@ -19992,6 +19991,8 @@ snapshots:
joycon@3.1.1: {}
+ js-tokens@10.0.0: {}
+
js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
@@ -20042,7 +20043,7 @@ snapshots:
acorn: 8.16.0
eslint-visitor-keys: 3.4.3
espree: 9.6.1
- semver: 7.7.4
+ semver: 7.8.0
jsonc-parser@3.3.1: {}
@@ -20325,19 +20326,19 @@ snapshots:
magicast@0.3.5:
dependencies:
- '@babel/parser': 7.29.2
+ '@babel/parser': 7.29.3
'@babel/types': 7.29.0
source-map-js: 1.2.1
magicast@0.5.2:
dependencies:
- '@babel/parser': 7.29.2
+ '@babel/parser': 7.29.3
'@babel/types': 7.29.0
source-map-js: 1.2.1
make-dir@4.0.0:
dependencies:
- semver: 7.7.4
+ semver: 7.8.0
mark.js@8.11.1: {}
@@ -21243,7 +21244,7 @@ snapshots:
dependencies:
citty: 0.2.2
pathe: 2.0.3
- tinyexec: 1.0.2
+ tinyexec: 1.1.2
nypm@0.6.6:
dependencies:
@@ -23147,11 +23148,11 @@ snapshots:
commander: 2.20.3
source-map-support: 0.5.21
- test-exclude@7.0.1:
+ test-exclude@7.0.2:
dependencies:
- '@istanbuljs/schema': 0.1.3
+ '@istanbuljs/schema': 0.1.6
glob: 10.5.0
- minimatch: 9.0.7
+ minimatch: 10.2.3
text-decoder@1.2.7:
dependencies:
@@ -23185,8 +23186,6 @@ snapshots:
tinyexec@0.3.2: {}
- tinyexec@1.0.2: {}
-
tinyexec@1.1.2: {}
tinyglobby@0.2.15:
@@ -23743,7 +23742,7 @@ snapshots:
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
- vite: 7.3.2(@types/node@24.12.4)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
+ vite: 6.4.2(@types/node@24.12.4)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -23764,7 +23763,7 @@ snapshots:
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
- vite: 7.3.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
+ vite: 6.4.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -23871,26 +23870,9 @@ snapshots:
tsx: 4.21.0
yaml: 2.9.0
- vite@7.3.2(@types/node@24.12.4)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0):
+ vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0):
dependencies:
- esbuild: 0.27.4
- fdir: 6.5.0(picomatch@4.0.4)
- picomatch: 4.0.4
- postcss: 8.5.10
- rollup: 4.59.0
- tinyglobby: 0.2.15
- optionalDependencies:
- '@types/node': 24.12.4
- fsevents: 2.3.3
- jiti: 1.21.7
- sass: 1.98.0
- terser: 5.46.0
- tsx: 4.21.0
- yaml: 2.9.0
-
- vite@7.3.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0):
- dependencies:
- esbuild: 0.27.4
+ esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
postcss: 8.5.10
@@ -23912,7 +23894,7 @@ snapshots:
picomatch: 4.0.4
postcss: 8.5.10
rollup: 4.59.0
- tinyglobby: 0.2.15
+ tinyglobby: 0.2.16
optionalDependencies:
'@types/node': 25.0.7
fsevents: 2.3.3
@@ -24011,16 +23993,16 @@ snapshots:
- universal-cookie
- yaml
- vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0):
+ vitest@3.2.6(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0):
dependencies:
'@types/chai': 5.2.3
- '@vitest/expect': 3.2.4
- '@vitest/mocker': 3.2.4(vite@7.3.2(@types/node@24.12.4)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
- '@vitest/pretty-format': 3.2.4
- '@vitest/runner': 3.2.4
- '@vitest/snapshot': 3.2.4
- '@vitest/spy': 3.2.4
- '@vitest/utils': 3.2.4
+ '@vitest/expect': 3.2.6
+ '@vitest/mocker': 3.2.6(vite@6.4.2(@types/node@24.12.4)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
+ '@vitest/pretty-format': 3.2.6
+ '@vitest/runner': 3.2.6
+ '@vitest/snapshot': 3.2.6
+ '@vitest/spy': 3.2.6
+ '@vitest/utils': 3.2.6
chai: 5.3.3
debug: 4.4.3
expect-type: 1.3.0
@@ -24030,10 +24012,10 @@ snapshots:
std-env: 3.10.0
tinybench: 2.9.0
tinyexec: 0.3.2
- tinyglobby: 0.2.15
+ tinyglobby: 0.2.16
tinypool: 1.1.1
tinyrainbow: 2.0.0
- vite: 7.3.2(@types/node@24.12.4)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
+ vite: 6.4.2(@types/node@24.12.4)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
vite-node: 3.2.4(@types/node@24.12.4)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
@@ -24054,16 +24036,16 @@ snapshots:
- tsx
- yaml
- vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0):
+ vitest@3.2.6(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0):
dependencies:
'@types/chai': 5.2.3
- '@vitest/expect': 3.2.4
- '@vitest/mocker': 3.2.4(vite@7.3.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
- '@vitest/pretty-format': 3.2.4
- '@vitest/runner': 3.2.4
- '@vitest/snapshot': 3.2.4
- '@vitest/spy': 3.2.4
- '@vitest/utils': 3.2.4
+ '@vitest/expect': 3.2.6
+ '@vitest/mocker': 3.2.6(vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
+ '@vitest/pretty-format': 3.2.6
+ '@vitest/runner': 3.2.6
+ '@vitest/snapshot': 3.2.6
+ '@vitest/spy': 3.2.6
+ '@vitest/utils': 3.2.6
chai: 5.3.3
debug: 4.4.3
expect-type: 1.3.0
@@ -24073,10 +24055,10 @@ snapshots:
std-env: 3.10.0
tinybench: 2.9.0
tinyexec: 0.3.2
- tinyglobby: 0.2.15
+ tinyglobby: 0.2.16
tinypool: 1.1.1
tinyrainbow: 2.0.0
- vite: 7.3.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
+ vite: 6.4.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
vite-node: 3.2.4(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
@@ -24119,7 +24101,7 @@ snapshots:
eslint-visitor-keys: 5.0.1
espree: 11.2.0
esquery: 1.7.0
- semver: 7.7.4
+ semver: 7.8.0
transitivePeerDependencies:
- supports-color
diff --git a/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts b/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts
index 978dc70a..347702f7 100644
--- a/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts
+++ b/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts
@@ -85,6 +85,10 @@ export interface TeamGraphData extends TeamViewSnapshot {
runtimeEntriesByMember?: Record
;
}
+export interface TeamGraphAdapterText {
+ hiddenBlockingLinks(count: number): string;
+}
+
function toGraphLaunchVisualState(
visualState: ReturnType['launchVisualState'] | undefined
): GraphNode['launchVisualState'] {
@@ -141,7 +145,8 @@ export class TeamGraphAdapter {
slotAssignments?: Record,
layoutMode: GraphLayoutMode = DEFAULT_TEAM_GRAPH_LAYOUT_MODE,
gridOwnerOrder?: readonly string[],
- activeTaskLogActivity?: Record
+ activeTaskLogActivity?: Record,
+ text?: TeamGraphAdapterText
): GraphDataPort {
if (teamData?.teamName !== teamName) {
return TeamGraphAdapter.#emptyResult(teamName);
@@ -227,7 +232,8 @@ export class TeamGraphAdapter {
memberNodeIdByAlias,
leadId,
leadName,
- activeTaskLogActivity
+ activeTaskLogActivity,
+ text
);
this.#buildProcessNodes(nodes, edges, teamData, teamName, memberNodeIdByAlias);
this.#attachActivityFeeds(nodes, teamData, teamName, leadId, leadName);
@@ -673,7 +679,8 @@ export class TeamGraphAdapter {
memberNodeIdByAlias?: ReadonlyMap,
leadId?: string,
leadName?: string,
- activeTaskLogActivity?: Record
+ activeTaskLogActivity?: Record,
+ text?: TeamGraphAdapterText
): void {
const taskStateById = new Map<
string,
@@ -915,9 +922,10 @@ export class TeamGraphAdapter {
sourceTaskIds: Array.from(edge.sourceTaskIds),
targetTaskIds: Array.from(edge.targetTaskIds),
label:
- edge.aggregateCount > 1 &&
- (edge.source.includes(':overflow:') || edge.target.includes(':overflow:'))
- ? `${edge.aggregateCount} hidden blocking links`
+ edge.aggregateCount > 1 &&
+ (edge.source.includes(':overflow:') || edge.target.includes(':overflow:'))
+ ? (text?.hiddenBlockingLinks(edge.aggregateCount) ??
+ `${edge.aggregateCount} hidden blocking links`)
: undefined,
}))
);
diff --git a/src/features/agent-graph/renderer/hooks/useTeamGraphAdapter.ts b/src/features/agent-graph/renderer/hooks/useTeamGraphAdapter.ts
index 5299417f..e0ccc949 100644
--- a/src/features/agent-graph/renderer/hooks/useTeamGraphAdapter.ts
+++ b/src/features/agent-graph/renderer/hooks/useTeamGraphAdapter.ts
@@ -5,6 +5,7 @@
import { useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react';
+import { useAppTranslation } from '@features/localization/renderer';
import { useTeamAgentRuntimeWatcher } from '@renderer/components/team/useTeamAgentRuntimeWatcher';
import { getSnapshot, subscribe } from '@renderer/services/commentReadStorage';
import { useStore } from '@renderer/store';
@@ -65,9 +66,17 @@ export function useTeamGraphAdapter(
options?: UseTeamGraphAdapterOptions
): GraphDataPort {
const isActive = options?.active ?? true;
+ const { t } = useAppTranslation('team');
const adapterRef = useRef(TeamGraphAdapter.create());
const inactiveGraphData = useMemo(() => emptyGraphData(teamName), [teamName]);
const lastActiveGraphDataRef = useRef(inactiveGraphData);
+ const adapterText = useMemo(
+ () => ({
+ hiddenBlockingLinks: (count: number) =>
+ t('agentGraph.blockingEdge.hiddenBlockingLinks', { count }),
+ }),
+ [t]
+ );
const {
teamSnapshot,
@@ -216,7 +225,8 @@ export function useTeamGraphAdapter(
effectiveSlotAssignments,
graphLayoutMode ?? DEFAULT_TEAM_GRAPH_LAYOUT_MODE,
gridOwnerOrder,
- activeTaskLogActivity
+ activeTaskLogActivity,
+ adapterText
);
}, [
isActive,
@@ -236,6 +246,7 @@ export function useTeamGraphAdapter(
graphLayoutMode,
gridOwnerOrder,
activeTaskLogActivity,
+ adapterText,
]);
useLayoutEffect(() => {
diff --git a/src/features/agent-graph/renderer/ui/GraphBlockingEdgePopover.tsx b/src/features/agent-graph/renderer/ui/GraphBlockingEdgePopover.tsx
index 27db9063..674db64d 100644
--- a/src/features/agent-graph/renderer/ui/GraphBlockingEdgePopover.tsx
+++ b/src/features/agent-graph/renderer/ui/GraphBlockingEdgePopover.tsx
@@ -21,26 +21,47 @@ function isOverflowNode(
return Boolean(node?.kind === 'task' && node.isOverflowStack);
}
-function describeNode(node: GraphNode | undefined, fallback: string): string {
+interface BlockingEdgeLabels {
+ hiddenTaskStack: string;
+ hiddenTasks: (count: number) => string;
+ task: string;
+ openBlockerStack: string;
+ openBlockedStack: string;
+ openBlockerTask: string;
+ openBlockedTask: string;
+}
+
+function describeNode(
+ node: GraphNode | undefined,
+ fallback: string,
+ labels: Pick
+): string {
if (!node) return fallback;
if (isOverflowNode(node)) {
return node.overflowCount && node.overflowCount > 1
- ? `${node.overflowCount} hidden tasks`
- : 'Hidden task stack';
+ ? labels.hiddenTasks(node.overflowCount)
+ : labels.hiddenTaskStack;
}
if (isTaskNode(node)) {
- return `${node.displayId ?? node.label} - ${node.sublabel ?? 'Task'}`;
+ return `${node.displayId ?? node.label} - ${node.sublabel ?? labels.task}`;
}
return node.label;
}
-function getActionLabel(node: GraphNode | undefined, role: 'blocker' | 'blocked'): string | null {
+function getActionLabel(
+ node: GraphNode | undefined,
+ role: 'blocker' | 'blocked',
+ labels: Pick<
+ BlockingEdgeLabels,
+ 'openBlockerStack' | 'openBlockedStack' | 'openBlockerTask' | 'openBlockedTask'
+ >
+): string | null {
if (!node) return null;
if (isOverflowNode(node)) {
- return role === 'blocker' ? 'Open blocker stack' : 'Open blocked stack';
+ return role === 'blocker' ? labels.openBlockerStack : labels.openBlockedStack;
}
if (isTaskNode(node)) {
- return role === 'blocker' ? 'Open blocker task' : 'Open blocked task';
+ return role === 'blocker' ? labels.openBlockerTask : labels.openBlockedTask;
}
return null;
}
@@ -71,10 +92,19 @@ export const GraphBlockingEdgePopover = ({
[teamData?.tasks]
);
const relationCount = edge.aggregateCount ?? 1;
- const sourceLabel = describeNode(sourceNode, edge.source);
- const targetLabel = describeNode(targetNode, edge.target);
- const sourceActionLabel = getActionLabel(sourceNode, 'blocker');
- const targetActionLabel = getActionLabel(targetNode, 'blocked');
+ const labels: BlockingEdgeLabels = {
+ hiddenTaskStack: t('agentGraph.blockingEdge.hiddenTaskStack'),
+ hiddenTasks: (count) => t('agentGraph.blockingEdge.hiddenTasks', { count }),
+ task: t('agentGraph.blockingEdge.task'),
+ openBlockerStack: t('agentGraph.blockingEdge.openBlockerStack'),
+ openBlockedStack: t('agentGraph.blockingEdge.openBlockedStack'),
+ openBlockerTask: t('agentGraph.blockingEdge.openBlockerTask'),
+ openBlockedTask: t('agentGraph.blockingEdge.openBlockedTask'),
+ };
+ const sourceLabel = describeNode(sourceNode, edge.source, labels);
+ const targetLabel = describeNode(targetNode, edge.target, labels);
+ const sourceActionLabel = getActionLabel(sourceNode, 'blocker', labels);
+ const targetActionLabel = getActionLabel(targetNode, 'blocked', labels);
const sourceHiddenTasks = resolveEdgeTaskPreview(sourceNode, edge.sourceTaskIds, tasksById);
const targetHiddenTasks = resolveEdgeTaskPreview(targetNode, edge.targetTaskIds, tasksById);
@@ -111,7 +141,7 @@ export const GraphBlockingEdgePopover = ({
variant="outline"
className="border-red-500/30 px-1.5 py-0 text-[10px] text-red-300"
>
- {relationCount} links
+ {t('agentGraph.blockingEdge.links', { count: relationCount })}
)}
diff --git a/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx b/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx
index 51e32def..808caffe 100644
--- a/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx
+++ b/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx
@@ -132,7 +132,14 @@ function hasOpenCodeEmptyStateWarning(preview: MemberLogPreviewMember | undefine
function resolveEmptyText(
preview: MemberLogPreviewMember | undefined,
loading: boolean,
- error: string | null
+ error: string | null,
+ labels: {
+ unsupportedProvider: string;
+ openCodeLogsDelayed: string;
+ logsUnavailable: string;
+ loadingLogs: string;
+ noRecentLogs: string;
+ }
): string {
const hasCodexUnsupportedWarning = preview?.warnings.some(
(warning) => warning.code === 'codex_member_wide_not_supported'
@@ -142,34 +149,47 @@ function resolveEmptyText(
(preview?.coverage.length ?? 0) > 0 &&
preview?.coverage.every((coverage) => coverage.provider === 'codex_native_trace');
if (hasOnlyCodexUnsupportedCoverage) {
- return 'Unsupported provider';
+ return labels.unsupportedProvider;
}
if ((preview?.items.length ?? 0) === 0 && hasOpenCodeDeliveryDelayedWarning(preview)) {
- return 'OpenCode logs delayed';
+ return labels.openCodeLogsDelayed;
}
if ((preview?.items.length ?? 0) === 0 && hasOpenCodeRuntimeWarning(preview)) {
- return 'Logs unavailable';
+ return labels.logsUnavailable;
}
- if (loading && !preview) return 'Loading logs';
- if (error && !preview) return 'Logs unavailable';
- return 'No recent logs';
+ if (loading && !preview) return labels.loadingLogs;
+ if (error && !preview) return labels.logsUnavailable;
+ return labels.noRecentLogs;
}
-function fallbackDisplayTitle(item: MemberLogPreviewItem): string {
+function fallbackDisplayTitle(
+ item: MemberLogPreviewItem,
+ labels: {
+ toolError: string;
+ toolResult: string;
+ toolUse: string;
+ thinking: string;
+ error: string;
+ logEvent: string;
+ }
+): string {
if (item.kind === 'tool_result') {
- return item.tone === 'error' ? 'Tool error' : 'Tool result';
+ return item.tone === 'error' ? labels.toolError : labels.toolResult;
}
if (item.kind === 'tool_use') {
- return item.toolName?.trim() || 'Tool use';
+ return item.toolName?.trim() || labels.toolUse;
}
if (item.kind === 'thinking') {
- return 'Thinking';
+ return labels.thinking;
}
- return item.tone === 'error' ? 'Error' : 'Log event';
+ return item.tone === 'error' ? labels.error : labels.logEvent;
}
-function compactDisplayTitle(item: MemberLogPreviewItem): string {
- const title = item.title.trim() || fallbackDisplayTitle(item);
+function compactDisplayTitle(
+ item: MemberLogPreviewItem,
+ labels: Parameters
[1]
+): string {
+ const title = item.title.trim() || fallbackDisplayTitle(item, labels);
if (title.toLowerCase() === 'tool result') {
return title;
}
@@ -205,7 +225,13 @@ function trimRepeatedTitlePrefix(preview: string, title: string): string {
function compactPreviewText(
item: MemberLogPreviewItem,
displayTitle: string,
- rawDisplayTitle = displayTitle
+ rawDisplayTitle = displayTitle,
+ labels: {
+ noErrorOutput: string;
+ noOutput: string;
+ noInput: string;
+ logEvent: string;
+ }
): string {
const preview = item.preview?.trim();
if (preview) {
@@ -217,12 +243,12 @@ function compactPreviewText(
return compact || preview;
}
if (item.kind === 'tool_result') {
- return item.tone === 'error' ? 'No error output' : 'No output';
+ return item.tone === 'error' ? labels.noErrorOutput : labels.noOutput;
}
if (item.kind === 'tool_use') {
- return 'No input';
+ return labels.noInput;
}
- return item.sourceLabel || 'Log event';
+ return item.sourceLabel || labels.logEvent;
}
function truncateCompactRowPreview(
@@ -281,6 +307,25 @@ export const GraphMemberLogPreviewHud = ({
onOpenMemberProfile,
}: GraphMemberLogPreviewHudProps): React.JSX.Element | null => {
const { t } = useAppTranslation('team');
+ const logPreviewLabels = useMemo(
+ () => ({
+ unsupportedProvider: t('agentGraph.logPreview.unsupportedProvider'),
+ openCodeLogsDelayed: t('agentGraph.logPreview.openCodeLogsDelayed'),
+ logsUnavailable: t('agentGraph.logPreview.logsUnavailable'),
+ loadingLogs: t('agentGraph.logPreview.loading'),
+ noRecentLogs: t('agentGraph.logPreview.noRecentLogs'),
+ toolError: t('agentGraph.logPreview.toolError'),
+ toolResult: t('agentGraph.logPreview.toolResult'),
+ toolUse: t('agentGraph.logPreview.toolUse'),
+ thinking: t('agentGraph.logPreview.thinking'),
+ error: t('agentGraph.logPreview.error'),
+ logEvent: t('agentGraph.logPreview.logEvent'),
+ noErrorOutput: t('agentGraph.logPreview.noErrorOutput'),
+ noOutput: t('agentGraph.logPreview.noOutput'),
+ noInput: t('agentGraph.logPreview.noInput'),
+ }),
+ [t]
+ );
const worldLayerRef = useRef(null);
const shellRefs = useRef(new Map());
const visibleKeyRef = useRef('');
@@ -514,9 +559,14 @@ export const GraphMemberLogPreviewHud = ({
const renderItem = useCallback(
(memberName: string, item: MemberLogPreviewItem) => {
const relativeTime = formatRelativeTime(item.timestamp);
- const rawDisplayTitle = compactDisplayTitle(item);
+ const rawDisplayTitle = compactDisplayTitle(item, logPreviewLabels);
const displayTitle = truncateCompactTitle(rawDisplayTitle);
- const fullPreviewText = compactPreviewText(item, displayTitle, rawDisplayTitle);
+ const fullPreviewText = compactPreviewText(
+ item,
+ displayTitle,
+ rawDisplayTitle,
+ logPreviewLabels
+ );
const previewText = truncateCompactRowPreview(fullPreviewText, displayTitle, relativeTime);
const titleText = compactRowLabel([rawDisplayTitle, relativeTime, fullPreviewText]);
const isHighlighted = highlightedItemIds.has(buildRenderedItemKey(memberName, item.id));
@@ -565,7 +615,7 @@ export const GraphMemberLogPreviewHud = ({
);
},
- [highlightedItemIds, openLogs]
+ [highlightedItemIds, logPreviewLabels, openLogs]
);
if (!enabled || ownerNodes.length === 0) {
@@ -631,7 +681,7 @@ export const GraphMemberLogPreviewHud = ({
className={`${INTERACTIVE_LOG_CONTROL_CLASS} flex h-[72px] min-h-[72px] items-center rounded-md border border-dashed border-white/10 bg-[rgba(8,14,28,0.28)] px-3 text-left text-[11px] text-slate-400/60`}
onClick={() => openLogs(memberName)}
>
- {resolveEmptyText(preview, loading, error)}
+ {resolveEmptyText(preview, loading, error, logPreviewLabels)}
)}
{preview && preview.overflowCount > 0 ? (
diff --git a/src/features/anthropic-runtime-profile/core/domain/resolveAnthropicRuntimeProfile.ts b/src/features/anthropic-runtime-profile/core/domain/resolveAnthropicRuntimeProfile.ts
index 35f466de..23d286a3 100644
--- a/src/features/anthropic-runtime-profile/core/domain/resolveAnthropicRuntimeProfile.ts
+++ b/src/features/anthropic-runtime-profile/core/domain/resolveAnthropicRuntimeProfile.ts
@@ -223,8 +223,8 @@ export function resolveAnthropicFastMode(params: {
'Fast mode is not supported by this Anthropic runtime.';
} else if (!params.selection.supportsFastMode) {
disabledReason = params.selection.displayName
- ? `Fast mode is available only for Opus 4.6. Selected model resolves to ${params.selection.displayName}.`
- : 'Fast mode is available only for Opus 4.6.';
+ ? `Fast mode is available only for Opus 4.8. Selected model resolves to ${params.selection.displayName}.`
+ : 'Fast mode is available only for Opus 4.8.';
} else if (!params.selection.providerFastModeAvailable) {
disabledReason =
params.selection.providerFastModeReason ?? 'Fast mode is currently unavailable.';
diff --git a/src/features/localization/renderer/locales/zh/common.json b/src/features/localization/renderer/locales/zh/common.json
index a9112964..fb2a9045 100644
--- a/src/features/localization/renderer/locales/zh/common.json
+++ b/src/features/localization/renderer/locales/zh/common.json
@@ -15,49 +15,49 @@
"copyToClipboard": "复制到剪贴板",
"moreActions": "更多操作",
"closeDialog": "关闭对话框",
- "goToDashboard": "转到仪表盘",
+ "goToDashboard": "转到控制台",
"or": "或",
"hide": "隐藏",
"resetSelection": "重置选择"
},
"code": {
- "line": "{{line}}线",
- "lines": "{{from}}-{{to}}",
- "moreLines": "(单位:千美元){{count}}更多线条... )",
- "moreLines_few": "(单位:千美元){{count}}更多线条... )",
- "moreLines_many": "(单位:千美元){{count}}更多线条... )",
- "moreLines_one": "(单位:千美元){{count}}更多线条... )",
- "moreLines_other": "(单位:千美元){{count}}更多线条... )",
+ "line": "第 {{line}} 行",
+ "lines": "第 {{from}}-{{to}} 行",
+ "moreLines": "({{count}} 更多行…)",
+ "moreLines_few": "({{count}} 更多行…)",
+ "moreLines_many": "({{count}} 更多行…)",
+ "moreLines_one": "({{count}} 更多行…)",
+ "moreLines_other": "({{count}} 更多行…)",
"code": "代码",
"preview": "预览",
- "markdownPreview": "标记下预览",
- "linesParenthesized": "(行数){{from}}- 怎么样?{{to}}页:1",
- "mermaidSyntaxError": "美人鱼语法错误"
+ "markdownPreview": "Markdown 预览",
+ "linesParenthesized": "(第 {{from}}-{{to}} 行)",
+ "mermaidSyntaxError": "Mermaid 语法错误"
},
"contextBadge": {
- "badge": "背景情况",
+ "badge": "上下文",
"breakdown": {
- "text": "文字",
- "thinking": "思维"
+ "text": "文本",
+ "thinking": "思考"
},
"detailsAria": "上下文注入细节",
- "sectionSummary": "{{title}} ({{count}}) ~ {{tokens}} 标志",
+ "sectionSummary": "{{title}} ({{count}}) ~{{tokens}} Token",
"sections": {
- "claudeMdFiles": "CLAUDE.md 文档",
- "mentionedFiles": "引用文件",
+ "claudeMdFiles": "CLAUDE.md 文件",
+ "mentionedFiles": "提及的文件",
"taskCoordination": "任务协调",
- "thinkingText": "思维+ 文字",
+ "thinkingText": "思考 + 文本",
"toolOutputs": "工具输出",
- "userMessages": "用户信件"
+ "userMessages": "用户消息"
},
- "title": "在此转弯时输入新上下文",
- "tokenCount": "~{{tokens}} 符号",
- "totalNewTokens": "新证券共计",
- "turn": "转动 {{turn}}",
- "sectionSummary_few": "{{title}} ({{count}}) ~ {{tokens}} 标志",
- "sectionSummary_many": "{{title}} ({{count}}) ~ {{tokens}} 标志",
- "sectionSummary_one": "{{title}} ({{count}}) ~ {{tokens}} 标志",
- "sectionSummary_other": "{{title}} ({{count}}) ~ {{tokens}} 标志"
+ "title": "本轮注入的新上下文",
+ "tokenCount": "~{{tokens}} Token",
+ "totalNewTokens": "新增 Token 总数",
+ "turn": "第 {{turn}} 轮",
+ "sectionSummary_few": "{{title}} ({{count}}) ~{{tokens}} Token",
+ "sectionSummary_many": "{{title}} ({{count}}) ~{{tokens}} Token",
+ "sectionSummary_one": "{{title}} ({{count}}) ~{{tokens}} Token",
+ "sectionSummary_other": "{{title}} ({{count}}) ~{{tokens}} Token"
},
"locales": {
"emptyMessage": "未找到语言。",
@@ -93,120 +93,120 @@
"vi": "越南语",
"zh": "中文"
},
- "searchPlaceholder": "搜索语言...",
- "selectPlaceholder": "选择应用语言...",
- "systemWithResolved": "系统 - {{locale}}"
+ "searchPlaceholder": "搜索语言…",
+ "selectPlaceholder": "选择应用语言…",
+ "systemWithResolved": "系统-{{locale}}"
},
"members": {
"emptyMessage": "未找到成员。",
- "searchPlaceholder": "搜索成员...",
+ "searchPlaceholder": "搜索成员…",
"unassigned": "未分配",
"teammateFallback": "队友"
},
"providerRuntime": {
"codex": {
"install": {
- "checking": "检查中",
- "downloading": "下载",
- "installCli": "安装代码CLI",
- "installing": "安装",
+ "checking": "检查",
+ "downloading": "正在下载",
+ "installCli": "安装 Codex CLI",
+ "installing": "安装中",
"retryInstall": "重试安装"
}
}
},
"search": {
"noMatchingSuggestions": "没有匹配的建议",
- "searching": "搜索中...",
- "searchingFiles": "正在搜索文件...",
- "findInConversation": "在对话中查找...",
+ "searching": "正在搜索…",
+ "searchingFiles": "正在搜索文件…",
+ "findInConversation": "在对话中查找…",
"resultCount": "{{current}} / {{total}}",
"resultCountCapped": "{{current}} / {{total}}+",
- "noResults": "无结果",
- "previousResultShortcut": "上一个结果 (Shift+Enter)",
- "nextResultShortcut": "下一个结果 (Enter)",
- "closeShortcut": "关闭 (Esc)",
- "nothingFound": "未找到内容",
- "placeholder": "搜索..."
+ "noResults": "没有结果",
+ "previousResultShortcut": "上一个结果(Shift+Enter)",
+ "nextResultShortcut": "下一个结果(Enter)",
+ "closeShortcut": "关闭(Esc)",
+ "nothingFound": "未找到任何内容",
+ "placeholder": "搜索…"
},
"schedules": {
"actions": {
- "addSchedule": "添加计划",
+ "addSchedule": "添加计划任务",
"clearFilters": "清除过滤器",
- "createSchedule": "创建计划",
+ "createSchedule": "创建计划任务",
"delete": "删除",
"edit": "编辑",
"pause": "暂停",
- "resume": "继续",
- "runNow": "快点跑"
+ "resume": "恢复",
+ "runNow": "立即运行"
},
"empty": {
- "description": "在任何团队中创建一个调度, 将 Claude 任务执行自动化, 并带有 cron 表达式 。 所有球队的日程表将在此登场.",
- "noMatches": "没有匹配当前过滤器的时间表",
- "title": "无计划任务"
+ "description": "在任何团队上创建一个计划任务,以使用 cron 表达式自动执行 Claude 任务。所有团队的计划任务都会出现在这里。",
+ "noMatches": "没有与当前过滤器匹配的计划任务",
+ "title": "没有计划任务"
},
"filters": {
- "allTeams": "所有小组"
+ "allTeams": "所有团队"
},
"item": {
- "loadingRunHistory": "正在装入运行历史...",
- "nextRun": "下一个:{{value}}",
- "noRunsYet": "尚未运行"
+ "loadingRunHistory": "正在加载运行历史记录…",
+ "nextRun": "下次运行: {{value}}",
+ "noRunsYet": "尚无运行记录"
},
- "loading": "正在装入时间表...",
- "searchPlaceholder": "搜索时间表...",
+ "loading": "加载计划任务…",
+ "searchPlaceholder": "搜索计划任务…",
"status": {
- "active": "活动",
- "all": "全体",
- "disabled": "已禁用",
- "paused": "暂停"
+ "active": "活跃",
+ "all": "全部",
+ "disabled": "已停用",
+ "paused": "已暂停"
},
- "title": "附表"
+ "title": "计划任务"
},
"sessions": {
"actions": {
"hide": "隐藏",
- "pin": "键",
- "unhide": "弃"
+ "pin": "固定",
+ "unhide": "取消隐藏"
},
"empty": {
- "noMatchingSessions": "没有匹配会话",
- "noMatchingSessionsDescription": "这个项目还没有匹配的会话 。",
- "noMatchingSessionsFiltered": "尝试另一个查询或重置提供者过滤器 。",
+ "noMatchingSessions": "没有匹配的会话",
+ "noMatchingSessionsDescription": "该项目还没有匹配的会话。",
+ "noMatchingSessionsFiltered": "尝试其他查询或重置提供商过滤器。",
"noSessions": "未找到会话",
- "noSessionsDescription": "这个项目还没有开庭",
- "selectProject": "选择查看会话的工程"
+ "noSessionsDescription": "该项目还没有会话",
+ "selectProject": "选择一个项目来查看会话"
},
"errors": {
- "loading": "装入会话出错"
+ "loading": "加载会话时出错"
},
- "loadedMatchingMore": "{{count}} 匹配会话至今已加载 - 滚动到加载更多 。",
- "loadingMore": "正在装入更多会话...",
- "pinned": "固定",
+ "loadedMatchingMore": "当前已加载 {{count}} 匹配会话 - 向下滚动以加载更多内容。",
+ "loadingMore": "正在加载更多会话…",
+ "pinned": "已固定",
"scrollToLoadMore": "滚动加载更多",
"search": {
"clear": "清除会话搜索",
- "placeholder": "搜索会话..."
+ "placeholder": "搜索会话…"
},
"selection": {
"cancel": "取消选择",
"exitMode": "退出选择模式",
- "hideSelected": "隐藏选中的会话",
- "pinSelected": "固定选中的会话",
+ "hideSelected": "隐藏选定的会话",
+ "pinSelected": "固定选定的会话",
"selectSessions": "选择会话",
- "selected": "{{count}}选中",
- "unhideSelected": "取消选中会话",
- "selected_few": "{{count}}选中",
- "selected_many": "{{count}}选中",
- "selected_one": "{{count}}选中",
- "selected_other": "{{count}}选中"
+ "selected": "已选择 {{count}}",
+ "unhideSelected": "取消隐藏选定的会话",
+ "selected_few": "已选择 {{count}}",
+ "selected_many": "已选择 {{count}}",
+ "selected_one": "已选择 {{count}}",
+ "selected_other": "已选择 {{count}}"
},
"sort": {
- "byContext": "按内容",
- "byContextTooltip": "按上下文的消费排序",
+ "byContext": "按上下文",
+ "byContextTooltip": "按上下文消耗排序",
"byRecentTooltip": "按最近排序",
- "contextLoadedOnly": "背景排序只排序已装入的会话 。"
+ "contextLoadedOnly": "上下文排序仅对加载的会话进行排名。"
},
- "title": "会议",
+ "title": "会话",
"visibility": {
"hideHidden": "隐藏隐藏的会话",
"showHidden": "显示隐藏的会话"
@@ -214,285 +214,285 @@
"worktree": {
"switch": "切换工作树"
},
- "loadedMatchingMore_few": "{{count}} 匹配会话至今已加载 - 滚动到加载更多 。",
- "loadedMatchingMore_many": "{{count}} 匹配会话至今已加载 - 滚动到加载更多 。",
- "loadedMatchingMore_one": "{{count}} 匹配会话至今已加载 - 滚动到加载更多 。",
- "loadedMatchingMore_other": "{{count}} 匹配会话至今已加载 - 滚动到加载更多 。",
- "failedToLoad": "装入会话失败",
- "loading": "正在装入会话...",
+ "loadedMatchingMore_few": "当前已加载 {{count}} 匹配会话 - 向下滚动以加载更多内容。",
+ "loadedMatchingMore_many": "当前已加载 {{count}} 匹配会话 - 向下滚动以加载更多内容。",
+ "loadedMatchingMore_one": "当前已加载 {{count}} 匹配会话 - 向下滚动以加载更多内容。",
+ "loadedMatchingMore_other": "当前已加载 {{count}} 匹配会话 - 向下滚动以加载更多内容。",
+ "failedToLoad": "加载会话失败",
+ "loading": "正在加载会话…",
"filter": {
"title": "过滤会话"
},
- "count": "{{count}}届会",
- "count_one": "{{count}}会议",
- "count_other": "{{count}}届会",
- "count_few": "{{count}}届会",
- "count_many": "{{count}}届会",
- "inProgress": "会话正在进行中..."
+ "count": "{{count}} 会话",
+ "count_one": "{{count}} 会话",
+ "count_other": "{{count}} 会话",
+ "count_few": "{{count}} 会话",
+ "count_many": "{{count}} 会话",
+ "inProgress": "会话正在进行中…"
},
"states": {
- "loading": "加载中...",
+ "loading": "加载中…",
"offline": "离线",
- "online": "在线",
+ "online": "在线的",
"unknown": "未知",
"error": "错误"
},
"markdown": {
- "imageFallback": "[图:{{label}}[. ]",
- "largeContentNotice": "内容非常大({{count}}字符). 显示原始预览以保持用户界面响应 。",
- "largeContentTitle": "大型内容显示为原始, 以防止 UI 冻结",
- "raw": "原始内容",
+ "imageFallback": "[图片:{{label}}]",
+ "largeContentNotice": "内容非常大({{count}} 字符)。显示原始预览以保持 UI 响应。",
+ "largeContentTitle": "大内容显示为原始内容以防止 UI 冻结",
+ "raw": "生的",
"rawPreview": "原始预览",
- "renderMarkdown": "降温标记",
- "showAll": "全部显示",
+ "renderMarkdown": "渲染 Markdown",
+ "showAll": "显示全部",
"showMore": "显示更多",
- "showRaw": "显示原始",
+ "showRaw": "显示原始数据",
"showingChars": "显示 {{shown}} / {{total}} 字符",
- "largeContentNotice_few": "内容非常大({{count}}字符). 显示原始预览以保持用户界面响应 。",
- "largeContentNotice_many": "内容非常大({{count}}字符). 显示原始预览以保持用户界面响应 。",
- "largeContentNotice_one": "内容非常大({{count}}字符). 显示原始预览以保持用户界面响应 。",
- "largeContentNotice_other": "内容非常大({{count}}字符). 显示原始预览以保持用户界面响应 。"
+ "largeContentNotice_few": "内容非常大({{count}} 字符)。显示原始预览以保持 UI 响应。",
+ "largeContentNotice_many": "内容非常大({{count}} 字符)。显示原始预览以保持 UI 响应。",
+ "largeContentNotice_one": "内容非常大({{count}} 字符)。显示原始预览以保持 UI 响应。",
+ "largeContentNotice_other": "内容非常大({{count}} 字符)。显示原始预览以保持 UI 响应。"
},
"terminal": {
- "checkOutputForDetails": "检查上面的终端输出详情",
- "closingInSeconds": "在 {{count}}s 关闭...",
- "closingInSeconds_few": "在 {{count}}s 关闭...",
- "closingInSeconds_many": "在 {{count}}s 关闭...",
- "closingInSeconds_one": "在 {{count}}s 关闭...",
- "closingInSeconds_other": "在 {{count}}s 关闭...",
- "completedSuccessfully": "已成功完成",
- "exitCode": "(出口代码{{code}}).",
+ "checkOutputForDetails": "检查上面的终端输出以了解详细信息",
+ "closingInSeconds": "结束于 {{count}}…",
+ "closingInSeconds_few": "结束于 {{count}}…",
+ "closingInSeconds_many": "结束于 {{count}}…",
+ "closingInSeconds_one": "结束于 {{count}}…",
+ "closingInSeconds_other": "结束于 {{count}}…",
+ "completedSuccessfully": "成功完成",
+ "exitCode": "(退出代码 {{code}})",
"processFailed": "进程失败",
"title": "终端"
},
"tokens": {
- "accumulatedWithoutDuplication": "整个届会累积而无重复",
- "cacheRead": "缓存已读",
- "cacheWrite": "快取写入",
- "costUsd": "费用(ZXCVTKEN0ZXCV)",
- "inputTokens": "输入键",
- "model": "型号",
- "outputTokens": "输出键",
- "phase": "阶段{{phase}}页:1{{total}}",
- "promptInputShare": "{{percent}} 快速输入的%",
+ "accumulatedWithoutDuplication": "整个会话持续时间累计,无重复",
+ "cacheRead": "缓存读取",
+ "cacheWrite": "缓存写入",
+ "costUsd": "成本(美元)",
+ "inputTokens": "输入 Token",
+ "model": "模型",
+ "outputTokens": "输出 Token",
+ "phase": "相 {{phase}}/{{total}}",
+ "promptInputShare": "提示词输入的 {{percent}}%",
"taskCoordination": "任务协调",
- "thinkingText": "思维+ 文字",
+ "thinkingText": "思考+文字",
"toolOutputs": "工具输出",
- "total": "共计",
- "userMessages": "用户信件",
- "visibleContext": "可见背景",
- "includesClaudeMd": "包括CLUDE.md×{{count}}",
- "claudeMd": "ZXCVKEN0ZXCV.MD 维基百科中的相关条目: 维基文库中相关的原始文献: 维基文库中相关的原始文献: 维基语录",
- "mentionedFiles": "@ 文件",
- "percentValue": "(单位:千美元){{percent}}(百分比)",
- "approxTokens": "~{{tokens}} 符号",
- "approxTokensParenthesized": "(~~) (中文(简体) ).{{tokens}}页:1"
+ "total": "总计",
+ "userMessages": "用户消息",
+ "visibleContext": "可见的上下文",
+ "includesClaudeMd": "包括。CLAUDE.md ×{{count}}",
+ "claudeMd": "CLAUDE.md",
+ "mentionedFiles": "@文件",
+ "percentValue": "({{percent}}%)",
+ "approxTokens": "~{{tokens}} Token",
+ "approxTokensParenthesized": "(~{{tokens}})"
},
"list": {
"actions": {
"copyTeam": "复制团队",
"createTeam": "创建团队",
- "deleteForever": "永远删除",
+ "deleteForever": "永久删除",
"deletePermanently": "永久删除",
"deleteTeam": "删除团队",
- "launching": "正在发射...",
- "launchTeam": "发射队",
- "relaunchTeam": "发射队",
+ "launching": "正在启动…",
+ "launchTeam": "启动团队",
+ "relaunchTeam": "重新启动团队",
"restore": "恢复",
- "restoreTeam": "恢复小组",
+ "restoreTeam": "恢复队伍",
"retry": "重试",
- "stopTeam": "停止小组",
- "stopping": "正在停止中..."
+ "stopTeam": "停队",
+ "stopping": "停止…"
},
"status": {
- "active": "活动",
- "deleted": "删除",
- "launching": "正在发射...",
+ "active": "活跃",
+ "deleted": "已删除",
+ "launching": "正在启动…",
"offline": "离线",
- "partialFailure": "发射失败的路段",
- "partialPending": "拖带待处理",
+ "partialFailure": "启动中途失败",
+ "partialPending": "引导挂起",
"partialSkipped": "启动跳过的成员",
- "running": "运行"
+ "running": "跑步"
},
"partial": {
- "pending": "上次发射还在调和",
- "skipped": "上次发射已经跳过了队友",
- "skippedWithCount": "最后一次发射跳过{{count}}/{{expected}}队友.",
- "skippedWithCount_few": "最后一次发射跳过{{count}}/{{expected}}队友.",
- "skippedWithCount_many": "最后一次发射跳过{{count}}/{{expected}}队友.",
- "skippedWithCount_one": "最后一次发射跳过{{count}}/{{expected}}队友.",
- "skippedWithCount_other": "最后一次发射跳过{{count}}/{{expected}}队友.",
- "stopped": "最后一次发射在所有队友加入之前就停止了.",
- "stoppedWithCount": "最后一次发射在{{count}}/{{expected}}队友加入之前停止.",
- "stoppedWithCount_few": "最后一次发射在{{count}}/{{expected}}队友加入之前停止.",
- "stoppedWithCount_many": "最后一次发射在{{count}}/{{expected}}队友加入之前停止.",
- "stoppedWithCount_one": "最后一次发射在{{count}}/{{expected}}队友加入之前停止.",
- "stoppedWithCount_other": "最后一次发射在{{count}}/{{expected}}队友加入之前停止."
+ "pending": "上次启动仍在协调中。",
+ "skipped": "上次启动跳过了队友。",
+ "skippedWithCount": "上次启动跳过了 {{count}}/{{expected}} 队友。",
+ "skippedWithCount_few": "上次启动跳过了 {{count}}/{{expected}} 队友。",
+ "skippedWithCount_many": "上次启动跳过了 {{count}}/{{expected}} 队友。",
+ "skippedWithCount_one": "上次启动跳过了 {{count}}/{{expected}} 队友。",
+ "skippedWithCount_other": "上次启动跳过了 {{count}}/{{expected}} 队友。",
+ "stopped": "上次启动在所有队友加入之前停止了。",
+ "stoppedWithCount": "上次启动在 {{count}}/{{expected}} 队友加入之前停止。",
+ "stoppedWithCount_few": "上次启动在 {{count}}/{{expected}} 队友加入之前停止。",
+ "stoppedWithCount_many": "上次启动在 {{count}}/{{expected}} 队友加入之前停止。",
+ "stoppedWithCount_one": "上次启动在 {{count}}/{{expected}} 队友加入之前停止。",
+ "stoppedWithCount_other": "上次启动在 {{count}}/{{expected}} 队友加入之前停止。"
},
- "noDescription": "无说明",
+ "noDescription": "无描述",
"solo": "独奏",
- "membersCount": "成员:{{count}}",
- "membersCount_few": "成员:{{count}}",
- "membersCount_many": "成员:{{count}}",
- "membersCount_one": "成员:{{count}}",
- "membersCount_other": "成员:{{count}}",
- "all": "全体",
- "moreCount": "+ 键{{count}}更多",
- "moreCount_one": "+ 键{{count}}更多",
- "moreCount_other": "+ 键{{count}}更多",
- "moreCount_few": "+ 键{{count}}更多",
- "moreCount_many": "+ 键{{count}}更多"
+ "membersCount": "成员:{{count}}",
+ "membersCount_few": "成员:{{count}}",
+ "membersCount_many": "成员:{{count}}",
+ "membersCount_one": "成员: {{count}}",
+ "membersCount_other": "成员:{{count}}",
+ "all": "全部",
+ "moreCount": "+{{count}} 更多",
+ "moreCount_one": "+{{count}} 更多",
+ "moreCount_other": "+{{count}} 更多",
+ "moreCount_few": "+{{count}} 更多",
+ "moreCount_many": "+{{count}} 更多"
},
"runtimeProvider": {
"defaults": {
- "scopeDescriptionAllProjects": "默认没有自己的 OpenCode 覆盖的每个工程 。",
- "scopeDescriptionProject": "只覆盖选中的项目 。 运行队伍不变.",
- "setAllProjectsDefault": "设置全部项目默认值",
- "setProjectDefault": "设定工程默认",
- "validationContext": "验证背景",
+ "scopeDescriptionAllProjects": "每个没有自己的 OpenCode 覆盖的项目的默认值。",
+ "scopeDescriptionProject": "仅覆盖选定的项目。运行团队没有变化。",
+ "setAllProjectsDefault": "设置所有项目默认值",
+ "setProjectDefault": "设置项目默认值",
+ "validationContext": "验证上下文",
"projectOverrideContext": "项目覆盖上下文",
- "selectProjectHint": "在测试本地模型或保存默认值之前选择一个项目 。",
- "allProjectsHint": "测试使用{{project}}. 默认适用,除非项目有超标。",
- "projectHint": "保存只覆盖 {{project}}."
+ "selectProjectHint": "在测试本地模型或保存默认值之前选择一个项目。",
+ "allProjectsHint": "测试使用 {{project}}。除非项目有覆盖,否则应用默认值。",
+ "projectHint": "保存仅覆盖 {{project}}。"
}
},
"sessionContext": {
"header": {
- "title": "背景情况",
+ "title": "上下文",
"closePanel": "关闭面板",
- "phase": "阶段:",
- "current": "当前",
- "view": "视图:",
+ "phase": "阶段:",
+ "current": "当前的",
+ "view": "看法:",
"category": "类别",
- "bySize": "按大小"
+ "bySize": "按尺寸"
},
"metrics": {
- "unavailable": "无法获取",
- "contextUsed": "使用的背景",
- "promptInput": "提示输入",
- "visibleContext": "可见背景",
- "ofContext": "上下文",
- "ofPrompt": "时速",
- "codexTelemetryUnavailable": "Codex 即时侧用尚未被当前运行时间遥测显示, 因此“ 即时输入” 和“ 上下文” 不可用, 而不是显示假零 。",
- "sessionCost": "会议费用:",
- "parentPlus": "父词 +",
- "subagents": "子剂",
+ "unavailable": "不可用",
+ "contextUsed": "使用的上下文",
+ "promptInput": "提示词输入",
+ "visibleContext": "可见的上下文",
+ "ofContext": "上下文的",
+ "ofPrompt": "提示词的",
+ "codexTelemetryUnavailable": "当前运行时遥测尚未公开 Codex 提示词侧使用情况,因此提示词输入和使用的上下文保持不可用,而不是显示假零。",
+ "sessionCost": "会话费用:",
+ "parentPlus": "家长+",
+ "subagents": "子智能体",
"details": "细节"
},
"help": {
"contextUsed": {
- "title": "使用的背景",
- "description": "正在占用模型上下文窗口的快速输入加输出符 。"
+ "title": "使用的上下文",
+ "description": "提示词输入以及当前占据模型上下文窗口的输出 Token。"
},
"promptInput": {
- "title": "提示输入",
- "description": "托肯斯在一代之前就被送到模型了. 对于克劳德,这包括`input_tokens + cache_creation_input_tokens + cache_read_input_tokens`."
+ "title": "提示词输入",
+ "description": "在生成之前发送到模型的 Token。对于 Claude 来说,这包括“input_tokens + cache_creation_input_tokens + cache_read_input_tokens”。"
},
"visibleContext": {
- "title": "可见背景",
- "description": "可检查的即时输入子集:文件,CLAUDE.md,工具输出,用户信息,以及您可以直接优化的类似注入."
+ "title": "可见的上下文",
+ "description": "提示词输入的可检查子集:文件、CLAUDE.md、工具输出、用户消息以及可以直接优化的类似注入。"
},
"availability": {
"title": "可用性",
- "description": "如果供应商运行时间尚未显示即时侧用量,则面板显示没有指标,而不是假装为零。"
+ "description": "如果提供商运行时尚未公开提示词侧使用情况,面板会将指标显示为不可用,而不是假装它们为零。"
}
},
"items": {
- "turn": "@ 翻转 {{turn}}",
- "tokensApprox": "~{{tokens}} 符号",
- "toolsCount": "{{count}}工具",
+ "turn": "@第 {{turn}} 轮",
+ "tokensApprox": "~{{tokens}} Token",
+ "toolsCount": "{{count}} 工具",
"toolsCount_one": "{{count}} 工具",
- "toolsCount_other": "{{count}}工具",
- "toolsCount_few": "{{count}}工具",
- "toolsCount_many": "{{count}}工具",
- "itemsCount": "{{count}}项目",
- "itemsCount_one": "{{count}}项目",
- "itemsCount_other": "{{count}}项目",
- "itemsCount_few": "{{count}}项目",
- "itemsCount_many": "{{count}}项目",
- "missing": "缺少",
- "thinking": "思维",
- "text": "文字"
+ "toolsCount_other": "{{count}} 工具",
+ "toolsCount_few": "{{count}} 工具",
+ "toolsCount_many": "{{count}} 工具",
+ "itemsCount": "{{count}} 项目",
+ "itemsCount_one": "{{count}} 项目",
+ "itemsCount_other": "{{count}} 项目",
+ "itemsCount_few": "{{count}} 项目",
+ "itemsCount_many": "{{count}} 项目",
+ "missing": "丢失的",
+ "thinking": "思考",
+ "text": "文本"
},
- "empty": "此会话中没有检测到上下文注入",
+ "empty": "此会话中未检测到上下文注入",
"view": {
"grouped": "分组",
- "flat": "平面"
+ "flat": "平坦的"
},
- "claudeMdFiles": "CLAUDE.md 文档",
- "mentionedFiles": "引用文件"
+ "claudeMdFiles": "CLAUDE.md 文件",
+ "mentionedFiles": "提及的文件"
},
"chat": {
"subagent": {
- "fallbackName": "副剂",
- "shutdownConfirmed": "关闭确认",
+ "fallbackName": "子智能体",
+ "shutdownConfirmed": "已确认关闭",
"summary": {
- "tools": "{{count}}工具",
+ "tools": "{{count}} 工具",
"tools_one": "{{count}} 工具",
- "tools_other": "{{count}}工具",
- "tools_few": "{{count}}工具",
- "tools_many": "{{count}}工具"
+ "tools_other": "{{count}} 工具",
+ "tools_few": "{{count}} 工具",
+ "tools_many": "{{count}} 工具"
},
"meta": {
"type": "类型",
- "duration": "会期",
- "model": "型号",
- "id": "身份证"
+ "duration": "持续时间",
+ "model": "模型",
+ "id": "ID"
},
"metrics": {
- "contextWindow": "背景窗口",
- "contextUsage": "背景使用情况",
+ "contextWindow": "上下文窗口",
+ "contextUsage": "上下文使用",
"mainContext": "主要背景",
- "totalOutput": "产出共计",
- "turns": "(单位:千美元){{count}}转弯).",
- "turns_one": "(单位:千美元){{count}}转弯).",
- "turns_other": "(单位:千美元){{count}}转弯).",
- "subagentContext": "子代理上下文",
- "phase": "{{phase}}阶段",
- "turns_few": "(单位:千美元){{count}}转弯).",
- "turns_many": "(单位:千美元){{count}}转弯)."
+ "totalOutput": "总产量",
+ "turns": "({{count}} 转)",
+ "turns_one": "({{count}} 转)",
+ "turns_other": "({{count}} 转)",
+ "subagentContext": "子智能体上下文",
+ "phase": "相 {{phase}}",
+ "turns_few": "({{count}} 转)",
+ "turns_many": "({{count}} 转)"
},
"trace": {
- "title": "执行追踪"
+ "title": "执行跟踪"
}
},
"user": {
- "you": "你们",
+ "you": "你",
"showMore": "显示更多",
- "showLess": "显示较少",
- "backgroundTask": "背景任务",
- "exitCode": "出口 {{code}}",
- "imagesAttached": "附上的 {{count}} 图像",
- "imagesAttached_one": "{{count}} 图像附着",
- "imagesAttached_few": "附上的 {{count}} 图像",
- "imagesAttached_many": "附上的 {{count}} 图像",
- "imagesAttached_other": "附上的 {{count}} 图像"
+ "showLess": "收起",
+ "backgroundTask": "后台任务",
+ "exitCode": "退出 {{code}}",
+ "imagesAttached": "附 {{count}} 图片",
+ "imagesAttached_one": "附 {{count}} 图片",
+ "imagesAttached_few": "附 {{count}} 图片",
+ "imagesAttached_many": "附 {{count}} 图片",
+ "imagesAttached_other": "附 {{count}} 图片"
},
"compact": {
"toggle": "切换压缩内容",
- "contextCompacted": "背景紧凑",
- "freedTokens": "(单位:千美元){{tokens}}(释放)",
- "phase": "{{phase}}阶段",
- "conversationCompacted": "已压缩",
- "summary": "将以前的信件摘要保存上下文 。 完整的对话历史保存在会话文件中.",
+ "contextCompacted": "上下文压缩",
+ "freedTokens": "({{tokens}} 已释放)",
+ "phase": "相 {{phase}}",
+ "conversationCompacted": "对话压缩",
+ "summary": "对之前的消息进行了总结以保存上下文。完整的对话历史记录保存在会话文件中。",
"compacted": "压缩"
},
"executionTrace": {
"empty": "无执行项目",
- "nested": "巢穴: {{name}}",
- "input": "投入"
+ "nested": "嵌套:{{name}}",
+ "input": "输入"
},
"items": {
- "empty": "无要显示的项目"
+ "empty": "没有可显示的项目"
},
"tools": {
- "teammateSpawned": "队友产卵",
- "shutdownRequested": "要求关闭 - >",
+ "teammateSpawned": "队友生成",
+ "shutdownRequested": "请求关闭 ->",
"noResultReceived": "未收到结果",
- "duration": "会期: {{duration}}",
+ "duration": "持续时间:{{duration}}",
"result": "结果",
"write": {
- "createdFile": "创建文件",
+ "createdFile": "已创建文件",
"wroteToFile": "写入文件"
},
"skill": {
@@ -501,27 +501,27 @@
}
},
"lastOutput": {
- "requestInterrupted": "被用户中断的请求",
- "planReadyForApproval": "准备核准计划"
+ "requestInterrupted": "请求被用户中断",
+ "planReadyForApproval": "计划已准备好等待批准"
},
"empty": {
"icon": "💬",
- "title": "没有对话历史",
- "description": "此会话尚未包含任何消息 。"
+ "title": "没有对话历史记录",
+ "description": "此会话尚不包含任何消息。"
},
"context": {
- "remainingPercent": "(单位:千美元){{percent}}剩余百分比)",
- "count": "背景情况({{count}})",
- "count_one": "背景情况({{count}})",
- "count_other": "背景情况({{count}})",
- "count_few": "背景情况({{count}})",
- "count_many": "背景情况({{count}})"
+ "remainingPercent": "({{percent}}%剩余)",
+ "count": "上下文({{count}})",
+ "count_one": "上下文({{count}})",
+ "count_other": "上下文({{count}})",
+ "count_few": "上下文({{count}})",
+ "count_many": "上下文({{count}})"
},
- "scrollToBottom": "向下滚动",
- "bottom": "底层",
+ "scrollToBottom": "滚动到底部",
+ "bottom": "底部",
"teammateMessage": {
- "message": "消息",
- "resent": "拒绝",
+ "message": "信息",
+ "resent": "重新发送",
"fallback": "队友消息"
},
"system": {
@@ -530,310 +530,310 @@
},
"tmuxInstaller": {
"summaryTitle": "tmux 未安装",
- "detectedOs": "探测到的OS: {{os}}",
- "runtimePath": "运行时间路径: {{path}}",
- "phase": "阶段:{{phase}}",
+ "detectedOs": "检测到的操作系统:{{os}}",
+ "runtimePath": "运行时路径:{{path}}",
+ "phase": "相位:{{phase}}",
"actions": {
"cancel": "取消",
- "manualGuide": "手册指南",
+ "manualGuide": "手动引导",
"hideSetupSteps": "隐藏设置步骤",
- "showSetupSteps": "显示设置步骤( {{count}})",
- "showSetupSteps_one": "显示设置步骤( {{count}})",
- "showSetupSteps_other": "显示设置步骤( {{count}})",
+ "showSetupSteps": "显示设置步骤 ({{count}})",
+ "showSetupSteps_one": "显示设置步骤 ({{count}})",
+ "showSetupSteps_other": "显示设置步骤 ({{count}})",
"recheck": "重新检查",
- "showSetupSteps_few": "显示设置步骤( {{count}})",
- "showSetupSteps_many": "显示设置步骤( {{count}})"
+ "showSetupSteps_few": "显示设置步骤 ({{count}})",
+ "showSetupSteps_many": "显示设置步骤 ({{count}})"
},
"installerProgress": "安装进度",
"input": {
- "placeholder": "向安装器发送输入",
+ "placeholder": "将输入发送给安装程序",
"send": "发送输入",
- "passwordNotice": "密码输入直接发送到安装终端,不添加到日志输出中."
+ "passwordNotice": "密码输入直接发送到安装程序终端,不会添加到日志输出中。"
},
"details": {
- "show": "显示细节",
- "hide": "隐藏细节"
+ "show": "显示详情",
+ "hide": "隐藏详细信息"
}
},
"commandPalette": {
"noRecentActivity": "最近没有活动",
- "sessionsCount": "{{count}}届会",
- "sessionsCount_one": "{{count}}会议",
- "sessionsCount_other": "{{count}}届会",
+ "sessionsCount": "{{count}} 会话",
+ "sessionsCount_one": "{{count}} 会话",
+ "sessionsCount_other": "{{count}} 会话",
"mode": {
"searchProjects": "搜索项目",
"searchAcrossProjects": "搜索所有项目",
- "searchInProject": "在工程中搜索"
+ "searchInProject": "在项目中搜索"
},
"currentProject": "当前项目",
- "global": "全球",
+ "global": "全球的",
"placeholders": {
- "projects": "搜索项目...",
- "conversations": "搜索对话..."
+ "projects": "搜索项目…",
+ "conversations": "搜索对话…"
},
"empty": {
- "noProjectsForQuery": "找不到“ {{query}}” 的项目",
- "noProjects": "未找到工程",
- "minChars": "至少输入2个要搜索的字符",
- "noFastResults": "“{{query}}”的近期会议没有快速结果",
- "noResults": "未发现“ {{query}}” 的结果"
+ "noProjectsForQuery": "未找到“{{query}}”的项目",
+ "noProjects": "未找到项目",
+ "minChars": "输入至少 2 个字符进行搜索",
+ "noFastResults": "在最近的会话中没有“{{query}}”的快速结果",
+ "noResults": "未找到“{{query}}”的结果"
},
"footer": {
- "projectsCount": "{{count}}项目",
- "projectsCount_one": "{{count}}项目",
- "projectsCount_other": "{{count}}项目",
- "results": "{{count}} {{speed}}结果",
- "results_one": "{{count}} {{speed}}结果",
- "results_other": "{{count}} {{speed}}结果",
- "resultsAcrossProjects": "{{count}} {{speed}}所有项目的成果",
- "resultsAcrossProjects_one": "{{count}} {{speed}}所有项目的结果",
- "resultsAcrossProjects_other": "{{count}} {{speed}}所有项目的成果",
- "fastPrefix": "快速",
- "typeToSearch": "要搜索的类型",
+ "projectsCount": "{{count}} 项目",
+ "projectsCount_one": "{{count}} 项目",
+ "projectsCount_other": "{{count}} 项目",
+ "results": "{{count}} {{speed}} 结果",
+ "results_one": "{{count}} {{speed}} 结果",
+ "results_other": "{{count}} {{speed}} 结果",
+ "resultsAcrossProjects": "{{count}} {{speed}} 所有项目的结果",
+ "resultsAcrossProjects_one": "{{count}} {{speed}} 所有项目的结果",
+ "resultsAcrossProjects_other": "{{count}} {{speed}} 所有项目的结果",
+ "fastPrefix": "快速地",
+ "typeToSearch": "输入搜索",
"navigate": "导航",
"select": "选择",
"open": "打开",
- "global": "全球",
+ "global": "全球的",
"close": "关闭",
- "results_few": "{{count}} {{speed}}结果",
- "results_many": "{{count}} {{speed}}结果",
- "resultsAcrossProjects_few": "{{count}} {{speed}}所有项目的成果",
- "resultsAcrossProjects_many": "{{count}} {{speed}}所有项目的成果",
- "projectsCount_few": "{{count}}项目",
- "projectsCount_many": "{{count}}项目",
+ "results_few": "{{count}} {{speed}} 结果",
+ "results_many": "{{count}} {{speed}} 结果",
+ "resultsAcrossProjects_few": "{{count}} {{speed}} 所有项目的结果",
+ "resultsAcrossProjects_many": "{{count}} {{speed}} 所有项目的结果",
+ "projectsCount_few": "{{count}} 项目",
+ "projectsCount_many": "{{count}} 项目",
"upDownKey": "↑↓",
- "escapeKey": "埃斯克语"
+ "escapeKey": "Esc 键"
},
- "sessionsCount_few": "{{count}}届会",
- "sessionsCount_many": "{{count}}届会"
+ "sessionsCount_few": "{{count}} 会话",
+ "sessionsCount_many": "{{count}} 会话"
},
"tasksPanel": {
"title": "任务",
- "searchPlaceholder": "搜索任务...",
- "pinned": "固定",
- "groupByLabel": "分组方式:",
- "groupByAria": "分组",
+ "searchPlaceholder": "搜索任务…",
+ "pinned": "已固定",
+ "groupByLabel": "分组依据:",
+ "groupByAria": "分组依据",
"groupModes": {
"none": "无",
"project": "项目",
"time": "时间"
},
- "showArchived": "显示存档",
- "hideArchived": "隐藏存档",
+ "showArchived": "显示已存档",
+ "hideArchived": "隐藏已存档",
"empty": {
"noMatchingTasks": "没有匹配的任务",
"noTasks": "未找到任务"
},
- "teamLabel": "团队: {{team}}",
+ "teamLabel": "队伍:{{team}}",
"showMore": "显示更多",
- "showLess": "显示较少",
+ "showLess": "收起",
"deleteConfirm": {
"title": "删除任务",
- "message": "将任务 #{{taskId}} 移动到垃圾堆中吗?",
+ "message": "将任务 #{{taskId}} 移至垃圾箱?",
"confirmLabel": "删除",
"cancelLabel": "取消"
},
"deleteFailed": {
"title": "删除任务失败",
- "fallbackMessage": "发生了意外错误",
- "confirmLabel": "还好"
+ "fallbackMessage": "发生意外错误",
+ "confirmLabel": "好的"
},
"sort": {
- "byTime": "时间",
- "byUnread": "由未读",
- "byProject": "按项目分列",
+ "byTime": "按时间",
+ "byUnread": "未读",
+ "byProject": "按项目",
"byTeam": "按团队"
}
},
"toolViewer": {
- "input": "投入",
- "replaceAll": "(替换全部)",
- "noInputRecorded": "此工具调用没有记录输入 。",
+ "input": "输入",
+ "replaceAll": "(全部替换)",
+ "noInputRecorded": "没有记录此工具调用的输入。",
"agent": {
- "action": "动作",
+ "action": "行动",
"teammate": "队友",
"team": "团队",
- "runtime": "运行时间",
+ "runtime": "运行时",
"type": "类型",
- "startupInstructionsHidden": "启动指令隐藏在UI中."
+ "startupInstructionsHidden": "启动指令隐藏在 UI 中。"
}
},
"taskContextMenu": {
- "unpin": "撤销",
- "pin": "键",
+ "unpin": "取消固定",
+ "pin": "固定",
"rename": "重命名",
"markUnread": "标记为未读",
- "unarchive": "无存档",
- "archive": "存档",
+ "unarchive": "取消存档",
+ "archive": "档案",
"deleteTask": "删除任务"
},
"updateDialog": {
"closeDialog": "关闭对话框",
- "updateAvailable": "可更新",
- "updateReady": "更新准备",
- "noReleaseNotes": "没有可用的发行注释 。",
+ "updateAvailable": "可用更新",
+ "updateReady": "更新就绪",
+ "noReleaseNotes": "没有可用的发行说明。",
"viewOnGitHub": "在 GitHub 上查看",
- "later": "回头见",
- "restartNow": "重新开始",
+ "later": "之后",
+ "restartNow": "立即重新启动",
"download": "下载"
},
"errorBoundary": {
- "title": "出问题了",
- "description": "应用程序中发生了意外错误 。 您可以尝试重新装入页面或重新设置错误状态 。",
- "componentStack": "组件堆栈",
+ "title": "出了点问题",
+ "description": "应用中发生意外错误。您可以尝试重新加载页面或重置错误状态。",
+ "componentStack": "组件栈",
"tryAgain": "再试一次",
- "copied": "复制",
- "copyErrorDetails": "复制错误细节",
- "reportBugOnGitHub": "GitHub 上的报告错误",
- "reloadApp": "重新装入 App",
- "diagnosticsNotice": "GitHub 错误报告和复制的诊断包括错误消息,堆栈痕迹,app版本,活动标签,选中团队,任务上下文,以及环境细节."
+ "copied": "已复制",
+ "copyErrorDetails": "复制错误详细信息",
+ "reportBugOnGitHub": "在 GitHub 上报告错误",
+ "reloadApp": "重新加载应用",
+ "diagnosticsNotice": "GitHub 错误报告和复制的诊断信息包括错误消息、堆栈跟踪、应用版本、活动选项卡、选定的团队、任务上下文和环境详细信息。"
},
"runtimeBackendSelector": {
- "label": "运行时间后端",
- "resolved": "决定:{{backend}}",
- "current": "当前",
- "recommended": "建议",
- "unavailable": "无法获取",
- "cannotSelectYet": "此后端尚未选中 。",
+ "label": "运行时后端",
+ "resolved": "已解决:{{backend}}",
+ "current": "当前的",
+ "recommended": "推荐",
+ "unavailable": "不可用",
+ "cannotSelectYet": "尚无法选择该后端。",
"auto": "自动",
- "autoCurrently": "自动( 目前: {{backend}})",
+ "autoCurrently": "自动(当前:{{backend}})",
"audience": {
- "internal": "内部"
+ "internal": "内部的"
},
"states": {
- "locked": "已锁定",
- "disabled": "已禁用",
+ "locked": "锁定",
+ "disabled": "已停用",
"authRequired": "需要认证",
- "runtimeMissing": "缺少运行时间",
- "degraded": "有辱人格",
- "unavailable": "无法获取"
+ "runtimeMissing": "运行时缺失",
+ "degraded": "降级",
+ "unavailable": "不可用"
}
},
"providerModelBadges": {
- "checking": "检查中",
- "unavailable": "无法获取",
+ "checking": "检查",
+ "unavailable": "不可用",
"checkFailed": "检查失败",
"free": "免费",
- "freeTooltip": "由OpenCode元数据报告. 可用性和限制可能会改变。"
+ "freeTooltip": "由 OpenCode 元数据报告。可用性和限制可能会发生变化。"
},
"taskFilters": {
- "status": "状态",
+ "status": "地位",
"clearAll": "全部清除",
- "selectAll": "全部选择",
+ "selectAll": "选择全部",
"team": "团队",
- "allTeams": "所有小组",
- "searchTeams": "搜索组...",
- "noTeamsFound": "没有找到团队",
+ "allTeams": "所有团队",
+ "searchTeams": "搜寻队伍…",
+ "noTeamsFound": "未找到队伍",
"project": "项目",
"allProjects": "所有项目",
- "searchProjects": "搜索项目...",
- "noProjects": "无项目",
+ "searchProjects": "搜索项目…",
+ "noProjects": "没有项目",
"comments": "评论",
- "apply": "应用",
+ "apply": "申请",
"read": {
- "all": "全体",
+ "all": "全部",
"unread": "未读",
- "read": "读取"
+ "read": "读"
},
"statusOptions": {
- "todo": "TODO 苏维埃社会主义共和国",
- "inProgress": "在ZXCVKEN0ZXCV",
- "needsFix": "NEEDS ZXCV 1ZXCV",
- "done": "DONE 苏维埃社会主义共和国",
- "review": "REVIEW 苏维埃社会主义共和国",
- "approved": "APPROVED 苏维埃社会主义共和国"
+ "todo": "待办事项",
+ "inProgress": "进行中",
+ "needsFix": "需要修复",
+ "done": "结束",
+ "review": "审核中",
+ "approved": "已批准"
}
},
"sessionItem": {
- "totalContext": "总背景: {{tokens}} 令牌",
- "context": "背景:{{tokens}}",
- "phase": "阶段{{phase}}编号:",
- "compactedTo": "(与{{tokens}}协议)"
+ "totalContext": "总上下文:{{tokens}} Token",
+ "context": "上下文:{{tokens}}",
+ "phase": "{{phase}} 相:",
+ "compactedTo": "(压缩为 {{tokens}})"
},
"notifications": {
"row": {
"team": "团队",
- "subagent": "子剂",
+ "subagent": "子智能体",
"markAsRead": "标记为已读",
"delete": "删除",
"viewInSession": "在会话中查看"
},
"title": "通知",
- "loading": "正在装入通知...",
+ "loading": "正在加载通知…",
"actions": {
- "markFilteredAsRead": "将过滤后标记为已读",
- "markAllAsRead": "全部标为已读",
- "markFilteredRead": "已过滤已读",
- "markAllRead": "标记全部已读",
- "clearFilteredNotifications": "清除过滤通知",
- "clearAllNotifications": "清除全部通知",
+ "markFilteredAsRead": "将筛选结果标记为已读",
+ "markAllAsRead": "全部标记为已读",
+ "markFilteredRead": "将筛选结果标记为已读",
+ "markAllRead": "全部标记为已读",
+ "clearFilteredNotifications": "清除过滤的通知",
+ "clearAllNotifications": "清除所有通知",
"clickToConfirm": "点击确认",
"clearFiltered": "清除过滤",
"clearAll": "全部清除"
},
"counts": {
- "unreadInFilter": "{{count}} 过滤器未读",
- "unreadInFilter_one": "{{count}} 过滤器未读",
- "unreadInFilter_few": "{{count}} 过滤器未读",
- "unreadInFilter_many": "{{count}} 过滤器未读",
- "unreadInFilter_other": "{{count}} 过滤器未读",
- "inFilter": "{{count}}在过滤器中",
- "inFilter_one": "{{count}}在过滤器中",
- "inFilter_few": "{{count}}在过滤器中",
- "inFilter_many": "{{count}}在过滤器中",
- "inFilter_other": "{{count}}在过滤器中",
- "unread": "{{count}}未读",
- "unread_one": "{{count}}未读",
- "unread_few": "{{count}}未读",
- "unread_many": "{{count}}未读",
- "unread_other": "{{count}}未读",
- "total": "{{count}}共计",
- "total_one": "{{count}}共计",
- "total_few": "{{count}}共计",
- "total_many": "{{count}}共计",
- "total_other": "{{count}}共计"
+ "unreadInFilter": "{{count}} 过滤器中未读",
+ "unreadInFilter_one": "{{count}} 过滤器中未读",
+ "unreadInFilter_few": "{{count}} 过滤器中未读",
+ "unreadInFilter_many": "{{count}} 过滤器中未读",
+ "unreadInFilter_other": "{{count}} 过滤器中未读",
+ "inFilter": "过滤器中的 {{count}}",
+ "inFilter_one": "过滤器中的 {{count}}",
+ "inFilter_few": "过滤器中的 {{count}}",
+ "inFilter_many": "过滤器中的 {{count}}",
+ "inFilter_other": "过滤器中的 {{count}}",
+ "unread": "{{count}} 未读",
+ "unread_one": "{{count}} 未读",
+ "unread_few": "{{count}} 未读",
+ "unread_many": "{{count}} 未读",
+ "unread_other": "{{count}} 未读",
+ "total": "{{count}} 总计",
+ "total_one": "{{count}} 总计",
+ "total_few": "{{count}} 总计",
+ "total_many": "{{count}} 总计",
+ "total_other": "{{count}} 总计"
},
"filters": {
- "other": "其他人员"
+ "other": "其他"
},
"empty": {
"noMatching": "没有匹配的通知",
"noNotifications": "无通知",
"tryDifferentFilter": "尝试不同的过滤器",
- "allCaughtUp": "你们都赶上了!"
+ "allCaughtUp": "你们都被抓住了!"
}
},
"updates": {
- "restartToUpdate": "重新开始更新",
- "updateApp": "更新应用程序",
- "downloadedRestartTooltip": "更新下载, 重新启动以应用",
+ "restartToUpdate": "重启即可更新",
+ "updateApp": "更新应用",
+ "downloadedRestartTooltip": "更新已下载,重启即可应用",
"newVersionAvailable": "新版本可用",
- "updatingApp": "更新应用程序",
- "updateReady": "更新就绪",
- "restartNow": "重新开始"
+ "updatingApp": "正在更新应用",
+ "updateReady": "更新准备就绪",
+ "restartNow": "立即重新启动"
},
"layout": {
"github": "GitHub",
- "discord": "调色板",
+ "discord": "不和谐",
"expandSidebar": "展开侧边栏",
- "collapseSidebarShortcut": "折叠侧边栏( {{shortcut}})",
- "sidebarView": "边栏视图",
+ "collapseSidebarShortcut": "折叠侧边栏({{shortcut}})",
+ "sidebarView": "侧边栏视图",
"resizeSidebar": "调整侧边栏大小",
- "closeTab": "关闭标签",
- "openedFromSearch": "从搜索打开",
+ "closeTab": "关闭选项卡",
+ "openedFromSearch": "从搜索中打开",
"pinnedSession": "固定会话",
- "jumpToSection": "跳转到区域",
- "newTab": "新建标签",
- "newTabDashboard": "新建标签( 磁盘)",
+ "jumpToSection": "跳转至部分",
+ "newTab": "新标签页",
+ "newTabDashboard": "新选项卡(控制台)",
"refreshSession": "刷新会话",
- "refreshSessionWithShortcut": "刷新会话( {{shortcut}})",
- "loadingTab": "正在装入标签",
+ "refreshSessionWithShortcut": "刷新会话 ({{shortcut}})",
+ "loadingTab": "加载选项卡",
"menu": {
"teams": "团队",
"settings": "设置",
"extensions": "扩展",
"search": "搜索",
- "schedules": "附表",
+ "schedules": "计划任务",
"docs": "文档",
"exportMarkdown": "导出为 Markdown",
"exportJson": "导出为 JSON",
@@ -841,27 +841,27 @@
"analyzeSession": "分析会话"
},
"tabMenu": {
- "closeTabs": "关闭 {{count}} 标签",
- "closeTabs_one": "关闭 {{count}} 标签",
- "closeTabs_few": "关闭 {{count}} 标签",
- "closeTabs_many": "关闭 {{count}} 标签",
- "closeTabs_other": "关闭 {{count}} 标签",
- "closeTab": "关闭标签",
- "closeOtherTabs": "关闭其他标签",
- "splitRight": "向右分割",
- "splitLeft": "向左分割",
- "pinToSidebar": "平移到侧边栏",
- "unpinFromSidebar": "从侧边栏撤消",
+ "closeTabs": "关闭 {{count}} 选项卡",
+ "closeTabs_one": "关闭 {{count}} 选项卡",
+ "closeTabs_few": "关闭 {{count}} 选项卡",
+ "closeTabs_many": "关闭 {{count}} 选项卡",
+ "closeTabs_other": "关闭 {{count}} 选项卡",
+ "closeTab": "关闭选项卡",
+ "closeOtherTabs": "关闭其他选项卡",
+ "splitRight": "右分割",
+ "splitLeft": "左分割",
+ "pinToSidebar": "固定到侧边栏",
+ "unpinFromSidebar": "从侧边栏取消固定",
"hideFromSidebar": "从侧边栏隐藏",
- "unhideFromSidebar": "从侧边栏解开",
- "closeAllTabs": "关闭全部标签"
+ "unhideFromSidebar": "从侧边栏取消隐藏",
+ "closeAllTabs": "关闭所有选项卡"
},
"sections": {
"team": "团队",
- "sessions": "会议",
- "kanban": "坎班语Name",
- "claudeLogs": "日志",
- "messages": "信件"
+ "sessions": "会话",
+ "kanban": "看板",
+ "claudeLogs": "Claude 日志",
+ "messages": "消息"
}
},
"editorFormatting": {
@@ -871,8 +871,8 @@
"code": "代码"
},
"diff": {
- "changed": "已更改",
- "noChangesDetected": "未检测到变化"
+ "changed": "改变了",
+ "noChangesDetected": "未检测到任何变化"
},
"codexLogin": {
"copyLoginLinkAndCode": "复制 ChatGPT 登录链接和代码",
@@ -888,13 +888,13 @@
"restore": "恢复"
},
"context": {
- "local": "当地",
+ "local": "本地",
"switchingTo": "切换到 {{workspace}}",
- "loadingWorkspace": "正在装入工作空间",
+ "loadingWorkspace": "正在加载工作区",
"switchWorkspace": "切换工作空间"
},
"repositories": {
- "noneAvailable": "没有可用的寄存器",
+ "noneAvailable": "没有可用的仓库",
"remove": "删除仓库"
},
"export": {
@@ -902,26 +902,26 @@
"sessionTitle": "导出会话"
},
"brand": {
- "claude": "Claude"
+ "claude": "克洛德"
},
"sessionReport": {
"noSessionData": "没有可用的会话数据",
- "title": "会议报告"
+ "title": "会话报告"
},
"sessionFilters": {
"project": {
- "selectProject": "选择工程"
+ "selectProject": "选择项目"
}
},
"tasks": {
"date": {
- "updatedPrefix": "上调",
- "updatedYesterday": "昨天上楼了",
- "yesterday": "昨日来"
+ "updatedPrefix": "更新",
+ "updatedYesterday": "昨天更新",
+ "yesterday": "昨天"
},
"reviewState": {
- "needsFix": "需要修改"
+ "needsFix": "需要修复"
},
- "unassigned": "未指定"
+ "unassigned": "未分配的"
}
}
diff --git a/src/features/localization/renderer/locales/zh/dashboard.json b/src/features/localization/renderer/locales/zh/dashboard.json
index a49d1c5f..9c84025b 100644
--- a/src/features/localization/renderer/locales/zh/dashboard.json
+++ b/src/features/localization/renderer/locales/zh/dashboard.json
@@ -1,17 +1,17 @@
{
"cliStatus": {
"actions": {
- "alreadyLoggedIn": "已经登录了吗?",
- "becomeSponsor": "成为提案国",
+ "alreadyLoggedIn": "已经登录?",
+ "becomeSponsor": "成为赞助商",
"cancel": "取消",
- "checkNow": "现在检查",
+ "checkNow": "立即查看",
"checkUpdates": "检查更新",
- "checking": "正在检查...",
+ "checking": "检查…",
"connect": "连接",
"extensions": "扩展",
"login": "登录",
"manage": "管理",
- "manageProviders": "管理供应商",
+ "manageProviders": "管理提供商",
"plan": "计划",
"recheck": "重新检查",
"recheckProvider": "重新检查 {{provider}}",
@@ -20,161 +20,161 @@
"useCode": "使用代码"
},
"atlas": {
- "alt": "地图集云",
- "description": "Atlas Cloud是一个全模式的AI推论平台,它让开发者获得一个单一的AI API来访问视频生成,图像生成,以及LLM API. 与其管理多个供应商集成,不如连接一次,并获得所有模式300+全方位模型的统一访问. 请检查access-date=中的日期值 (帮助) Atlas Cloud新编码计划推广 更方便预算 API访问.",
- "openCodeProvider": "打开代码提供者",
- "plan": "阿特拉斯云编码计划",
- "sponsor": "发起人"
+ "alt": "Atlas Cloud",
+ "description": "Atlas Cloud 是一个全模态 AI 推理平台,为开发者提供单一 AI API 来访问视频生成、图像生成和 LLM API。您无需管理多个提供商集成,只需连接一次即可统一访问跨所有模态的 300 多个精选模型。查看 Atlas Cloud 的新编码计划促销活动,以获取更实惠的 API 访问权限。",
+ "openCodeProvider": "OpenCode 提供商",
+ "plan": "Atlas Cloud 编码计划",
+ "sponsor": "赞助"
},
"errors": {
- "checkStatusFailed": "检查 CLI 状态失败",
+ "checkStatusFailed": "无法检查 CLI 状态",
"installationFailed": "安装失败",
- "refreshFailed": "检查更新失败 。 检查您的网络连接并再次尝试 。",
- "runtimeUpdatedRefreshFailed": "运行时间已更新, 但无法刷新提供者状态 。"
+ "refreshFailed": "无法检查更新。检查您的网络连接并重试。",
+ "runtimeUpdatedRefreshFailed": "运行时已更新,但无法刷新提供商状态。"
},
"hints": {
- "backgroundStatus": "{{runtime}}状态将在背景中检查.",
- "codexApiKeyFallback": "{{hint}} ZXCV 1ZXCV 如果您切换了认证模式,则可以使用密钥倒置。",
- "codexAutoApiKey": "{{hint}} 苏维埃社会主义共和国 自动会继续使用API密钥,直到ChatGPT连接.",
- "codexFinishLogin": "在浏览器中完成 ChatGPT 登录 。 如果提示, 请输入显示的代码 。",
- "codexNoActiveLogin": "用法限制仅在Codex CLI看到一个活动中的ChatGPT账户后出现. 现在它没有报告ChatGPT的登录。",
- "codexNoActiveManagedSession": "用法限制仅在Codex CLI看到一个活动中的ChatGPT账户后出现. 本地 Codex 账户数据已存在, 但目前没有选择活动管理会话 。",
- "codexReconnectNeeded": "用法限制仅在 Codex 刷新当前选中的 ChatGPT 会话后才会出现 。 现在本地会议需要重新连接。",
- "firstCheckSlow": "第一次检查可能要30秒",
- "loginRequiredForTeams": "浏览会话和项目在不登录的情况下工作. 只需要登录即可运行代理团队.",
- "troubleshootTitle": "如果你确定你登录, 尝试这些步骤:"
+ "backgroundStatus": "{{runtime}} 状态将在后台检查。",
+ "codexApiKeyFallback": "如果您切换认证模式,{{hint}} API 密钥备用选项可用。",
+ "codexAutoApiKey": "{{hint}} Auto 将继续使用 API 密钥,直到连接 ChatGPT。",
+ "codexFinishLogin": "在浏览器中完成 ChatGPT 登录。如有提示,请输入显示的代码。",
+ "codexNoActiveLogin": "仅当 Codex CLI 看到有效的 ChatGPT 帐户后,才会出现用量限制。目前,它报告没有有效的 ChatGPT 登录。",
+ "codexNoActiveManagedSession": "仅当 Codex CLI 看到有效的 ChatGPT 帐户后,才会出现用量限制。本地 Codex 帐户数据存在,但目前未选择有效的托管会话。",
+ "codexReconnectNeeded": "仅在 Codex 刷新当前选定的 ChatGPT 会话后才会出现用量限制。现在本地会话需要重新连接。",
+ "firstCheckSlow": "第一次检查最多可能需要 30 秒",
+ "loginRequiredForTeams": "无需登录即可浏览会话和项目。仅需要登录才能运行 Agent Team。",
+ "troubleshootTitle": "如果您确定已登录,请尝试以下步骤:"
},
"installer": {
- "checkingLatest": "正在检查最新版本...",
- "downloading": "正在下载 {{runtime}}...",
- "installing": "正在安装 {{runtime}}...",
+ "checkingLatest": "正在检查最新版本…",
+ "downloading": "正在下载 {{runtime}}…",
+ "installing": "正在安装 {{runtime}}…",
"success": "成功安装 {{runtime}} v{{version}}",
- "verifying": "正在验证校验和..."
+ "verifying": "正在验证校验和…"
},
"labels": {
- "apiKeyRequired": "需要的 API 密钥",
- "comingSoon": "马上就来",
- "collapseProviderDetails": "折叠提供者细节",
- "expandProviderDetails": "扩展提供者细节",
+ "apiKeyRequired": "需要 API 密钥",
+ "comingSoon": "即将推出",
+ "collapseProviderDetails": "折叠提供商详细信息",
+ "expandProviderDetails": "展开提供商详细信息",
"generateLink": "生成链接",
"loadingRateLimits": "速率限制加载",
- "loggedOut": "供应商已登录",
+ "loggedOut": "提供商已注销",
"loginAuthFailed": "认证失败",
- "loginAuthUpdated": "更新认证",
+ "loginAuthUpdated": "认证已更新",
"loginComplete": "登录完成",
"loginFailed": "登录失败",
"loginTitle": "登录",
"logoutFailed": "注销失败",
- "logoutTitle": "注销",
+ "logoutTitle": "退出",
"notLoggedIn": "未登录",
"openLogin": "打开登录",
- "providerActionRequired": "需要提供者采取的行动",
- "resets": "重新发送 {{time}}",
- "runtimeLoginTitle": "{{runtime}} 苏维埃社会主义共和国 登录"
+ "providerActionRequired": "需要提供商采取行动",
+ "resets": "复位 {{time}}",
+ "runtimeLoginTitle": "{{runtime}} 登录"
},
"loading": {
- "aiProviders": "正在检查 AI 提供者...",
- "claudeCli": "正在检查克劳德CLI..."
+ "aiProviders": "检查 AI 提供商…",
+ "claudeCli": "检查 Claude CLI…"
},
"provider": {
"authenticated": "已认证",
- "backend": "后端: {{backend}}",
- "checkingAuthentication": "正在检查认证...",
- "checkingProviders": "正在检查提供者...",
- "configuredLocalCount": "{{count}} 本地配置",
- "configuredLocalCount_few": "{{count}} 本地配置",
- "configuredLocalCount_many": "{{count}} 本地配置",
- "configuredLocalCount_one": "{{count}} 本地配置",
- "configuredLocalCount_other": "{{count}} 本地配置",
- "configuredLocalTitle": "从您的 OpenCode 配置导入本地 OpenCode 路由 。",
- "connectedCount": "供应商:{{connected}}/{{denominator}}连接",
- "freeModels": "免费模式",
- "freeModelsTitle": "OpenCode 包含一些免费的模型选项, 如在您的设置中可用时的 Big Pickle 。 通过OpenCode的OpenRouter也可以曝光自由模型,但并不是每个OpenCode/OpenRouter模型都是免费的. 可用性和限制可能会改变。",
- "loadingModels": "正在装入模型...",
- "modelsUnavailable": "此运行时间构建无法使用的模型",
- "runtime": "运行时间: {{runtime}}",
- "verifiedCount": "{{count}} 经核查",
- "verifiedCount_few": "{{count}} 经核查",
- "verifiedCount_many": "{{count}} 经核查",
- "verifiedCount_one": "{{count}} 经核查",
- "verifiedCount_other": "{{count}} 经核查",
- "verifiedTitle": "带有成功执行证明的 OpenCode 路由 。"
+ "backend": "后端:{{backend}}",
+ "checkingAuthentication": "正在检查认证…",
+ "checkingProviders": "正在检查提供商…",
+ "configuredLocalCount": "{{count}} 配置本地",
+ "configuredLocalCount_few": "{{count}} 配置本地",
+ "configuredLocalCount_many": "{{count}} 配置本地",
+ "configuredLocalCount_one": "{{count}} 配置本地",
+ "configuredLocalCount_other": "{{count}} 配置本地",
+ "configuredLocalTitle": "从 OpenCode 配置导入的本地 OpenCode 路由。",
+ "connectedCount": "提供商:{{connected}}/{{denominator}} 连接",
+ "freeModels": "免费模型",
+ "freeModelsTitle": "OpenCode 包含免费模型选项,例如您的设置中可用的 Big Pickle。 OpenRouter 通过 OpenCode 也可以公开免费模型,但并非每个 OpenCode/OpenRouter 模型都是免费的。可用性和限制可能会发生变化。",
+ "loadingModels": "正在加载模型…",
+ "modelsUnavailable": "模型不适用于此运行时构建",
+ "runtime": "运行时:{{runtime}}",
+ "verifiedCount": "{{count}} 已验证",
+ "verifiedCount_few": "{{count}} 已验证",
+ "verifiedCount_many": "{{count}} 已验证",
+ "verifiedCount_one": "{{count}} 已验证",
+ "verifiedCount_other": "{{count}} 已验证",
+ "verifiedTitle": "OpenCode 路由具有成功的执行证明。"
},
"runtime": {
- "configuredHealthCheckFailed": "配置的 {{runtime}} 失败启动健康检查.",
- "configuredNotFound": "未找到配置的 {{runtime}} 。",
- "foundButFailed": "发现 {{runtime}} 失败启动",
- "healthCheckFailedDescription": "该应用程序发现了配置的{{runtime}},但其启动健康检查失败. 修理或重新安装,然后重试。",
+ "configuredHealthCheckFailed": "配置的 {{runtime}} 启动运行状况检查失败。",
+ "configuredNotFound": "未找到配置的 {{runtime}}。",
+ "foundButFailed": "找到 {{runtime}} 但启动失败",
+ "healthCheckFailedDescription": "应用找到配置的 {{runtime}},但其启动健康检查失败。修复或重新安装,然后重试。",
"install": "安装 {{runtime}}",
- "installRequiredDescription": "{{runtime}}是团队提供和会话管理所需的. 安装开始 。",
- "isRequired": "需要{{runtime}}",
- "reinstall": "莱因斯托尔 {{runtime}}"
+ "installRequiredDescription": "团队配置和会话管理需要 {{runtime}}。安装它即可开始。",
+ "isRequired": "{{runtime}} 为必填项",
+ "reinstall": "重新安装 {{runtime}}"
},
"runtimeInstall": {
- "checking": "检查中",
- "codexTitle": "在应用数据中安装代码CLI",
- "downloading": "下载",
- "downloadingPercent": "下载 {{percent}}%",
+ "checking": "检查",
+ "codexTitle": "将 Codex CLI 安装到应用数据中",
+ "downloading": "正在下载",
+ "downloadingPercent": "正在下载 {{percent}}%",
"install": "安装",
- "installing": "安装",
- "openCodeTitle": "安装 OpenCode 运行时间到应用数据",
+ "installing": "安装中",
+ "openCodeTitle": "将 OpenCode 运行时安装到应用数据中",
"retryInstall": "重试安装"
},
"troubleshoot": {
- "again": "再来一次",
- "authStatusCommand": "您所配置的 CLI 认证状态命令",
- "checkLoggedIn": "- 检查它是否显示\"Logged in\"",
- "click": "单击",
- "loginCommand": "运行时间登录命令",
- "logoutCommand": "运行时间登录命令",
- "openTerminal": "打开终端并运行:",
- "reloginPrefix": "如果上面写着登录但应用程序看不到的话,请试试:",
- "sameRuntime": "确保您的终端中的 CLI 与应用程序使用的运行时间相同",
+ "again": "再次",
+ "authStatusCommand": "您配置的 CLI 认证状态命令",
+ "checkLoggedIn": "- 检查是否显示“已登录”",
+ "click": "点击",
+ "loginCommand": "运行时登录命令",
+ "logoutCommand": "运行时注销命令",
+ "openTerminal": "打开终端并运行:",
+ "reloginPrefix": "如果显示已登录,但应用看不到它,请尝试:",
+ "sameRuntime": "确保终端中的 CLI 与应用使用的运行时相同",
"statusCacheHint": "- 有时状态会缓存几秒钟",
- "then": "接下来"
+ "then": "然后"
},
"warnings": {
- "multipleApiKeysMissing": "一个或多个提供者被设定为API密钥模式,但没有配置API密钥. 打开管理供应商以添加密钥或切换连接模式.",
- "multipleApiKeysNeedAttention": "一个或多个提供者被设定为API密钥模式,需要关注. 打开管理供应商来审查保存的密钥或切换连接模式 。",
- "notAuthenticated": "{{runtime}}已经安装,但您没有认证 。 团队提供和AI功能需要登录.",
- "singleApiKeyMissing": "{{provider}}被设定为API密钥模式,但没有配置API密钥. 打开管理供应商以添加密钥或切换连接模式 。",
- "singleApiKeyNeedsAttention": "{{provider}}设定为API密钥模式,但没有连接. 打开管理提供者来审查保存的密钥或切换连接模式 。"
+ "multipleApiKeysMissing": "一个或多个提供商设置为 API 密钥模式,但未配置 API 密钥。打开管理提供商以添加密钥或切换连接模式。",
+ "multipleApiKeysNeedAttention": "一个或多个提供商已设置为 API 密钥模式,需要引起注意。打开管理提供商以查看保存的密钥或切换连接模式。",
+ "notAuthenticated": "{{runtime}} 已安装,但您未经过认证。团队配置和 AI 功能需要登录。",
+ "singleApiKeyMissing": "{{provider}} 设置为 API 密钥模式,但未配置 API 密钥。打开管理提供商以添加密钥或切换连接模式。",
+ "singleApiKeyNeedsAttention": "{{provider}} 设置为 API 密钥模式,但未连接。打开管理提供商以查看保存的密钥或切换连接模式。"
}
},
"recentProjects": {
- "selectFolderTitle": "选择工程文件夹",
+ "selectFolderTitle": "选择项目文件夹",
"selectFolder": "选择文件夹",
- "failedToLoad": "装入工程失败",
+ "failedToLoad": "无法加载项目",
"retry": "重试",
- "noProjects": "未找到工程",
- "noMatches": "没有“ {{query}}” 的匹配",
+ "noProjects": "未找到项目",
+ "noMatches": "没有匹配“{{query}}”",
"noRecentProjects": "未找到最近的项目",
- "emptyDescription": "最近Claude和Codex的活动会在这里出现.",
- "loadMore": "装入更多",
+ "emptyDescription": "最近的 Claude 和 Codex 活动将出现在这里。",
+ "loadMore": "加载更多",
"card": {
- "deleted": "删除",
- "projectFolderMissing": "项目文件夹已不存在",
+ "deleted": "已删除",
+ "projectFolderMissing": "项目文件夹不再存在",
"taskCounts": {
- "active": "{{count}}活动",
- "active_one": "{{count}}活动",
- "active_other": "{{count}}活动",
- "active_few": "{{count}}活动",
- "active_many": "{{count}}活动",
- "pending": "{{count}}待处理",
- "pending_one": "{{count}}待处理",
- "pending_other": "{{count}}待处理",
- "pending_few": "{{count}}待处理",
- "pending_many": "{{count}}待处理",
- "done": "{{count}}已执行",
- "done_one": "{{count}}已执行",
- "done_other": "{{count}}已执行",
- "done_few": "{{count}}已执行",
- "done_many": "{{count}}已执行"
+ "active": "{{count}} 活跃",
+ "active_one": "{{count}} 活跃",
+ "active_other": "{{count}} 活跃",
+ "active_few": "{{count}} 活跃",
+ "active_many": "{{count}} 活跃",
+ "pending": "{{count}} 待定",
+ "pending_one": "{{count}} 待定",
+ "pending_other": "{{count}} 待定",
+ "pending_few": "{{count}} 待定",
+ "pending_many": "{{count}} 待定",
+ "done": "{{count}} 完成",
+ "done_one": "{{count}} 完成",
+ "done_other": "{{count}} 完成",
+ "done_few": "{{count}} 完成",
+ "done_many": "{{count}} 完成"
}
},
"title": "最近的项目",
"searchResults": "搜索结果",
- "searchPlaceholder": "搜索项目..."
+ "searchPlaceholder": "搜索项目…"
},
"actions": {
"selectTeam": "选择团队",
@@ -182,16 +182,16 @@
"clearSearch": "清除搜索"
},
"windowsAdmin": {
- "title": "建议使用 Windows 管理员模式",
- "description": "OpenCode 运行时间检查可以在代理 Teams AI 没有提升时超时. 在启动 OpenCode 团队前以管理员身份重新启动应用程序 。"
+ "title": "推荐使用 Windows 管理员模式",
+ "description": "当 Agent Teams AI 未提升时,OpenCode 运行时检查可能会超时。在启动 OpenCode 团队之前,使用以管理员身份运行重新启动应用。"
},
"webPreview": {
- "title": "打开桌面应用程序以完整功能",
- "description": "浏览器版本仍在开发中. 这里的项目行动、整合和现场状态更新可能有限。 使用桌面应用程序可靠地访问所有特性 。"
+ "title": "打开桌面应用以获取完整功能",
+ "description": "浏览器版本仍在开发中。项目操作、集成和实时状态更新可能会受到限制。使用桌面应用可靠地访问所有功能。"
},
"updateBanner": {
"newVersionAvailable": "新版本可用",
- "restartNow": "重新开始",
- "viewDetails": "查看细节"
+ "restartNow": "立即重新启动",
+ "viewDetails": "查看详情"
}
}
diff --git a/src/features/localization/renderer/locales/zh/errors.json b/src/features/localization/renderer/locales/zh/errors.json
index 11560dec..2c845706 100644
--- a/src/features/localization/renderer/locales/zh/errors.json
+++ b/src/features/localization/renderer/locales/zh/errors.json
@@ -1,3 +1,3 @@
{
- "fallback": "出了点问题"
+ "fallback": "出了点问题。"
}
diff --git a/src/features/localization/renderer/locales/zh/extensions.json b/src/features/localization/renderer/locales/zh/extensions.json
index f0f490e1..a086de37 100644
--- a/src/features/localization/renderer/locales/zh/extensions.json
+++ b/src/features/localization/renderer/locales/zh/extensions.json
@@ -2,171 +2,171 @@
"store": {
"actions": {
"addCustom": "添加自定义",
- "openDashboard": "打开挂板",
+ "openDashboard": "打开控制台",
"refreshCatalog": "刷新目录"
},
"capabilities": {
- "mcp": "磁共振:{{status}}",
- "plugins": "插件: {{status}}",
- "skills": "技能:{{status}}"
+ "mcp": "MCP:{{status}}",
+ "plugins": "插件:{{status}}",
+ "skills": "技能:{{status}}"
},
- "desktopOnly": "只有桌面应用程序可用 。",
+ "desktopOnly": "仅在桌面应用中可用。",
"provider": {
- "checkingStatus": "正在检查提供者状态...",
+ "checkingStatus": "正在检查提供商状态…",
"connected": "已连接",
- "loading": "正在装入...",
+ "loading": "加载中…",
"needsSetup": "需要设置",
"readyToConfigure": "准备配置",
"unsupported": "不支持"
},
"runtime": {
- "checkingAvailabilityDescription": "扩展需要配置的运行时间来管理插件,MCP服务器,技能和供应商连接.",
- "checkingAvailabilityTitle": "检查扩展运行时间可用性",
- "failedToStartDescription": "在运行时间通过启动健康检查之前,延长时间将被禁用。 打开Dash板修理或重新安装.",
- "failedToStartTitle": "已找到配置的运行时间, 但启动失败",
- "multimodelCapabilitiesDescription": "提供方的支持可因各节而异. 插件仅在运行时间明确宣布支持时显示 。",
- "multimodelCapabilitiesTitle": "多模式运行时间能力",
- "needsSignInDescription": "{{runtime}}被发现了{{version}},但插件安装被禁用,直到从Dashboard上签名.",
- "needsSignInTitle": "{{runtime}}需要登录",
- "notAvailableDescription": "扩展功能在运行时间安装之前被禁用. 打开Dash板安装并重试.",
- "notAvailableTitle": "配置的运行时间不可用",
- "readyDescription": "插件可以从此页{{versionSuffix}}安装.",
- "readyTitle": "{{runtime}}准备好了",
- "requiredForMutations": "安装或卸载扩展需要配置的运行时间. 从Dashboard上安装或修复."
+ "checkingAvailabilityDescription": "扩展需要配置的运行时来管理插件、MCP 服务器、技能和提供商连接。",
+ "checkingAvailabilityTitle": "检查扩展运行时可用性",
+ "failedToStartDescription": "在运行时通过其启动运行状况检查之前,扩展将被禁用。打开控制台进行修复或重新安装。",
+ "failedToStartTitle": "已找到配置的运行时但启动失败",
+ "multimodelCapabilitiesDescription": "不同功能的提供商支持可能不同。仅当运行时明确声明支持时才会显示插件。",
+ "multimodelCapabilitiesTitle": "多模型运行时功能",
+ "needsSignInDescription": "找到了 {{runtime}}{{version}},但在您从控制台登录之前,插件安装将被禁用。",
+ "needsSignInTitle": "{{runtime}} 需要登录",
+ "notAvailableDescription": "在安装运行时之前,扩展将被禁用。打开控制台进行安装并重试。",
+ "notAvailableTitle": "配置的运行时不可用",
+ "readyDescription": "可以从此页面安装插件 {{versionSuffix}}。",
+ "readyTitle": "{{runtime}} 已准备好",
+ "requiredForMutations": "安装或卸载扩展需要配置的运行时。从控制台安装或修复它。"
},
- "sessionsRestartWarning": "运行会话在重新启动前不会接收扩展更改 。",
+ "sessionsRestartWarning": "正在运行的会话在重新启动之前不会获取扩展更改。",
"tabs": {
"apiKeys": {
- "description": "在线服务的秘密密钥 。 在此添加它们, 这样插件, 服务器和集成可以连接和工作 。",
- "label": "API 苏维埃社会主义共和国 键"
+ "description": "在线服务的密钥。将它们添加到此处,以便插件、服务器和集成可以连接和工作。",
+ "label": "API 密钥"
},
"mcpServers": {
- "description": "连接到外部工具和应用程序 。 他们让运行时间读取数据或者做超过这个应用程序的行动.",
- "label": "MCP 苏维埃社会主义共和国 服务器"
+ "description": "与外部工具和应用的连接。它们让运行时读取数据或执行此应用之外的操作。",
+ "label": "MCP 服务器"
},
"plugins": {
- "description": "跑步时间加一点 在多模型模式中,当支持时,它们目前适用于Anthropic会话。 更广泛的提供者支助正在发展之中。",
+ "description": "运行时的小附加组件。在多模型模式下,它们当前适用于受支持的 Anthropic 会话。更广泛的提供商支持正在开发中。",
"label": "插件"
},
"skills": {
- "description": "为普通工作准备的指令。 它们有助于运行时间更一致地处理可重复的任务.",
+ "description": "常见工作的现成说明。它们帮助运行时更一致地处理可重复的任务。",
"label": "技能"
}
},
"title": "扩展"
},
"pluginsPanel": {
- "activeFilters": "{{count}}活动",
- "browseByFit": "以适合方式浏览",
+ "activeFilters": "{{count}} 活跃",
+ "browseByFit": "按适合浏览",
"capabilities": "能力",
"categories": "类别",
"clearAllFilters": "清除所有过滤器",
"clearFilters": "清除过滤器",
"counts": {
- "capabilities": "{{count}}能力",
- "categories": "{{count}}类别",
- "plugins": "{{count}}插件",
- "capabilities_few": "{{count}}能力",
- "capabilities_many": "{{count}}能力",
- "capabilities_one": "{{count}}能力",
- "capabilities_other": "{{count}}能力",
- "categories_few": "{{count}}类别",
- "categories_many": "{{count}}类别",
- "categories_one": "{{count}}类别",
- "categories_other": "{{count}}类别",
- "plugins_few": "{{count}}插件",
- "plugins_many": "{{count}}插件",
- "plugins_one": "{{count}}插件",
- "plugins_other": "{{count}}插件"
+ "capabilities": "{{count}} 能力",
+ "categories": "{{count}} 类别",
+ "plugins": "{{count}} 插件",
+ "capabilities_few": "{{count}} 能力",
+ "capabilities_many": "{{count}} 能力",
+ "capabilities_one": "{{count}} 能力",
+ "capabilities_other": "{{count}} 能力",
+ "categories_few": "{{count}} 类别",
+ "categories_many": "{{count}} 类别",
+ "categories_one": "{{count}} 类别",
+ "categories_other": "{{count}} 类别",
+ "plugins_few": "{{count}} 插件",
+ "plugins_many": "{{count}} 插件",
+ "plugins_one": "{{count}} 插件",
+ "plugins_other": "{{count}} 插件"
},
"empty": {
- "description": "稍后检查新插件",
- "filteredDescription": "尝试调整搜索或过滤标准",
- "filteredTitle": "没有匹配过滤器的插件",
+ "description": "稍后回来查看新插件",
+ "filteredDescription": "尝试调整您的搜索或过滤条件",
+ "filteredTitle": "没有插件符合您的过滤器",
"title": "没有可用的插件"
},
- "filterDescription": "按类别、能力或安装状态缩小目录。",
- "installedOnly": "仅安装",
- "providerSupportNotice": "目前只为Anthropic(克劳德)会议保证插件支持。 我们正在支持插件 跨所有代理。",
- "resultsUpdateInstantly": "精炼过滤器时立即更新结果。",
- "searchPlaceholder": "搜索插件...",
- "selectedCount": "{{count}}选中",
- "showing": "显示{{shown}}页:1{{total}}插件",
+ "filterDescription": "按类别、功能或安装状态缩小目录范围。",
+ "installedOnly": "仅已安装",
+ "providerSupportNotice": "目前仅保证 Anthropic (Claude) 会话的插件支持。我们正在努力支持所有智能体的插件。",
+ "resultsUpdateInstantly": "当您优化过滤器时,结果会立即更新。",
+ "searchPlaceholder": "搜索插件…",
+ "selectedCount": "已选择 {{count}}",
+ "showing": "正在显示 {{shown}} / {{total}} 个插件",
"sort": {
"category": "类别",
- "nameAsc": "名称 A- Z",
- "nameDesc": "名字为Z - A级",
- "popular": "大众"
+ "nameAsc": "名称 A-Z",
+ "nameDesc": "名称 Z-A",
+ "popular": "受欢迎的"
},
- "activeFilters_few": "{{count}}活动",
- "activeFilters_many": "{{count}}活动",
- "activeFilters_one": "{{count}}活动",
- "activeFilters_other": "{{count}}活动",
- "selectedCount_few": "{{count}}选中",
- "selectedCount_many": "{{count}}选中",
- "selectedCount_one": "{{count}}选中",
- "selectedCount_other": "{{count}}选中"
+ "activeFilters_few": "{{count}} 活跃",
+ "activeFilters_many": "{{count}} 活跃",
+ "activeFilters_one": "{{count}} 活跃",
+ "activeFilters_other": "{{count}} 活跃",
+ "selectedCount_few": "已选择 {{count}}",
+ "selectedCount_many": "已选择 {{count}}",
+ "selectedCount_one": "已选择 {{count}}",
+ "selectedCount_other": "已选择 {{count}}"
},
"customMcp": {
"actions": {
"add": "添加",
"cancel": "取消",
"install": "安装",
- "installing": "安装中..."
+ "installing": "正在安装…"
},
- "description": "在没有目录的情况下手动添加服务器 。",
+ "description": "手动添加服务器,无需目录。",
"errors": {
"installFailed": "安装失败",
- "invalidServerName": "无效的服务器名称 。 使用字母数字字符,破折号,下划线,点.",
- "npmPackageRequired": "需要 npm 软件包名称",
- "serverNameRequired": "需要服务器名称",
- "serverUrlRequired": "需要服务器 URL"
+ "invalidServerName": "服务器名称无效。使用字母数字字符、破折号、下划线、点。",
+ "npmPackageRequired": "npm 包名是必需的",
+ "serverNameRequired": "服务器名称为必填项",
+ "serverUrlRequired": "服务器 URL 为必填项"
},
"fields": {
"environmentVariables": "环境变量",
- "headers": "页眉",
- "npmPackage": "npm 软件包",
+ "headers": "标头",
+ "npmPackage": "npm 包",
"scope": "范围",
"serverName": "服务器名称",
- "serverUrl": "服务器 URL",
+ "serverUrl": "服务器地址",
"transport": "运输",
"transportType": "运输类型",
- "versionOptional": "版本( 可选)"
+ "versionOptional": "版本(可选)"
},
"title": "添加自定义 MCP 服务器",
"transport": {
- "httpSse": "ZXCVKEN0ZXCV / ZXCVTKEN1ZXCV",
- "stdio": "Stdio (npm) (英语)."
+ "httpSse": "HTTP/SSE",
+ "stdio": "Stdio (npm)"
},
"placeholders": {
- "headerName": "页眉 - Name",
- "envVarName": "ENV_VAR_NAME 苏维埃社会主义共和国",
+ "headerName": "标头名称",
+ "envVarName": "ENV_VAR_NAME",
"serverName": "我的服务器",
- "latest": "最新消息",
+ "latest": "最新的",
"value": "价值",
- "serverUrl": "https://api.example.com/mcp 苏维埃社会主义共和国"
+ "serverUrl": "https://api.example.com/mcp"
}
},
"mcpDetail": {
"auth": {
- "remoteMayNeedHeaders": "远程 MCP服务器可能仍然需要自定义头或API密钥,即使登记册没有描述它们. 如果安装后连接失败, 请检查提供者 docs 。",
- "required": "此服务器需要认证"
+ "remoteMayNeedHeaders": "即使注册表没有描述,远程 MCP 服务器可能仍然需要自定义标头或 API 密钥。如果安装后连接失败,请检查提供商文档。",
+ "required": "该服务器需要认证"
},
"diagnostics": {
- "launchTarget": "发射目标"
+ "launchTarget": "发送目标"
},
"form": {
"autoFilled": "自动填充",
"environmentVariables": "环境变量",
- "headers": "页眉",
+ "headers": "标头",
"scope": "范围",
"serverName": "服务器名称"
},
"install": {
- "httpTransport": "HTTP: (英语).{{transport}}",
- "manualSetupDescription": "此服务器需要手动设置 。 检查仓库以获取安装指令 。",
+ "httpTransport": "HTTP:{{transport}}",
+ "manualSetupDescription": "该服务器需要手动设置。检查仓库以获取安装说明。",
"manualSetupRequired": "需要手动设置",
- "npmPackage": "注:{{package}}",
+ "npmPackage": "npm: {{package}}",
"manage": "管理安装",
"install": "安装服务器"
},
@@ -177,25 +177,25 @@
},
"metadata": {
"author": "作者",
- "githubStars": "GitHub 星号",
+ "githubStars": "GitHub 之星",
"hosting": "托管",
"installType": "安装类型",
- "license": "许可证",
- "published": "已出版",
+ "license": "执照",
+ "published": "已发表",
"source": "来源",
- "updated": "更新",
+ "updated": "已更新",
"version": "版本"
},
"scope": {
- "local": "当地",
+ "local": "本地",
"project": "项目"
},
"tools": {
- "title": "工具 ({{count}})",
- "title_few": "工具 ({{count}})",
- "title_many": "工具 ({{count}})",
- "title_one": "工具 ({{count}})",
- "title_other": "工具 ({{count}})"
+ "title": "工具({{count}})",
+ "title_few": "工具({{count}})",
+ "title_many": "工具({{count}})",
+ "title_one": "工具({{count}})",
+ "title_other": "工具({{count}})"
},
"placeholders": {
"serverName": "我的服务器"
@@ -205,86 +205,86 @@
"actions": {
"cancel": "取消",
"createSkill": "创建技能",
- "preparing": "准备...",
- "reviewAndCreate": "审查和创建",
- "reviewAndSave": "审查并保存",
+ "preparing": "正在准备…",
+ "reviewAndCreate": "审核和创建",
+ "reviewAndSave": "查看并保存",
"saveSkill": "保存技能"
},
"advanced": {
- "customDescription": "此技能使用自定义的标记下格式, 因此在此直接编辑 。",
- "customTitle": "2. SKILL.md编辑器.",
- "description": "大多数人可以跳过这个。 只有在您想要直接控制原始标记下文件时才打开 。",
+ "customDescription": "该技能使用自定义的 markdown 格式,所以直接在这里编辑。",
+ "customTitle": "2.SKILL.md 编辑器",
+ "description": "大多数人可以跳过这一点。仅当您想直接控制原始 Markdown 文件时才打开它。",
"hide": "隐藏高级编辑器",
"resetFromStructuredFields": "从结构化字段重置",
"show": "显示高级编辑器",
- "title": "4. 高级 SKILL.md 编辑器"
+ "title": "4.高级 SKILL.md 编辑器"
},
"basics": {
- "description": "给这种技能一个明确的名字,选择谁可以使用它,并决定它应该住在哪里.",
- "title": "1. 基本情况"
+ "description": "给这个技能一个清晰的名称,选择谁可以使用它,并决定它应该存在的位置。",
+ "title": "1. 基础知识"
},
"description": {
- "create": "用简单的语言描述工作流程,审查将要创建的文件,然后保存它。",
- "edit": "更新此技能, 审查由此产生的文件更改, 然后保存它 。"
+ "create": "用通俗易懂的语言描述工作流程,查看将创建的文件,然后保存。",
+ "edit": "更新此技能,查看生成的文件更改,然后保存。"
},
"extraFiles": {
- "addedFiles": "添加的文件:",
+ "addedFiles": "添加的文件:",
"assets": "资产",
- "assetsDescription": "添加截图或捆绑的介质,前提是它们有助于解释工作流程.",
- "description": "只有当支持文件、脚本或资产的技能确实需要时,才添加它们。",
- "lockedForEdits": "根和文件夹已锁定用于编辑",
- "optionalDescription": "添加将包含在审查中的启动文件,并与 `SKILL.md` 一起编写.",
+ "assetsDescription": "仅当屏幕截图或捆绑媒体有助于解释工作流程时才添加它们。",
+ "description": "仅当该技能确实需要时才添加支持文档、脚本或资产。",
+ "lockedForEdits": "根目录和文件夹已锁定以进行编辑",
+ "optionalDescription": "添加将包含在评论中并与“SKILL.md”一起编写的入门文件。",
"optionalTitle": "可选文件",
- "references": "参考资料",
- "referencesDescription": "添加支持文件、 链接或运行时间可以查看的例子 。",
+ "references": "参考",
+ "referencesDescription": "添加运行时可以查看的支持文档、链接或示例。",
"scripts": "脚本",
- "scriptsDescription": "添加帮助命令或设置注释. 在分享这种技能之前仔细审查。",
+ "scriptsDescription": "添加帮助命令或设置注释。在分享此技能之前请仔细查看。",
"title": "3. 额外文件"
},
"fields": {
"compatibility": "兼容性",
- "description": "说明",
+ "description": "描述",
"folderName": "文件夹名称",
- "folderNameHint": "我们从技能名称中自动提出这个建议,所以审查立即进行。",
- "invocation": "应如何使用",
- "license": "许可证",
+ "folderNameHint": "我们会根据技能名称自动建议此操作,以便立即进行审核。",
+ "invocation": "应该如何使用",
+ "license": "执照",
"name": "技能名称",
- "notes": "额外纸条或护栏",
- "root": "在哪里储存",
- "scope": "谁能用呢?",
- "steps": "接下来的主要步骤",
- "whenToUse": "什么时候才能找到这个"
+ "notes": "额外注释或护栏",
+ "root": "存放在哪里",
+ "scope": "谁可以使用它",
+ "steps": "需要遵循的主要步骤",
+ "whenToUse": "何时达到这个目标"
},
"instructions": {
- "description": "这些章节为您生成技能文件, 因此您不需要编辑降标, 除非您想要 。",
- "locked": "结构化字段被锁定,因为您切换到下面的手动`SKILL.md`编辑.",
- "title": "2. 指示"
+ "description": "这些部分会为您生成技能文件,因此除非您愿意,否则不需要编辑 Markdown。",
+ "locked": "结构化字段被锁定,因为您切换到下面的手动“SKILL.md”编辑。",
+ "title": "2. 使用说明"
},
"invocation": {
- "auto": "可自动使用",
- "manualOnly": "只有在你要求的时候"
+ "auto": "可以自动使用",
+ "manualOnly": "只有当你要求的时候"
},
"placeholders": {
- "description": "这种技巧能帮助什么",
- "name": "写简洁的技巧名称",
- "notes": "示例:列出缺失的测试,回归和风险假设.",
- "steps": "1. 检查有关档案。\n2. 先解释主要风险。\n3. 提出最安全的解决办法。",
- "whenToUse": "示例: 当任务为代码检讨或错误分类请求时使用此功能 。",
- "license": "MIT 苏维埃社会主义共和国",
- "compatibility": "圆形码,光标"
+ "description": "这项技能有什么帮助",
+ "name": "写出简洁的技能名称",
+ "notes": "示例:指出缺失的测试、回归和风险假设。",
+ "steps": "1.检查相关文件。 2、先说明主要风险。 3.提出最安全的修复建议。",
+ "whenToUse": "示例:当任务是代码审核或错误分类请求时使用此选项。",
+ "license": "麻省理工学院",
+ "compatibility": "Claude Code,光标"
},
"review": {
- "creating": "创造技能",
- "hint": "首先审查文件更改,然后在下一步确认保存 。",
+ "creating": "创建技能",
+ "hint": "首先查看文件更改,然后在下一步中确认保存。",
"saving": "保存此技能"
},
"root": {
- "codexOnly": "- 只有代码",
+ "codexOnly": "- 仅 Codex",
"shared": "- 共享"
},
"scope": {
- "project": "项目:{{project}}",
- "projectUnavailable": "无法执行的项目",
+ "project": "项目:{{project}}",
+ "projectUnavailable": "项目不可用",
"user": "用户"
},
"title": {
@@ -297,10 +297,10 @@
"cancel": "取消",
"delete": "删除",
"deleteSkill": "删除技能",
- "deleting": "正在删除...",
+ "deleting": "正在删除…",
"editSkill": "编辑技能",
"openFolder": "打开文件夹",
- "openSkillFile": "打开 SKILL.md",
+ "openSkillFile": "打开技能.md",
"retry": "重试"
},
"badges": {
@@ -308,381 +308,381 @@
"autoUse": "自动使用",
"hasScripts": "有脚本",
"manualUse": "手动使用",
- "references": "参考资料",
- "storedIn": "存放于{{root}}"
+ "references": "参考",
+ "storedIn": "存储在 {{root}} 中"
},
"deleteDialog": {
- "description": "删除此技能并移动到回收站?",
- "descriptionWithName": "删除\"{{name}}\"并将其移动到垃圾堆? 如果需要, 您可以稍后从回收站恢复它 。",
- "title": "删除技能?"
+ "description": "删除该技能并将其移至垃圾箱?",
+ "descriptionWithName": "删除“{{name}}”并将其移至垃圾箱?如果需要,您可以稍后从“垃圾箱”中恢复它。",
+ "title": "删除技能?"
},
- "descriptionFallback": "检查发现了技能元数据和原始指令.",
+ "descriptionFallback": "检查发现的技能元数据和原始指令。",
"errors": {
"deleteFailed": "删除技能失败",
- "loadFailed": "无法装入此技能 。"
+ "loadFailed": "无法加载该技能。"
},
"files": {
- "advancedDetails": "高级文件细节",
+ "advancedDetails": "高级文件详细信息",
"assets": "资产",
- "references": "参考资料",
+ "references": "参考",
"scripts": "脚本",
- "storedAt": "存放于"
+ "storedAt": "存储于"
},
"includes": {
"assets": "资产",
- "instructionsOnly": "只是技巧指导",
- "references": "参考文献",
+ "instructionsOnly": "只是技能说明",
+ "references": "参考",
"scripts": "脚本"
},
"invocation": {
- "auto": "匹配任务时自动运行 。",
- "manualOnly": "只有在你明确要求的时候才能跑"
+ "auto": "匹配任务时自动运行。",
+ "manualOnly": "仅当您明确要求时才运行。"
},
"issues": {
- "bundledScripts": "此技能包括捆绑的脚本",
- "reviewCarefully": "在使用之前仔细审查一下这个技能"
+ "bundledScripts": "该技能包括捆绑脚本",
+ "reviewCarefully": "使用此技能之前请仔细查看"
},
- "loading": "正在装入技能细节...",
+ "loading": "正在加载技能详细信息…",
"scope": {
- "personal": "你的个人能力",
- "projectOnly": "只有这个项目"
+ "personal": "你的个人技能",
+ "projectOnly": "仅此项目"
},
"summary": {
"howUsed": "如何使用",
- "included": "怎么会这样",
- "whoCanUse": "谁能用呢?"
+ "included": "附带什么",
+ "whoCanUse": "谁可以使用它"
},
- "titleFallback": "技能细节"
+ "titleFallback": "技能详情"
},
"skillsPanel": {
"actions": {
"createSkill": "创建技能",
- "import": "导入"
+ "import": "进口"
},
"badges": {
"assets": "资产",
"hasScripts": "有脚本",
- "needsAttention": "需要关注",
- "references": "参考资料",
- "storedIn": "存放于{{root}}"
+ "needsAttention": "需要注意",
+ "references": "参考",
+ "storedIn": "存储在 {{root}} 中"
},
- "configuredRuntime": "已配置的运行时间",
+ "configuredRuntime": "配置的运行时",
"counts": {
- "codexOnly": "{{count}} 苏维埃社会主义共和国 仅编码",
- "personal": "{{count}}个人",
- "project": "{{count}}项目",
- "shared": "共享 {{count}}",
- "total": "{{count}}共计",
- "codexOnly_few": "{{count}} 苏维埃社会主义共和国 仅编码",
- "codexOnly_many": "{{count}} 苏维埃社会主义共和国 仅编码",
- "codexOnly_one": "{{count}} 苏维埃社会主义共和国 仅编码",
- "codexOnly_other": "{{count}} 苏维埃社会主义共和国 仅编码",
- "personal_few": "{{count}}个人",
- "personal_many": "{{count}}个人",
- "personal_one": "{{count}}个人",
- "personal_other": "{{count}}个人",
- "project_few": "{{count}}项目",
- "project_many": "{{count}}项目",
- "project_one": "{{count}}项目",
- "project_other": "{{count}}项目",
- "shared_few": "共享 {{count}}",
- "shared_many": "共享 {{count}}",
- "shared_one": "共享 {{count}}",
- "shared_other": "共享 {{count}}",
- "total_few": "{{count}}共计",
- "total_many": "{{count}}共计",
- "total_one": "{{count}}共计",
- "total_other": "{{count}}共计"
+ "codexOnly": "{{count}} 仅限 Codex",
+ "personal": "{{count}} 个人",
+ "project": "{{count}} 项目",
+ "shared": "{{count}} 共享",
+ "total": "{{count}} 总计",
+ "codexOnly_few": "{{count}} 仅限 Codex",
+ "codexOnly_many": "{{count}} 仅限 Codex",
+ "codexOnly_one": "{{count}} 仅限 Codex",
+ "codexOnly_other": "{{count}} 仅限 Codex",
+ "personal_few": "{{count}} 个人",
+ "personal_many": "{{count}} 个人",
+ "personal_one": "{{count}} 个人",
+ "personal_other": "{{count}} 个人",
+ "project_few": "{{count}} 项目",
+ "project_many": "{{count}} 项目",
+ "project_one": "{{count}} 项目",
+ "project_other": "{{count}} 项目",
+ "shared_few": "{{count}} 共享",
+ "shared_many": "{{count}} 共享",
+ "shared_one": "{{count}} 共享",
+ "shared_other": "{{count}} 共享",
+ "total_few": "{{count}} 总计",
+ "total_many": "{{count}} 总计",
+ "total_one": "{{count}} 总计",
+ "total_other": "{{count}} 总计"
},
"empty": {
- "noMatches": "没有与搜索相匹配的技能",
- "noMatchesDescription": "尝试不同的搜索术语或切换过滤器 。",
+ "noMatches": "没有符合您搜索条件的技能",
+ "noMatchesDescription": "尝试不同的搜索词或切换过滤器。",
"noSkills": "还没有技能",
- "noSkillsDescription": "创建您的第一个教授可重复工作流程的技能, 或者导入您已经使用的工作流程 。"
+ "noSkillsDescription": "创建您的第一项技能来教授可重复的工作流程,或导入您已经使用的技能。"
},
"filters": {
"all": "所有技能",
- "codexOnly": "仅编码",
+ "codexOnly": "仅 Codex",
"hasScripts": "有脚本",
- "needsAttention": "需要关注",
- "personal": "个人",
+ "needsAttention": "需要注意",
+ "personal": "个人的",
"project": "项目",
"shared": "共享"
},
"hero": {
- "codexAvailable": "使用 `.codex` 当一个技能应该只保留 Codex 时.",
- "codexUnavailable": "现有的`.codex`技能在这里仍然可以编辑,但新的 Codex 唯一技能需要启用 Codex 运行时间.",
- "description": "技能是可重复使用的指示,有助于运行时间更一致地处理同样的任务.",
- "guidance": "将个人技能用于所有你想要的习惯。 将工程技能用于仅在一个代码库内讲得通的工作流程.",
- "personalContext": "你现在只看到个人技能",
- "projectContext": "您看到的是{{project}}的技能 加上您的个人技能。",
+ "codexAvailable": "当技能应仅适用于 Codex 时,请使用“.codex”。",
+ "codexUnavailable": "现有的“.codex”技能在此处保持可编辑状态,但新的仅限 Codex 的技能需要启用 Codex 运行时。",
+ "description": "技能是可重用的指令,可帮助运行时更一致地处理同类任务。",
+ "guidance": "使用个人技能来养成你想要的随处习惯。将项目技能用于仅在一个代码库中有意义的工作流程。",
+ "personalContext": "您现在只能看到您的个人技能。",
+ "projectContext": "您正在看到 {{project}} 的技能以及您的个人技能。",
"title": "教授可重复的工作"
},
"invocation": {
"auto": "适合时自动运行",
- "manualOnly": "只当您明确要求时运行"
+ "manualOnly": "仅在您明确要求时运行"
},
"loading": {
- "loading": "正在装入技能...",
- "refreshing": "刷新技能中..."
+ "loading": "技能加载中…",
+ "refreshing": "技能刷新…"
},
- "runtimeAudience": "`.claude`,`.cursor`和`.agents`的共享技能可以提供给{{audience}}. 存储在`.codex`中的技能在 Codex 支持可用时只保留 Codex.",
+ "runtimeAudience": "{{audience}} 可以使用“.claude”、“.cursor”和“.agents”中的共享技能。当 Codex 支持可用时,存储在“.codex”中的技能仅适用于 Codex。",
"scope": {
- "project": "这个项目",
- "user": "个人"
+ "project": "此项目",
+ "user": "个人的"
},
- "searchPlaceholder": "以技能名称或它能帮助什么搜索...",
+ "searchPlaceholder": "按技能名称或它的帮助进行搜索…",
"sections": {
"personal": {
- "description": "随处可见你想要的卫生用品和指示",
+ "description": "您想要的习惯和说明随处可见。",
"title": "个人技能"
},
"project": {
- "description": "只有这个密码库才有意义的工作流程。",
+ "description": "仅对此代码库有意义的工作流程。",
"title": "项目技能"
}
},
"sort": {
- "label": "排序技能",
- "name": "名称",
- "recent": "近期"
+ "label": "排序技巧",
+ "name": "姓名",
+ "recent": "最近的"
},
"status": {
- "hasScripts": "包括脚本, 所以仔细审查",
- "needsAttention": "依赖它之前需要注意",
+ "hasScripts": "包含脚本,请仔细查看",
+ "needsAttention": "在依赖它之前需要注意",
"ready": "准备使用"
},
"success": {
- "created": "技能创建成功 。",
- "imported": "成功导入技能 。",
- "saved": "技能保存成功 。"
+ "created": "技能创建成功。",
+ "imported": "技能导入成功。",
+ "saved": "技能保存成功。"
}
},
"pluginDetail": {
- "unknown": "未知数",
+ "unknown": "未知",
"metadata": {
"author": "作者",
"category": "类别",
"source": "来源",
"version": "版本",
"capabilities": "能力",
- "installs": "安装"
+ "installs": "安装量"
},
"scope": {
- "label": "范围:",
+ "label": "范围:",
"options": {
- "user": "用户( 全球)",
- "project": "项目(分摊)",
- "local": "当地( 被忽略)"
+ "user": "用户(全球)",
+ "project": "项目(共享)",
+ "local": "本地(gitignored)"
}
},
"links": {
"homepage": "主页",
- "contact": "联系人"
+ "contact": "接触"
},
"readme": {
- "loading": "正在装入 ZXCVTKEN0ZXCV...",
- "empty": "没有ZXCVKEN0ZXCV。"
+ "loading": "正在加载自述文件…",
+ "empty": "没有可用的自述文件。"
}
},
"skillImport": {
"title": "导入技能",
- "description": "选择一个已有的技能文件夹, 审查要复制的内容, 然后导入到您支持的技能位置 。",
+ "description": "选择一个现有技能文件夹,查看将复制的内容,然后将其导入到您支持的技能位置之一。",
"steps": {
"chooseFolder": {
- "title": "1. 选择技能文件夹",
- "description": "此文件夹应该已经包含 `SKILL.md`, `Skill.md`, 或 `skill.md` 文件 。"
+ "title": "1.选择技能文件夹",
+ "description": "该文件夹应该已包含“SKILL.md”、“Skill.md”或“skill.md”文件。"
},
"location": {
- "title": "2. 决定属于何处",
- "description": "个人技能随处可见。 项目技能只为一个代码库出现."
+ "title": "2. 决定它所属的地方",
+ "description": "个人技能无处不在。项目技能仅针对一个代码库显示。"
}
},
"fields": {
"sourceFolder": "源文件夹",
"destinationFolderName": "目标文件夹名称",
- "audience": "谁能用呢?",
- "storage": "在哪里储存"
+ "audience": "谁可以使用它",
+ "storage": "存放在哪里"
},
"placeholders": {
- "defaultFolderName": "源文件夹名称的默认值"
+ "defaultFolderName": "默认为源文件夹名称"
},
"actions": {
"browse": "浏览",
"cancel": "取消",
- "preparing": "准备...",
- "reviewAndImport": "审查和导入",
+ "preparing": "正在准备…",
+ "reviewAndImport": "审核和导入",
"importSkill": "导入技能",
"backToImport": "返回导入"
},
"scope": {
"user": "用户",
- "project": "项目:{{project}}",
- "projectUnavailable": "无法执行的项目"
+ "project": "项目:{{project}}",
+ "projectUnavailable": "项目不可用"
},
"rootSuffix": {
- "codexOnly": "- 只有代码",
+ "codexOnly": "- 仅 Codex",
"shared": "- 共享"
},
- "reviewHint": "首先审查复制的文件,然后在下一步确认导入.",
+ "reviewHint": "首先检查复制的文件,然后在下一步中确认导入。",
"reviewLabel": "导入此技能",
"errors": {
- "missingSkillFile": "此文件夹看起来还不是技能 。 它需要一个SKILL.md,Skill.md,或技能Md文件.",
- "symbolicLinks": "此文件夹包含符号链接 。 导入真实文件而不是链接 。",
- "tooManyFiles": "此技能文件夹太大, 无法同时导入 。 删除额外文件并再次尝试 。",
- "tooLarge": "此技能文件夹太大, 无法安全导入 。 调整大资产,再试一次。",
- "invalidFolderName": "用字母、数字、点、破折号或下划线选择一个更简单的目的文件夹名称。",
- "mustBeDirectory": "选择要导入的文件夹, 而不是一个文件 。",
- "reviewFailed": "审查导入更改失败",
- "importFailed": "导入技能失败"
+ "missingSkillFile": "这个文件夹看起来还不像技能。它需要 SKILL.md、Skill.md 或 skill.md 文件。",
+ "symbolicLinks": "该文件夹包含符号链接。导入真实文件而不是链接。",
+ "tooManyFiles": "该技能文件夹太大,无法一次导入。删除多余的文件并重试。",
+ "tooLarge": "该技能文件夹太大,无法安全导入。修剪大量资产并重试。",
+ "invalidFolderName": "使用字母、数字、点、破折号或下划线选择一个更简单的目标文件夹名称。",
+ "mustBeDirectory": "选择要导入的文件夹,而不是单个文件。",
+ "reviewFailed": "无法审核导入更改",
+ "importFailed": "技能导入失败"
}
},
"mcpPanel": {
"sort": {
- "nameAsc": "名称 A-Z",
- "nameDesc": "名称 Z A",
+ "nameAsc": "名称 A→Z",
+ "nameDesc": "名称 Z→A",
"toolsDesc": "大多数工具"
},
"health": {
- "title": "MCP 苏维埃社会主义共和国 健康状况",
- "checkingViaRuntime": "通过{{runtime}}检查已安装的ZXVTOKEN0ZXCV服务器.",
- "lastChecked": "上次选中的 {{time}}",
- "description": "从此页面运行诊断,以验证已安装的MCP连接.",
- "checking": "正在检查...",
+ "title": "MCP 健康状况",
+ "checkingViaRuntime": "通过 {{runtime}} 检查已安装的 MCP 服务器…",
+ "lastChecked": "最后检查 {{time}}",
+ "description": "从此页面运行诊断以验证已安装的 MCP 连接。",
+ "checking": "检查…",
"checkStatus": "检查状态"
},
"diagnostics": {
- "title": "运行时间 MCP 诊断",
- "serversCount": "{{count}}服务器",
+ "title": "运行时 MCP 诊断",
+ "serversCount": "{{count}} 服务器",
"serversCount_one": "{{count}} 服务器",
- "serversCount_other": "{{count}}服务器",
- "waiting": "等待诊断结果...",
+ "serversCount_other": "{{count}} 服务器",
+ "waiting": "等待诊断结果…",
"disableReasons": {
- "checkingRuntimeStatus": "正在检查运行时间状态...",
- "checkingRuntimeAvailability": "正在检查运行时间可用性...",
- "runtimeFailedToStart": "已找到配置的运行时间, 但启动失败 。 打开Dash板进行修理或重新安装.",
- "runtimeRequired": "需要配置的运行时间 。 从Dashboard上安装或修复."
+ "checkingRuntimeStatus": "正在检查运行时状态…",
+ "checkingRuntimeAvailability": "检查运行时可用性…",
+ "runtimeFailedToStart": "已找到配置的运行时,但启动失败。打开控制台进行修复或重新安装。",
+ "runtimeRequired": "需要配置的运行时。从控制台安装或修复它。"
},
- "serversCount_few": "{{count}}服务器",
- "serversCount_many": "{{count}}服务器"
+ "serversCount_few": "{{count}} 服务器",
+ "serversCount_many": "{{count}} 服务器"
},
- "searchPlaceholder": "搜索 MCP 服务器...",
+ "searchPlaceholder": "搜索 MCP 服务器…",
"runtime": {
- "notAvailable": "{{runtime}}不详",
- "notInstalled": "{{runtime}}未安装",
- "requiredDescription": "MCP 健康检查要求{{runtime}}。去Dashboard安装或修复它。"
+ "notAvailable": "{{runtime}} 不可用",
+ "notInstalled": "{{runtime}} 未安装",
+ "requiredDescription": "MCP 运行状况检查需要 {{runtime}}。转至控制台进行安装或修复。"
},
"empty": {
"searchTitle": "未找到服务器",
"title": "没有可用的 MCP 服务器",
- "searchDescription": "尝试不同的搜索名词",
- "description": "稍后检查新服务器"
+ "searchDescription": "尝试不同的搜索词",
+ "description": "稍后回来查看新服务器"
},
- "loadMore": "装入更多"
+ "loadMore": "加载更多"
},
"apiKeys": {
- "description": "安全存储 API 键,用于安装 MCP 服务器时自动填充.",
+ "description": "安全存储 API 密钥,以便在安装 MCP 服务器时自动填充。",
"storage": {
- "osKeychain": "密钥通过{{backend}}加密,并以限制文件权限存储(只有所有者).",
- "localEncryption": "OS 密钥链不可用 - 密钥在本地用 AES-256加密. 为了加强保护,安装密钥环服务(gnome-keyring,kwallet)."
+ "osKeychain": "密钥通过 {{backend}} 加密并以受限文件权限存储(仅限所有者)。",
+ "localEncryption": "操作系统钥匙串不可用 - 密钥使用 AES-256 在本地加密。为了获得更强的保护,请安装密钥环服务(gnome-keyring、kwallet)。"
},
"actions": {
"add": "添加 API 密钥",
- "addFirst": "添加您的第一个密钥",
+ "addFirst": "添加您的第一把钥匙",
"edit": "编辑",
- "copied": "已复制!",
- "copyEnvVarName": "复制环境变量名",
+ "copied": "复制了!",
+ "copyEnvVarName": "复制环境变量名称",
"confirmDelete": "再次点击确认",
"delete": "删除"
},
"empty": {
- "title": "没有保存 ZXCVKEN0ZXCV 密钥",
- "description": "在安装 MCP 服务器时将密钥添加到自动填充环境变量中."
+ "title": "未保存 API 密钥",
+ "description": "安装 MCP 服务器时添加密钥以自动填充环境变量。"
},
"form": {
"addTitle": "添加 API 密钥",
"editTitle": "编辑 API 密钥",
- "addDescription": "在MCP服务器安装中存储用于自动填充的API密钥.",
- "editDescription": "更新关键细节 。 您必须重新输入值 。",
- "keychainUnavailable": "OS 键链不可用 - 用 AES-256 本地加密的键. 为OS级保护安装 gnome- keyring 。",
- "name": "名称",
- "namePlaceholder": "例如,OpenAI生产",
+ "addDescription": "存储 API 密钥以便在 MCP 服务器安装中自动填充。",
+ "editDescription": "更新关键细节。您必须重新输入该值。",
+ "keychainUnavailable": "操作系统钥匙串不可用 - 使用 AES-256 在本地加密的密钥。安装 gnome-keyring 以实现操作系统级别的保护。",
+ "name": "姓名",
+ "namePlaceholder": "例如 OpenAI 制作",
"environmentVariableName": "环境变量名称",
- "envVarPlaceholder": "例如,OPENAI_API_KEY",
- "value": "数值",
- "reenterValue": "重新输入密钥值",
- "valuePlaceholder": "嘘...",
+ "envVarPlaceholder": "例如 OPENAI_API_KEY",
+ "value": "价值",
+ "reenterValue": "重新输入键值",
+ "valuePlaceholder": "sk-…",
"scope": "范围",
- "userScopeLabel": "用户( 全球)",
- "projectScopeLabel": "项目:{{project}}",
- "projectUnavailable": "无法执行的项目",
- "boundTo": "界于{{path}}",
+ "userScopeLabel": "用户(全球)",
+ "projectScopeLabel": "项目:{{project}}",
+ "projectUnavailable": "项目不可用",
+ "boundTo": "绑定到 {{path}}",
"cancel": "取消",
- "saving": "正在保存...",
+ "saving": "保存…",
"update": "更新",
"save": "保存",
"errors": {
- "invalidEnvVarFormat": "用字母,数字,下划线 必须以字母或下划线开头 。",
- "nameRequired": "需要名称",
- "envVarRequired": "需要环境变量名称",
- "invalidEnvVar": "无效的环境变量名称",
- "valueRequired": "需要关键值",
- "projectScopeRequiresProject": "工程范围 API 密钥需要运行中的项目",
+ "invalidEnvVarFormat": "使用字母、数字、下划线。必须以字母或下划线开头。",
+ "nameRequired": "姓名为必填项",
+ "envVarRequired": "环境变量名称为必填项",
+ "invalidEnvVar": "环境变量名称无效",
+ "valueRequired": "键值必填",
+ "projectScopeRequiresProject": "项目范围的 API 密钥需要一个活动项目",
"saveFailed": "保存失败"
}
}
},
"skillReview": {
- "title": "审查技能变化",
- "description": "{{reviewLabel}}先预览文件系统更改. 在下文确认之前,没有文字。",
- "noPreview": "没有可用的预览 。",
- "confirmPromptPrefix": "查看下面的 diff, 然后使用",
- "confirmPromptSuffix": "应用这些修改。",
- "noChanges": "尚未检测到文件更改 。",
+ "title": "查看技能变更",
+ "description": "{{reviewLabel}} 首先预览文件系统更改。在您确认以下内容之前,不会写入任何内容。",
+ "noPreview": "没有可用的预览。",
+ "confirmPromptPrefix": "查看下面的差异,然后使用",
+ "confirmPromptSuffix": "应用这些更改。",
+ "noChanges": "尚未检测到文件更改。",
"binaryBadge": "二进制",
- "binaryPreviewHidden": "未显示二进制文件预览 。 文件将被复制为-is 。",
+ "binaryPreviewHidden": "不显示二进制文件预览。文件将按原样复制。",
"summary": {
"fileChanges": "{{count}} 文件更改",
"fileChanges_one": "{{count}} 文件更改",
"fileChanges_other": "{{count}} 文件更改",
- "new": "{{count}}新设",
- "updated": "更新 {{count}}",
- "removed": "{{count}}删除",
+ "new": "{{count}} 新",
+ "updated": "{{count}} 已更新",
+ "removed": "{{count}} 已删除",
"binary": "{{count}} 二进制",
"fileChanges_few": "{{count}} 文件更改",
"fileChanges_many": "{{count}} 文件更改"
}
},
"mcpCard": {
- "toolsCount": "{{count}}工具",
+ "toolsCount": "{{count}} 工具",
"toolsCount_one": "{{count}} 工具",
- "toolsCount_other": "{{count}}工具",
- "envCount": "{{count}}内存",
- "envCount_one": "{{count}} 掩体",
- "envCount_other": "{{count}}内存",
- "auth": "自动",
- "byAuthor": "由{{author}}公司制作",
+ "toolsCount_other": "{{count}} 工具",
+ "envCount": "{{count}} 环境",
+ "envCount_one": "{{count}} 环境",
+ "envCount_other": "{{count}} 环境",
+ "auth": "授权",
+ "byAuthor": "通过 {{author}}",
"hosting": {
- "remote": "远程",
- "local": "当地",
- "both": "两者"
+ "remote": "偏僻的",
+ "local": "本地",
+ "both": "两个都"
},
- "toolsCount_few": "{{count}}工具",
- "toolsCount_many": "{{count}}工具",
- "envCount_few": "{{count}}内存",
- "envCount_many": "{{count}}内存",
+ "toolsCount_few": "{{count}} 工具",
+ "toolsCount_many": "{{count}} 工具",
+ "envCount_few": "{{count}} 环境",
+ "envCount_many": "{{count}} 环境",
"repository": "仓库",
"website": "网站"
},
"installButton": {
- "installing": "安装中...",
- "removing": "删除...",
- "done": "写好了",
+ "installing": "正在安装…",
+ "removing": "正在删除…",
+ "done": "完毕",
"retry": "重试",
"uninstall": "卸载",
"install": "安装"
},
"pluginCard": {
- "official": "官方"
+ "official": "官方的"
}
}
diff --git a/src/features/localization/renderer/locales/zh/report.json b/src/features/localization/renderer/locales/zh/report.json
index 198ca917..99f3d9b4 100644
--- a/src/features/localization/renderer/locales/zh/report.json
+++ b/src/features/localization/renderer/locales/zh/report.json
@@ -1,31 +1,31 @@
{
"cost": {
- "breakdownTitle": "成本细目(每100个令牌)",
- "cacheRead": "缓存已读",
- "cacheWrite": "快取写入",
- "cost": "费用",
- "input": "投入",
- "noCommits": "无承诺",
- "noLinesChanged": "无行更改",
- "output": "产出",
- "parent": "父母:{{cost}}",
- "parentCost": "父母费用",
- "perCommit": "提交",
- "perCommitFormula": "{{count}}总费用",
- "perCommitFormula_few": "{{count}}总费用",
- "perCommitFormula_many": "{{count}}总费用",
- "perCommitFormula_one": "{{count}}总费用",
- "perCommitFormula_other": "{{count}}总费用",
- "perLineChanged": "每行变化",
- "perLineFormula": "{{count}}项目",
- "perLineFormula_few": "{{count}}线路",
- "perLineFormula_many": "{{count}}线路",
- "perLineFormula_one": "{{count}}项目",
- "perLineFormula_other": "{{count}}线路",
- "subagent": "副剂: {{cost}}",
- "subagentCost": "亚剂费用",
+ "breakdownTitle": "成本明细(每 100 万个 Token)",
+ "cacheRead": "缓存读取",
+ "cacheWrite": "缓存写入",
+ "cost": "成本",
+ "input": "输入",
+ "noCommits": "没有提交",
+ "noLinesChanged": "没有改变行",
+ "output": "输出",
+ "parent": "父级:{{cost}}",
+ "parentCost": "父级成本",
+ "perCommit": "每次提交",
+ "perCommitFormula": "总成本 ÷ {{count}} 提交",
+ "perCommitFormula_few": "总成本 ÷ {{count}} 提交",
+ "perCommitFormula_many": "总成本 ÷ {{count}} 提交",
+ "perCommitFormula_one": "总成本 ÷ {{count}} 提交",
+ "perCommitFormula_other": "总成本 ÷ {{count}} 提交",
+ "perLineChanged": "每行更改",
+ "perLineFormula": "总成本 ÷ {{count}} 行",
+ "perLineFormula_few": "总成本 ÷ {{count}} 行",
+ "perLineFormula_many": "总成本 ÷ {{count}} 行",
+ "perLineFormula_one": "总成本 ÷ {{count}} 行",
+ "perLineFormula_other": "总成本 ÷ {{count}} 行",
+ "subagent": "子智能体:{{cost}}",
+ "subagentCost": "子智能体成本",
"title": "成本分析",
- "total": "共计"
+ "total": "总计"
},
"insights": {
"agent": "代理人",
@@ -33,185 +33,185 @@
"agent_many": "代理人",
"agent_one": "代理人",
"agent_other": "代理人",
- "agentTree": "代理树 ({{count}}) 代理树 ({{unit}})",
- "background": "(背景情况)",
+ "agentTree": "智能体树 ({{count}} {{unit}})",
+ "background": "(后台)",
"bashCommands": "Bash 命令",
- "outOfScopeFindings": "范围外调查结果({{count}})",
- "questionsAsked": "提出的问题({{count}})",
+ "outOfScopeFindings": "超出范围的调查结果 ({{count}})",
+ "questionsAsked": "提出的问题 ({{count}})",
"repeated": "重复",
- "skillsInvoked": "被举报技能({{count}})",
- "taskDispatches": "任务调度({{count}})",
- "tasksCreated": "创建的任务( {{count}})",
+ "skillsInvoked": "调用的技能 ({{count}})",
+ "taskDispatches": "任务调度 ({{count}})",
+ "tasksCreated": "已创建任务 ({{count}})",
"teamMode": "团队模式",
- "teams": "团队:{{teams}}",
- "title": "会话透视",
- "total": "共计",
- "unique": "独一无二",
- "skillsInvoked_few": "被举报技能({{count}})",
- "skillsInvoked_many": "被举报技能({{count}})",
- "skillsInvoked_one": "被举报技能({{count}})",
- "skillsInvoked_other": "被举报技能({{count}})",
- "taskDispatches_few": "任务调度({{count}})",
- "taskDispatches_many": "任务调度({{count}})",
- "taskDispatches_one": "任务调度({{count}})",
- "taskDispatches_other": "任务调度({{count}})",
- "tasksCreated_few": "创建的任务( {{count}})",
- "tasksCreated_many": "创建的任务( {{count}})",
- "tasksCreated_one": "创建的任务( {{count}})",
- "tasksCreated_other": "创建的任务( {{count}})",
- "questionsAsked_few": "提出的问题({{count}})",
- "questionsAsked_many": "提出的问题({{count}})",
- "questionsAsked_one": "提出的问题({{count}})",
- "questionsAsked_other": "提出的问题({{count}})",
- "agentTree_few": "代理树 ({{count}}) 代理树 ({{unit}})",
- "agentTree_many": "代理树 ({{count}}) 代理树 ({{unit}})",
- "agentTree_one": "代理树 ({{count}}) 代理树 ({{unit}})",
- "agentTree_other": "代理树 ({{count}}) 代理树 ({{unit}})",
- "outOfScopeFindings_few": "范围外调查结果({{count}})",
- "outOfScopeFindings_many": "范围外调查结果({{count}})",
- "outOfScopeFindings_one": "范围外调查结果({{count}})",
- "outOfScopeFindings_other": "范围外调查结果({{count}})",
- "keyTakeaways": "关键外卖"
+ "teams": "队伍:{{teams}}",
+ "title": "会话见解",
+ "total": "总计",
+ "unique": "唯一",
+ "skillsInvoked_few": "调用的技能 ({{count}})",
+ "skillsInvoked_many": "调用的技能 ({{count}})",
+ "skillsInvoked_one": "调用的技能 ({{count}})",
+ "skillsInvoked_other": "调用的技能 ({{count}})",
+ "taskDispatches_few": "任务调度 ({{count}})",
+ "taskDispatches_many": "任务调度 ({{count}})",
+ "taskDispatches_one": "任务调度 ({{count}})",
+ "taskDispatches_other": "任务调度 ({{count}})",
+ "tasksCreated_few": "已创建任务 ({{count}})",
+ "tasksCreated_many": "已创建任务 ({{count}})",
+ "tasksCreated_one": "已创建任务 ({{count}})",
+ "tasksCreated_other": "已创建任务 ({{count}})",
+ "questionsAsked_few": "提出的问题 ({{count}})",
+ "questionsAsked_many": "提出的问题 ({{count}})",
+ "questionsAsked_one": "提出的问题 ({{count}})",
+ "questionsAsked_other": "提出的问题 ({{count}})",
+ "agentTree_few": "智能体树 ({{count}} {{unit}})",
+ "agentTree_many": "智能体树 ({{count}} {{unit}})",
+ "agentTree_one": "智能体树 ({{count}} {{unit}})",
+ "agentTree_other": "智能体树 ({{count}} {{unit}})",
+ "outOfScopeFindings_few": "超出范围的调查结果 ({{count}})",
+ "outOfScopeFindings_many": "超出范围的调查结果 ({{count}})",
+ "outOfScopeFindings_one": "超出范围的调查结果 ({{count}})",
+ "outOfScopeFindings_other": "超出范围的调查结果 ({{count}})",
+ "keyTakeaways": "要点"
},
"quality": {
"chars": "字符",
- "corrections": "惩戒",
+ "corrections": "修正",
"failed": "失败",
"fileReadRedundancy": "文件读取冗余",
- "firstMessage": "第一个消息",
- "firstRun": "第一个运行",
+ "firstMessage": "第一条消息",
+ "firstRun": "第一次运行",
"frictionRate": "摩擦率",
- "lastRun": "上次运行",
- "messagesBeforeWork": "工作前的信件",
+ "lastRun": "最后一次运行",
+ "messagesBeforeWork": "开始工作前消息",
"passed": "通过",
- "promptQuality": "提示质量",
- "readsPerUniqueFile": "读取/ 唯一文件",
- "snapshot": "简介",
+ "promptQuality": "提示词质量",
+ "readsPerUniqueFile": "读取/唯一文件",
+ "snapshot": "快照",
"snapshot_few": "快照",
"snapshot_many": "快照",
- "snapshot_one": "简介",
+ "snapshot_one": "快照",
"snapshot_other": "快照",
- "startupOverhead": "启动间接费用",
- "testProgression": "测试进度",
+ "startupOverhead": "启动开销",
+ "testProgression": "测试进展",
"title": "质量信号",
- "tokensBeforeWork": "工作前托肯斯语Name",
- "totalReads": "读数共计",
- "uniqueFiles": "独一无二的文件",
- "userMessages": "用户信件",
- "percentOfTotal": "占总数的百分比"
+ "tokensBeforeWork": "工作前的 Token",
+ "totalReads": "总读取次数",
+ "uniqueFiles": "唯一文件",
+ "userMessages": "用户消息",
+ "percentOfTotal": "占总数的%"
},
"tokens": {
- "apiCalls": "API 苏维埃社会主义共和国 电话",
+ "apiCalls": "API 调用",
"cacheCreate": "缓存创建",
"cacheEfficiency": "缓存效率",
- "cacheRead": "缓存已读",
- "cacheReadPct": "快取读取%",
+ "cacheRead": "缓存读取",
+ "cacheReadPct": "缓存读取率",
"coldStart": "冷启动",
- "cost": "费用",
- "input": "投入",
- "model": "型号",
- "no": "没有",
- "output": "产出",
- "readWriteRatio": "R/W比率",
- "title": "调用",
- "total": "共计",
- "yes": "对"
+ "cost": "成本",
+ "input": "输入",
+ "model": "模型",
+ "no": "否",
+ "output": "输出",
+ "readWriteRatio": "读/写比",
+ "title": "Token 使用",
+ "total": "总计",
+ "yes": "是"
},
"subagents": {
- "title": "副剂",
+ "title": "子智能体",
"metrics": {
- "count": "计数",
- "totalTokens": "共计",
- "totalDuration": "期间共计",
- "totalCost": "费用共计"
+ "count": "数量",
+ "totalTokens": "Token 总数",
+ "totalDuration": "总持续时间",
+ "totalCost": "总成本"
},
"table": {
- "description": "说明",
+ "description": "描述",
"type": "类型",
- "tokens": "键",
- "duration": "会期",
- "cost": "费用"
+ "tokens": "Token",
+ "duration": "持续时间",
+ "cost": "成本"
}
},
"overview": {
- "title": "概览",
- "yes": "对",
- "no": "没有",
+ "title": "概述",
+ "yes": "是",
+ "no": "否",
"metrics": {
- "duration": "会期",
- "messages": "信件",
- "contextUsage": "背景使用情况",
+ "duration": "持续时间",
+ "messages": "消息",
+ "contextUsage": "上下文使用",
"compactions": "压缩",
- "branch": "处",
- "subagents": "副剂",
+ "branch": "分支",
+ "subagents": "子智能体",
"project": "项目",
- "sessionId": "会话编号"
+ "sessionId": "会话 ID"
}
},
"timeline": {
- "title": "时间线和活动( A)",
+ "title": "时间线与活动",
"idleAnalysis": "空闲分析",
"metrics": {
- "idleGaps": "空闲差距",
- "totalIdle": "闲置共计",
- "activeTime": "活动时间",
- "idlePercent": "闲置%"
+ "idleGaps": "空闲间隙",
+ "totalIdle": "总空闲时间",
+ "activeTime": "活跃时间",
+ "idlePercent": "空闲率"
},
- "modelSwitches": "型号开关({{count}})",
- "modelSwitches_one": "型号开关({{count}})",
- "modelSwitches_other": "型号开关({{count}})",
- "messageNumber": "# 迈克 #{{number}}",
- "keyEvents": "关键事件",
- "modelSwitches_few": "型号开关({{count}})",
- "modelSwitches_many": "型号开关({{count}})"
+ "modelSwitches": "模型切换({{count}})",
+ "modelSwitches_one": "模型切换({{count}})",
+ "modelSwitches_other": "模型切换({{count}})",
+ "messageNumber": "消息#{{number}}",
+ "keyEvents": "重要事件",
+ "modelSwitches_few": "模型切换({{count}})",
+ "modelSwitches_many": "模型切换({{count}})"
},
"tools": {
"title": "工具使用",
- "summary": "{{formattedCount}} 跨越{{toolCount}}工具的总通话量",
+ "summary": "共 {{formattedCount}} 次调用,涵盖 {{toolCount}} 个工具",
"columns": {
"tool": "工具",
- "calls": "电话",
+ "calls": "调用次数",
"errors": "错误",
- "successPercent": "成功率(%)",
- "health": "卫生"
+ "successPercent": "成功率",
+ "health": "健康状态"
}
},
"git": {
"title": "Git 活动",
"commits": "提交",
- "pushes": "推动",
- "linesAdded": "添加的行数",
- "linesRemoved": "删除的行",
- "branchesCreated": "创建分支"
+ "pushes": "推送",
+ "linesAdded": "新增行数",
+ "linesRemoved": "删除行数",
+ "branchesCreated": "创建的分支"
},
"friction": {
- "title": "Friction 信号",
- "rate": "滑动率:{{rate}}百分比(%)",
- "correctionsCount": "{{count}}更正",
- "correctionsCount_one": "{{count}}更正",
- "corrections": "惩戒",
- "thrashingSignals": "闪烁信号",
- "repeatedBashCommands": "重复的巴什命令",
- "reworkedFiles": "重修的文件( 3+编辑)",
- "correctionsCount_few": "{{count}}更正",
- "correctionsCount_many": "{{count}}更正",
- "correctionsCount_other": "{{count}}更正"
+ "title": "摩擦信号",
+ "rate": "摩擦率:{{rate}}%",
+ "correctionsCount": "{{count}} 修正",
+ "correctionsCount_one": "{{count}} 修正",
+ "corrections": "修正",
+ "thrashingSignals": "反复修改信号",
+ "repeatedBashCommands": "重复的 Bash 命令",
+ "reworkedFiles": "返工文件(3 次以上编辑)",
+ "correctionsCount_few": "{{count}} 修正",
+ "correctionsCount_many": "{{count}} 修正",
+ "correctionsCount_other": "{{count}} 修正"
},
"errors": {
"title": "错误",
- "permissionDenied": "拒绝权限",
- "messageIndex": "# 迈克 #{{index}}",
- "input": "投入",
+ "permissionDenied": "权限被拒绝",
+ "messageIndex": "消息#{{index}}",
+ "input": "输入",
"error": "错误",
- "count": "{{count}}错误",
+ "count": "{{count}} 错误",
"count_one": "{{count}} 错误",
- "permissionDenialCount": "{{count}} 许可拒绝",
- "permissionDenialCount_one": "{{count}} 许可被拒绝",
- "count_few": "{{count}}错误",
- "count_many": "{{count}}错误",
- "count_other": "{{count}}错误",
- "permissionDenialCount_few": "{{count}} 许可拒绝",
- "permissionDenialCount_many": "{{count}} 许可拒绝",
- "permissionDenialCount_other": "{{count}} 许可拒绝"
+ "permissionDenialCount": "{{count}} 权限拒绝次数",
+ "permissionDenialCount_one": "{{count}} 权限拒绝次数",
+ "count_few": "{{count}} 错误",
+ "count_many": "{{count}} 错误",
+ "count_other": "{{count}} 错误",
+ "permissionDenialCount_few": "{{count}} 权限拒绝次数",
+ "permissionDenialCount_many": "{{count}} 权限拒绝次数",
+ "permissionDenialCount_other": "{{count}} 权限拒绝次数"
}
}
diff --git a/src/features/localization/renderer/locales/zh/settings.json b/src/features/localization/renderer/locales/zh/settings.json
index 91c4fd5a..2813cffa 100644
--- a/src/features/localization/renderer/locales/zh/settings.json
+++ b/src/features/localization/renderer/locales/zh/settings.json
@@ -1,22 +1,22 @@
{
"tabs": {
"advanced": {
- "description": "动力用户选项:导出/导入配置,重设默认,以及原始配置编辑.",
+ "description": "高级用户选项:导出/导入配置、重置默认值和编辑原始配置。",
"label": "高级"
},
"general": {
- "description": "核心应用程序首选如主题,语言,显示密度,以及启动行为.",
+ "description": "核心应用首选项,例如主题、语言、显示密度和启动行为。",
"label": "常规"
},
- "infoAriaLabel": "何谓{{label}}? 。 。 。",
+ "infoAriaLabel": "{{label}} 是什么?",
"notifications": {
- "description": "控制何时以及如何通知代理活动、任务完成和错误。",
+ "description": "控制您何时以及如何收到有关智能体活动、任务完成和错误的通知。",
"label": "通知"
}
},
"view": {
- "description": "管理您的应用程序偏好",
- "loading": "正在装入设置...",
+ "description": "管理您的应用首选项",
+ "loading": "正在加载设置…",
"title": "设置"
},
"runtimeProvider": {
@@ -26,139 +26,139 @@
},
"defaults": {
"allProjects": "所有项目",
- "allProjectsHint": "测试使用{{project}}. 默认适用,除非项目有超标。",
- "loadingContexts": "正在装入上下文...",
- "projectHint": "保存只覆盖 {{project}}.",
+ "allProjectsHint": "测试使用 {{project}}。除非项目有覆盖,否则应用默认值。",
+ "loadingContexts": "正在加载上下文…",
+ "projectHint": "保存仅覆盖 {{project}}。",
"projectOverrideContext": "项目覆盖上下文",
- "scopeDescriptionAllProjects": "默认没有自己的 OpenCode 覆盖的每个工程 。",
- "scopeDescriptionProject": "只覆盖选中的项目 。 运行队伍不变.",
- "selectProjectContext": "选择工程上下文",
- "selectProjectHint": "在测试本地模型或保存默认值之前选择一个项目 。",
+ "scopeDescriptionAllProjects": "每个没有自己的 OpenCode 覆盖的项目的默认值。",
+ "scopeDescriptionProject": "仅覆盖选定的项目。运行团队没有变化。",
+ "selectProjectContext": "选择项目上下文",
+ "selectProjectHint": "在测试本地模型或保存默认值之前选择一个项目。",
"selectValidationContext": "选择验证上下文",
- "setAllProjectsDefault": "设置全部项目默认值",
- "setProjectDefault": "设定工程默认",
- "thisProject": "这个项目",
+ "setAllProjectsDefault": "设置所有项目默认值",
+ "setProjectDefault": "设置项目默认值",
+ "thisProject": "此项目",
"title": "OpenCode 默认值",
- "validationContext": "验证背景"
+ "validationContext": "验证上下文"
},
"diagnostics": {
- "copied": "复制的诊断",
- "copiedShort": "复制",
+ "copied": "已复制诊断信息",
+ "copiedShort": "已复制",
"copy": "复制诊断",
- "hints": "提示",
- "likelyCause": "可能的原因是:",
- "windowsSymlinkAdminHint": "Windows: 以管理员身份运行 Agent Teams AI"
+ "hints": "提示词",
+ "likelyCause": "可能的原因:",
+ "windowsSymlinkAdminHint": "Windows:以管理员身份运行 Agent Teams AI"
},
"models": {
- "alreadyDefault": "这已经是所选的 OpenCode 默认值 。",
- "empty": "没有找到模型 。",
- "emptyFree": "没有找到免费模型 。",
- "emptyRecommended": "未找到推荐的模型 。",
- "emptyRecommendedFree": "未找到推荐的自由模型 。",
- "freeOnly": "仅自由",
- "launchableDescription": "您可以在团队拾取器中测试或使用的路由:本地配置,免费内置模型,以及当前默认.",
- "launchableTitle": "可启动的 OpenCode 模型",
- "loadingRoutes": "正在装入 OpenCode 模式路线...",
- "noRoutesMatch": "没有OpenCode模式路线匹配\"{{query}}\".",
- "noneReported": "尚未报告可发射的OpenCode模型路线。 在 OpenCode 中配置本地路由, 或者使用提供商标签来检查目录提供者 。",
- "recommendedOnly": "仅建议",
- "searchPlaceholder": "搜索模式",
- "selectProjectBeforeTesting": "在测试模型前选择项目上下文 。",
- "selectProjectBeforeTestingDefaults": "在测试或保存 OpenCode 默认值之前选择工程上下文 。",
- "useInTeamPicker": "用于团队拾取器",
- "testInProgress": "模型测试已在运行。",
- "validationContextRequired": "请先在上方选择验证上下文, 才能启用 Test 和 Set default。保存到 team picker 只会为新团队记录 route。",
- "actionsUnavailable": "操作暂时不可用。",
- "defaultSaveInProgress": "正在保存 OpenCode default。",
- "routeUnavailableAuth": "此 provider 需要先完成身份验证, 才能使用此模型。",
- "routeUnavailableFailed": "此 model route 未通过上次 execution test。",
- "routeUnavailableGeneric": "此 model route 当前无法使用。",
- "routeUnavailableUnknown": "此模型是当前 OpenCode default, 但尚未出现在 live catalog 中。"
+ "alreadyDefault": "这已经是选定的 OpenCode 默认值。",
+ "empty": "未找到模型。",
+ "emptyFree": "未找到免费模型。",
+ "emptyRecommended": "未找到推荐模型。",
+ "emptyRecommendedFree": "未找到推荐的免费模型。",
+ "freeOnly": "仅免费",
+ "launchableDescription": "来自 OpenCode 配置、免费内置模型和当前默认值的已知路由。本地路由在准备团队启动之前需要成功测试。",
+ "launchableTitle": "OpenCode 模型路由",
+ "loadingRoutes": "正在加载 OpenCode 模型路由…",
+ "noRoutesMatch": "没有 OpenCode 模型路由与“{{query}}”匹配。",
+ "noneReported": "尚未报告 OpenCode 模型路由。在 OpenCode 中配置本地路由或使用“提供商”选项卡检查目录提供商。",
+ "recommendedOnly": "仅推荐",
+ "searchPlaceholder": "搜索模型",
+ "selectProjectBeforeTesting": "在测试模型之前选择项目上下文。",
+ "selectProjectBeforeTestingDefaults": "在测试或保存 OpenCode 默认值之前选择项目上下文。",
+ "testInProgress": "模型测试已经开始。",
+ "useInTeamPicker": "保存用于团队选择器",
+ "validationContextRequired": "选择上面的验证上下文以启用测试并设置默认值。保存团队选择器仅存储新团队的路由。",
+ "actionsUnavailable": "暂时无法执行操作。",
+ "defaultSaveInProgress": "正在保存 OpenCode 默认值。",
+ "routeUnavailableAuth": "该提供商需要先进行认证,然后才能使用该模型。",
+ "routeUnavailableFailed": "该模型路由未能通过最后一次执行测试。",
+ "routeUnavailableGeneric": "该模型路由目前无法使用。",
+ "routeUnavailableUnknown": "该模型是当前 OpenCode 默认模型,但在实时目录中尚不可用。"
},
"providers": {
- "catalog": "OpenCode 提供者目录",
- "countFallback": "OpenCode 供应商",
- "description": "{{count}}。首先显示连接和推荐的提供者。",
- "loadMore": "装入更多提供者",
- "loading": "正在装入 OpenCode 提供者",
- "noMatches": "没有提供者匹配搜索。",
- "noneReported": "管理运行时间未报告 OpenCode 提供者 。",
- "recommended": "建议",
+ "catalog": "OpenCode 提供商目录",
+ "countFallback": "OpenCode 提供商",
+ "description": "{{count}}。首先显示已连接和推荐的提供商。",
+ "loadMore": "加载更多提供商",
+ "loading": "加载 OpenCode 提供商",
+ "noMatches": "没有提供商与该搜索匹配。",
+ "noneReported": "托管运行时未报告 OpenCode 提供商。",
+ "recommended": "推荐",
"refreshCatalog": "刷新目录",
- "searchPlaceholder": "搜索提供者",
- "description_few": "{{count}}。首先显示连接和推荐的提供者。",
- "description_many": "{{count}}。首先显示连接和推荐的提供者。",
- "description_one": "{{count}}。首先显示连接和推荐的提供者。",
- "description_other": "{{count}}。首先显示连接和推荐的提供者。"
+ "searchPlaceholder": "搜索提供商",
+ "description_few": "{{count}}。首先显示已连接和推荐的提供商。",
+ "description_many": "{{count}}。首先显示已连接和推荐的提供商。",
+ "description_one": "{{count}}。首先显示已连接和推荐的提供商。",
+ "description_other": "{{count}}。首先显示已连接和推荐的提供商。"
},
"setup": {
- "loading": "正在装入提供者设置..."
+ "loading": "正在加载提供商设置…"
},
"summary": {
- "defaultModel": "OpenCode 默认值: {{model}}",
- "loading": "正在装入管理的 OpenCode 运行时间、 连接的提供者和模型默认...",
- "source": "资料来源:{{source}}。",
- "title": "OpenCode 运行时间"
+ "defaultModel": "OpenCode 默认:{{model}}",
+ "loading": "正在加载托管 OpenCode 运行时、连接的提供商和模型默认值…",
+ "source": "来源:{{source}}",
+ "title": "OpenCode 运行时"
},
"tabs": {
"models": "模型",
- "providers": "供应商"
+ "providers": "提供商"
},
"modelRoutes": {
- "searchPlaceholder": "搜索模式路线"
+ "searchPlaceholder": "搜索模型路由"
},
"badges": {
- "usedInTeamPicker": "选手用的",
+ "usedInTeamPicker": "为团队选择器保存",
"free": "免费",
- "local": "当地",
- "configured": "配置",
+ "local": "本地",
+ "configured": "已配置",
+ "knownRoute": "已知路由",
"connected": "已连接",
- "verified": "已核实",
+ "verified": "已验证",
"needsTest": "需要测试",
"failed": "失败",
- "unknown": "不详",
- "default": "默认",
- "knownRoute": "已知 route"
+ "unknown": "未知",
+ "default": "默认"
},
"compatibleEndpoint": {
- "baseUrlPlaceholder": "http://localhost:1234 苏维埃社会主义共和国"
+ "baseUrlPlaceholder": "http://localhost:1234"
}
},
"general": {
"agentLanguage": {
- "description": "代理通信语言",
- "descriptionWithDetected": "代理通信语言(发现:{{detected}})",
- "emptyMessage": "未找到语言 。",
+ "description": "智能体沟通语言",
+ "descriptionWithDetected": "智能体沟通语言(检测到:{{detected}})",
+ "emptyMessage": "未找到语言。",
"label": "语言",
- "searchPlaceholder": "搜索语言...",
- "selectPlaceholder": "选择语言...",
- "title": "代理语言"
+ "searchPlaceholder": "搜索语言…",
+ "selectPlaceholder": "选择语言…",
+ "title": "智能体语言"
},
"appLanguage": {
- "description": "应用程序接口的语言.",
+ "description": "应用界面的语言。",
"label": "语言",
- "title": "App 语言"
+ "title": "应用语言"
},
"appearance": {
"autoExpandAIGroups": {
- "description": "在打开记录或收到新消息时自动扩展每个响应转折",
- "label": "默认扩展 AI 响应"
+ "description": "打开记录或收到新消息时自动展开每个响应回合",
+ "label": "默认展开 AI 响应"
},
"nativeTitleBar": {
- "description": "使用默认系统窗口框架而不是自定义标题栏",
- "label": "使用本地标题栏",
+ "description": "使用默认的系统窗口框架而不是自定义标题栏",
+ "label": "使用原生标题栏",
"restartConfirm": {
- "confirmLabel": "重新开始",
- "message": "应用程序需要重新启动以应用标题栏更改 。 现在重新开始?",
- "title": "需要重新启动"
+ "confirmLabel": "重启",
+ "message": "应用需要重新启动才能应用标题栏更改。现在重新启动?",
+ "title": "需要重启"
}
},
"theme": {
"description": "选择您喜欢的颜色主题",
"label": "主题",
"options": {
- "dark": "阴暗",
- "light": "光线",
+ "dark": "深色",
+ "light": "浅色",
"system": "系统"
}
},
@@ -166,7 +166,7 @@
},
"browserAccess": {
"serverMode": {
- "description": "启动 HTTP 服务器从浏览器访问UI 或嵌入 Iframes",
+ "description": "启动 HTTP 服务器以从浏览器访问 UI 或嵌入 iframe 中",
"label": "启用服务器模式"
},
"title": "浏览器访问"
@@ -174,86 +174,86 @@
"localClaudeRoot": {
"actions": {
"selectFolder": "选择文件夹",
- "selectFolderManually": "手工选择文件夹",
+ "selectFolderManually": "手动选择文件夹",
"useAutoDetect": "使用自动检测",
"useFolder": "使用文件夹",
"usePath": "使用路径",
"useThisPath": "使用此路径",
- "useWsl": "使用Linux/WSL吗?"
+ "useWsl": "使用 Linux/WSL?"
},
"confirm": {
"noProjectsDir": {
- "message": "此文件夹不含“ 项目” 目录 。 继续?",
- "title": "未找到工程目录"
+ "message": "该文件夹不包含“projects”目录。仍要继续吗?",
+ "title": "找不到项目目录"
},
"notClaudeDir": {
- "message": "这个文件夹被命名为\"{{folderName}}\",而不是\".claude\". 继续?",
- "title": "选中的文件夹不是. claude"
+ "message": "该文件夹名为“{{folderName}}”,而不是“.claude”。仍要继续吗?",
+ "title": "所选文件夹不是 .claude"
},
"noWslPaths": {
- "message": "无法自动找到 Claude 数据的 WSL distros 。 手动选择文件夹吗?",
- "title": "无 ZXCVTKEN0ZXCV 找到克劳德路径"
+ "message": "无法自动找到带有 Claude 数据的 WSL 发行版。手动选择文件夹?",
+ "title": "未找到 WSL Claude 路径"
},
"wslNoProjectsDir": {
- "message": "\"{{path}}\"不包含\"项目\"目录. 继续?",
- "title": "WSL 路径缺失项目目录"
+ "message": "“{{path}}”不包含“projects”目录。仍要继续吗?",
+ "title": "WSL 路径缺少项目目录"
}
},
"current": {
- "autoDetected": "自动探测: {{path}}",
- "autoDetectedPath": "使用自动检测路径",
+ "autoDetected": "自动检测到:{{path}}",
+ "autoDetectedPath": "使用自动检测的路径",
"customPath": "使用自定义路径",
"label": "当前本地根"
},
- "description": "选择哪个本地文件夹被当作您的 Claude 数据根",
+ "description": "选择将哪个本地文件夹视为您的 Claude 数据根",
"errors": {
- "detectWslFailed": "检测 WSL 失败 克劳德根路径",
- "loadFailed": "装入本地 Claude 根设置失败",
- "updateFailed": "更新 Claude root 失败"
+ "detectWslFailed": "无法检测 WSL Claude 根路径",
+ "loadFailed": "无法加载本地 Claude 根目录 设置",
+ "updateFailed": "Claude 根目录更新失败"
},
- "title": "当地克劳德·根",
+ "title": "本地 Claude 根目录",
"wslModal": {
- "closeAriaLabel": "关闭 WSL 路径模式",
- "description": "检测到的WSL分发量和克劳德根候选人",
+ "closeAriaLabel": "关闭 WSL 路径对话框",
+ "description": "检测到的 WSL 发行版和 Claude 根候选",
"noProjectsDir": "未检测到项目目录",
- "title": "选择 WSL 克劳德根"
+ "title": "选择 WSL Claude 根目录"
}
},
"privacy": {
"telemetry": {
- "description": "通过发送匿名崩溃和性能数据帮助改进应用程序",
+ "description": "通过发送匿名崩溃和性能数据来帮助改进应用",
"label": "发送崩溃报告"
},
"title": "隐私"
},
"server": {
- "runningOn": "运行中",
- "standaloneModeDescription": "以独立模式运行 。 HTTP服务器始终活跃. 没有系统通知 -- -- 通知触发器只记录在应用程序中。",
+ "runningOn": "运行于",
+ "standaloneModeDescription": "以独立模式运行。 HTTP 服务器始终处于活动状态。系统通知不可用 - 通知触发器仅记录在应用中。",
"title": "服务器"
},
"startup": {
"launchAtLogin": {
- "description": "登录时自动启动应用程序",
+ "description": "登录时自动启动应用",
"label": "登录时启动"
},
"showDockIcon": {
- "description": "在端口显示应用程序图标( MacOS)",
- "label": "显示端口图标"
+ "description": "在 Dock 中显示应用图标 (macOS)",
+ "label": "显示Dock图标"
},
"title": "启动"
}
},
"notifications": {
"dev": {
- "descriptionPrefix": "通知在开发模式中可能行不通。 macOS将应用程序识别为\"Electron\"(bundle ID).",
- "descriptionSuffix": ")代替生产应用名称. 检查系统设置 > 通知 > 电子以验证权限 。",
- "title": "Dev 模式"
+ "descriptionPrefix": "通知可能无法在开发模式下工作。 macOS 将应用标识为“Electron”(捆绑 ID",
+ "descriptionSuffix": ") 而不是生产应用名称。检查系统设置 > 通知 > Electron 以验证权限。",
+ "title": "开发模式"
},
"ignoredRepositories": {
- "description": "这些寄存器发出的通知将被忽略",
- "empty": "没有忽略寄存器",
- "selectPlaceholder": "选择要忽略的仓库...",
- "title": "已忽略仓库"
+ "description": "来自这些仓库的通知将被忽略",
+ "empty": "没有忽略任何仓库",
+ "selectPlaceholder": "选择要忽略的仓库…",
+ "title": "忽略的仓库"
},
"settings": {
"enabled": {
@@ -261,114 +261,114 @@
"label": "启用系统通知"
},
"sound": {
- "description": "当通知出现时播放声音",
+ "description": "出现通知时播放声音",
"label": "播放声音"
},
"subagentErrors": {
- "description": "检测并通知子代理会话中的错误",
- "label": "包含子代理错误"
+ "description": "检测并通知子智能体会话中的错误",
+ "label": "包括子智能体错误"
},
"title": "通知设置"
},
"snooze": {
- "clear": "清除 Snooze",
- "description": "临时暂停通知",
- "descriptionWithTime": "喷到{{time}}",
- "label": "Snooze 通知",
+ "clear": "取消暂停",
+ "description": "暂时暂停通知",
+ "descriptionWithTime": "暂停至 {{time}}",
+ "label": "暂停通知",
"options": {
- "15": "15分钟",
- "30": "30分钟",
- "60": "1小时",
- "120": "2小时",
- "240": "4小时",
- "-1": "到明天为止"
+ "15": "15 分钟",
+ "30": "30 分钟",
+ "60": "1 小时",
+ "120": "2 小时",
+ "240": "4 小时",
+ "-1": "直到明天"
},
- "selectDuration": "选择持续时间..."
+ "selectDuration": "选择持续时间…"
},
"taskCompletion": {
- "description": "克劳德完成任务时获得本地OS通知 - 声音,横幅,和Dock/taskbar徽章. 工作在macOS,Linux,和Windows上.",
- "installPlugin": "安装 Claude- Notifics-go 插件",
+ "description": "当 Claude 完成任务时接收原生操作系统通知 - 声音、横幅和 Dock/任务栏徽章。适用于 macOS、Linux 和 Windows。",
+ "installPlugin": "安装 claude-notifications-go 插件",
"title": "任务完成通知"
},
"team": {
"allTasksCompleted": {
- "description": "当团队中的每一项任务达到完成状态时通知",
+ "description": "当团队中的每项任务达到完成状态时发出通知",
"label": "所有任务已完成"
},
"autoResumeOnRateLimit": {
- "description": "当克劳德报告重设时间时,在限制重排后为团队主力安排后续的推力",
- "label": "费率限制后自动恢复"
+ "description": "当 Claude 报告重置时间时,在限额重置后为团队领导安排后续继续",
+ "label": "速率限制后自动恢复"
},
"clarifications": {
- "description": "当任务需要输入时显示本地 OS 通知",
+ "description": "当任务需要您的输入时显示原生操作系统通知",
"label": "任务澄清通知"
},
"crossTeamMessage": {
- "description": "当另一个团队发出消息时通知",
- "label": "跨团队信件通知"
+ "description": "当有来自其他团队的消息到达时发出通知",
+ "label": "跨团队消息通知"
},
"leadInbox": {
- "description": "通知队友向队长发送信息的时间",
- "label": "铅信箱通知"
+ "description": "当队友向团队负责人发送消息时发出通知",
+ "label": "团队负责人收件箱通知"
},
"statusChange": {
- "description": "当任务状态发生变化时显示本地 OS 通知",
- "label": "任务状态更改通知",
+ "description": "当任务状态更改时显示原生操作系统通知",
+ "label": "任务状态变更通知",
"onlySolo": {
- "description": "只在球队没有队友时通知",
- "label": "仅在独奏模式下"
+ "description": "仅当团队没有队友时通知",
+ "label": "仅在单人模式下"
},
"statuses": {
- "description": "哪个目标状态触发通知",
- "label": "通知这些状况",
+ "description": "哪些目标状态会触发通知",
+ "label": "通知这些状态",
"options": {
- "approved": "核定数",
+ "approved": "已批准",
"completed": "已完成",
- "deleted": "删除",
- "in_progress": "开始",
- "needsFix": "需要修改",
- "pending": "待决",
- "review": "审查"
+ "deleted": "已删除",
+ "in_progress": "进行中",
+ "needsFix": "需要修复",
+ "pending": "待办的",
+ "review": "审核"
}
}
},
"taskComments": {
- "description": "代理评论任务时显示本地 OS 通知",
- "label": "任务注释通知"
+ "description": "当智能体对任务发表评论时显示原生操作系统通知",
+ "label": "任务评论通知"
},
"taskCreated": {
- "description": "创建新任务时显示本地 OS 通知",
+ "description": "创建新任务时显示原生操作系统通知",
"label": "任务创建通知"
},
"teamLaunched": {
- "description": "当一个团队完成发射并准备就绪时通知",
- "label": "小组发出通知"
+ "description": "当团队完成启动并准备就绪时发出通知",
+ "label": "团队启动通知"
},
"title": "团队通知",
"toolApproval": {
- "description": "当一个工具需要您批准时通知( Allow/ Deny), 而应用程序没有焦点",
- "label": "工具核准通知"
+ "description": "当应用未聚焦时工具需要您的批准(允许/拒绝)时发出通知",
+ "label": "工具批准通知"
},
"userInbox": {
- "description": "队友给你发信息时通知",
+ "description": "当队友给你发送消息时通知",
"label": "用户收件箱通知"
}
},
"test": {
"action": "发送测试",
- "description": "发送测试通知以验证交付",
+ "description": "发送测试通知以验证送达情况",
"failedToSend": "发送测试通知失败",
"label": "测试通知",
- "sending": "正在发送...",
- "sent": "发送!",
+ "sending": "正在发送…",
+ "sent": "发送!",
"unknownError": "未知错误"
}
},
"advanced": {
"about": {
- "appIconAlt": "应用程序图标",
- "description": "集合自主平行工作的AI代理团队,跨团队沟通,在kanban板上管理任务——内置代码审查,直播过程监控,以及完整的工具可见度.",
- "standalone": "独立",
+ "appIconAlt": "应用图标",
+ "description": "组建 Agent Teams AI 团队,这些团队可以并行自主工作、跨团队沟通并在看板上管理任务 - 具有内置代码审核、实时流程监控和完整的工具可见性。",
+ "standalone": "独立式",
"title": "关于",
"version": "版本 {{version}}"
},
@@ -381,31 +381,31 @@
"title": "配置"
},
"updates": {
- "available": "页:1{{version}}可用",
+ "available": "v{{version}} 可用",
"check": "检查更新",
- "checking": "正在检查...",
- "ready": "更新就绪",
- "unknownVersion": "不详",
- "upToDate": "最新数据"
+ "checking": "检查…",
+ "ready": "更新准备就绪",
+ "unknownVersion": "未知",
+ "upToDate": "最新"
},
- "appName": "特工团队AI"
+ "appName": "Agent Teams AI"
},
"configEditor": {
"errors": {
- "loadFailed": "装入配置失败",
+ "loadFailed": "加载配置失败",
"saveFailed": "保存配置失败"
},
"footer": {
- "autoSave": "编辑后自动更改保存",
+ "autoSave": "更改编辑后自动保存",
"toClose": "关闭",
- "escapeKey": "埃斯科"
+ "escapeKey": "Esc 键"
},
- "loading": "正在装入配置...",
+ "loading": "正在加载配置…",
"status": {
"invalidJson": "无效的 JSON",
"saveFailed": "保存失败",
"saved": "已保存",
- "saving": "正在保存..."
+ "saving": "保存…"
},
"title": "编辑配置"
},
@@ -416,57 +416,57 @@
"title": "添加自定义触发器"
},
"builtin": {
- "description": "默认触发随应用程序而来。 您可以启用或禁用它们并自定义它们的图案 。",
+ "description": "应用附带的默认触发器。您可以启用或禁用它们并自定义它们的模式。",
"title": "内置触发器"
},
"card": {
- "builtinBadge": "内建",
+ "builtinBadge": "内置",
"collapseAriaLabel": "折叠",
- "deleteAriaLabel": "删除触发",
+ "deleteAriaLabel": "删除触发器",
"editNameAriaLabel": "编辑名称",
- "expandAriaLabel": "扩展"
+ "expandAriaLabel": "展开"
},
"color": {
- "customHexTitle": "自定义六进制颜色",
+ "customHexTitle": "自定义十六进制颜色",
"invalidHex": "无效的十六进制"
},
"configuration": {
- "alertIfGreaterThan": "警告如果 >",
- "emptyPatternHint": "留空以匹配所有内容 。 使用JavaScript regex语法.",
- "errorStatusDescription": "当工具执行报告错误时触发( is error: true).",
- "tokensUnit": "符号",
- "matchPatternPlaceholder": "例如,错误QQ失败XX例外"
+ "alertIfGreaterThan": "如果 > 则发出警报",
+ "emptyPatternHint": "留空以匹配所有内容。使用 JavaScript 正则表达式语法。",
+ "errorStatusDescription": "当工具执行报告错误 (is_error: true) 时触发。",
+ "tokensUnit": "Token",
+ "matchPatternPlaceholder": "例如,错误|失败|异常"
},
"custom": {
- "description": "创建自己的触发器以获取特定模式或工具输出的通知。",
- "empty": "尚未配置自定义触发器 。",
+ "description": "创建您自己的触发器以获取特定模式或工具输出的通知。",
+ "empty": "尚未配置自定义触发器。",
"title": "自定义触发器"
},
"errors": {
- "invalidRegexPattern": "无效的正则图案"
+ "invalidRegexPattern": "无效的正则表达式模式"
},
"fields": {
"contentType": "内容类型",
"matchField": "匹配字段",
- "matchPattern": "匹配模式( Regex)",
- "scopeToolName": "范围/ 工具名称",
- "scopeToolNameOptional": "范围/ 工具名称( 可选)",
- "threshold": "阈值",
- "tokenType": "托肯类型",
- "triggerNamePlaceholder": "例如,构建失败提醒",
- "triggerNameRequired": "触发名称 *"
+ "matchPattern": "匹配模式(正则表达式)",
+ "scopeToolName": "范围/工具名称",
+ "scopeToolNameOptional": "范围/工具名称(可选)",
+ "threshold": "临界点",
+ "tokenType": "Token 类型",
+ "triggerNamePlaceholder": "例如,构建失败警报",
+ "triggerNameRequired": "触发器名称 *"
},
"ignorePatterns": {
- "hint": "按 Enter 键可添加。 如果模式匹配, 则跳过通知 。",
- "placeholder": "添加忽略 regex...",
+ "hint": "按 Enter 键添加。如果任何模式匹配,则跳过通知。",
+ "placeholder": "添加忽略正则表达式…",
"removeAriaLabel": "删除忽略模式",
- "summary": "高级:排除规则",
- "title": "忽略图案( 如果匹配则滑动)"
+ "summary": "高级:排除规则",
+ "title": "忽略模式(如果匹配则跳过)"
},
"options": {
"contentTypes": {
"text": "文本输出",
- "thinking": "思维",
+ "thinking": "思考",
"tool_result": "工具结果",
"tool_use": "工具使用"
},
@@ -474,61 +474,61 @@
"args": "参数",
"command": "命令",
"content": "内容",
- "description": "说明",
+ "description": "描述",
"file_path": "文件路径",
- "fullInput": "全部输入( JSON)",
- "glob": "Glob 过滤器",
+ "fullInput": "完整输入 (JSON)",
+ "glob": "全局过滤器",
"new_string": "新字符串",
"old_string": "旧字符串",
"path": "路径",
- "pattern": "图案",
- "prompt": "提示",
+ "pattern": "模式",
+ "prompt": "提示词",
"query": "查询",
"skill": "技能名称",
- "subagent_type": "副剂类型",
- "text": "文本内容",
+ "subagent_type": "子智能体类型",
+ "text": "文字内容",
"thinking": "思考内容",
- "url": "URL 苏维埃社会主义共和国"
+ "url": "URL"
},
"modes": {
"content_match": "内容模式",
"error_status": "执行错误",
- "token_threshold": "高调使用"
+ "token_threshold": "高 Token 使用率"
},
"tokenTypes": {
- "input": "输入键",
- "output": "输出键",
- "total": "共计"
+ "input": "输入 Token",
+ "output": "输出 Token",
+ "total": "Token 总数"
},
"toolNames": {
- "anyTool": "任意工具"
+ "anyTool": "任何工具"
}
},
"preview": {
- "defaultTestTriggerName": "测试触发器",
- "detectedSuffix": "错误会被检测到",
- "more": "...还有{{count}}更多",
- "more_few": "...还有{{count}}更多",
- "more_many": "...还有{{count}}更多",
- "more_one": "...还有{{count}}更多",
- "more_other": "...还有{{count}}更多",
- "testTrigger": "测试触发器",
- "testing": "测试...",
+ "defaultTestTriggerName": "测试触发",
+ "detectedSuffix": "错误将会被检测到",
+ "more": "…以及 {{count}} 更多",
+ "more_few": "…以及 {{count}} 更多",
+ "more_many": "…以及 {{count}} 更多",
+ "more_one": "…以及 {{count}} 更多",
+ "more_other": "…以及 {{count}} 更多",
+ "testTrigger": "测试触发",
+ "testing": "测试…",
"title": "预览",
- "truncatedWarning": "搜索提前停止( 超时或计数限制) 。 实际匹配可能更高。",
+ "truncatedWarning": "搜索提前停止(超时或计数限制)。实际匹配可能会更高。",
"viewSession": "查看会话"
},
"repositoryScope": {
- "empty": "没有选择寄存器 - 触发适用于所有寄存器",
- "hint": "当选择寄存器时,这只触发这些寄存器中的错误。",
- "placeholder": "选择要添加的仓库...",
- "summary": "高级:仓库范围",
- "title": "对仓库的限制( 仅适用于选定的寄存器)"
+ "empty": "未选择仓库 - 触发器适用于所有仓库",
+ "hint": "选择仓库后,仅当这些仓库中出现错误时才会触发此触发器。",
+ "placeholder": "选择要添加的仓库…",
+ "summary": "高级:仓库范围",
+ "title": "限制仓库(仅适用于选定的仓库)"
},
"sections": {
"configuration": "配置",
"dotColor": "点颜色",
- "generalInfo": "常规信息",
+ "generalInfo": "一般信息",
"triggerCondition": "触发条件"
}
},
@@ -541,53 +541,53 @@
"save": "保存"
},
"authMethods": {
- "agent": "SSH 苏维埃社会主义共和国 探员",
- "auto": "自动( 从 SSH 配置)",
+ "agent": "SSH 代理",
+ "auto": "自动(来自 SSH 配置)",
"password": "密码",
"privateKey": "私钥"
},
"deleteConfirm": {
"confirmLabel": "删除",
- "message": "您确定要删除“ {{name}}” 吗? 这一点是无法消除的。",
+ "message": "您确定要删除“{{name}}”吗?此操作无法撤消。",
"title": "删除配置文件"
},
- "description": "保存 SSH 连接配置用于快速重联",
+ "description": "保存 SSH 连接配置文件以便快速重新连接",
"empty": {
"description": "添加 SSH 配置文件以快速连接",
- "title": "没有保存配置文件"
+ "title": "没有保存的配置文件"
},
"form": {
"authentication": "认证",
- "host": "东道主",
+ "host": "主机",
"name": "名称",
- "passwordPrompt": "连接时会提醒您密码 。",
+ "passwordPrompt": "连接时系统会提示您输入密码。",
"port": "端口",
"privateKeyPath": "私钥路径",
"username": "用户名",
"namePlaceholder": "我的服务器",
- "hostPlaceholder": "主机名称或IP",
+ "hostPlaceholder": "主机名或 IP",
"usernamePlaceholder": "用户"
},
- "loading": "正在装入配置文件...",
- "title": "工作空间简介"
+ "loading": "正在加载配置文件…",
+ "title": "工作区配置文件"
},
"connection": {
"actions": {
"connect": "连接",
- "connecting": "正在连接...",
- "disconnect": "断开连接",
+ "connecting": "正在连接…",
+ "disconnect": "断开",
"testConnection": "测试连接",
- "testing": "测试..."
+ "testing": "测试…"
},
"currentMode": {
"description": "会话文件的数据源",
"label": "当前模式",
- "local": "当地({{path}})"
+ "local": "本地 ({{path}})"
},
- "description": "连接到远程机器以查看在那里运行的 Claude 代码会话",
+ "description": "连接到远程计算机以查看在那里运行的 Claude Code 会话",
"form": {
"authentication": "认证",
- "host": "东道主",
+ "host": "主机",
"password": "密码",
"port": "端口",
"privateKeyPath": "私钥路径",
@@ -599,14 +599,14 @@
"title": "保存的配置文件"
},
"ssh": {
- "title": "ZXCVKEN0ZXCV 连接"
+ "title": "SSH 连接"
},
"status": {
- "connectedTo": "已连接到 {{host}}",
+ "connectedTo": "连接到 {{host}}",
"remoteSessions": "通过 SSH 查看远程会话"
},
"test": {
- "failed": "连接失败: {{error}}",
+ "failed": "连接失败:{{error}}",
"success": "连接成功",
"unknownError": "未知错误"
},
@@ -619,131 +619,131 @@
"connectChatGpt": "连接 ChatGPT",
"delete": "删除",
"disable": "禁用",
- "disconnectAccount": "断开账户",
+ "disconnectAccount": "断开帐户连接",
"generateLink": "生成链接",
"openLogin": "打开登录",
"reconnectAnthropic": "重新连接 Anthropic",
"refresh": "刷新",
- "replaceKey": "替换密钥",
- "saveEndpoint": "保存结束点",
+ "replaceKey": "更换密钥",
+ "saveEndpoint": "保存端点",
"saveKey": "保存密钥",
- "saving": "正在保存...",
+ "saving": "保存…",
"setApiKey": "设置 API 密钥",
"updateKey": "更新密钥",
"useCode": "使用代码"
},
"apiKey": {
- "loadingStoredCredentials": "正在装入存储的证书...",
+ "loadingStoredCredentials": "正在加载存储的凭据…",
"projectScope": "项目",
"scope": "范围",
- "storedIn": "存放于{{backend}}",
+ "storedIn": "存储在 {{backend}} 中",
"userScope": "用户",
- "storedInApp": "存储在应用程序中",
+ "storedInApp": "存储在应用中",
"providers": {
"anthropic": {
- "name": "ZXCVKEN0ZXCV 键",
+ "name": "Anthropic API 密钥",
"title": "API 密钥",
- "description": "使用直Anthropic API密钥进行API传动访问. 您的 Anthropic 订阅会话在切换回时仍然可用 。",
- "placeholder": "SK -ANT - -这..."
+ "description": "使用直接 Anthropic API 密钥进行 API 计费访问。当您切换回来时,您的 Anthropic 订阅会话仍然可用。",
+ "placeholder": "sk-ant-…"
},
"codex": {
- "name": "代码API 密钥",
+ "name": "Codex API 密钥",
"title": "API 密钥",
- "description": "使用 OpenAI API 键作为二级代码认证路径. 如果您将 Codex 切换到 API 密钥模式,该应用将会将 OPENAI_API_KEY 反射为 CODEX_API_KEY 用于本地发射.",
- "placeholder": "sk -proj - -这..."
+ "description": "使用 OpenAI API 密钥作为辅助 Codex 认证路径。如果您将 Codex 切换到 API 密钥模式,应用会将 OPENAI_API_KEY 镜像到 CODEX_API_KEY 以进行本机启动。",
+ "placeholder": "sk-proj-…"
},
"gemini": {
- "name": "双子座 API 密钥",
+ "name": "Gemini API 密钥",
"title": "API 访问权限",
- "description": "使用`GEMINI_API_KEY`为双子座ZXCVKEN1ZXCV后端. CLI ZXVKEN3ZXCV和ADC不要求这样做.",
- "placeholder": "艾尔扎..."
+ "description": "对 Gemini API 后端使用“GEMINI_API_KEY”。 CLI SDK 和 ADC 不需要它。",
+ "placeholder": "AIza…"
}
}
},
"codex": {
"account": {
- "appServer": "应用服务器: {{state}}",
+ "appServer": "应用服务器:{{state}}",
"connected": "已连接",
- "description": "管理本地的 Codex app-server 账户会话, 授权订阅支持的本地启动 。",
- "loginInProgress": "正在登录",
- "plan": "计划:{{plan}}",
+ "description": "管理本地 Codex 应用服务器帐户会话,为订阅支持的本机启动提供支持。",
+ "loginInProgress": "登录正在进行中",
+ "plan": "计划:{{plan}}",
"reconnectRequired": "需要重新连接",
- "title": "ChatGPT 账户",
+ "title": "ChatGPT 帐户",
"hints": {
- "autoUsesApiKeyUntilChatgpt": "{{message}} 苏维埃社会主义共和国 自动会继续使用检测到的API密钥,直到ChatGPT连接.",
- "detectedApiKeyNeedsApiMode": "{{message}} 苏维埃社会主义共和国 检测到的API密钥仅在您切换到API密钥模式后使用.",
- "localArtifactsNoSession": "Codex CLI目前未报告运行中的ChatGPT账户. 本地 Codex 账户数据已存在,但没有选择活动管理会话 。 使用限制仅在Codex CLI看到后出现.",
- "noActiveAccount": "Codex CLI目前未报告运行中的ChatGPT账户. 使用限制仅在Codex CLI看到后出现.",
- "reconnectBeforeUsage": "Codex有一个本地选择的ChatGPT账户,但当前会话需要重新连接才能在此加载使用限制.",
- "usageLimitsAfterReport": "用法限制在Codex为连接的ChatGPT账户报告后在此出现."
+ "autoUsesApiKeyUntilChatgpt": "{{message}} Auto 将继续使用检测到的 API 密钥,直到连接 ChatGPT。",
+ "detectedApiKeyNeedsApiMode": "{{message}} 检测到的 API 密钥仅在您将 Codex 切换到 API 密钥模式后使用。",
+ "localArtifactsNoSession": "Codex CLI 当前报告没有有效的 ChatGPT 帐户。本地 Codex 帐户数据存在,但未选择有效的托管会话。仅当 Codex CLI 看到用量限制后,此处才会显示用量限制。",
+ "noActiveAccount": "Codex CLI 当前报告没有有效的 ChatGPT 帐户。仅当 Codex CLI 看到用量限制后,此处才会显示用量限制。",
+ "reconnectBeforeUsage": "Codex 有一个本地选择的 ChatGPT 帐户,但当前会话需要重新连接才能在此处加载用量限制。",
+ "usageLimitsAfterReport": "Codex 报告连接的 ChatGPT 帐户的用量限制后,此处会显示用量限制。"
}
},
"install": {
- "checking": "检查中",
- "downloading": "下载",
- "installCli": "安装代码CLI",
- "installing": "安装",
+ "checking": "检查",
+ "downloading": "正在下载",
+ "installCli": "安装 Codex CLI",
+ "installing": "安装中",
"retryInstall": "重试安装",
- "title": "在应用数据中安装代码CLI"
+ "title": "将 Codex CLI 安装到应用数据中"
},
"rateLimits": {
- "credits": "贷项",
- "creditsDescription": "信用与基于窗口的订阅使用分开显示,可能无法用于计划支持的ChatGPT会话.",
- "noSecondaryWindow": "Codex 没有返回此账户快照的二级窗口 。",
- "notReported": "未报告",
- "primaryReset": "初级重置",
+ "credits": "积分",
+ "creditsDescription": "积分与基于窗口的订阅使用情况分开显示,并且可能不适用于计划支持的 ChatGPT 会话。",
+ "noSecondaryWindow": "Codex 未返回此帐户快照的辅助窗口。",
+ "notReported": "未报道",
+ "primaryReset": "主要重置",
"primaryUsed": "主要使用",
"primaryWindow": "主窗口",
- "remainingLeft": "{{value}}左边",
- "remainingUnknown": "剩余未知",
- "secondaryReset": "二级重置",
- "secondaryUsed": "二级",
- "secondaryWindow": "二级窗口",
- "usedQuotaNote": "这些百分比显示使用的配额,而不是剩余的配额。",
- "weeklyReset": "每周重设",
- "weeklyUsed": "每周使用次数",
- "weeklyUsedOneWeek": "每周使用(1w)",
+ "remainingLeft": "{{value}} 左",
+ "remainingUnknown": "仍未知",
+ "secondaryReset": "二次复位",
+ "secondaryUsed": "二次使用",
+ "secondaryWindow": "辅助窗口",
+ "usedQuotaNote": "这些百分比显示已用配额,而不是剩余配额。",
+ "weeklyReset": "每周重置",
+ "weeklyUsed": "每周使用",
+ "weeklyUsedOneWeek": "每周使用(1w)",
"weeklyWindow": "每周窗口",
- "secondaryFallback": "中学",
- "secondaryWindowNote": "周限在{{window}}窗口中单独显示.",
- "usageExplanationGeneric": "显示用过配额, 而非剩余配额 。",
- "usageExplanationWindowOnly": "在当前 {{window}} 窗口中显示使用配额, 而不是保留配额 。",
- "usageExplanationWithRemaining": "{{used}}使用 - 关于{{remaining}}留在当前{{window}}图标。"
+ "secondaryFallback": "辅助",
+ "secondaryWindowNote": "每周限制单独显示在 {{window}} 窗口中。",
+ "usageExplanationGeneric": "显示已用配额,而不是剩余配额。",
+ "usageExplanationWindowOnly": "显示当前 {{window}} 窗口中已使用的配额,而不是剩余配额。",
+ "usageExplanationWithRemaining": "已使用 {{used}};当前 {{window}} 窗口约剩余 {{remaining}}。"
}
},
"compatibleEndpoint": {
- "authToken": "证书",
- "authTokenMissing": "未配置 Auth 令牌 。",
- "baseUrl": "ZXCVKEN0ZXCV基地",
- "description": "使用 Anthropic 兼容本地运行时间终点 。",
- "keepSavedToken": "留空以保留保存的符号",
- "title": "本地/ 兼容端点",
- "tokenStatus": "托肯 {{status}}",
+ "authToken": "认证 Token",
+ "authTokenMissing": "未配置认证 Token。",
+ "baseUrl": "基本 URL",
+ "description": "使用与 Anthropic 兼容的本地运行时端点。",
+ "keepSavedToken": "留空以保留保存的 Token",
+ "title": "本地/兼容端点",
+ "tokenStatus": "Token {{status}}",
"validation": {
- "baseUrlRequired": "需要URL基地",
- "firstPartyAnthropic": "使用自动、 订阅或 API 键用于第一当事方 Anthropic",
- "httpRequired": "Base URL 必须使用 http:// 或 https://",
- "invalidUrl": "无效的 URL",
- "noCredentials": "基地URL不得包括全权证书"
+ "baseUrlRequired": "基本 URL 为必填项",
+ "firstPartyAnthropic": "对第一方 Anthropic 使用自动、订阅或 API 密钥",
+ "httpRequired": "基本 URL 必须使用 http:// 或 https://",
+ "invalidUrl": "无效 URL",
+ "noCredentials": "基本 URL 不得包含凭据"
},
"status": {
- "endpointDisabledTokenKept": "端点已禁用 。 保存了保存的纪念物。",
- "endpointSaved": "结束点已保存",
- "endpointSavedTokenMissing": "结尾已保存 。 未配置 Auth 令牌 。"
+ "endpointDisabledTokenKept": "端点已禁用。保存的 Token 被保留。",
+ "endpointSaved": "端点已保存",
+ "endpointSavedTokenMissing": "端点已保存。未配置认证 Token。"
}
},
"connection": {
- "authenticationMethod": "认证方法",
+ "authenticationMethod": "认证方式",
"descriptions": {
- "anthropic": "选择应用启动的 Anthropic 会话如何认证 。",
- "codex": "在本地运行时间启动时选择 Codex 是否更喜欢您的 ChatGPT 订阅, 还是选择 API 密钥 。",
- "gemini": "配置可选的 API 访问 。 CLI ZXKEN2ZXCV和ADC仍然自动被发现.",
- "opencode": "OpenCode认证和提供者目录由OpenCode运行时间管理."
+ "anthropic": "选择应用启动的 Anthropic 会话的认证方式。",
+ "codex": "选择当本机运行时启动时,Codex 是否应该首选您的 ChatGPT 订阅或 API 密钥。",
+ "gemini": "配置可选的 API 访问权限。 CLI SDK 和 ADC 仍会自动发现。",
+ "opencode": "OpenCode 认证和提供商库存由 OpenCode 运行时管理。"
},
- "method": "连接方法",
- "mode": "模式: {{mode}}",
- "selected": "选中",
- "switching": "正在切换...",
+ "method": "连接方式",
+ "mode": "模式:{{mode}}",
+ "selected": "已选择",
+ "switching": "切换…",
"title": "连接"
},
"connectionCards": {
@@ -751,244 +751,244 @@
"title": "API 密钥"
},
"anthropic": {
- "apiKeyDescription": "使用ANTHROPIC_API_KEY和Anthropic ZXCVKEN1ZXCV计费.",
- "autoDescription": "使用 Anthropic 运行时默认和最佳本地证书可用 。",
- "hint": "自动保存 Anthropic 的默认本地证书分辨率 。",
- "subscriptionDescription": "使用本地的 Anthropic 签名和订阅权限 。",
- "subscriptionTitle": "人类订阅"
+ "apiKeyDescription": "使用 ANTHROPIC_API_KEY 和 Anthropic API 计费。",
+ "autoDescription": "使用 Anthropic 运行时默认值和可用的最佳本地凭据。",
+ "hint": "Auto 使 Anthropic 保持其默认的本地凭证解析。",
+ "subscriptionDescription": "使用您本地的 Anthropic 登录会话和订阅访问权限。",
+ "subscriptionTitle": "Anthropic 订阅"
},
"auto": {
"title": "自动"
},
"codex": {
- "apiKeyDescription": "使用OPENAI_API_KEY和CODEX_API_KEY为本地的Codex发射计费.",
- "autoDescription": "优先使用您的 ChatGPT 账户和订阅 。 仅在需要时使用 API 密钥模式.",
- "chatgptDescription": "使用您连接的 ChatGPT 账户和 Codex 订阅 。",
- "chatgptTitle": "ChatGPT 账户",
- "hint": "Codex总是贯穿本土运行时间. 自动在返回到 API 密钥证书之前,更喜欢您的 ChatGPT 账户 。"
+ "apiKeyDescription": "使用 OPENAI_API_KEY 和 CODEX_API_KEY 计费进行本机 Codex 启动。",
+ "autoDescription": "首选您的 ChatGPT 帐户和订阅。仅在需要时使用 API 密钥模式。",
+ "chatgptDescription": "使用您连接的 ChatGPT 帐户和 Codex 订阅。",
+ "chatgptTitle": "ChatGPT 帐户",
+ "hint": "Codex 始终在本机运行时运行。 Auto 在回退到 API 密钥凭据之前更喜欢使用您的 ChatGPT 帐户。"
}
},
- "description": "管理每个提供者的连接方式,并在支持时管理哪个后端的多模式运行时间应该使用.",
+ "description": "管理每个提供商的连接方式,以及在支持时多模型运行时应使用哪个后端。",
"fastMode": {
"defaultOff": "默认关闭",
- "description": "在解析模型和运行时间允许时,默认应用Claude Code Fast模式用于新的Anthropic团队启动.",
- "disabledHint": "新的Anthropic发射保持正常速度,除非一个团队明确启用快速模式.",
- "enabledHint": "新的Anthropic发射将在解析模型支持时默认请求快速模式.",
- "notExposed": "此 Anthropic 运行时间不显示快模式 。",
- "preferFast": "首选快",
- "title": "快模式默认",
- "unavailableForRuntime": "此 Anthropic 运行时间目前无法使用快速模式 。"
+ "description": "当解析的模型和运行时允许时,默认为新的 Anthropic 团队启动应用于 Claude Code Fast 模式。",
+ "disabledHint": "除非团队明确启用快速模式,否则新的 Anthropic 发布将保持正常速度。",
+ "enabledHint": "当解析的模型支持快速模式时,新的 Anthropic 启动将默认请求快速模式。",
+ "notExposed": "此 Anthropic 运行时未公开快速模式。",
+ "preferFast": "喜欢快",
+ "title": "默认快速模式",
+ "unavailableForRuntime": "目前,快速模式对此 Anthropic 运行时不可用。"
},
"alerts": {
- "anthropicApiKeyMissing": "选择了 API 密钥模式,但是还没有Anthropic API证书可用.",
- "anthropicStoredKeyAvailable": "保存的 API 密钥是可用的,但应用程序启动的Anthropic会话仅在切换到 API 密钥模式后才使用.",
- "anthropicSubscriptionMissing": "选择了 Athropic 订阅模式 。 与 Anthropic 签约使用此提供者 。",
- "authTokenMissing": "未配置 Auth 令牌 。 许多本地的Anthropic兼容端点需要非空代币.",
- "chatgptLoginPending": "等待 ChatGPT 账户登录完成...",
- "chatgptLoginStarting": "开始 ChatGPT 登录...",
- "codexApiKeyMissing": "选择了 API 密钥模式,但还没有OPENAI_API_KEY或CODEX_API_KEY证书可用.",
- "codexLocalArtifactsNoSession": "Codex CLI目前没有活跃的ChatGPT账户. 本地 Codex 账户数据已存在,但没有选择活动管理会话 。",
- "codexNeedsReconnect": "Codex有一个本地选择的ChatGPT账户,但当前会话需要重新连接.",
- "codexNoChatgptAccount": "Codex CLI目前没有活跃的ChatGPT账户. 连接 ChatGPT 以使用您的订阅 。",
- "codexNoCredential": "尚没有 ChatGPT 账户或 API 密钥 。",
- "geminiApiUnavailable": "双子座 API目前无法使用. 在此配置 `GEMINI_API_KEY` 或使用有效的 Google ADC 证书 。",
- "withApiKeyFallback": "{{message}} 苏维埃社会主义共和国 切换到API密钥模式,使用检测到的API密钥."
+ "anthropicApiKeyMissing": "已选择 API 密钥模式,但尚无可用的 Anthropic API 凭证。",
+ "anthropicStoredKeyAvailable": "已保存的 API 密钥可用,但应用启动的 Anthropic 会话仅在您切换到 API 密钥模式后才使用它。",
+ "anthropicSubscriptionMissing": "选择 Anthropic 订阅模式。登录 Anthropic 以使用此提供商。",
+ "authTokenMissing": "未配置认证 Token。许多本地 Anthropic 兼容端点需要非空 Token。",
+ "chatgptLoginPending": "等待 ChatGPT 帐户登录完成…",
+ "chatgptLoginStarting": "正在开始 ChatGPT 登录…",
+ "codexApiKeyMissing": "选择了 API 密钥模式,但尚无可用的 OPENAI_API_KEY 或 CODEX_API_KEY 凭据。",
+ "codexLocalArtifactsNoSession": "Codex CLI 目前没有有效的 ChatGPT 帐户。本地 Codex 帐户数据存在,但未选择有效的托管会话。",
+ "codexNeedsReconnect": "Codex 有一个本地选择的 ChatGPT 帐户,但当前会话需要重新连接。",
+ "codexNoChatgptAccount": "Codex CLI 目前没有有效的 ChatGPT 帐户。连接 ChatGPT 以使用您的订阅。",
+ "codexNoCredential": "尚无可用的 ChatGPT 帐户或 API 密钥。",
+ "geminiApiUnavailable": "Gemini API 目前不可用。在此处配置“GEMINI_API_KEY”或使用有效的 Google ADC 凭据。",
+ "withApiKeyFallback": "{{message}} 切换到 API 密钥模式以使用检测到的 API 密钥。"
},
"authModeDescriptions": {
"anthropic": {
- "apiKey": "强制启动 Anthropic 会话使用 API 密钥证书 。",
- "auto": "使用运行时间默认行为 。 此应用程序中保存的 API 密钥仅在切换到 API 密钥模式后使用.",
- "oauth": "强制应用启动的 Anthropic 会话使用本地的 Anthropic 订阅会话 。"
+ "apiKey": "强制应用启动的 Anthropic 会话使用 API 密钥凭证。",
+ "auto": "使用运行时默认行为。仅当您切换到 API 密钥模式后,才会使用此应用中保存的 API 密钥。",
+ "oauth": "强制应用启动的 Anthropic 会话使用本地 Anthropic 订阅会话。"
},
"codex": {
- "apiKey": "强制本土代码发射使用OPENAI_API_KEY / CODEX_API_KEY计费.",
- "auto": "优先使用您的 ChatGPT 账户 。 仅在需要的时候才返回到 API 密钥模式.",
- "chatgpt": "强制本地代码启动使用您连接的 ChatGPT 账户和订阅 。"
+ "apiKey": "强制本机 Codex 启动使用 OPENAI_API_KEY / CODEX_API_KEY 计费。",
+ "auto": "首选您的 ChatGPT 帐户(如果可用)。仅在需要时回退到 API 密钥模式。",
+ "chatgpt": "强制本机 Codex 启动以使用您连接的 ChatGPT 帐户和订阅。"
}
},
"progress": {
- "applyingConnectionChanges": "正在应用连接更改...",
- "refreshingProviderStatus": "正在刷新提供者状态...",
- "savingCompatibleEndpoint": "保存兼容的终点...",
- "switchingAnthropicSubscription": "正在切换到 Anthropic 订阅...",
- "switchingApiKey": "正在切换到 API 密钥...",
- "switchingApiKeyMode": "正在切换到 API 密钥模式...",
- "switchingAuto": "正在切换到自动...",
- "switchingChatgpt": "正在切换到 ChatGPT 账户模式..."
+ "applyingConnectionChanges": "正在应用连接更改…",
+ "refreshingProviderStatus": "正在刷新提供商状态…",
+ "savingCompatibleEndpoint": "正在保存兼容端点…",
+ "switchingAnthropicSubscription": "正在切换到 Anthropic 订阅…",
+ "switchingApiKey": "正在切换到 API 密钥…",
+ "switchingApiKeyMode": "正在切换到 API 密钥模式…",
+ "switchingAuto": "切换到自动…",
+ "switchingChatgpt": "正在切换到 ChatGPT 帐户模式…"
},
- "provider": "供应商",
+ "provider": "提供商",
"runtime": {
"descriptions": {
- "anthropic": "Anthropic目前没有单独的运行时间后端选择器.",
- "codex": "Codex现在只运行于本地运行时路径.",
- "gemini": "选择双子座运行时的后端多模型应使用哪个 。",
- "opencode": "OpenCode使用自己的管理运行时主机. 桌面目前只显示状态 。"
+ "anthropic": "Anthropic 目前没有单独的运行时后端选择器。",
+ "codex": "Codex 现在仅通过本机运行时路径运行。",
+ "gemini": "选择应使用哪个 Gemini 运行时后端多模型。",
+ "opencode": "OpenCode 使用自己的托管运行时主机。桌面当前仅公开状态。"
},
- "title": "运行时间",
- "updating": "更新运行时间..."
+ "title": "运行时",
+ "updating": "正在更新运行时…"
},
- "runtimeSummary": "运行时间: {{runtime}}",
+ "runtimeSummary": "运行时:{{runtime}}",
"status": {
- "configured": "配置",
- "enabled": "已启用",
+ "configured": "已配置",
+ "enabled": "启用",
"notConfigured": "未配置",
"notSet": "未设置",
- "off": "关",
- "unknown": "未知数"
+ "off": "关闭",
+ "unknown": "未知"
},
- "title": "提供者设置",
+ "title": "提供商设置",
"usage": {
"apiKey": "使用 API 密钥",
- "apiKeyRequired": "需要的 API 密钥",
- "compatibleEndpoint": "使用兼容的终点",
+ "apiKeyRequired": "需要 API 密钥",
+ "compatibleEndpoint": "使用兼容端点",
"notConnected": "未连接",
"usingMethod": "使用 {{method}}"
},
"errors": {
- "apiKeyDeletedRefreshFailed": "API密钥已删除, 但无法刷新提供者状态 。",
- "apiKeySavedRefreshFailed": "API 密钥已保存, 但无法刷新提供者状态 。",
- "connectionUpdatedRefreshFailed": "连接已更新, 但更新提供者状态失败 。",
+ "apiKeyDeletedRefreshFailed": "API 密钥已删除,但无法刷新提供商状态。",
+ "apiKeySavedRefreshFailed": "API 密钥已保存,但无法刷新提供商状态。",
+ "connectionUpdatedRefreshFailed": "连接已更新,但无法刷新提供商状态。",
"deleteApiKey": "删除 API 密钥失败",
"disableEndpoint": "禁用端点失败",
- "endpointDisabledRefreshFailed": "端点已禁用, 但无法刷新提供者状态 。",
- "endpointSavedRefreshFailed": "端点已保存, 但无法刷新提供者状态 。",
- "refreshCodexAccount": "刷新 Codex 账户失败",
- "saveApiKey": "保存 API 密钥失败",
+ "endpointDisabledRefreshFailed": "端点已禁用,但无法刷新提供商状态。",
+ "endpointSavedRefreshFailed": "端点已保存,但无法刷新提供商状态。",
+ "refreshCodexAccount": "刷新 Codex 帐户失败",
+ "saveApiKey": "无法保存 API 密钥",
"saveEndpoint": "保存端点失败",
- "updateAnthropicFastMode": "更新 Anthropic 快模式失败",
+ "updateAnthropicFastMode": "无法更新 Anthropic Fast 模式",
"updateConnection": "更新连接失败",
- "updateRuntimeBackend": "更新运行时间后端失败",
- "apiKeyRequired": "需要API键"
+ "updateRuntimeBackend": "无法更新运行时后端",
+ "apiKeyRequired": "需要 API 密钥"
},
"connectionUi": {
"authMode": {
"auto": "自动",
- "oauth": "订阅/ OAuth",
- "chatgpt": "ChatGPT 账户",
+ "oauth": "订阅/OAuth",
+ "chatgpt": "ChatGPT 帐户",
"apiKey": "API 密钥",
- "anthropicSubscription": "人类订阅"
+ "anthropicSubscription": "Anthropic 订阅"
},
"authMethod": {
"apiKey": "API 密钥",
- "apiKeyHelper": "API 密钥帮助器",
- "oauth": "奥奥特",
- "claudeSubscription": "克劳德订阅",
- "geminiCli": "双子座 CLI",
- "googleAccount": "谷歌账户",
- "serviceAccount": "服务账户"
+ "apiKeyHelper": "API 密钥助手",
+ "oauth": "开放认证",
+ "claudeSubscription": "Claude 订阅",
+ "geminiCli": "Gemini 命令行界面",
+ "googleAccount": "谷歌帐户",
+ "serviceAccount": "服务帐户"
},
"runtime": {
- "codexNative": "本地代码",
- "currentRuntime": "当前运行时间",
- "selectedRuntime": "选中运行时间",
- "summary": "{{prefix}}编号:{{runtime}}"
+ "codexNative": "Codex 本机",
+ "currentRuntime": "当前运行时",
+ "selectedRuntime": "选定运行时",
+ "summary": "{{prefix}}: {{runtime}}"
},
"status": {
- "checking": "正在检查...",
+ "checking": "检查…",
+ "modelsAvailable": "可选模型",
"checked": "已检查",
- "providerActivity": "提供者活动",
+ "providerActivity": "提供商活动",
"notConnected": "未连接",
- "startingChatGptLogin": "开始 ChatGPT 登录...",
- "waitingForChatGptLogin": "正在等待 ChatGPT 账户登录...",
- "chatGptVerificationDegraded": "检测到 ChatGPT 账户 - 账户验证正在退化 。",
- "chatGptAccountReady": "已准备好 ChatGPT 账户",
- "apiKeyReady": "ZXCVKEN0ZXCV 密钥已准备好",
- "codexLocalAccountNeedsReconnect": "Codex有一个本地选择的ChatGPT账户,但当前会话需要重新连接.",
- "codexNoActiveManagedSession": "Codex CLI报告没有活动查特GPT登录. 本地 Codex 账户数据已存在,但没有选择活动管理会话 。",
- "codexNoActiveChatGptLogin": "Codex CLI 报告未激活 ChatGPT 登录",
- "connectChatGptForSubscription": "连接 ChatGPT 账户以使用您的 Codex 订阅 。",
- "codexNativeReady": "本地代码已就绪",
- "codexNativeUnavailable": "无法使用本地编码",
- "unavailableInCurrentRuntime": "当前运行时间不可用",
+ "startingChatGptLogin": "正在开始 ChatGPT 登录…",
+ "waitingForChatGptLogin": "正在等待 ChatGPT 帐户登录…",
+ "chatGptVerificationDegraded": "检测到 ChatGPT 帐户 - 帐户验证当前已降级。",
+ "chatGptAccountReady": "ChatGPT 帐户已准备就绪",
+ "apiKeyReady": "API 密钥已准备就绪",
+ "codexLocalAccountNeedsReconnect": "Codex 有一个本地选择的 ChatGPT 帐户,但当前会话需要重新连接。",
+ "codexNoActiveManagedSession": "Codex CLI 报告没有有效的 ChatGPT 登录。本地 Codex 帐户数据存在,但未选择有效的托管会话。",
+ "codexNoActiveChatGptLogin": "Codex CLI 报告没有有效的 ChatGPT 登录",
+ "connectChatGptForSubscription": "连接 ChatGPT 帐户以使用您的 Codex 订阅。",
+ "codexNativeReady": "Codex 原生就绪",
+ "codexNativeUnavailable": "Codex 原生不可用",
+ "unavailableInCurrentRuntime": "当前运行时不可用",
"connectedViaApiKey": "通过 API 密钥连接",
- "apiKeyConfiguredNotVerified": "配置了 API 密钥,但尚未验证",
- "apiKeyModeMissingCredential": "选择了 API 密钥模式,但没有配置 API 密钥",
- "connectedVia": "通过{{method}}连接",
- "unableToVerify": "无法校验",
- "modelsAvailable": "模型可用"
+ "apiKeyConfiguredNotVerified": "API 密钥已配置,但尚未验证",
+ "apiKeyModeMissingCredential": "选择了 API 密钥模式,但未配置 API 密钥",
+ "connectedVia": "通过 {{method}} 连接",
+ "unableToVerify": "无法验证"
},
"mode": {
- "selectedAuth": "选中的认证: {{authMode}}",
- "preferredAuth": "首选认证: {{authMode}}"
+ "selectedAuth": "所选认证:{{authMode}}",
+ "preferredAuth": "首选授权:{{authMode}}"
},
"credential": {
- "apiKeyConfigured": "API 密钥配置",
- "savedApiKeyAvailable": "管理中保存可用的 API 密钥",
- "apiKeyAlsoConfigured": "API 密钥也在管理中配置",
+ "apiKeyConfigured": "API 密钥已配置",
+ "savedApiKeyAvailable": "已保存的 API 密钥可在“管理”中使用",
+ "apiKeyAlsoConfigured": "API 密钥也在“管理”中配置",
"apiKeyConfiguredInManage": "API 密钥在管理中配置",
- "apiKeyFallbackInManage": "API 密钥作为倒置管理中也可以使用",
- "availableAsFallback": "{{summary}} - 以倒置形式提供",
- "savedApiKeyAvailableIfSwitch": "如果您切换到 API 密钥模式, 管理中可保存 API 密钥",
- "availableIfSwitch": "{{summary}} - 切换到API键模式时可用",
- "autoWillUseUntilChatGpt": "{{summary}} - 自动使用,直到ChatGPT连接为止"
+ "apiKeyFallbackInManage": "API 密钥也可在“管理”中用作备用",
+ "availableAsFallback": "{{summary}} - 可作为备用",
+ "savedApiKeyAvailableIfSwitch": "如果您切换到 API 密钥模式,保存的 API 密钥可在“管理”中使用",
+ "availableIfSwitch": "{{summary}} - 如果切换到 API 密钥模式则可用",
+ "autoWillUseUntilChatGpt": "{{summary}} - 自动将使用此直到 ChatGPT 连接"
},
"actions": {
"connect": "连接",
- "connectAnthropic": "连接Anthropic",
+ "connectAnthropic": "连接 Anthropic",
"connectChatGpt": "连接 ChatGPT",
- "disconnect": "断开连接",
+ "disconnect": "断开",
"openLogin": "打开登录"
},
"disconnect": {
- "anthropicTitle": "断开Anthropic订阅?",
- "anthropic": "从 Claude CLI 运行时间中删除本地的 Anthropic 订阅会话 。",
- "anthropicWithApiKey": "从 Claude CLI 运行时间中删除本地的 Anthropic 订阅会话 。 管理保存的 API 密钥 。",
- "geminiTitle": "断开双子座CLI?",
- "gemini": "这清除了本地的双子座 ZXCVKEN0ZXCV会话元数据. 外部的ADC证书和保存的API密钥不删除."
+ "anthropicTitle": "断开 Anthropic 订阅?",
+ "anthropic": "这将从 Claude CLI 运行时中删除本地 Anthropic 订阅会话。",
+ "anthropicWithApiKey": "这将从 Claude CLI 运行时中删除本地 Anthropic 订阅会话。管理中保存的 API 密钥仍然可用。",
+ "geminiTitle": "断开 Gemini CLI?",
+ "gemini": "这将清除本地 Gemini CLI 会话元数据。外部 ADC 凭据和保存的 API 密钥不会被删除。"
}
}
},
"cliRuntime": {
"actions": {
"checkForUpdates": "检查更新",
- "checking": "正在检查...",
+ "checking": "检查…",
"extensions": "扩展",
"installRuntime": "安装 {{runtime}}",
"manage": "管理",
"recheck": "重新检查",
- "reinstallRuntime": "莱因斯托尔 {{runtime}}",
+ "reinstallRuntime": "重新安装 {{runtime}}",
"retry": "重试",
"update": "更新"
},
"installer": {
- "checkingLatest": "正在检查最新版本...",
- "downloading": "正在下载...",
+ "checkingLatest": "正在检查最新版本…",
+ "downloading": "正在下载…",
"failed": "安装失败",
"installed": "已安装 v{{version}}",
- "installing": "安装中...",
- "latest": "最新消息",
- "verifying": "正在验证校验和..."
+ "installing": "正在安装…",
+ "latest": "最新的",
+ "verifying": "正在验证校验和…"
},
"labels": {
- "multimodel": "多模式"
+ "multimodel": "多模型"
},
"loading": {
- "aiProviders": "正在检查 AI 提供者...",
- "claudeCli": "正在检查克劳德CLI..."
+ "aiProviders": "检查 AI 提供商…",
+ "claudeCli": "检查 Claude CLI…"
},
"provider": {
- "backend": "后端: {{backend}}",
- "loadingModels": "正在装入模型...",
- "modelsUnavailable": "此运行时间构建无法使用的模型",
- "runtime": "运行时间: {{runtime}}"
+ "backend": "后端:{{backend}}",
+ "loadingModels": "正在加载模型…",
+ "modelsUnavailable": "模型不适用于此运行时构建",
+ "runtime": "运行时:{{runtime}}"
},
"providerTerminal": {
"authFailed": "认证失败",
- "authUpdated": "更新认证",
- "loggedOut": "供应商已登录",
+ "authUpdated": "认证已更新",
+ "loggedOut": "提供商已注销",
"login": "登录",
- "logout": "注销",
+ "logout": "退出",
"logoutFailed": "注销失败"
},
"status": {
- "configuredNotFound": "未找到配置的 {{runtime}} 。",
- "foundButFailed": "发现 {{runtime}} 失败启动",
- "healthCheckFailed": "配置的 {{runtime}} 失败启动健康检查.",
- "notInstalled": "{{runtime}}未安装"
+ "configuredNotFound": "未找到配置的 {{runtime}}。",
+ "foundButFailed": "找到 {{runtime}} 但启动失败",
+ "healthCheckFailed": "配置的 {{runtime}} 启动运行状况检查失败。",
+ "notInstalled": "{{runtime}} 未安装"
},
- "title": "CLI 苏维埃社会主义共和国 运行时间"
+ "title": "CLI 运行时"
},
"cliStatus": {
- "versionUpgrade": "v{{current}} - > v{{latest}}"
+ "versionUpgrade": "v{{current}} -> v{{latest}}"
}
}
diff --git a/src/features/localization/renderer/locales/zh/team.json b/src/features/localization/renderer/locales/zh/team.json
index 49abb356..f33b5739 100644
--- a/src/features/localization/renderer/locales/zh/team.json
+++ b/src/features/localization/renderer/locales/zh/team.json
@@ -1,171 +1,171 @@
{
"activity": {
"actions": {
- "createTaskFromMessage": "从信件创建任务",
+ "createTaskFromMessage": "从消息创建任务",
"editMessage": "编辑消息",
- "expandMessage": "扩展消息",
- "replyToMessage": "对信件的答复",
- "restartTeam": "重新启动团队"
+ "expandMessage": "展开消息",
+ "replyToMessage": "回复消息",
+ "restartTeam": "重启团队"
},
"authError": {
- "description": "认证失败 。 重新召集小组将重新开会,并可能解决这个问题。 如果问题依然存在, 请检查您的 API 证书, 或稍后再试 。"
+ "description": "认证失败。重新启动团队将刷新会话并可能解决此问题。如果问题仍然存在,请检查您的 API 凭据或稍后重试。"
},
"automation": {
- "reviewPickup": "要求队友接受审查",
- "stallNudge": "要求队友继续停滞不前的任务",
+ "reviewPickup": "要求队友接手审核",
+ "stallNudge": "要求队友继续停滞的任务",
"workSyncBody": "要求队友同步当前工作"
},
"badges": {
"automation": "自动化",
- "bootstrap": "拖带",
+ "bootstrap": "引导程序",
"command": "命令",
- "comment": "注释",
- "live": "现场直播",
- "note": "说明",
- "rateLimited": "费率有限",
+ "comment": "评论",
+ "live": "实时",
+ "note": "备注",
+ "rateLimited": "速率受限",
"restart": "重新启动",
"result": "结果",
- "session": "届会",
- "stallNudge": "缓冲",
+ "session": "会话",
+ "stallNudge": "停滞提醒",
"start": "开始",
"workSync": "工作同步",
"agentError": "代理错误",
"apiError": "API 错误"
},
"bootstrap": {
- "acknowledged": "靴子已经确认",
- "restarting": "重新开始队友",
- "starting": "开始队友"
+ "acknowledged": "已确认引导",
+ "restarting": "重启队友",
+ "starting": "正在启动队友"
},
- "rawJson": "原始 ZXCVTKEN0ZXCV",
+ "rawJson": "原始 JSON",
"unread": "未读",
"thoughts": {
- "count": "{{count}} 想法",
- "count_one": "{{count}}认为",
- "expand": "扩展想法",
+ "count": "{{count}} 条思考",
+ "count_one": "{{count}} 条思考",
+ "expand": "展开思考",
"showMore": "显示更多",
- "showLess": "显示较少",
- "count_few": "{{count}} 想法",
- "count_many": "{{count}} 想法",
- "count_other": "{{count}} 想法",
- "toolSummary": "{{summary}}",
- "titleForMember": "{{name}}- 考虑"
+ "showLess": "收起",
+ "count_few": "{{count}} 条思考",
+ "count_many": "{{count}} 条思考",
+ "count_other": "{{count}} 条思考",
+ "toolSummary": "🔧{{summary}}",
+ "titleForMember": "{{name}} - 思考"
},
"timeline": {
- "loadingMessages": "正在装入信件...",
- "noMessages": "无消息",
- "emptyHint": "向成员发送信息以查看活动.",
+ "loadingMessages": "正在加载消息…",
+ "noMessages": "没有消息",
+ "emptyHint": "向成员发送消息以查看活动。",
"newSession": "新会话",
- "olderCount": "+ 键{{count}}年龄",
- "showMore": "显示{{count}}更多",
- "showAll": "全部显示",
- "olderCount_one": "+ 键{{count}}年龄",
- "olderCount_few": "+ 键{{count}}年龄",
- "olderCount_many": "+ 键{{count}}年龄",
- "olderCount_other": "+ 键{{count}}年龄"
+ "olderCount": "+{{count}} 条较早消息",
+ "showMore": "再显示 {{count}} 条",
+ "showAll": "显示全部",
+ "olderCount_one": "+{{count}} 条较早消息",
+ "olderCount_few": "+{{count}} 条较早消息",
+ "olderCount_many": "+{{count}} 条较早消息",
+ "olderCount_other": "+{{count}} 条较早消息"
},
"pendingReplies": {
- "title": "等待答复",
- "openMember": "开放成员",
- "messageSentAwaitingReply": "发送信件,等待答复",
- "awaitingReply": "等待答复",
- "externalTeam": "外部小组",
- "crossTeamAwaitingReply": "发送跨小组信息,等待答复",
+ "title": "等待回复",
+ "openMember": "打开成员",
+ "messageSentAwaitingReply": "消息已发送,等待回复",
+ "awaitingReply": "等待回复",
+ "externalTeam": "外部团队",
+ "crossTeamAwaitingReply": "跨团队消息已发送,等待回复",
"user": "用户",
- "awaitingApproval": "等待核准"
+ "awaitingApproval": "等待批准"
},
"reply": {
- "replyingTo": "答复",
- "action": "答复"
+ "replyingTo": "正在回复",
+ "action": "回复"
},
"activeTasks": {
"inProgress": "进行中",
- "expandInProgress": "展开进行中任务",
- "collapseInProgress": "折叠进行中任务",
- "reviewing": "正在审查",
+ "expandInProgress": "展开进行中项",
+ "collapseInProgress": "折叠进行中项",
+ "reviewing": "审核",
"workingOn": "正在处理"
},
"expandDialog": {
- "description": "扩展信件视图"
+ "description": "扩展消息视图"
}
},
"create": {
"actions": {
"create": "创建",
- "creating": "正在创建...",
+ "creating": "正在创建…",
"openExisting": "打开现有团队",
- "skipPreflightAndCreate": "跳过飞行前创建"
+ "skipPreflightAndCreate": "跳过预检并创建"
},
"conflict": {
- "description": "在同一个目录中运行两个团队是危险的——他们可能与编辑相同的文件发生冲突. 考虑使用不同的目录或git工作树进行隔离.",
- "title": "另一个团队\"{{team}}\"已经在运行这个工作目录",
- "workingDirectory": "工作目录:"
+ "description": "在同一目录中运行两个团队是有风险的 - 它们可能会在编辑相同文件时发生冲突。考虑使用不同的目录或 Git worktree进行隔离。",
+ "title": "另一个团队“{{team}}”已经在该工作目录中运行",
+ "workingDirectory": "工作目录:"
},
"description": {
- "copy": "创建一个基于现有团队的新团队。",
- "create": "组织你的团队 选择如何开始"
+ "copy": "在现有团队的基础上创建一个新团队。",
+ "create": "设置您的团队并选择其开始方式。"
},
"errors": {
- "nameExists": "团队名称已经存在",
- "nameLaunching": "这个名字的团队正在启动",
- "createConfigFailed": "创建团队配置失败",
- "loadProjectsFailed": "装入工程失败"
+ "nameExists": "团团队名称称已存在",
+ "nameLaunching": "具有此名称的团队目前正在启动",
+ "createConfigFailed": "无法创建团队配置",
+ "loadProjectsFailed": "无法加载项目"
},
"fields": {
- "color": "颜色( 可选)",
- "description": "描述( 可选)",
- "prompt": "启动小组领导( 可选)",
- "teamName": "团队名称"
+ "color": "颜色(可选)",
+ "description": "说明(可选)",
+ "prompt": "提示团队负责人(可选)",
+ "teamName": "团团队名称称"
},
"launchAfterCreate": {
- "description": "立即通过当地克劳德·CLI启动球队.",
+ "description": "通过本地 Claude CLI 立即启动团队。",
"label": "创建后运行命令"
},
- "localOnly": "仅以本地电子模式提供 。",
- "onDisk": "磁盘上:",
+ "localOnly": "仅在本地 Electron 模式下可用。",
+ "onDisk": "在磁盘上:",
"placeholders": {
- "description": "团队宗旨简述.",
- "prompt": "在供给期间给团队领导的指示.."
+ "description": "团队用途的简要说明",
+ "prompt": "为团队负责人提供说明…"
},
"saved": "已保存",
"solo": {
- "description": "只有团队领队(主进程)将启动——不培养队友. 在您所选择的运行时间( Claude Code, Codex, OpenCode, 双子座) 中像常规代理会话一样工作, 但可以访问任务板进行规划 。 避免队友在高处协调,从而节省代币. 之后可以从团队设置中添加成员.",
- "label": "独奏队"
+ "description": "只有团队领导(主进程)才会启动 - 不会启动任何队友。与您选择的运行时(Claude Code、Codex、OpenCode、Gemini)中的常规智能体会话一样工作,但可以访问任务看板进行规划。通过避免队友协调开销来节省 Token。您可以稍后从团队设置添加成员。",
+ "label": "单人团队"
},
"title": {
"copy": "复制团队",
"create": "创建团队"
},
"optional": {
- "launchSettingsTitle": "可选发射设置",
- "launchSettingsDescription": "快速,安全,和CLI 超过住在这里当你需要它们。",
+ "launchSettingsTitle": "可选启动设置",
+ "launchSettingsDescription": "如有需要,可在此设置提示词、安全和 CLI 覆盖项。",
"teamDetailsTitle": "可选团队详情",
- "teamDetailsDescription": "保存默认的流量压缩, 并仅在您需要额外上下文或自定义颜色时打开 。"
+ "teamDetailsDescription": "保持默认流程紧凑,仅当您需要额外的上下文或自定义颜色时才打开它。"
},
"prepare": {
- "unsupportedPreload": "当前预装版本不支持团队:预装. 重新启动dev应用程序.",
- "selectWorkingDirectory": "选择一个工作目录来验证发射环境.",
- "someProvidersNeedAttention": "一些选定的提供者需要注意。",
- "readyWithNotes": "所有选定的提供者都已准备好,并附有注释。",
- "ready": "所有选中的提供者都准备好了 。",
- "failed": "准备选中的提供者失败",
- "checkingProviders": "正在检查选中的提供者...",
- "preparingEnvironment": "正在准备环境...",
- "selectedProvidersReadyWithNotes": "选中的提供者已就绪( 有注释)",
- "selectedProvidersReady": "选定供应商准备就绪"
+ "unsupportedPreload": "当前preload 版本不支持 team:prepareProvisioning。重新启动开发应用。",
+ "selectWorkingDirectory": "选择一个工作目录来验证启动环境。",
+ "someProvidersNeedAttention": "部分选定提供商需要注意。",
+ "readyWithNotes": "所有选定的提供商均已准备就绪,并附有注释。",
+ "ready": "所有选定提供商均已就绪。",
+ "failed": "无法准备选定的提供商",
+ "checkingProviders": "检查选定的提供商…",
+ "preparingEnvironment": "准备环境…",
+ "selectedProvidersReadyWithNotes": "选定的提供商已准备就绪(带注释)",
+ "selectedProvidersReady": "选定的提供商已准备就绪"
},
"validation": {
- "nameMustContainLetterOrDigit": "名称必须至少包含一个字母或数字",
- "nameTooLong": "名称太长( 最多 128 个字符)",
- "selectWorkingDirectory": "选择工作目录( cwd)",
- "memberNameRequired": "成员名称不能为空",
- "memberNameInvalid": "成员名称必须从字母开始,只使用[a-zA-Z0-9. ],最大128个字符",
- "memberNamesUnique": "成员名字必须是独一无二的",
- "openCodeLeadModelRequired": "OpenCode领导需要一个选定的模型.",
- "openCodeTeammateRequired": "OpenCode领队至少需要一名OpenCode队友.",
- "teamLaunching": "团队目前正在启动",
- "teamNameExists": "团队名称已经存在",
- "checkFormFields": "检查窗体字段"
+ "nameMustContainLetterOrDigit": "名称必须包含至少一个字母或数字",
+ "nameTooLong": "名称太长(最多 128 个字符)",
+ "selectWorkingDirectory": "选择工作目录(cwd)",
+ "memberNameRequired": "成员名不能为空",
+ "memberNameInvalid": "成员名称必须以字母数字开头,仅使用 [a-zA-Z0-9._-],最多 128 个字符",
+ "memberNamesUnique": "成员名称必须是唯一的",
+ "openCodeLeadModelRequired": "OpenCode 引线需要选定的模型。",
+ "openCodeTeammateRequired": "OpenCode 领导者需要至少一名 OpenCode 队友。",
+ "teamLaunching": "目前团队正在启动",
+ "teamNameExists": "团团队名称称已存在",
+ "checkFormFields": "检查表单字段"
}
},
"editTeam": {
@@ -173,117 +173,117 @@
"cancel": "取消",
"save": "保存"
},
- "addMemberLockReason": "使用专门的添加成员对话框在团队直播时添加新的队友.",
- "description": "更改团队名称、描述和颜色",
+ "addMemberLockReason": "在团队上线时,使用专用的“添加成员”对话框添加新的队友。",
+ "description": "更改团团队名称称、描述和颜色",
"errors": {
- "changesSavedRefreshFailed": "团队更改已保存, 但无法刷新最新视图: {{message}}",
- "liveRenameBlocked": "现有队友在球队直播时不能重新命名. 更名为: {{names}}",
- "memberNameEmpty": "成员名称不能为空",
- "memberNameInvalid": "成员名称必须从字母开始,只使用[a-zA-Z0-9. ],最大128个字符",
- "memberNameNumericSuffix": "成员名称\"{{name}}\"不允许使用(保留给克劳德·CLI自动后缀). 改为\"{{base}}\".",
- "memberNameReserved": "成员名称\"{{name}}\"保留.",
- "memberNamesUnique": "成员名字在保存前必须是独有的",
- "newLiveTeammates": "在团队直播时从专用的Add成员对话框中添加新的队友. 编辑团队只支持更新现有的队友.",
- "provisioning": "在提供时无法编辑团队设置 。 等待发射完成,然后再试一次.",
- "restartFailedMany": "团队被救,但未能重新启动这些队友: {{failures}}",
- "restartFailedOne": "团队已保存, 但未能重新启动此队友: {{failures}}",
+ "changesSavedRefreshFailed": "团队更改已保存,但无法刷新最新视图:{{message}}",
+ "liveRenameBlocked": "当团队上线时,现有队友无法重命名。更名:{{names}}",
+ "memberNameEmpty": "成员名不能为空",
+ "memberNameInvalid": "成员名称必须以字母数字开头,仅使用 [a-zA-Z0-9._-],最多 128 个字符",
+ "memberNameNumericSuffix": "不允许成员名称“{{name}}”(为 Claude CLI 自动后缀保留)。请改用“{{base}}”。",
+ "memberNameReserved": "成员名“{{name}}”已保留",
+ "memberNamesUnique": "保存前成员名称必须是唯一的",
+ "newLiveTeammates": "当团队在线时,通过专用的“添加成员”对话框添加新的队友。编辑团队仅支持更新现有队友。",
+ "provisioning": "当配置仍在进行时,无法编辑团队设置。等待启动完成,然后重试。",
+ "restartFailedMany": "团队已保存,但无法重新启动这些队友:{{failures}}",
+ "restartFailedOne": "团队已保存,但无法重新启动该队友:{{failures}}",
"saveFailed": "保存失败",
- "settingsChanged": "此对话框打开时, 团队设置已更改 。 在保存前重新打开并审查最新状态 。",
- "settingsSavedMembersAndRefreshFailed": "团队设置已保存, 但成员更改失败: {{message}}. 刷新也失败: {{refreshError}}",
- "settingsSavedMembersFailed": "团队设置已保存, 但成员更改失败: {{message}}",
- "settingsSavedRefreshFailed": "团队设置已保存, 但无法刷新最新视图: {{message}}",
- "teamNameEmpty": "团队名称不能为空",
- "unsupportedMixedPrimaryMutation": "混合OpenCode团队中的主力队友的现场编辑尚未获得支持. 停止队伍,编辑名册,然后重启. 受影响:{{names}}"
+ "settingsChanged": "此对话框打开时团队设置发生更改。重新打开它并在保存之前查看最新状态。",
+ "settingsSavedMembersAndRefreshFailed": "团队设置已保存,但成员更改失败:{{message}}。刷新也失败:{{refreshError}}",
+ "settingsSavedMembersFailed": "团队设置已保存,但成员更改失败:{{message}}",
+ "settingsSavedRefreshFailed": "团队设置已保存,但无法刷新最新视图:{{message}}",
+ "teamNameEmpty": "团团队名称称不能为空",
+ "unsupportedMixedPrimaryMutation": "尚不支持对混合 OpenCode 团队中主要拥有的队友进行实时编辑。停止团队,编辑名单,然后重新启动。受影响:{{names}}"
},
"fields": {
- "colorOptional": "颜色( 可选)",
- "description": "说明",
- "name": "名称"
+ "colorOptional": "颜色(可选)",
+ "description": "描述",
+ "name": "姓名"
},
- "memberRestartWarning": "保存将重新启动此队友应用角色,工作流程,工作树隔离,提供者,模型,努力,或MCP访问更改.",
+ "memberRestartWarning": "保存将重新启动该队友以应用角色、工作流、工作树隔离、提供商、模型、工作量或 MCP 访问更改。",
"notices": {
- "liveRenameBlocked": "活救因现有队友改名而受阻. 还原那些身份更改或先阻止队伍.",
- "newLiveTeammates": "在团队直播时无法从Edit Team中添加新的队友. 使用添加成员对话框代替 。",
- "provisioning": "团队供给仍在进行中. 编辑暂时锁定,直到发射结束。",
- "restartMany": "保存会重新启动或重新启动这些队友应用角色,工作流程,工作树隔离,提供商,模型,努力,或MCP访问更改:{{names}}.",
- "restartOne": "保存会重新启动或重新启动这个队友应用角色,工作流程,工作树隔离,提供商,模型,努力,或MCP访问更改:{{names}}.",
- "unsupportedMixedPrimaryMutation": "混合OpenCode球队中主力队友的现场编辑/移除需要停赛并重启球队:{{names}}."
+ "liveRenameBlocked": "由于现有队友被重命名,实时保存被阻止。恢复这些身份更改或首先停止团队。",
+ "newLiveTeammates": "当团队处于活动状态时,无法从“编辑团队”添加新队友。请改用“添加成员”对话框。",
+ "provisioning": "团队配置仍在进行中。编辑暂时锁定,直到启动完成。",
+ "restartMany": "保存将重新启动或重新启动这些团队成员以应用角色、工作流、工作树隔离、提供商、模型、工作量或 MCP 访问更改:{{names}}。",
+ "restartOne": "保存将重新启动或重新启动该队友以应用角色、工作流、工作树隔离、提供商、模型、工作量或 MCP 访问更改:{{names}}。",
+ "unsupportedMixedPrimaryMutation": "对混合 OpenCode 团队中主要拥有的队友进行实时编辑/删除需要停止并重新启动团队:{{names}}。"
},
"placeholders": {
- "description": "团队说明(可选)",
- "teamName": "团队名称"
+ "description": "团队描述(可选)",
+ "teamName": "团团队名称称"
},
"teamLead": {
- "changeRuntime": "更改前置运行时间",
- "changeRuntimeDescription": "开放再发射团队,以改变主导提供者,型号,或努力.",
- "modelLockReason": "团队前导运行时间由再发射团队管理.",
- "readOnlyHint": "团队主名和角色在这里只读。 在主行打开运行时间面板以更改提供者,模型,或努力.",
- "role": "团队领导"
+ "changeRuntime": "更改引导运行时",
+ "changeRuntimeDescription": "打开重新启动团队以更改主要提供商、模型或工作量。",
+ "modelLockReason": "团队领导运行时由重新启动团队管理。",
+ "readOnlyHint": "团队负责人姓名和角色在此处保持只读状态。打开前导行上的运行时面板以更改提供商、模型或工作量。",
+ "role": "团队负责人"
},
"title": "编辑团队"
},
"memberDraft": {
"actions": {
"remove": "删除成员",
- "removeAria": "删除{{name}}",
+ "removeAria": "删除 {{name}}",
"restore": "恢复成员",
"restoreAria": "恢复 {{name}}"
},
"anthropicContext": {
"defaultSetting": "默认上下文设置",
- "description": "此次发射的Anthropic背景是全团队的:{{mode}}. 使用前置运行时面板的限制性上下文复选框来更改它.",
- "limitEnabled": "200K 限制启用"
+ "description": "此次启动的 Anthropic 上下文是团队范围的:{{mode}}。使用主运行时面板的“限制上下文”复选框来更改它。",
+ "limitEnabled": "启用 200K 限制"
},
"mcp": {
"buttonInherit": "MCP 继承",
- "buttonScopes": "ZXCVKEN0ZXCV 瞄准镜",
+ "buttonScopes": "MCP 范围",
"chooseScopes": "选择范围",
- "inheritLead": "继承铅",
- "lockedInfo": "MCP代理团队只允许所有队友使用. 这个队友将只使用特工团队服务器发射.",
+ "inheritLead": "继承领先",
+ "lockedInfo": "仅对所有队友启用 Agent Teams MCP。该队友将仅使用 Agent Teams 服务器启动。",
"mode": "MCP 模式",
"scopes": {
- "local": "当地",
+ "local": "本地",
"project": "项目",
"user": "用户"
},
"serverNames": "服务器名称",
- "settingInfo": "MCP特工团队 发射这个团队的队友 只有特工团队服务器。 范围和允许列表模式仅适用于本次队友发射.",
- "strictAllowlist": "严格许可列表",
- "tooltip": "{{label}}:控制这个成员的MCP继承政策.",
- "agentTeamsMcp": "特工团队 MCP"
+ "settingInfo": "Agent Teams MCP 仅使用 Agent Teams 服务器启动此队友。范围和许可名单模式仅适用于该队友启动。",
+ "strictAllowlist": "严格的许可名单",
+ "tooltip": "{{label}}:控制该成员的 MCP 继承策略",
+ "agentTeamsMcp": "Agent Team MCP"
},
"model": {
- "ariaLabel": "{{provider}}供应商,{{model}}",
- "currentLeadRuntime": "当前运行时间",
+ "ariaLabel": "{{provider}} 提供商,{{model}}",
+ "currentLeadRuntime": "当前领先运行时",
"default": "默认",
- "inheritedTooltip": "提供方、模型和努力都是在同步的同时从牵头方继承的。",
- "leadSuffix": "{{label}}(牵头)",
- "liveDisabled": "提供商、模型和努力的变化在团队活动时被禁用。 重新连接团队以安全应用它们.",
- "lockedActionFallback": "提前运行时间改变开放Relaunch Team,其中供应商,模型,和努力可以更新.",
- "restartWholeTeam": "保存这些运行时间变化会重启整个团队."
+ "inheritedTooltip": "启用同步时,提供商、模型和工作量将从团队负责人继承。",
+ "leadSuffix": "{{label}}(团队负责人)",
+ "liveDisabled": "当团队上线时,提供商、模型和工作量更改将被禁用。重新连接团队以安全地应用它们。",
+ "lockedActionFallback": "领导运行时更改打开重新启动团队,可以在其中更新提供商、模型和工作。",
+ "restartWholeTeam": "保存这些运行时更改会重新启动整个团队。"
},
- "nameAria": "成员 {{index}} 名称",
- "nameFallback": "委员 {{index}}",
- "noRole": "无角色",
+ "nameAria": "成员 {{index}} 姓名",
+ "nameFallback": "成员 {{index}}",
+ "noRole": "没有角色",
"removed": "已删除",
"workflow": {
"addTooltip": "添加队友工作流程",
"editTooltip": "编辑队友工作流程",
- "label": "工作流量( 可选)",
- "placeholder": "这个特工应该如何行为,与他人互动...",
+ "label": "工作流程(可选)",
+ "placeholder": "这个代理应该如何表现、与他人互动……",
"saved": "已保存"
},
"worktree": {
- "description": "把这个队友放在另外的树上 应用/拒绝更改工作树的目标,而不是主工作空间.",
+ "description": "在单独的 Git worktree中运行该队友。应用/拒绝更改的目标是工作树,而不是主要工作区。",
"label": "工作树"
},
"addMembers": {
"title": "添加成员",
- "description": "在 {{teamName}} 中增加新成员"
+ "description": "添加新成员至 {{teamName}}"
},
"placeholders": {
"name": "成员名称",
- "mcpServers": "gi,哨兵"
+ "mcpServers": "github、哨兵"
}
},
"detail": {
@@ -292,44 +292,44 @@
"cancel": "取消",
"delete": "删除",
"editCode": "编辑代码",
- "launch": "发射",
- "remove": "删除",
- "stop": "停下来",
+ "launch": "启动",
+ "remove": "消除",
+ "stop": "停止",
"task": "任务",
"visualize": "可视化"
},
"deleteTeam": {
- "description": "删除团队\"{{team}}\"?. 这一行动是不可逆转的。 所有团队数据和任务将被删除.",
+ "description": "删除队伍“{{team}}”?此操作是不可逆转的。所有团队数据和任务都将被删除。",
"title": "删除团队"
},
"draft": {
- "descriptionPrefix": "这是一个起草团队 -",
- "descriptionSuffix": "已配置为{{count}} {{member}}但还没有被CLI提供。 点击启动以选择一个模型并启动团队.",
- "descriptionSuffix_few": "已配置为{{count}} {{member}}但还没有被CLI提供。 点击启动以选择一个模型并启动团队.",
- "descriptionSuffix_many": "已配置为{{count}} {{member}}但还没有被CLI提供。 点击启动以选择一个模型并启动团队.",
- "descriptionSuffix_one": "已配置为{{count}} {{member}}但还没有被CLI提供。 点击启动以选择一个模型并启动团队.",
- "descriptionSuffix_other": "已配置为{{count}} {{member}}但还没有被CLI提供。 点击启动以选择一个模型并启动团队.",
+ "descriptionPrefix": "这是一支选秀队伍——",
+ "descriptionSuffix": "已使用 {{count}} {{member}} 配置,但尚未由 CLI 进行配置。单击启动以选择模型并启动团队。",
+ "descriptionSuffix_few": "已使用 {{count}} {{member}} 配置,但尚未由 CLI 进行配置。单击启动以选择模型并启动团队。",
+ "descriptionSuffix_many": "已使用 {{count}} {{member}} 配置,但尚未由 CLI 进行配置。单击启动以选择模型并启动团队。",
+ "descriptionSuffix_one": "已使用 {{count}} {{member}} 配置,但尚未由 CLI 进行配置。单击启动以选择模型并启动团队。",
+ "descriptionSuffix_other": "已使用 {{count}} {{member}} 配置,但尚未由 CLI 进行配置。单击启动以选择模型并启动团队。",
"member": "成员",
"member_few": "成员",
"member_many": "成员",
"member_one": "成员",
"member_other": "成员",
- "title": "尚未启动小组"
+ "title": "团队尚未启动"
},
- "invalidTab": "无效的团队标签",
- "kanbanSafeData": "完全装入 kanban 失败 。 显示安全数据 。",
- "loadFailed": "装入团队失败",
- "loading": "装入团队",
- "loadingSidebar": "正在装入团队侧边栏",
+ "invalidTab": "团队选项卡无效",
+ "kanbanSafeData": "无法完全加载看板。显示安全数据。",
+ "loadFailed": "无法加载团队",
+ "loading": "装货队",
+ "loadingSidebar": "正在加载团队侧边栏",
"offline": {
- "offline": "队伍下线了",
- "partialFailed": "上次发射失败",
- "partialMissing": "上次发射失败 。{{missing}}页:1{{expected}}队友没有加入",
- "reconciling": "上次发射还在调和"
+ "offline": "团队离线",
+ "partialFailed": "上次启动中途失败",
+ "partialMissing": "上次启动中途失败 - {{missing}}/{{expected}} 队友没有加入",
+ "reconciling": "上次启动仍在协调中"
},
- "previous": "上一个:{{paths}}",
+ "previous": "上一篇:{{paths}}",
"removeMember": {
- "description": "从队伍中删除\"{{member}}\"?. 任务和信件会被保留,但此名称不能重复使用.",
+ "description": "从团队中删除“{{member}}”?任务和消息将被保留,但该名称不能重复使用。",
"title": "删除成员"
},
"sections": {
@@ -337,30 +337,30 @@
},
"solo": "独奏",
"status": {
- "active": "活动",
- "launching": "正在发射...",
- "running": "运行"
+ "active": "活跃",
+ "launching": "正在启动…",
+ "running": "运行中"
},
"telemetry": {
- "cpu": "CPU 苏维埃社会主义共和国",
- "memory": "内存"
+ "cpu": "中央处理器",
+ "memory": "记忆"
},
"tooltips": {
"deleteTeam": "删除团队",
"editTeam": "编辑团队",
- "editUnavailableProvisioning": "正在提供时无法编辑团队",
- "openBuiltInEditor": "在内置编辑器中打开工程",
+ "editUnavailableProvisioning": "当配置仍在进行时,编辑团队不可用",
+ "openBuiltInEditor": "在内置编辑器中打开项目",
"openTeamGraph": "打开团队图",
- "stopTeam": "停止小组"
+ "stopTeam": "停队"
},
- "waitingForProvisioning": "一旦提供完毕,小组数据就会出现",
+ "waitingForProvisioning": "配置完成后将显示团队数据",
"context": {
- "title": "背景情况",
- "loading": "正在加载...",
- "noSessionLoaded": "未加载会话",
+ "title": "上下文",
+ "loading": "加载中…",
+ "noSessionLoaded": "没有加载会话",
"closePanel": "关闭 {{team}} 上下文面板",
- "loadingContext": "正在加载上下文...",
- "openLeadSession": "打开团队负责人会话以查看上下文。"
+ "loadingContext": "正在加载上下文…",
+ "openLeadSession": "打开团队领导会话以查看上下文。"
}
},
"review": {
@@ -368,59 +368,59 @@
"actions": {
"accept": "接受",
"discard": "丢弃",
- "discardTooltip": "丢弃此文件的所有编辑",
- "keepMyDraft": "留着我的草稿",
+ "discardTooltip": "放弃对此文件的所有编辑",
+ "keepMyDraft": "保留我的草稿",
"reject": "拒绝",
- "reloadFromDisk": "从磁盘重新装入",
+ "reloadFromDisk": "从磁盘重新加载",
"restore": "恢复",
- "restoreTooltip": "从预览中创建/ 重存此文件",
+ "restoreTooltip": "从预览中在磁盘上创建/恢复此文件",
"saveFile": "保存文件",
- "saveFileTooltip": "文件保存到磁盘"
+ "saveFileTooltip": "将文件保存到磁盘"
},
"badges": {
- "deleted": "DELETED 苏维埃社会主义共和国",
- "manualReview": "MANUAL ZXCV 1ZXCV",
- "new": "NEW 苏维埃社会主义共和国",
- "worktree": "WORKTREE 苏维埃社会主义共和国"
+ "deleted": "已删除",
+ "manualReview": "人工审核",
+ "new": "新的",
+ "worktree": "工作树"
},
"contentSource": {
"disk-current": "当前磁盘",
- "file-history": "文件历史",
- "git-fallback": "Git 倒计时",
- "ledger-exact": "任务编辑器",
- "ledger-snapshot": "编辑快照",
- "snippet-reconstruction": "已重建",
- "unavailable": "内容不详"
+ "file-history": "文件历史记录",
+ "git-fallback": "Git 回退",
+ "ledger-exact": "任务分类帐",
+ "ledger-snapshot": "账本快照",
+ "snippet-reconstruction": "重建",
+ "unavailable": "内容不可用"
},
"contentUnavailable": {
- "badge": "内容不详",
- "description": "分类账记录了这一变化的元数据,但全文内容不详. 这通常指二进制,大,或只包含散列的内容.",
- "safety": "为避免不安全的磁盘写入,此文件的自动接受/拒绝被禁用 。",
- "title": "没有文本内容"
+ "badge": "内容不可用",
+ "description": "账本记录了此更改的元数据,但全文内容不可用。这通常意味着二进制、大型或仅哈希内容。",
+ "safety": "对此文件禁用自动接受/拒绝,以避免不安全的磁盘写入。",
+ "title": "文本内容不可用"
},
"disabled": {
- "acceptRejectContentUnavailable": "接受/拒绝被禁用, 因为没有完整的文本内容 。",
- "acceptRejectMissingOnDisk": "在磁盘上缺少文件时, 接受/ 拒绝被禁用 。",
- "rejectBaselineUnavailable": "拒绝被禁用, 因为没有原始基线 。",
- "rejectContentUnavailable": "拒绝被禁用, 因为没有完整的文本内容 。",
- "rejectManualLedgerReview": "拒绝被禁用, 因为此分类账更改有二进制、 较大或无法使用的内容 。"
+ "acceptRejectContentUnavailable": "由于全文内容不可用,接受/拒绝被禁用。",
+ "acceptRejectMissingOnDisk": "当文件在磁盘上丢失时,接受/拒绝将被禁用。",
+ "rejectBaselineUnavailable": "由于原始基线不可用,拒绝被禁用。",
+ "rejectContentUnavailable": "由于全文内容不可用,拒绝被禁用。",
+ "rejectManualLedgerReview": "拒绝被禁用,因为此分类账更改包含二进制、大型或不可用的内容。"
},
"externalChange": {
- "changedOnDisk": "在磁盘上更改",
- "deletedOnDisk": "磁盘上已删除",
- "recreatedOnDisk": "在磁盘上重现"
+ "changedOnDisk": "磁盘上已更改",
+ "deletedOnDisk": "已在磁盘上删除",
+ "recreatedOnDisk": "在磁盘上重新创建"
},
"missingOnDisk": {
- "badge": "磁盘丢失",
- "description": "我们仍然可以从代理日志中显示一个预览,但您的文件系统已无法同步 。",
+ "badge": "磁盘上丢失",
+ "description": "我们仍然可以显示代理日志的预览,但您的文件系统不同步。",
"restorePrefix": "使用",
"restoreSuffix": "将预览内容写回磁盘。",
- "restoreUnavailable": "无法自动恢复完整文件内容 。",
- "title": "磁盘上缺少文件"
+ "restoreUnavailable": "无法自动恢复完整文件内容。",
+ "title": "磁盘上文件丢失"
},
"pathChange": {
- "from": "来自{{path}}",
- "to": "前往{{path}}"
+ "from": "来自 {{path}}",
+ "to": "至 {{path}}"
},
"worktree": {
"isolated": "孤立的工作树"
@@ -428,308 +428,308 @@
},
"toolbar": {
"stats": {
- "pending": "{{count}}待处理",
- "pending_one": "{{count}}待处理",
- "pending_other": "{{count}}待处理",
- "accepted": "接受{{count}}",
- "accepted_one": "接受{{count}}",
- "accepted_other": "接受{{count}}",
- "rejected": "{{count}}拒绝",
- "rejected_one": "{{count}}拒绝",
- "rejected_other": "{{count}}拒绝",
- "acrossFiles": "横跨 {{count}} 文件",
- "acrossFiles_one": "横跨 {{count}} 文件",
- "acrossFiles_other": "横跨 {{count}} 文件",
- "edited": "编辑 {{count}}",
- "edited_one": "编辑 {{count}}",
- "edited_other": "编辑 {{count}}",
- "pending_few": "{{count}}待处理",
- "pending_many": "{{count}}待处理",
- "accepted_few": "接受{{count}}",
- "accepted_many": "接受{{count}}",
- "rejected_few": "{{count}}拒绝",
- "rejected_many": "{{count}}拒绝",
- "acrossFiles_few": "横跨 {{count}} 文件",
- "acrossFiles_many": "横跨 {{count}} 文件",
- "edited_few": "编辑 {{count}}",
- "edited_many": "编辑 {{count}}"
+ "pending": "{{count}} 待定",
+ "pending_one": "{{count}} 待定",
+ "pending_other": "{{count}} 待定",
+ "accepted": "{{count}} 已接受",
+ "accepted_one": "{{count}} 已接受",
+ "accepted_other": "{{count}} 已接受",
+ "rejected": "{{count}} 被拒绝",
+ "rejected_one": "{{count}} 被拒绝",
+ "rejected_other": "{{count}} 被拒绝",
+ "acrossFiles": "跨 {{count}} 文件",
+ "acrossFiles_one": "跨 {{count}} 文件",
+ "acrossFiles_other": "跨 {{count}} 文件",
+ "edited": "{{count}} 已编辑",
+ "edited_one": "{{count}} 已编辑",
+ "edited_other": "{{count}} 已编辑",
+ "pending_few": "{{count}} 待定",
+ "pending_many": "{{count}} 待定",
+ "accepted_few": "{{count}} 已接受",
+ "accepted_many": "{{count}} 已接受",
+ "rejected_few": "{{count}} 被拒绝",
+ "rejected_many": "{{count}} 被拒绝",
+ "acrossFiles_few": "跨 {{count}} 文件",
+ "acrossFiles_many": "跨 {{count}} 文件",
+ "edited_few": "{{count}} 已编辑",
+ "edited_many": "{{count}} 已编辑"
},
"actions": {
- "auto": "自动",
- "undo": "撤销",
- "acceptAll": "全部接受( O)",
+ "auto": "汽车",
+ "undo": "撤消",
+ "acceptAll": "全部接受",
"rejectAll": "全部拒绝",
- "applying": "正在应用...",
- "applyRejections": "应用拒绝"
+ "applying": "正在申请…",
+ "applyRejections": "申请拒绝"
},
"tooltips": {
- "autoOn": "滚动到结尾时查看的自动标记文件( ON)",
- "autoOff": "滚动到结束时查看的自动标记文件( OFF)",
- "undo": "撤消上次审查操作( Ctrl+Z)",
- "acceptAll": "接受所有文件中的所有更改",
- "rejectAll": "在所有文件中拒绝所有可安全拒绝的更改",
- "rejectAllDisabled": "没有待决文件有安全的原始基线可以拒绝 。",
- "applyRejections": "将已拒绝的hunks应用到磁盘中; 接受的更改保留为- is"
+ "autoOn": "滚动到末尾时自动将文件标记为已查看(开启)",
+ "autoOff": "滚动到末尾时自动将文件标记为已查看(关闭)",
+ "undo": "撤消上次审阅操作 (Ctrl+Z)",
+ "acceptAll": "接受所有文件的所有更改",
+ "rejectAll": "拒绝所有文件中所有可安全拒绝的更改",
+ "rejectAllDisabled": "没有待处理的文件具有可以拒绝的安全原始基线。",
+ "applyRejections": "将已拒绝的差异片段应用到磁盘;已接受的更改保持原样"
}
},
"diffError": {
- "title": "提供 diff 视图失败",
- "unexpected": "渲染 diff 时发生了意外错误 。",
+ "title": "无法渲染差异视图",
+ "unexpected": "渲染差异时发生意外错误。",
"actions": {
"retry": "重试"
},
"raw": {
- "show": "显示原始 diff 数据",
- "file": "文件: {{file}}",
- "original": "--- 原件",
- "modified": "• 已修改",
- "charsTotal": "... ({{count}}字符总数)",
- "charsTotal_one": ". (......{{count}}字符总计)",
- "charsTotal_other": "... ({{count}}字符总数)",
- "charsTotal_few": "... ({{count}}字符总数)",
- "charsTotal_many": "... ({{count}}字符总数)"
+ "show": "显示原始差异数据",
+ "file": "文件:{{file}}",
+ "original": "- - 原来的",
+ "modified": "+++ 修改",
+ "charsTotal": "…({{count}} 字符总数)",
+ "charsTotal_one": "…({{count}} 字符总数)",
+ "charsTotal_other": "…({{count}} 字符总数)",
+ "charsTotal_few": "…({{count}} 字符总数)",
+ "charsTotal_many": "…({{count}} 字符总数)"
}
},
"fileTree": {
"viewed": "已查看",
"badges": {
- "new": "新设",
- "deleted": "删除"
+ "new": "新的",
+ "deleted": "已删除"
},
"collapseFolder": "折叠 {{name}}",
- "expandFolder": "扩展 {{name}}",
+ "expandFolder": "展开 {{name}}",
"empty": {
- "noChangedFiles": "没有更改文件",
+ "noChangedFiles": "没有更改的文件",
"noMatchingFiles": "没有匹配的文件"
},
- "searchPlaceholder": "搜索文件...",
+ "searchPlaceholder": "搜索文件…",
"filters": {
"unresolved": "未解决",
"rejected": "被拒绝",
- "new": "新设",
+ "new": "新的",
"clear": "清除"
}
},
"diffControls": {
- "previousChunk": "上一块",
+ "previousChunk": "上一个块",
"nextChunk": "下一个块",
- "rejectChange": "拒绝更改 (QQN)",
- "acceptChange": "接受更改 (QQY)",
- "undo": "撤销",
- "keep": "保留",
- "rejectShortcut": "页:1",
- "acceptShortcut": "⌘"
+ "rejectChange": "拒绝更改 (⌘N)",
+ "acceptChange": "接受更改 (⌘Y)",
+ "undo": "撤消",
+ "keep": "保持",
+ "rejectShortcut": "⌘N",
+ "acceptShortcut": "⌘Y"
},
"conflict": {
- "title": "已发现冲突",
- "description": "此文件自代理更改后已被修改",
+ "title": "检测到冲突",
+ "description": "自代理更改后此文件已被修改",
"cancel": "取消",
"saveResolution": "保存分辨率",
"editManually": "手动编辑",
- "useOriginal": "使用原样",
- "keepCurrent": "保持当前"
+ "useOriginal": "使用原始版本",
+ "keepCurrent": "保留当前版本"
},
"fullDiffLoading": {
- "titleOne": "准备完整的 Diff",
- "titleMany": "准备 {{count}} 完整 Diffs",
- "subtitleForFile": "最终确定{{file}}的准确编辑器diff.",
- "subtitleCurrentFile": "最后确定当前文件的确切编辑器 diff 。",
- "subtitleMany": "解析当前装入的文件基线前后的精确度 。",
- "previewsReady": "{{count}}预览已准备好",
- "previewsReady_one": "{{count}} 预览准备",
+ "titleOne": "准备完整的差异",
+ "titleMany": "准备 {{count}} 完整差异",
+ "subtitleForFile": "最终确定 {{file}} 的确切编辑器差异。",
+ "subtitleCurrentFile": "完成当前文件的确切编辑器差异。",
+ "subtitleMany": "解析当前加载文件的准确之前/之后基线。",
+ "previewsReady": "{{count}} 预览就绪",
+ "previewsReady_one": "{{count}} 预览就绪",
"editorViewLoading": "编辑器视图加载",
- "filesInProgress": "{{count}}文件正在进行",
- "filesInProgress_one": "{{count}}文件正在进行",
- "filesReady": "{{ready}}页:1{{total}}文件准备",
- "progressDescription": "{{ready}}准备{{loading}}还在装货 预览 diff 保留在下方可见, 其余基线得到解决 。",
- "singleDescription": "预览 diff 保留在下面, 而准确的基线得到解决 。",
- "previewsReady_few": "{{count}}预览已准备好",
- "previewsReady_many": "{{count}}预览已准备好",
- "previewsReady_other": "{{count}}预览已准备好",
- "filesInProgress_few": "{{count}}文件正在进行",
- "filesInProgress_many": "{{count}}文件正在进行",
- "filesInProgress_other": "{{count}}文件正在进行"
+ "filesInProgress": "{{count}} 文件正在进行中",
+ "filesInProgress_one": "{{count}} 文件正在进行中",
+ "filesReady": "{{ready}}/{{total}} 文件准备就绪",
+ "progressDescription": "{{ready}} 已准备就绪,{{loading}} 仍在加载。当剩余的基线得到解决时,预览差异在下面保持可见。",
+ "singleDescription": "当准确的基线得到解决时,预览差异在下面保持可见。",
+ "previewsReady_few": "{{count}} 预览就绪",
+ "previewsReady_many": "{{count}} 预览就绪",
+ "previewsReady_other": "{{count}} 预览就绪",
+ "filesInProgress_few": "{{count}} 文件正在进行中",
+ "filesInProgress_many": "{{count}} 文件正在进行中",
+ "filesInProgress_other": "{{count}} 文件正在进行中"
},
- "fileMissingPrefix": "磁盘上缺少文件 。 此 diff 可能只是代理日志的预览 。 使用",
+ "fileMissingPrefix": "磁盘上文件丢失。此差异可能只是代理日志的预览。使用",
"restore": "恢复",
"fileMissingSuffix": "在磁盘上创建文件。",
"filePlaceholder": {
- "loading": "正在装入",
- "description": "为此文件准备一个完整的编辑器 diff 。"
+ "loading": "加载中",
+ "description": "为此文件准备完整的编辑器差异。"
},
"loading": {
- "diff": "DIFF 苏维埃社会主义共和国",
- "ledgerObjectsProcessed": "{{count}}处理的分类账对象",
- "ledgerObjectsProcessed_one": "处理的 {{count}} 分类账对象",
- "ledgerObjectsProcessed_other": "{{count}}处理的分类账对象",
- "ledgerObjectsProcessed_few": "{{count}}处理的分类账对象",
- "ledgerObjectsProcessed_many": "{{count}}处理的分类账对象",
+ "diff": "差分法",
+ "ledgerObjectsProcessed": "{{count}} 处理的账本对象",
+ "ledgerObjectsProcessed_one": "{{count}} 已处理的账本对象",
+ "ledgerObjectsProcessed_other": "{{count}} 处理的账本对象",
+ "ledgerObjectsProcessed_few": "{{count}} 处理的账本对象",
+ "ledgerObjectsProcessed_many": "{{count}} 处理的账本对象",
"phases": {
- "readingLedger": "正在读取任务分类账...",
- "resolvingFiles": "正在解析文件状态...",
- "checkingWorktree": "正在检查工作树的背景...",
- "preparingDiffs": "正在准备审查 diffs..."
+ "readingLedger": "读取任务分类帐…",
+ "resolvingFiles": "正在解析文件状态…",
+ "checkingWorktree": "正在检查工作树上下文…",
+ "preparingDiffs": "正在准备审核差异…"
}
},
"progress": {
- "viewed": "{{viewed}}页:1{{total}}浏览"
+ "viewed": "{{viewed}}/{{total}} 已查看"
},
"scope": {
- "readMore": "更多信息",
+ "readMore": "阅读更多",
"tiers": {
"exact": {
- "title": "准确确定任务范围",
- "detail": "在会话日志中找到的开始和完成标记。 diff只包含在这一具体任务中做出的修改——其他修改相同文件的任务被排除在外."
+ "title": "任务范围精确确定",
+ "detail": "在会话日志中找到开始和完成 Token。差异仅包括在此特定任务持续时间所做的更改 - 排除修改相同文件的其他任务。"
},
"endEstimated": {
- "title": "终点估计",
- "detail": "只有起始标记被找到 - 任务还没有完成标记 。 从任务开始到会话结束的更改 。 如果在同一会话中此任务之后还有其它任务,则其更改也可能包括在内."
+ "title": "估计结束边界",
+ "detail": "仅找到开始 Token - 任务尚无完成 Token。显示从任务开始到会话结束的变化。如果同一会话中此任务之后运行了其他任务,则也可能包括它们的更改。"
},
"startEstimated": {
- "title": "开始边界估计",
- "detail": "只有完成标记被找到——没有捕获工程的开始. 如果在同一会话中其他任务运行于此任务之前, 也可以包含对相同文件的更改 。"
+ "title": "估计的起始边界",
+ "detail": "仅找到完成 Token - 未捕获工作开始。如果同一会话中在此任务之前运行了其他任务,则也可能包括它们对相同文件的更改。"
},
"allSession": {
- "title": "显示全部会话更改",
- "detail": "在会话日志中找不到任务标记 。 无法孤立此任务 - 从整个会话中显示所有文件更改, 包括从其他任务中更改 。 这可以在较旧的CLI版本或非标准工作流程中发生."
+ "title": "显示所有会话更改",
+ "detail": "在会话日志中找不到任务 Token。无法隔离此任务 - 显示整个会话中的所有文件更改,包括其他任务的更改。较早消息的 CLI 版本或非标准工作流程可能会发生这种情况。"
}
},
"ledger": {
"exact": {
- "title": "任务分类账记录的变化",
- "detail": "管弦乐手在代理执行此任务时捕获了这些文件的更改.",
- "badge": "编辑器精确"
+ "title": "任务分类帐捕获的更改",
+ "detail": "当代理执行此任务时,编排器捕获了这些文件更改。",
+ "badge": "账本精确"
},
"limited": {
- "title": "可审查性有限的变动",
- "detail": "管弦乐手为这个任务捕捉了这些文件的更改,但至少从一个快照或只使用元数据的来源捕捉到一个更改. 如果有的话,请审查准确的文本diff;二进制或不可用的内容可能需要人工审查。",
- "mixedBadge": "混合可审查性",
- "needsReviewBadge": "需求审查"
+ "title": "捕获的变更具有有限的可审核性",
+ "detail": "编排器捕获了此任务的这些文件更改,但至少有一个更改是从快照或仅元数据源捕获的。查看确切的文本差异(如果有);二进制或不可用的内容可能需要手动审核。",
+ "mixedBadge": "混合审核性",
+ "needsReviewBadge": "需要审核"
}
},
"workInterval": {
- "title": "按持续的工作间隔划分",
- "detail": "任务启动标记在会话日志中没有,因此diff被存储在棋盘上的任务工作间隔所标定.",
- "badge": "间隔范围d"
+ "title": "按持续工作间隔确定范围",
+ "detail": "任务开始 Token 在会话日志中不可用,因此差异的范围由板上存储的任务工作间隔决定。",
+ "badge": "区间范围"
},
"confidence": {
- "high": "高度信心",
- "medium": "中度信任度",
- "low": "信心不足",
- "bestEffort": "尽最大的努力"
+ "high": "高置信度",
+ "medium": "中等置信度",
+ "low": "信心度低",
+ "bestEffort": "尽最大努力"
}
},
"shortcuts": {
"title": "键盘快捷键",
"actions": {
- "nextChange": "下一次更改",
- "previousChange": "以前的变化",
+ "nextChange": "下一步更改",
+ "previousChange": "之前的变更",
"nextFile": "下一个文件",
"previousFile": "上一个文件",
- "acceptChange": "接受更改",
- "rejectChange": "拒绝更改",
+ "acceptChange": "接受变更",
+ "rejectChange": "拒绝改变",
"saveFile": "保存文件",
- "undo": "撤销",
- "redo": "重装",
+ "undo": "撤消",
+ "redo": "重做",
"toggleShortcuts": "切换快捷键",
"closeDialog": "关闭对话框"
}
},
"timeline": {
- "empty": "没有编辑事件",
- "titleWithCount": "编辑时间线( {{count}})"
+ "empty": "无编辑事件",
+ "titleWithCount": "编辑时间线 ({{count}})"
},
"continuousScroll": {
- "empty": "无可审查文件更改"
+ "empty": "没有可审核的文件更改"
},
"empty": {
- "noSafeDiff": "没有安全的 diff 可用",
+ "noSafeDiff": "没有可用的安全差异",
"noFileChangesRecorded": "没有记录文件更改",
- "noSafeDiffDescription": "任务分类账没有披露此任务的安全文件 。",
- "noSafeDiffDiagnosticsDescription": "任务分类账没有披露此任务的安全文件 。 下文的诊断解释了原因。",
- "noFileEventsYet": "任务分类账尚未有此任务的文件事件 。",
- "noFileEvents": "任务分类账对此任务没有文件事件."
+ "noSafeDiffDescription": "任务分类帐未公开此任务的安全文件差异。",
+ "noSafeDiffDiagnosticsDescription": "任务分类帐未公开此任务的安全文件差异。下面的诊断解释了原因。",
+ "noFileEventsYet": "任务分类帐尚无此任务的文件事件。",
+ "noFileEvents": "任务分类帐没有该任务的文件事件。"
}
},
"messages": {
"actions": {
- "bottomSheetActions": "信件底页动作",
- "collapseAll": "折叠所有信件",
- "collapseSheet": "折叠工作表",
- "expandAll": "展开所有信件",
- "expandSheet": "扩展工作表",
- "floatComposer": "浮点编曲",
- "floatMessagesComposer": "Float 信件编曲",
+ "bottomSheetActions": "消息底部工作表操作",
+ "collapseAll": "折叠所有消息",
+ "collapseSheet": "折叠表",
+ "expandAll": "展开所有消息",
+ "expandSheet": "展开表",
+ "floatComposer": "浮动作曲家",
+ "floatMessagesComposer": "浮动消息编辑器",
"hideSearch": "隐藏搜索",
- "loadOlder": "装入旧信件",
- "markAllRead": "全部标为已读",
- "messageActions": "消息动作",
- "moveMessagesToBottomSheet": "将信件移至底盘",
- "moveMessagesToSidebar": "将信件移动到侧边栏",
- "moveToBottomSheet": "移动到底部工作表",
- "moveToInline": "移动到内线",
- "moveToSidebar": "移动到侧边栏",
- "panelActions": "信件面板动作",
- "searchMessages": "搜索信件"
+ "loadOlder": "加载旧消息",
+ "markAllRead": "全部 Token 为已读",
+ "messageActions": "消息操作",
+ "moveMessagesToBottomSheet": "将消息移至底部工作表",
+ "moveMessagesToSidebar": "将消息移至侧边栏",
+ "moveToBottomSheet": "移至底部工作表",
+ "moveToInline": "移至内联",
+ "moveToSidebar": "移至侧边栏",
+ "panelActions": "消息面板操作",
+ "searchMessages": "搜索消息"
},
"delivery": {
- "copied": "复制",
- "copyDebugDetails": "复制调试细节",
+ "copied": "已复制",
+ "copyDebugDetails": "复制调试详细信息",
"details": "细节",
"fields": {
- "acceptanceUnknown": "未知的接受",
- "delivered": "已交付",
+ "acceptanceUnknown": "接受未知",
+ "delivered": "发表",
"diagnostics": "诊断",
- "ledgerStatus": "分类账现状",
- "messageId": "信件编号",
- "providerId": "提供者",
- "queuedBehindMessageId": "队列已排入 MessageId",
- "reason": "理由",
- "responsePending": "响应",
+ "ledgerStatus": "账本状态",
+ "messageId": "消息 ID",
+ "providerId": "提供商 ID",
+ "queuedBehindMessageId": "队列后面消息 ID",
+ "reason": "原因",
+ "responsePending": "响应待处理",
"responseState": "响应状态",
- "statusMessageId": "状态信息",
- "userVisibleMessage": "用户可视度",
- "userVisibleNextReviewAt": "用户VisibleNextReviewAt",
- "userVisibleReasonCode": "用户可视Reason代码",
- "userVisibleState": "用户可视状态",
- "visibleReplyCorrelation": "可见校正",
- "visibleReplyMessageId": "可见回转MessageId"
+ "statusMessageId": "状态消息 ID",
+ "userVisibleMessage": "用户可见消息",
+ "userVisibleNextReviewAt": "userVisibleNextReviewAt",
+ "userVisibleReasonCode": "用户可见原因代码",
+ "userVisibleState": "用户可见状态",
+ "visibleReplyCorrelation": "可见回复相关性",
+ "visibleReplyMessageId": "可见回复消息 ID"
}
},
- "panelMode": "信件面板模式",
- "title": "信件",
+ "panelMode": "消息面板模式",
+ "title": "消息",
"unread": {
- "new": "{{count}}新设",
- "unread": "{{count}}未读",
- "new_few": "{{count}}新设",
- "new_many": "{{count}}新设",
- "new_one": "{{count}}新设",
- "new_other": "{{count}}新设",
- "unread_few": "{{count}}未读",
- "unread_many": "{{count}}未读",
- "unread_one": "{{count}}未读",
- "unread_other": "{{count}}未读"
+ "new": "{{count}} 新",
+ "unread": "{{count}} 未读",
+ "new_few": "{{count}} 新",
+ "new_many": "{{count}} 新",
+ "new_one": "{{count}} 新",
+ "new_other": "{{count}} 新",
+ "unread_few": "{{count}} 未读",
+ "unread_many": "{{count}} 未读",
+ "unread_one": "{{count}} 未读",
+ "unread_other": "{{count}} 未读"
},
"filter": {
"ariaLabel": "过滤消息",
"tooltip": "过滤消息",
"from": "从",
- "to": "改为",
+ "to": "到",
"noData": "无数据",
- "showStatusUpdates": "显示状态更新( idle/ shutdown)",
+ "showStatusUpdates": "显示状态更新(空闲/关闭)",
"actions": {
- "reset": "重设",
+ "reset": "重置",
"save": "保存"
}
},
"status": {
- "title": "状态"
+ "title": "地位"
},
"actionMode": {
- "label": "行动模式"
+ "label": "动作模式"
},
"search": {
- "placeholder": "搜索..."
+ "placeholder": "搜索…"
}
},
"modelSelector": {
@@ -738,362 +738,362 @@
"connected": "已连接",
"failed": "失败",
"free": "免费",
- "local": "当地",
- "needsTest": "需求测试",
+ "local": "本地",
+ "needsTest": "需要测试",
"verified": "已验证",
- "unavailable": "无法获取",
+ "unavailable": "不可用",
"issue": "问题"
},
- "customModelId": "自定义模式 ID",
- "label": "型号( 可选)",
- "multimodelRequired": "Codex和双子座需要多模型模式.",
+ "customModelId": "自定义模型 ID",
+ "label": "模型(可选)",
+ "multimodelRequired": "Codex 和 Gemini 需要多模型模式。",
"openCode": {
- "allSources": "所有 OpenCode 来源",
- "filterSource": "过滤 {{source}}",
+ "allSources": "所有 OpenCode 源",
+ "filterSource": "过滤器 {{source}}",
"filterSources": "过滤 OpenCode 源",
- "freeOnly": "仅自由",
- "freeTooltip": "OpenCode标记此模式为免费.",
- "loadingModels": "正在装入 OpenCode 模型...",
- "noSourcesFound": "没有找到消息源 。",
- "recommendedOnly": "仅建议",
+ "freeOnly": "仅免费",
+ "freeTooltip": "OpenCode 将此模型 Token 为免费。",
+ "loadingModels": "正在加载 OpenCode 模型…",
+ "noSourcesFound": "未找到来源。",
+ "recommendedOnly": "仅推荐",
"searchSources": "搜索来源",
- "sourcesCount": "{{count}} 苏维埃社会主义共和国 打开代码源",
- "sourcesCount_few": "{{count}} 苏维埃社会主义共和国 打开代码源",
- "sourcesCount_many": "{{count}} 苏维埃社会主义共和国 打开代码源",
- "sourcesCount_one": "{{count}} 苏维埃社会主义共和国 打开代码源",
- "sourcesCount_other": "{{count}} 苏维埃社会主义共和国 打开代码源"
+ "sourcesCount": "{{count}} OpenCode 源代码",
+ "sourcesCount_few": "{{count}} OpenCode 源代码",
+ "sourcesCount_many": "{{count}} OpenCode 源代码",
+ "sourcesCount_one": "{{count}} OpenCode 源代码",
+ "sourcesCount_other": "{{count}} OpenCode 源代码"
},
- "reason": "理由:{{reason}}",
- "runtimeModelsSyncing": "从当前运行时间中明确模式加载. 列表同步时默认值仍然可用 。",
+ "reason": "原因:{{reason}}",
+ "runtimeModelsSyncing": "从当前运行时加载显式模型。同步列表时,默认值仍然可用。",
"fastMode": {
- "codexLabel": "快模式(2x 信用)",
- "optionalLabel": "快模式( 可选)",
- "defaultOff": "默认( 关闭)",
- "fast": "快点",
- "off": "关",
- "defaultFast": "默认( 快速)",
- "defaultResolvesTo": "默认当前解决为 {{mode}}.",
- "runtimeBackedHint": "Fast模式是运行时备份的,只有在解析的Anthropic发射模式支持时才会解锁."
+ "codexLabel": "快速模式(2x 学分)",
+ "optionalLabel": "快速模式(可选)",
+ "defaultOff": "默认(关闭)",
+ "fast": "快速地",
+ "off": "关闭",
+ "defaultFast": "默认(快速)",
+ "defaultResolvesTo": "当前默认解析为 {{mode}}。",
+ "runtimeBackedHint": "快速模式是运行时支持的,并且只有在解析的 Anthropic 启动模型支持它时才会解锁。"
},
"anthropicExtraUsage": {
- "pricingDocs": "读取 Athropic 定价文件"
+ "pricingDocs": "阅读人择定价文档"
},
- "searchModels": "搜索模式",
+ "searchModels": "搜索模型",
"defaultModel": "默认",
"empty": {
- "noSearchMatches": "没有匹配此搜索的模型 。",
- "recommendedFreeOpenCode": "当前运行时间列表中没有推荐的免费OpenCode模型.",
- "freeOpenCode": "当前运行时间列表中没有免费的OpenCode模型.",
- "recommendedOpenCode": "当前运行时间列表中没有推荐的 OpenCode 模型 。",
- "noModels": "当前运行时间列表中没有可用的模型 。"
+ "noSearchMatches": "没有与此搜索匹配的模型。",
+ "recommendedFreeOpenCode": "当前运行时列表中没有推荐的免费 OpenCode 模型。",
+ "freeOpenCode": "当前运行时列表中没有可用的免费 OpenCode 模型。",
+ "recommendedOpenCode": "当前运行时列表中没有推荐的 OpenCode 模型。",
+ "noModels": "当前运行时列表中没有可用的模型。"
},
"openCodeStatus": {
"notReadyTitle": "OpenCode 尚未准备好团队启动",
- "freeModelsAvailableTitle": "OpenCode 免费模式",
- "providerNotConnectedTitle": "OpenCode 提供者没有连接",
- "readyTitle": "打开代码",
- "readyMessage": "OpenCode通过提供者准备 。 为这个团队选择使用 OpenCode 模型 。",
- "useOpenCode": "使用 OpenCode 代码",
+ "freeModelsAvailableTitle": "提供 OpenCode 免费模型",
+ "providerNotConnectedTitle": "OpenCode 提供商未连接",
+ "readyTitle": "OpenCode 已准备就绪",
+ "readyMessage": "OpenCode 通过提供商准备工作。选择它以为此团队使用 OpenCode 模型。",
+ "useOpenCode": "使用 OpenCode",
"badges": {
- "check": "检查",
+ "check": "查看",
"install": "安装",
"free": "免费",
"setup": "设置"
},
"summary": {
- "checking": "OpenCode 状态: 检查运行时间",
- "status": "OpenCode 状态: {{parts}}"
+ "checking": "OpenCode 状态:检查运行时",
+ "status": "OpenCode 状态:{{parts}}"
},
"summaryParts": {
"teamLaunchBlocked": "团队启动受阻",
- "providerOptional": "提供者连接可选",
- "providerModelsNeedSetup": "需要设置供应商支持的模型",
- "teamLaunchReady": "准备发射",
- "runtimeDetected": "检测到运行时间",
- "runtimeMissing": "缺少运行时间",
- "freeWithoutAuth": "无认证可自由使用的模式",
- "providerConnected": "连接提供者",
- "providerNotConnected": "未连接提供者"
+ "providerOptional": "提供商连接可选",
+ "providerModelsNeedSetup": "提供商支持的模型需要设置",
+ "teamLaunchReady": "团队启动准备就绪",
+ "runtimeDetected": "检测到运行时",
+ "runtimeMissing": "运行时缺失",
+ "freeWithoutAuth": "无需授权即可使用免费模型",
+ "providerConnected": "提供商已连接",
+ "providerNotConnected": "提供商未连接"
},
"messages": {
- "checking": "应用程序仍在检查 OpenCode 运行时间 。 等待提供者状态完成, 然后再次尝试 。",
- "unsupported": "OpenCode 不安装, 找不到, 也不支持检测到的运行时间 。 安装或更新 OpenCode, 然后刷新提供者状态 。 也可以使用主页上的安装按钮.",
- "freeAvailable": "检测到 OpenCode 。 您可以使用免费的OpenCode模型,如大泡菜,而不连接一个提供者. 仅在您需要供应商支持的模型时连接提供者 。",
- "noFreeListed": "OpenCode被检测,但尚未列出免费的OpenCode模型. 刷新提供者状态, 或在 OpenCode 为提供者支持的模型连接一个提供者 。",
- "launchBlocked": "OpenCode已安装并认证,但代理团队发射准备受阻.",
- "ready": "OpenCode已准备好进行团队启动."
+ "checking": "该应用仍在检查 OpenCode 运行时。等待提供商状态完成,然后重试。",
+ "unsupported": "OpenCode 未安装、未找到或检测到的运行时不受支持。安装或更新 OpenCode,然后刷新提供商状态。您还可以使用主页上的“安装”按钮。",
+ "freeAvailable": "检测到 OpenCode。您可以使用免费的 OpenCode 模型,例如 Big Pickle,而无需连接提供商。仅当您需要提供商支持的模型时才连接提供商。",
+ "noFreeListed": "已检测到 OpenCode,但尚未列出免费的 OpenCode 模型。刷新提供商状态,或在 OpenCode 中连接提供商以获取提供商支持的模型。",
+ "launchBlocked": "OpenCode 已安装并经过认证,但 Agent Teams 启动准备工作被阻止。",
+ "ready": "OpenCode 已准备好供团队启动。"
},
- "loadingRuntime": "OpenCode 运行时状态仍在装入中 。"
+ "loadingRuntime": "OpenCode 运行时状态仍在加载中。"
},
"advisory": {
- "pingNotConfirmed": "小平没有确认",
- "note": "说明"
+ "pingNotConfirmed": "Ping 未确认",
+ "note": "备注"
},
"placeholders": {
- "customModelId": "开源软件国际化之简体中文组"
+ "customModelId": "openai/gpt-oss-20b"
},
"routeGroups": {
- "openCodeConfig": "打开代码配置",
+ "openCodeConfig": "OpenCode 配置",
"builtinFree": "免费内置",
- "connectedProviders": "连接提供者",
+ "connectedProviders": "互联提供商",
"otherCatalog": "其他 OpenCode 目录"
},
"pricing": {
"free": "免费",
- "inputShort": "以 {{rate}} 计算",
- "outputShort": "在{{rate}}区外",
- "perMillionSummary": "{{summary}}/ 1海里",
- "inputTitle": "输入: {{rate}} 每1M个符号",
- "outputTitle": "输出: {{rate}} 每1M个令牌",
- "cacheReadTitle": "缓存读取: {{rate}} 每1M个令牌",
- "cacheWriteTitle": "缓存写法: {{rate}} 每1M个令牌"
+ "inputShort": "在 {{rate}}",
+ "outputShort": "输出 {{rate}}",
+ "perMillionSummary": "{{summary}}/1M",
+ "inputTitle": "输入:每 1M Token {{rate}}",
+ "outputTitle": "输出:每 100 万个 Token {{rate}}",
+ "cacheReadTitle": "缓存读取:每 1M Token {{rate}}",
+ "cacheWriteTitle": "缓存写入:每 1M Token {{rate}}"
},
"defaultTooltip": {
- "anthropicCompatibleWithResolved": "使用与Anthropic兼容的终点默认模式.\n目前解决{{model}}.",
- "anthropicCompatible": "使用与Anthropic兼容的终点默认模式.",
- "anthropic": "使用克劳德团队默认模式.\n决定使用1M上下文的{{longContextModel}},或者在启用限制上下文时使用200K上下文的{{limitedContextModel}}.",
- "openCodeWithResolved": "使用 OpenCode 默认模式 。\n目前解决{{model}}.",
- "openCode": "使用 OpenCode 运行时默认模式 。",
- "runtime": "为选定的提供者使用运行时间默认值 。"
+ "anthropicCompatibleWithResolved": "使用与 Anthropic 兼容的端点默认模型。 目前解析为 {{model}}。",
+ "anthropicCompatible": "使用与 Anthropic 兼容的端点默认模型。",
+ "anthropic": "使用 Claude 团队默认模型。 当启用限制上下文时,解析为具有 1M 上下文的 {{longContextModel}} 或具有 200K 上下文的 {{limitedContextModel}}。",
+ "openCodeWithResolved": "使用 OpenCode 默认模型。 目前解析为 {{model}}。",
+ "openCode": "使用 OpenCode 运行时默认模型。",
+ "runtime": "使用所选提供商的运行时默认值。"
},
- "multimodelOff": "多模式关闭",
- "unavailableInRuntime": "当前运行时间不可用"
+ "multimodelOff": "多模型关闭",
+ "unavailableInRuntime": "当前运行时不可用"
},
"taskDetail": {
"actions": {
"cancel": "取消",
"delete": "删除",
- "markResolved": "标记已解决",
+ "markResolved": "标记为已解决",
"save": "保存"
},
"attachments": {
- "commentAttachment": "注释附件",
- "fromComments": "从评论",
- "preview": "预览{{filename}}"
+ "commentAttachment": "评论附件",
+ "fromComments": "来自评论",
+ "preview": "预览 {{filename}}"
},
"changes": {
"badges": {
- "attention": "请注意",
- "noSafeDiff": "没有安全 diff"
+ "attention": "注意力",
+ "noSafeDiff": "没有安全差异"
},
"empty": {
"noFileChangesRecorded": "没有记录文件更改",
"noFileChangesRecordedYet": "尚未记录文件更改",
- "noReviewableChangesRecovered": "未找到可审查文件更改",
- "noSafeDiffAvailable": "没有安全的 diff 可用"
+ "noReviewableChangesRecovered": "未恢复可审核的文件更改",
+ "noSafeDiffAvailable": "没有可用的安全差异"
},
- "loadFailed": "装入任务更改摘要失败",
- "loading": "正在装入更改...",
- "fileCount": "{{count}}文件",
+ "loadFailed": "无法加载任务更改摘要",
+ "loading": "正在加载更改…",
+ "fileCount": "{{count}} 文件",
"fileRowsHidden": "{{count}} 文件行隐藏",
- "moreDiagnostics": "{{count}}更多的诊断",
+ "moreDiagnostics": "{{count}} 更多诊断",
"moreFiles": "{{count}} 更多文件",
"openInEditor": "在编辑器中打开",
"openTask": "打开任务 {{subject}}",
"refresh": "刷新更改",
- "refreshFailed": "刷新失败: {{error}}",
- "refreshing": "刷新中",
- "refreshingChanges": "正在刷新更改...",
- "refreshTeamChanges": "刷新团队更改",
+ "refreshFailed": "刷新失败:{{error}}",
+ "refreshing": "清爽",
+ "refreshingChanges": "令人耳目一新的变化…",
+ "refreshTeamChanges": "刷新团队变更",
"refreshShort": "刷新",
- "reviewDiff": "复核 diff",
- "reviewTaskDiff": "审查任务 diff",
- "scannedCandidateTasks": "扫描{{requested}}页:1{{eligible}}候选任务",
- "tasksDeferred": "{{count}} 任务推迟此通过",
- "title": "变动",
- "fileCount_few": "{{count}}文件",
- "fileCount_many": "{{count}}文件",
- "fileCount_one": "{{count}}文件",
- "fileCount_other": "{{count}}文件",
+ "reviewDiff": "查看差异",
+ "reviewTaskDiff": "检查任务差异",
+ "scannedCandidateTasks": "已扫描 {{requested}} / {{eligible}} 个候选任务",
+ "tasksDeferred": "{{count}} 任务推迟了本次传递",
+ "title": "变化",
+ "fileCount_few": "{{count}} 文件",
+ "fileCount_many": "{{count}} 文件",
+ "fileCount_one": "{{count}} 文件",
+ "fileCount_other": "{{count}} 文件",
"fileRowsHidden_few": "{{count}} 文件行隐藏",
"fileRowsHidden_many": "{{count}} 文件行隐藏",
"fileRowsHidden_one": "{{count}} 文件行隐藏",
"fileRowsHidden_other": "{{count}} 文件行隐藏",
- "moreDiagnostics_few": "{{count}}更多的诊断",
- "moreDiagnostics_many": "{{count}}更多的诊断",
- "moreDiagnostics_one": "{{count}}更多的诊断",
- "moreDiagnostics_other": "{{count}}更多的诊断",
+ "moreDiagnostics_few": "{{count}} 更多诊断",
+ "moreDiagnostics_many": "{{count}} 更多诊断",
+ "moreDiagnostics_one": "{{count}} 更多诊断",
+ "moreDiagnostics_other": "{{count}} 更多诊断",
"moreFiles_few": "{{count}} 更多文件",
"moreFiles_many": "{{count}} 更多文件",
"moreFiles_one": "{{count}} 更多文件",
"moreFiles_other": "{{count}} 更多文件",
- "tasksDeferred_few": "{{count}} 任务推迟此通过",
- "tasksDeferred_many": "{{count}} 任务推迟此通过",
- "tasksDeferred_one": "{{count}} 任务推迟此通过",
- "tasksDeferred_other": "{{count}} 任务推迟此通过"
+ "tasksDeferred_few": "{{count}} 任务推迟了本次传递",
+ "tasksDeferred_many": "{{count}} 任务推迟了本次传递",
+ "tasksDeferred_one": "{{count}} 任务推迟了本次传递",
+ "tasksDeferred_other": "{{count}} 任务推迟了本次传递"
},
"clarification": {
- "awaitingLead": "等待组长作出澄清",
- "awaitingUser": "等待你们澄清"
+ "awaitingLead": "等待团队负责人的澄清",
+ "awaitingUser": "等待您的澄清"
},
"description": {
- "add": "点击可添加描述...",
+ "add": "点击添加描述…",
"edit": "编辑描述",
- "placeholder": "任务描述( 支持减少标记)"
+ "placeholder": "任务描述(支持 markdown)"
},
"loading": {
"fetchingTeamData": "获取团队数据",
- "title": "正在装入任务..."
+ "title": "正在加载任务…"
},
"logs": {
- "newArriving": "到达新任务日志"
+ "newArriving": "新任务日志到达"
},
"notFound": "未找到任务",
"related": {
- "blockedBy": "被封锁",
- "blocks": "块",
- "linkedFrom": "链接于",
+ "blockedBy": "被阻止",
+ "blocks": "积木",
+ "linkedFrom": "链接自",
"links": "链接",
"title": "相关任务"
},
"review": {
- "reviewer": "审查者:{{reviewer}}"
+ "reviewer": "审稿人:{{reviewer}}"
},
"sections": {
- "attachments": "附录",
- "changes": "变动",
+ "attachments": "附件",
+ "changes": "变化",
"comments": "评论",
- "description": "说明",
+ "description": "描述",
"taskLogs": "任务日志",
- "workflowHistory": "工作流程历史"
+ "workflowHistory": "工作流程历史记录"
},
- "unassigned": "未指定",
+ "unassigned": "未分配",
"workflow": {
- "implementationTimeTitle": "持续工作间隔的执行时间",
- "inProgressTime": "进展时间 {{duration}}"
+ "implementationTimeTitle": "持续工作间隔的实施时间",
+ "inProgressTime": "进行中时间 {{duration}}"
},
"comments": {
- "renderLimit": "显示最新的 {{formattedCount}} 评论,以保持UI响应.",
+ "renderLimit": "显示最新的 {{formattedCount}} 评论以保持 UI 响应。",
"badges": {
- "approved": "核定数",
- "reviewRequested": "要求的审查"
+ "approved": "已批准",
+ "reviewRequested": "已请求审核"
},
"unknownTime": "未知时间",
"actions": {
- "reply": "答复",
- "replyToComment": "对评论的答复",
- "showMore": "显示更多评论意见( {{visible}}/ {{total}})",
+ "reply": "回复",
+ "replyToComment": "回复评论",
+ "showMore": "显示更多评论 ({{visible}}/{{total}})",
"cancelReply": "取消回复",
- "comment": "注释"
+ "comment": "评论"
},
"attachments": {
"previewAlt": "附件预览",
"downloadFailed": "下载失败"
},
- "replyingTo": "答复",
+ "replyingTo": "正在回复",
"input": {
- "placeholder": "添加注释... (输入要发送)",
- "charsLeft": "{{count}}左边的字符",
- "charsLeft_one": "{{count}}字符左边",
- "charsLeft_other": "{{count}}左边的字符",
- "charsLeft_few": "{{count}}左边的字符",
- "charsLeft_many": "{{count}}左边的字符"
+ "placeholder": "添加评论…(输入发送)",
+ "charsLeft": "{{count}} 剩余字符数",
+ "charsLeft_one": "{{count}} 左字符",
+ "charsLeft_other": "{{count}} 剩余字符数",
+ "charsLeft_few": "{{count}} 剩余字符数",
+ "charsLeft_many": "{{count}} 剩余字符数"
}
},
"workflowTimeline": {
- "empty": "没有记录工作流程历史",
- "currentImplementationInterval": "当前执行间隔",
- "implementationIntervalEnded": "执行间隔在此过渡时结束",
- "runningPrefix": "运行",
- "createdAs": "创建于",
- "by": "由",
- "reassigned": "调任",
- "assignedTo": "被指派",
- "unassignedFrom": "未指派自",
- "ownerChanged": "所有者已更改",
- "reviewRequested": "要求的审查",
- "reviewStarted": "开始审查",
- "changesRequested": "要求的更改",
- "approved": "核定数",
+ "empty": "没有记录工作流程历史记录",
+ "currentImplementationInterval": "当前实施间隔",
+ "implementationIntervalEnded": "实施间隔在此过渡时结束",
+ "runningPrefix": "正在运行",
+ "createdAs": "创建为",
+ "by": "经过",
+ "reassigned": "重新分配",
+ "assignedTo": "分配给",
+ "unassignedFrom": "未分配自",
+ "ownerChanged": "所有者已变更",
+ "reviewRequested": "已请求审核",
+ "reviewStarted": "审核开始",
+ "changesRequested": "要求更改",
+ "approved": "已批准",
"unknownEvent": "未知事件"
},
"reviewStates": {
- "approved": "核定数",
- "needsFix": "需要修正",
- "inReview": "正在审查"
+ "approved": "已批准",
+ "needsFix": "需要修复",
+ "inReview": "审核中"
}
},
"tasks": {
"createTask": {
- "assignee": "受指派者",
- "assigneeOptional": "指定对象( 可选)",
- "blockedByOptional": "被任务阻挡( 可选)",
- "blockedBySummary": "任务将被 {{tasks}} 屏蔽",
+ "assignee": "受让人",
+ "assigneeOptional": "受让人(可选)",
+ "blockedByOptional": "被任务阻止(可选)",
+ "blockedBySummary": "任务将被阻止:{{tasks}}",
"cancel": "取消",
"create": "创建",
- "creating": "正在创建...",
- "description": "任务将在团队的任务/目录中创建,并出现在坎班板上.",
- "descriptionOptional": "描述( 可选)",
- "detailsPlaceholder": "任务细节( 支持降级)",
+ "creating": "正在创建…",
+ "description": "该任务将在团队的 tasks/目录中创建并显示在看板上。",
+ "descriptionOptional": "说明(可选)",
+ "detailsPlaceholder": "任务详情(支持 markdown)",
"hideOptionalFields": "隐藏可选字段",
"offlineNotice": {
- "after": "- 启动执行小组",
- "before": "队伍离线了 任务将添加到"
+ "after": "- 启动团队开始执行。",
+ "before": "团队离线。该任务将被添加到"
},
- "promptOptional": "对受让人的提示(可选)",
- "promptPlaceholder": "团队成员自定义指令...",
- "relatedOptional": "相关任务(可选)",
- "relatedSummary": "相关:{{tasks}}",
+ "promptOptional": "提示词受让人(可选)",
+ "promptPlaceholder": "为团队成员定制说明…",
+ "relatedOptional": "相关任务(可选)",
+ "relatedSummary": "相关:{{tasks}}",
"saved": "已保存",
- "searchTasks": "搜索任务...",
+ "searchTasks": "搜索任务…",
"selectMember": "选择成员",
- "selectMemberOptional": "选择成员...",
+ "selectMemberOptional": "选择成员…",
"showOptionalFields": "显示可选字段",
- "startImmediately": "马上开始",
- "startOfflineHint": "队伍离线了 启动小组 立即启动任务",
- "subject": "议题",
- "subjectPlaceholder": "需要做些什么?",
+ "startImmediately": "立即开始",
+ "startOfflineHint": "团队离线。首先启动团队以立即开始任务。",
+ "subject": "主题",
+ "subjectPlaceholder": "需要做什么?",
"title": "创建任务",
- "todo": "TODO 苏维埃社会主义共和国"
+ "todo": "待办事项"
},
"list": {
"columns": {
- "blockedBy": "被封锁",
- "blocks": "块",
- "id": "身份证",
- "owner": "拥有者",
- "status": "状态",
- "subject": "议题"
+ "blockedBy": "被阻止",
+ "blocks": "积木",
+ "id": "ID",
+ "owner": "所有者",
+ "status": "地位",
+ "subject": "主题"
},
- "empty": "本团队没有任务",
+ "empty": "该团队没有任务",
"filters": {
- "allOwners": "所有拥有者",
+ "allOwners": "所有业主",
"allStatuses": "所有状态",
"ownerAria": "按所有者过滤任务",
"statusAria": "按状态过滤任务"
},
- "showing": "显示{{shown}}页:1{{total}}"
+ "showing": "显示 {{shown}} 或 {{total}}"
},
"status": {
- "completed": "已完成",
- "deleted": "删除",
- "inProgress": "进度( P)",
- "pending": "待处理"
+ "completed": "完全的",
+ "deleted": "已删除",
+ "inProgress": "进行中",
+ "pending": "待办的"
},
"statusSummary": {
- "progressAria": "任务完成 {{completed}}/{{total}}",
- "inProgress": "{{count}}进度( P)",
- "inProgress_one": "{{count}}进度( P)",
- "inProgress_other": "{{count}}进度( P)",
- "inProgress_few": "{{count}}进度( P)",
- "inProgress_many": "{{count}}进度( P)",
- "pending": "{{count}}待处理",
- "pending_one": "{{count}}待处理",
- "pending_other": "{{count}}待处理",
- "pending_few": "{{count}}待处理",
- "pending_many": "{{count}}待处理",
- "completed": "{{count}}已完成",
- "completed_one": "{{count}}已完成",
- "completed_other": "{{count}}已完成",
- "completed_few": "{{count}}已完成",
- "completed_many": "{{count}}已完成"
+ "progressAria": "任务 {{completed}}/{{total}} 已完成",
+ "inProgress": "{{count}} 进行中",
+ "inProgress_one": "{{count}} 进行中",
+ "inProgress_other": "{{count}} 进行中",
+ "inProgress_few": "{{count}} 进行中",
+ "inProgress_many": "{{count}} 进行中",
+ "pending": "{{count}} 待定",
+ "pending_one": "{{count}} 待定",
+ "pending_other": "{{count}} 待定",
+ "pending_few": "{{count}} 待定",
+ "pending_many": "{{count}} 待定",
+ "completed": "{{count}} 已完成",
+ "completed_one": "{{count}} 已完成",
+ "completed_other": "{{count}} 已完成",
+ "completed_few": "{{count}} 已完成",
+ "completed_many": "{{count}} 已完成"
},
- "unassigned": "未指定",
- "teamPrefix": "团队:",
- "openTask": "打开任务",
+ "unassigned": "未分配",
+ "teamPrefix": "团队:",
+ "openTask": "开放任务",
"deleteConfirm": {
"title": "删除任务",
- "message": "将任务 #{{taskId}} 移动到垃圾堆中吗?",
+ "message": "将任务 #{{taskId}} 移至垃圾箱?",
"confirmLabel": "删除",
"cancelLabel": "取消"
}
@@ -1102,130 +1102,130 @@
"actions": {
"cancel": "取消",
"closeEditor": "关闭编辑器",
- "closeTab": "关闭标签",
- "closeTooltip": "关闭编辑器( Esc)",
+ "closeTab": "关闭选项卡",
+ "closeTooltip": "关闭编辑器 (Esc)",
"discard": "丢弃",
- "discardAndClose": "丢弃关闭( C)",
- "keep": "保留",
- "keepMine": "留着我的",
+ "discardAndClose": "丢弃并关闭",
+ "keep": "保持",
+ "keepMine": "保留我的",
"keyboardShortcuts": "键盘快捷键",
"overwrite": "覆盖",
- "refreshAria": "刷新( F5)",
+ "refreshAria": "刷新 (F5)",
"refreshTooltip": "刷新 git 状态 (F5)",
- "reload": "重新装入",
+ "reload": "重新加载",
"retry": "重试",
"save": "保存",
- "saveAllAndClose": "保存全部关闭( C)"
+ "saveAllAndClose": "全部保存并关闭"
},
- "ariaLabel": "工程编辑器",
+ "ariaLabel": "项目编辑",
"dialogs": {
- "conflictDescription": "文件在打开后已被外部修改 。 用您的更改覆盖吗?",
- "conflictTitle": "保存冲突",
- "unsavedDescription": "你有未保存的变化。 你想怎么办?",
- "unsavedFileDescription": "此文件有未保存的更改 。 你想怎么办?",
+ "conflictDescription": "自您打开该文件以来,该文件已被外部修改。用您的更改覆盖?",
+ "conflictTitle": "拯救冲突",
+ "unsavedDescription": "您有未保存的更改。你想做什么?",
+ "unsavedFileDescription": "该文件有未保存的更改。你想做什么?",
"unsavedTitle": "未保存的更改"
},
"newFile": {
"validation": {
"nameRequired": "名称不能为空",
- "invalidName": "无效的名称",
+ "invalidName": "名称无效",
"invalidCharacters": "名称包含无效字符",
- "nameTooLong": "名字太长了"
+ "nameTooLong": "名字太长"
},
"placeholders": {
- "fileName": "文件名...",
- "folderName": "文件夹名称..."
+ "fileName": "文件名…",
+ "folderName": "文件夹名称…"
},
"aria": {
- "newFileName": "新建文件名称",
+ "newFileName": "新文件名",
"newFolderName": "新文件夹名称"
}
},
- "draftRecovered": "从上一个会话恢复未保存的更改 。",
+ "draftRecovered": "恢复了上一个会话中未保存的更改。",
"externalChange": {
- "changed": "文件在磁盘上更改 。",
- "deleted": "文件在磁盘上已不存在 。"
+ "changed": "磁盘上的文件已更改。",
+ "deleted": "文件不再存在于磁盘上。"
},
- "saveFailed": "保存失败: {{error}}",
+ "saveFailed": "保存失败:{{error}}",
"sidebar": {
- "explorer": "浏览器",
+ "explorer": "探险家",
"hide": "隐藏侧边栏",
- "hideWithShortcut": "隐藏侧边栏( {{shortcut}})",
+ "hideWithShortcut": "隐藏侧边栏({{shortcut}})",
"show": "显示侧边栏",
- "showWithShortcut": "显示侧边栏( {{shortcut}})"
+ "showWithShortcut": "显示侧边栏 ({{shortcut}})"
},
"searchInFiles": {
"title": "在文件中搜索",
"closeSearch": "关闭搜索",
- "closeSearchShortcut": "关闭搜索( Esc)",
- "searchPlaceholder": "搜索...",
- "matchCase": "匹配大小写",
- "matchCaseToggle": "页:1",
+ "closeSearchShortcut": "关闭搜索 (Esc)",
+ "searchPlaceholder": "搜索…",
+ "matchCase": "火柴盒",
+ "matchCaseToggle": "氨基酸",
"noResults": "未找到结果",
- "resultsSummary": "{{count}}匹配在{{fileCount}}文件",
- "resultsSummary_one": "{{count}}匹配在{{fileCount}}文件",
- "truncated": "(结)",
- "resultsSummary_few": "{{count}}匹配在{{fileCount}}文件",
- "resultsSummary_many": "{{count}}匹配在{{fileCount}}文件",
- "resultsSummary_other": "{{count}}匹配在{{fileCount}}文件"
+ "resultsSummary": "{{count}} 在 {{fileCount}} 文件中匹配",
+ "resultsSummary_one": "在 {{fileCount}} 个文件中找到 {{count}} 个匹配项",
+ "truncated": "(截断)",
+ "resultsSummary_few": "{{count}} 在 {{fileCount}} 文件中匹配",
+ "resultsSummary_many": "{{count}} 在 {{fileCount}} 文件中匹配",
+ "resultsSummary_other": "{{count}} 在 {{fileCount}} 文件中匹配"
},
"fileTree": {
- "failedToLoadFiles": "装入文件失败: {{error}}",
- "loading": "正在装入文件...",
+ "failedToLoadFiles": "加载文件失败:{{error}}",
+ "loading": "正在加载文件…",
"empty": "未找到文件",
- "dropForProjectRoot": "丢到这里进行工程根",
- "moveToTrash": "移动到回收站",
- "moveToTrashConfirm": "将\"{{name}}\"移动到垃圾堆?.",
+ "dropForProjectRoot": "放到此处获取项目根目录",
+ "moveToTrash": "移至垃圾箱",
+ "moveToTrashConfirm": "将“{{name}}”移至垃圾箱?",
"cancel": "取消"
},
"goToLine": {
- "title": "跳转到行",
- "position": "(目前:{{current}},合计:{{total}})",
- "placeholder": "行号,+抵消, -抵消,或%",
- "go": "走开"
+ "title": "前往行",
+ "position": "(当前:{{current}},总计:{{total}})",
+ "placeholder": "行号、+偏移量、-偏移量或%",
+ "go": "去"
},
"searchPanel": {
- "previousMatch": "上一个匹配",
- "nextMatch": "下一个匹配",
+ "previousMatch": "上一场比赛",
+ "nextMatch": "下一场比赛",
"close": "关闭",
- "replacePlaceholder": "替换",
- "replace": "替换",
+ "replacePlaceholder": "代替",
+ "replace": "代替",
"replaceNext": "替换下一个",
- "all": "全体",
+ "all": "全部",
"replaceAll": "全部替换"
},
"statusBar": {
- "position": "无{{line}}页:1{{col}}",
- "enableWatcher": "启用文件监视器",
- "disableWatcher": "禁用文件监视器",
- "watch": "监视",
+ "position": "列 {{line}},列 {{col}}",
+ "enableWatcher": "启用文件观察器",
+ "disableWatcher": "禁用文件观察器",
+ "watch": "手表",
"watching": "观看",
- "watchExternalChanges": "注意外部变化",
- "disableExternalWatcher": "禁用外部更改监视器",
- "encodingUtf8": "ZXCVKEN0ZXCV-8 维基百科中的相关条目: 维基文库中相关的原始文献: 维基文库中相关的原始文献: 维基文库中相关的原始文献: 维基语录",
- "spaces": "空间: {{count}}"
+ "watchExternalChanges": "留意外部变化",
+ "disableExternalWatcher": "禁用外部更改观察程序",
+ "encodingUtf8": "UTF-8",
+ "spaces": "空间:{{count}}"
},
"imagePreview": {
- "loading": "正在装入预览...",
+ "loading": "正在加载预览…",
"openFullSize": "打开全尺寸预览",
"openSystemViewer": "在系统查看器中打开"
},
"quickOpen": {
"title": "快速打开",
- "searchPlaceholder": "按名称搜索文件...",
- "loading": "正在装入文件...",
+ "searchPlaceholder": "按名称搜索文件…",
+ "loading": "正在加载文件…",
"empty": "未找到文件"
},
"errorBoundary": {
- "crashed": "编辑器崩溃",
+ "crashed": "编辑器崩溃了",
"unknownError": "未知错误"
},
"binaryPlaceholder": {
- "file": "二进制文件( {{size}})"
+ "file": "二进制文件({{size}})"
},
"unsavedChanges": "未保存的更改",
"empty": {
- "selectFile": "从树上选择要编辑的文件"
+ "selectFile": "从树中选择要编辑的文件"
},
"search": {
"toggleReplace": "切换替换",
@@ -1238,298 +1238,298 @@
"search": "搜索",
"navigation": "导航",
"editing": "编辑",
- "markdown": "标记",
+ "markdown": "Markdown",
"general": "常规"
},
"actions": {
"quickOpen": "快速打开",
"save": "保存",
"saveAll": "全部保存",
- "closeTab": "关闭标签",
+ "closeTab": "关闭选项卡",
"findInFile": "在文件中查找",
"searchInFiles": "在文件中搜索",
- "goToLine": "跳转到行",
- "nextTab": "下一个标签",
- "previousTab": "上一个标签",
- "cycleTabs": "循环标签",
+ "goToLine": "前往行",
+ "nextTab": "下一个选项卡",
+ "previousTab": "上一个选项卡",
+ "cycleTabs": "循环选项卡",
"toggleSidebar": "切换侧边栏",
- "undo": "撤销",
- "redo": "重装",
- "selectNextMatch": "选择下一个匹配",
- "toggleComment": "切换注释",
- "splitPreview": "拆分预览",
+ "undo": "撤消",
+ "redo": "重做",
+ "selectNextMatch": "选择下一场比赛",
+ "toggleComment": "切换评论",
+ "splitPreview": "分割预览",
"fullPreview": "完整预览",
"closeEditor": "关闭编辑器"
}
},
"toolbar": {
- "enableWordWrap": "启用文字包",
- "disableWordWrap": "禁用词包",
+ "enableWordWrap": "启用自动换行",
+ "disableWordWrap": "禁用自动换行",
"closeSplitPreview": "关闭分割预览",
"closePreview": "关闭预览"
}
},
"launch": {
"actions": {
- "createSchedule": "创建计划",
- "creating": "正在创建...",
- "goToDashboard": "转到 Dashboard",
- "launchTeam": "发射队",
- "launching": "正在发射...",
- "relaunchTeam": "发射队",
- "relaunching": "正在重新启动...",
+ "createSchedule": "创建计划任务",
+ "creating": "正在创建…",
+ "goToDashboard": "转到控制台",
+ "launchTeam": "启动团队",
+ "launching": "正在启动…",
+ "relaunchTeam": "重新启动团队",
+ "relaunching": "重新启动…",
"saveChanges": "保存更改",
- "saving": "正在保存..."
+ "saving": "保存…"
},
"billing": {
- "prefix": "从2026年6月15日开始,Anthropic法案",
- "readArticle": "读取 Athropic 文章",
- "suffix": "和代理SDK的用途 从代理SDK的月度信用,与交互式Claude代码限制分开. 信用转帐周期和未使用的信用不转帐。"
+ "prefix": "自 2026 年 6 月 15 日起,Anthropic 账单",
+ "readArticle": "阅读人类文章",
+ "suffix": "Agent SDK 使用量来自每月 Agent SDK 积分,独立于交互式 Claude Code 限制。积分会重置每个计费周期,未使用的积分不会结转。"
},
"conflict": {
- "description": "在同一个目录中运行两个团队是危险的——他们可能与编辑相同的文件发生冲突. 考虑使用不同的目录或git工作树进行隔离.",
- "title": "另一个团队\"{{team}}\"已经在运行这个工作目录",
- "workingDirectory": "工作目录:"
+ "description": "在同一目录中运行两个团队是有风险的 - 它们可能会在编辑相同文件时发生冲突。考虑使用不同的目录或 Git worktree进行隔离。",
+ "title": "另一个团队“{{team}}”已经在该工作目录中运行",
+ "workingDirectory": "工作目录:"
},
"description": {
- "createSchedule": "自动执行 Claude 任务",
- "createScheduleForTeam": "\"{{team}}\"团队的自动运行时间表",
- "editSchedule": "\"{{team}}\"编组时间表.",
- "launchPrefix": "启动团队",
- "launchSuffix": "通过当地克劳德·CLI.",
+ "createSchedule": "安排自动执行 Claude 任务",
+ "createScheduleForTeam": "为团队“{{team}}”安排自动运行",
+ "editSchedule": "编辑“{{team}}”团队的日程",
+ "launchPrefix": "开始组队",
+ "launchSuffix": "通过本地 Claude CLI。",
"relaunchPrefix": "停止当前运行",
- "relaunchSuffix": "并通过当地Claude CLI重新开始."
+ "relaunchSuffix": "并通过本地 Claude CLI 再次启动它。"
},
"prepare": {
"action": {
- "launch": "发射",
+ "launch": "启动",
"relaunch": "重新启动"
},
- "blocked": "运行时环境不可用 - {{action}} 被屏蔽",
- "checkingProviders": "正在检查选中的提供者...",
- "failed": "准备选中的提供者失败",
- "preflight": "在{{action}}之前进行飞行前检查以发现错误",
- "preparingEnvironment": "正在准备环境...",
- "ready": "所有选中的提供者都准备好了 。",
- "readyWithNotes": "所有选定的提供者都已准备好,并附有注释。",
- "unsupportedPreload": "当前预装版本不支持团队:预装. 重新启动dev应用程序.",
- "selectWorkingDirectory": "选择一个工作目录来验证发射环境.",
- "someProvidersNeedAttention": "一些选定的提供者需要注意。"
+ "blocked": "运行时环境不可用 - {{action}} 被阻止",
+ "checkingProviders": "检查选定的提供商…",
+ "failed": "无法准备选定的提供商",
+ "preflight": "飞行前检查以在 {{action}} 之前捕获错误",
+ "preparingEnvironment": "准备环境…",
+ "ready": "所有选定提供商均已就绪。",
+ "readyWithNotes": "所有选定的提供商均已准备就绪,并附有注释。",
+ "unsupportedPreload": "当前preload 版本不支持 team:prepareProvisioning。重新启动开发应用。",
+ "selectWorkingDirectory": "选择一个工作目录来验证启动环境。",
+ "someProvidersNeedAttention": "部分选定提供商需要注意。"
},
"prompt": {
- "label": "提示",
- "oneShotPrefix": "这个提示会传递给",
- "oneShotSuffix": "用于一弹即决",
+ "label": "提示词",
+ "oneShotPrefix": "该提示词将被传递到",
+ "oneShotSuffix": "用于一次性执行",
"saved": "已保存",
- "schedulePlaceholder": "命令克劳德按计划执行...",
- "teamLeadOptional": "启动小组领导( 可选)",
- "teamLeadPlaceholder": "关于团队领导的指示..."
+ "schedulePlaceholder": "指示 Claude 按计划执行……",
+ "teamLeadOptional": "提示团队负责人(可选)",
+ "teamLeadPlaceholder": "给团队领导的指示…"
},
- "providerChanged": "供应商从{{from}}改为{{to}}. 之前的牵头会话不会恢复,牵头会从新的上下文开始,这样新的运行时间就会被正确应用.",
- "relaunchFreshSession": "团队重启开始新的领头会议。 持久团队状态,任务板,和成员配置被补水进入发射即时.",
+ "providerChanged": "提供商从 {{from}} 更改为 {{to}}。之前的引导会话将不会恢复,并且引导将以新的上下文开始,以便正确应用新的运行时。",
+ "relaunchFreshSession": "团队重新启动将开始新的领导会话。持久的团队状态、任务看板和成员配置会重新融入到启动提示词中。",
"relaunchWarning": {
- "description": "保存这些设置将停止目前的团队进程,坚持更新的名册,并以新的运行时间再次启动团队.",
- "title": "重启将重新启动当前团队运行"
+ "description": "保存这些设置将停止当前的团队进程,保留更新的名单,并使用新的运行时再次启动团队。",
+ "title": "重新启动将重新开始当前的团队运行"
},
"schedule": {
- "labelOptional": "标签( 可选的)",
- "labelPlaceholder": "例如,每日代码审查,夜间测试...",
- "maxBudgetUsd": "最大预算",
- "maxTurns": "最大转弯",
+ "labelOptional": "标签(可选)",
+ "labelPlaceholder": "例如,每日代码审核、每晚测试……",
+ "maxBudgetUsd": "最高预算(美元)",
+ "maxTurns": "最大转数",
"noLimit": "无限制",
- "noMatches": "没有队伍符合你的搜索。",
- "noTeams": "没有小组可用。 先创建团队.",
- "searchTeams": "搜索组...",
- "selectTeam": "选择一个团队...",
+ "noMatches": "没有团队符合您的搜索。",
+ "noTeams": "没有可用的团队。首先创建一个团队。",
+ "searchTeams": "搜寻队伍…",
+ "selectTeam": "选择一个团队…",
"team": "团队",
- "title": "时间表"
+ "title": "日程"
},
"title": {
- "createSchedule": "创建计划",
- "editSchedule": "编辑计划",
- "launch": "发射队",
- "relaunch": "发射队"
+ "createSchedule": "创建计划任务",
+ "editSchedule": "编辑日程",
+ "launch": "发送团队",
+ "relaunch": "重新启动团队"
},
"errors": {
- "loadProjectsFailed": "装入工程失败",
- "saveScheduleFailed": "保存调度失败",
- "relaunchFailed": "重新启动团队失败",
- "launchFailed": "发射团队失败"
+ "loadProjectsFailed": "无法加载项目",
+ "saveScheduleFailed": "保存日程失败",
+ "relaunchFailed": "无法重新启动团队",
+ "launchFailed": "启动团队失败"
},
"validation": {
- "openCodeLeadModelRequired": "OpenCode领导需要一个选定的模型.",
- "openCodeTeammateRequired": "OpenCode领队至少需要一名OpenCode队友.",
- "selectWorkingDirectory": "选择工作目录( cwd)",
- "fixMemberNames": "在发射前确定成员姓名",
- "memberNamesUnique": "成员名字在发射前必须是独一无二的"
+ "openCodeLeadModelRequired": "OpenCode 引线需要选定的模型。",
+ "openCodeTeammateRequired": "OpenCode 领导者需要至少一名 OpenCode 队友。",
+ "selectWorkingDirectory": "选择工作目录(cwd)",
+ "fixMemberNames": "启动前修复成员名称",
+ "memberNamesUnique": "启动前成员名称必须是唯一的"
},
"optionalSettings": {
"relaunchTitle": "重新启动设置",
- "title": "可选发射设置",
- "relaunchDescription": "在重启团队之前审查名册和预赛时间.",
- "description": "将发射流集中在项目路径上,只有在需要额外控制时才能扩展."
+ "title": "可选启动设置",
+ "relaunchDescription": "在重新启动团队之前,请检查名单和领导运行时。",
+ "description": "将启动流程集中在项目路径上,并且仅在需要额外控制时才扩展此流程。"
}
},
"list": {
"actions": {
"copyTeam": "复制团队",
"createTeam": "创建团队",
- "deleteForever": "永远删除",
+ "deleteForever": "永久删除",
"deletePermanently": "永久删除",
"deleteTeam": "删除团队",
- "launching": "正在发射...",
- "launchTeam": "发射队",
- "relaunchTeam": "发射队",
+ "launching": "正在启动…",
+ "launchTeam": "启动团队",
+ "relaunchTeam": "重新启动团队",
"restore": "恢复",
- "restoreTeam": "恢复小组",
+ "restoreTeam": "恢复队伍",
"retry": "重试",
- "stopTeam": "停止小组",
- "stopping": "正在停止中..."
+ "stopTeam": "停队",
+ "stopping": "停止…"
},
"electronOnly": {
- "description": "在浏览器模式下,无法访问本地的`~/.claude/teams`目录.",
- "title": "团队只能使用电动模式"
+ "description": "在浏览器模式下,无法访问本地“~/.claude/teams”目录。",
+ "title": "Teams 仅在 Electron 模式下可用"
},
"empty": {
- "description": "在此创建团队开始 。 它会自动出现在列表中.",
- "localOnly": "团队创建仅以本地电机模式提供.",
- "title": "没有找到团队"
+ "description": "在此创建一个团队以开始。它会自动显示在列表中。",
+ "localOnly": "团队创建仅在本地 Electron 模式下可用。",
+ "title": "未找到队伍"
},
"filter": {
"clearAll": "全部清除",
"label": "过滤团队",
- "projectPriority": "项目优先事项",
- "status": "状态"
+ "projectPriority": "项目优先级",
+ "status": "地位"
},
- "loadFailed": "装入团队失败",
- "loading": "正在装入团队...",
- "localOnly": "仅以本地电态模式提供.",
- "membersCount": "成员:{{count}}",
- "membersCount_few": "成员:{{count}}",
- "membersCount_many": "成员:{{count}}",
- "membersCount_one": "成员:{{count}}",
- "membersCount_other": "成员:{{count}}",
- "noDescription": "无说明",
- "noMatches": "没有匹配当前过滤器的团队",
+ "loadFailed": "无法加载团队",
+ "loading": "正在加载团队…",
+ "localOnly": "仅在本地 Electron 模式下可用。",
+ "membersCount": "成员:{{count}}",
+ "membersCount_few": "成员:{{count}}",
+ "membersCount_many": "成员:{{count}}",
+ "membersCount_one": "成员: {{count}}",
+ "membersCount_other": "成员:{{count}}",
+ "noDescription": "无描述",
+ "noMatches": "没有符合当前筛选条件的团队",
"partial": {
- "pending": "上次发射还在调和",
- "skipped": "上次发射已经跳过了队友",
- "skippedWithCount": "最后一次发射跳过{{count}}/{{expected}}队友.",
- "skippedWithCount_few": "最后一次发射跳过{{count}}/{{expected}}队友.",
- "skippedWithCount_many": "最后一次发射跳过{{count}}/{{expected}}队友.",
- "skippedWithCount_one": "最后一次发射跳过{{count}}/{{expected}}队友.",
- "skippedWithCount_other": "最后一次发射跳过{{count}}/{{expected}}队友.",
- "stopped": "最后一次发射在所有队友加入之前就停止了.",
- "stoppedWithCount": "最后一次发射在{{count}}/{{expected}}队友加入之前停止.",
- "stoppedWithCount_few": "最后一次发射在{{count}}/{{expected}}队友加入之前停止.",
- "stoppedWithCount_many": "最后一次发射在{{count}}/{{expected}}队友加入之前停止.",
- "stoppedWithCount_one": "最后一次发射在{{count}}/{{expected}}队友加入之前停止.",
- "stoppedWithCount_other": "最后一次发射在{{count}}/{{expected}}队友加入之前停止."
+ "pending": "上次启动仍在协调中。",
+ "skipped": "上次启动跳过了队友。",
+ "skippedWithCount": "上次启动跳过了 {{count}}/{{expected}} 队友。",
+ "skippedWithCount_few": "上次启动跳过了 {{count}}/{{expected}} 队友。",
+ "skippedWithCount_many": "上次启动跳过了 {{count}}/{{expected}} 队友。",
+ "skippedWithCount_one": "上次启动跳过了 {{count}}/{{expected}} 队友。",
+ "skippedWithCount_other": "上次启动跳过了 {{count}}/{{expected}} 队友。",
+ "stopped": "上次启动在所有队友加入之前停止了。",
+ "stoppedWithCount": "上次启动在 {{count}}/{{expected}} 队友加入之前停止。",
+ "stoppedWithCount_few": "上次启动在 {{count}}/{{expected}} 队友加入之前停止。",
+ "stoppedWithCount_many": "上次启动在 {{count}}/{{expected}} 队友加入之前停止。",
+ "stoppedWithCount_one": "上次启动在 {{count}}/{{expected}} 队友加入之前停止。",
+ "stoppedWithCount_other": "上次启动在 {{count}}/{{expected}} 队友加入之前停止。"
},
- "searchPlaceholder": "搜索组...",
+ "searchPlaceholder": "搜寻队伍…",
"sections": {
- "otherTeams": "其他小组",
- "projectTeams": "{{project}}的小组",
- "selectedProject": "选定项目"
+ "otherTeams": "其他队伍",
+ "projectTeams": "{{project}} 的团队",
+ "selectedProject": "选定的项目"
},
"solo": "独奏",
"status": {
- "active": "活动",
- "deleted": "删除",
- "launching": "正在发射...",
+ "active": "活跃",
+ "deleted": "已删除",
+ "launching": "正在启动…",
"offline": "离线",
- "partialFailure": "发射失败的路段",
- "partialPending": "拖带待处理",
+ "partialFailure": "启动中途失败",
+ "partialPending": "引导挂起",
"partialSkipped": "启动跳过的成员",
- "running": "运行"
+ "running": "运行中"
},
"title": "选择团队",
- "trash": "废物({{count}})",
- "trash_few": "废物({{count}})",
- "trash_many": "废物({{count}})",
- "trash_one": "废物({{count}})",
- "trash_other": "废物({{count}})",
+ "trash": "垃圾 ({{count}})",
+ "trash_few": "垃圾 ({{count}})",
+ "trash_many": "垃圾 ({{count}})",
+ "trash_one": "垃圾 ({{count}})",
+ "trash_other": "垃圾 ({{count}})",
"deleteDraft": {
"title": "删除草稿",
- "message": "删除草稿组\"{{teamName}}\"? 这一点是无法消除的。",
+ "message": "删除选秀队“{{teamName}}”?此操作无法撤消。",
"confirmLabel": "删除",
"cancelLabel": "取消"
},
"moveToTrash": {
- "title": "移动到垃圾桶",
- "message": "将团队\"{{teamName}}\"移到垃圾堆?. 你可以待会再修",
- "confirmLabel": "移动到垃圾桶",
+ "title": "移至垃圾箱",
+ "message": "将团队“{{teamName}}”移至垃圾箱?您可以稍后恢复它。",
+ "confirmLabel": "移至垃圾箱",
"cancelLabel": "取消"
},
"deleteForever": {
"title": "永久删除",
- "message": "永久删除“{{teamName}}”小组? 所有数据都将丢失。",
- "confirmLabel": "永远删除",
+ "message": "永久删除团队“{{teamName}}”?所有数据都将丢失。",
+ "confirmLabel": "永久删除",
"cancelLabel": "取消"
}
},
"messageComposer": {
"crossTeam": {
- "hint": "提示:跨团队消息到目标团队领先. 如果你想让答案回到你的团队 而不是你,在信息中明确说。"
+ "hint": "提示词:跨团队消息会发送至目标团队负责人。如果您希望回复返回给您的团队领导而不是您,请在消息中明确说明。"
},
"attachments": {
- "attachFiles": "附加文件( 粘贴或拖放) (D)",
- "unavailable": "无附件",
- "disabledHint": "为在线团队领队和在线OpenCode队友提供文件附件支持. 删除附件或切换收件人。",
+ "attachFiles": "附加文件(粘贴或拖放)",
+ "unavailable": "附件不可用",
+ "disabledHint": "在线团队领导和在线 OpenCode 队友支持文件附件。删除附件或切换收件人。",
"restrictions": {
- "crossTeam": "跨团队信件不支持文件附件",
- "teamOffline": "团队必须在线附加文件",
+ "crossTeam": "跨团队消息不支持文件附件",
+ "teamOffline": "团队必须在线才能附加文件",
"unsupportedRecipient": "文件可以发送给团队领导或 OpenCode 队友",
- "openCodeOffline": "团队必须在线为 OpenCode 队友附加文件",
- "sending": "等待当前信件在添加文件前完成发送",
- "maximumReached": "达到的最大附件",
- "leadOnly": "文件只能发送给团队领导"
+ "openCodeOffline": "团队必须在线才能为 OpenCode 队友附加文件",
+ "sending": "等待当前消息发送完成后再添加文件",
+ "maximumReached": "已达到最大附件数",
+ "leadOnly": "文件只能发送给团队负责人"
}
},
"slash": {
"restrictions": {
- "attachments": "斜线命令需要现场团队领导, 不能随附件一起发送",
- "crossTeam": "斜线命令只能运行在目前的团队领导上",
- "notLead": "斜线命令只能发送给团队领导",
- "leadOffline": "斜线命令要求团队线索在线"
+ "attachments": "斜线命令需要现场团队领导,并且不能通过附件发送",
+ "crossTeam": "斜线命令只能在当前团队领导上运行",
+ "notLead": "斜线命令只能发送给队长",
+ "leadOffline": "斜线命令需要团队领导在线"
}
},
"status": {
- "reusedCrossTeamRequest": "重新使用最近的跨小组请求",
+ "reusedCrossTeamRequest": "重用最近的跨团队请求",
"teamOffline": "离线"
},
"revision": {
- "editing": "正在编辑上一条消息",
+ "editing": "编辑上一条消息",
"cancel": "取消",
- "tooltip": "让代理忽略上一条消息,并将其文本恢复到编辑器。"
+ "tooltip": "要求代理忽略之前的消息并将其恢复给作曲家。"
},
"input": {
- "charsLeft": "{{count}}左边的字符",
- "charsLeft_one": "{{count}}字符左边",
- "charsLeft_other": "{{count}}左边的字符",
- "teamLaunchingPlaceholder": "团队正在发布... 消息将被排队 以发送收件箱。",
- "crossTeamPlaceholder": "跨团队消息给 {{team}}...",
+ "charsLeft": "{{count}} 剩余字符数",
+ "charsLeft_one": "{{count}} 左字符",
+ "charsLeft_other": "{{count}} 剩余字符数",
+ "teamLaunchingPlaceholder": "团队正在启动…消息将排队等待收件箱传递。",
+ "crossTeamPlaceholder": "跨团队消息至 {{team}}…",
"teamFallback": "团队",
- "placeholder": "写入信件... (输入发送, Shift+ Enter for new line)",
- "slashTip": "提示:您可以使用\"/\"来运行任何克劳德命令.",
- "charsLeft_few": "{{count}}左边的字符",
- "charsLeft_many": "{{count}}左边的字符"
+ "placeholder": "写一条消息…(Enter 发送,Shift+Enter 换行)",
+ "slashTip": "提示词:您可以使用“/”来运行任何 Claude 命令。",
+ "charsLeft_few": "{{count}} 剩余字符数",
+ "charsLeft_many": "{{count}} 剩余字符数"
},
"teamSelector": {
- "thisTeam": "这个团队",
- "current": "当前",
- "online": "在线",
+ "thisTeam": "这支团队",
+ "current": "当前的",
+ "online": "在线的",
"offline": "离线",
- "onlineTitle": "在线",
+ "onlineTitle": "在线的",
"offlineTitle": "离线"
},
"recipient": {
- "select": "选择...",
- "searchPlaceholder": "搜索...",
- "noResults": "无结果"
+ "select": "选择…",
+ "searchPlaceholder": "搜索…",
+ "noResults": "没有结果"
},
"actions": {
- "voiceToText": "语音对文本",
+ "voiceToText": "语音转文字",
"send": "发送",
"sendingUnavailableLaunching": "团队启动时无法发送"
}
@@ -1539,638 +1539,638 @@
"ariaLabel": "过滤日志",
"tooltip": "过滤日志",
"sections": {
- "stream": "流线",
+ "stream": "溪流",
"content": "内容"
},
"kinds": {
- "output": "产出",
- "thinking": "思维",
+ "output": "输出",
+ "thinking": "思考",
"tool": "工具调用"
},
"actions": {
- "reset": "重设",
+ "reset": "重置",
"save": "保存"
},
"streams": {
- "stdout": "静态",
- "stderr": "标准"
+ "stdout": "标准输出",
+ "stderr": "标准错误"
}
},
- "rawLineCount": "{{formattedCount}}原始线条",
- "rawLineCount_one": "{{formattedCount}}原始线条",
- "rawLinesCaptured": "{{count}}被俘获",
- "emptyRawLogs": "{{count}};还没有一个是助理/工具输出.",
+ "rawLineCount": "{{formattedCount}} 原线",
+ "rawLineCount_one": "{{formattedCount}} 原线",
+ "rawLinesCaptured": "{{count}} 捕获",
+ "emptyRawLogs": "{{count}};还没有一个是助手/工具输出。",
"noLogsYet": "还没有日志。",
- "teamNotRunning": "球队没有运行。",
- "searchPlaceholder": "搜索日志...",
+ "teamNotRunning": "团队没有运行。",
+ "searchPlaceholder": "搜索日志…",
"clearSearch": "清除搜索",
- "newCount": "+ 键{{count}}新设",
- "loading": "正在装入...",
+ "newCount": "+{{count}} 新",
+ "loading": "加载中…",
"showMore": "显示更多",
- "noLogsCaptured": "没有记录",
- "noMatchingLogs": "无匹配日志 。",
- "rawLineCount_few": "{{formattedCount}}原始线条",
- "rawLineCount_many": "{{formattedCount}}原始线条",
- "rawLineCount_other": "{{formattedCount}}原始线条",
+ "noLogsCaptured": "没有捕获日志。",
+ "noMatchingLogs": "没有匹配的日志。",
+ "rawLineCount_few": "{{formattedCount}} 原线",
+ "rawLineCount_many": "{{formattedCount}} 原线",
+ "rawLineCount_other": "{{formattedCount}} 原线",
"openFullscreen": "打开全屏日志",
- "fullscreen": "全屏幕",
+ "fullscreen": "全屏",
"viewingFullscreen": "以全屏模式查看",
"logsTitle": "日志",
"sourceSelect": {
- "placeholder": "选择日志源...",
- "searchPlaceholder": "搜索日志来源...",
- "emptyMessage": "未找到日志来源 。",
+ "placeholder": "选择日志源…",
+ "searchPlaceholder": "搜索日志源…",
+ "emptyMessage": "未找到日志源。",
"ariaLabel": "日志来源",
- "leadLabel": "铅",
- "selectSourceEmpty": "选择日志源 。",
+ "leadLabel": "带领",
+ "selectSourceEmpty": "选择日志源。",
"leadDescription": "团队负责人",
- "removedLabel": "已移除",
- "removedDescription": "已移除"
+ "removedLabel": "已删除",
+ "removedDescription": "已删除"
}
},
"agentGraph": {
"popover": {
- "externalTeam": "外部小组",
+ "externalTeam": "外部团队",
"process": {
- "startedBy": "开始于:",
- "at": "时间:",
+ "startedBy": "开始于:",
+ "at": "在:",
"openUrl": "打开 URL"
},
"overflow": {
"hiddenTasks": "隐藏任务",
- "empty": "没有可用的隐藏任务 。"
+ "empty": "没有可用的隐藏任务。"
},
"member": {
- "lead": "铅",
- "workingOn": "工作情况",
- "recentTools": "最近的工具",
+ "lead": "带领",
+ "workingOn": "正在处理",
+ "recentTools": "最近使用的工具",
"spawn": {
"waitingToStart": "等待开始",
"starting": "开始",
"failed": "失败"
},
"state": {
- "active": "活动",
- "idle": "闲置",
+ "active": "活跃",
+ "idle": "闲置的",
"offline": "离线",
"runningTool": "运行工具"
},
"activeTool": {
- "running": "运行中的工具",
+ "running": "运行工具",
"failed": "工具失败",
- "finished": "工具已完成"
+ "finished": "工具完成"
},
"actions": {
- "message": "消息",
- "profile": "简介",
+ "message": "信息",
+ "profile": "轮廓",
"task": "任务"
}
}
},
"logPreview": {
"logs": "日志",
- "loading": "正在装入日志",
- "more": "+ 键{{count}}更多",
- "more_one": "+ 键{{count}}更多",
- "more_other": "+ 键{{count}}更多",
- "more_few": "+ 键{{count}}更多",
- "more_many": "+ 键{{count}}更多",
- "unsupportedProvider": "不支持的提供商",
+ "loading": "加载日志",
+ "more": "+{{count}} 更多",
+ "more_one": "+{{count}} 更多",
+ "more_other": "+{{count}} 更多",
+ "more_few": "+{{count}} 更多",
+ "more_many": "+{{count}} 更多",
+ "unsupportedProvider": "不受支持的提供商",
"openCodeLogsDelayed": "OpenCode 日志延迟",
"logsUnavailable": "日志不可用",
- "noRecentLogs": "没有最近日志",
+ "noRecentLogs": "没有最近的日志",
"toolError": "工具错误",
"toolResult": "工具结果",
"toolUse": "工具使用",
- "thinking": "思考中",
+ "thinking": "思考",
"error": "错误",
- "logEvent": "日志事件",
- "noErrorOutput": "没有错误输出",
- "noOutput": "没有输出",
- "noInput": "没有输入"
+ "logEvent": "记录事件",
+ "noErrorOutput": "无错误输出",
+ "noOutput": "无输出",
+ "noInput": "无输入"
},
"blockingEdge": {
- "title": "封锁依赖性",
+ "title": "阻塞依赖",
"blocks": "块",
"close": "关闭",
- "blockingHiddenTasks": "屏蔽隐藏任务",
- "blockedHiddenTasks": "已屏蔽隐藏任务",
- "links_one": "{{count}} 个链接",
- "links_other": "{{count}} 个链接",
- "hiddenBlockingLinks_one": "{{count}} 个隐藏阻塞链接",
- "hiddenBlockingLinks_other": "{{count}} 个隐藏阻塞链接",
- "hiddenTaskStack": "隐藏任务堆栈",
- "hiddenTasks_one": "{{count}} 个隐藏任务",
- "hiddenTasks_other": "{{count}} 个隐藏任务",
+ "blockingHiddenTasks": "阻止隐藏任务",
+ "blockedHiddenTasks": "被阻止的隐藏任务",
+ "links_one": "{{count}} 链接",
+ "links_other": "{{count}} 链接",
+ "hiddenBlockingLinks_one": "{{count}} 隐藏阻止链接",
+ "hiddenBlockingLinks_other": "{{count}} 隐藏的阻止链接",
+ "hiddenTaskStack": "隐藏任务栈",
+ "hiddenTasks_one": "{{count}} 隐藏任务",
+ "hiddenTasks_other": "{{count}} 隐藏任务",
"task": "任务",
- "openBlockerStack": "打开阻塞方堆栈",
- "openBlockedStack": "打开被阻塞方堆栈",
- "openBlockerTask": "打开阻塞任务",
- "openBlockedTask": "打开被阻塞任务"
+ "openBlockerStack": "打开拦截器堆栈",
+ "openBlockedStack": "打开阻塞的堆栈",
+ "openBlockerTask": "打开拦截器任务",
+ "openBlockedTask": "打开被阻止的任务"
},
"activityHud": {
"activity": "活动",
"noRecentActivity": "最近没有活动",
- "more": "+ 键{{count}}更多",
- "more_one": "+ 键{{count}}更多",
- "more_other": "+ 键{{count}}更多",
- "more_few": "+ 键{{count}}更多",
- "more_many": "+ 键{{count}}更多"
+ "more": "+{{count}} 更多",
+ "more_one": "+{{count}} 更多",
+ "more_other": "+{{count}} 更多",
+ "more_few": "+{{count}} 更多",
+ "more_many": "+{{count}} 更多"
},
"provisioning": {
- "launchDetails": "发射详情",
- "launchDetailsDescription": "详细团队启动进度,直播输出和CLI日志."
+ "launchDetails": "启动详情",
+ "launchDetailsDescription": "详细的团队启动进度、实时输出和 CLI 日志。"
}
},
"projectPath": {
"label": "项目",
"source": {
- "claude": "克劳德找到的",
- "codex": "由 Codex 找到",
- "mixed": "由克劳德和Codex发现"
+ "claude": "Claude 发现",
+ "codex": "由 Codex 发现",
+ "mixed": "由 Claude 和 Codex 发现"
},
"deleted": {
- "title": "项目文件夹已不存在",
- "label": "删除"
+ "title": "项目文件夹不再存在",
+ "label": "已删除"
},
"mode": {
- "projectList": "从项目列表",
+ "projectList": "来自项目清单",
"customPath": "自定义路径"
},
- "loadingProjects": "正在装入工程...",
- "selectProject": "选择一个工程...",
- "searchPlaceholder": "按名称或路径搜索工程",
- "empty": "什么都没有",
- "selectFromList": "从列表中选择一个工程",
- "noProjects": "未找到工程, 请切换到自定义路径 。",
+ "loadingProjects": "正在加载项目…",
+ "selectProject": "选择一个项目…",
+ "searchPlaceholder": "按名称或路径搜索项目",
+ "empty": "未找到任何内容",
+ "selectFromList": "从列表中选择一个项目",
+ "noProjects": "未找到项目,切换到自定义路径。",
"customWorkingDirectory": "自定义工作目录",
"browse": "浏览",
- "createAutomatically": "如果目录不存在,则会自动创建."
+ "createAutomatically": "如果该目录不存在,则会自动创建。"
},
"members": {
"badges": {
"worktree": "工作树"
},
"runtimeTelemetry": {
- "title": "本地运行时间负荷",
- "description": "只有父母和子女参与。 远程 LLM 推论不包括在内.",
- "cpu": "CPU 苏维埃社会主义共和国",
- "memory": "内存",
- "summedRss": "合计 RSS",
- "sharedHost": "共享 OpenCode 主机参数 。 它不仅限于这个成员。",
- "processTreeCapped": "过程树被封顶 。",
- "rssHint": "RSS可以包括共享页面,因此它最好读作负载信号,而不是独家内存."
+ "title": "本地运行时负载",
+ "description": "仅限父进程和子进程。不包括远程 LLM 推理。",
+ "cpu": "中央处理器",
+ "memory": "记忆",
+ "summedRss": "RSS 汇总",
+ "sharedHost": "共享 OpenCode 主机指标。这并不是该成员独有的。",
+ "processTreeCapped": "该示例的进程树已被限制。",
+ "rssHint": "RSS 可以包含共享页面,因此最好将其作为加载信号来读取,而不是独占内存。"
},
"editor": {
"title": "成员",
"addMember": "添加成员",
"editAsJson": "编辑为 JSON",
- "runInSeparateWorktrees": "在不同的工作树上运行队友",
- "agentTeamsMcpOnly": "仅MCP代理团队",
- "removedCount": "删除( N){{count}}页:1",
- "removedModelLockReason": "删除的成员被保留用于软删除历史. 恢复其编辑设置。",
- "memberNamesUnique": "成员名字必须是独一无二的"
+ "runInSeparateWorktrees": "在单独的工作树中运行队友",
+ "agentTeamsMcpOnly": "仅限 Agent Team MCP",
+ "removedCount": "已删除 ({{count}})",
+ "removedModelLockReason": "已删除的成员将保留软删除历史记录。恢复它们以编辑设置。",
+ "memberNamesUnique": "成员名称必须是唯一的"
},
"stats": {
- "computing": "计算数据...",
- "empty": "没有可用的数据",
- "lines": "线条",
- "linesInfo": "略说. 编辑和写入工具的精确度 。 Bash文件的写法是从命令模式(heredoc,回声,sed)中估计出来的,可能报告不足.",
+ "computing": "计算统计数据…",
+ "empty": "无可用统计数据",
+ "lines": "行",
+ "linesInfo": "近似。适用于编辑和写入工具。 Bash 文件写入是根据命令模式(heredoc、echo、sed)估计的,可能会被低估。",
"files": "文件",
"toolCalls": "工具调用",
- "tokens": "键",
+ "tokens": "Token",
"toolUsage": "工具使用",
- "filesTouched": "触摸过的文件( {{count}})",
+ "filesTouched": "触及的文件 ({{count}})",
"viewAllChanges": "查看所有更改",
- "showLess": "显示较少",
- "moreFiles": "+ 键{{count}}更多",
- "footer": "{{count}}计算{{computedAgo}}",
- "footer_one": "{{count}}计算{{computedAgo}}",
- "footer_few": "{{count}}计算{{computedAgo}}",
- "footer_many": "{{count}}计算{{computedAgo}}",
- "footer_other": "{{count}}计算{{computedAgo}}"
+ "showLess": "收起",
+ "moreFiles": "+{{count}} 更多",
+ "footer": "{{count}} 会话·计算出的 {{computedAgo}}",
+ "footer_one": "{{count}} 会话·计算出的 {{computedAgo}}",
+ "footer_few": "{{count}} 会话·计算出的 {{computedAgo}}",
+ "footer_many": "{{count}} 会话·计算出的 {{computedAgo}}",
+ "footer_other": "{{count}} 会话·计算出的 {{computedAgo}}"
},
"logs": {
- "searching": "正在搜索日志...",
+ "searching": "正在搜索日志…",
"empty": "未找到日志",
- "waitingForTaskActivity": "任务正在进行中 - 等待会话活动(自动更新)...",
- "noTaskActivity": "任务尚未进行会话活动",
- "noMemberActivity": "这个成员还没有记录会议活动",
- "leadSessionTooltip": "完整的团队牵头会话日志 - 对全局管弦乐背景有用,而非此代理",
- "memberSessionTooltip": "完全持久队友会话日志 - 当工作运行在根成员会话中而不是子代理文件时有用",
+ "waitingForTaskActivity": "任务正在进行中 - 等待会话活动(自动刷新)…",
+ "noTaskActivity": "此任务还没有会话活动",
+ "noMemberActivity": "该成员尚未记录会话活动",
+ "leadSessionTooltip": "完整的团队领导会话日志 - 对于全局编排上下文有用,而不是特定于该代理",
+ "memberSessionTooltip": "完整的持久队友会话日志 - 当工作在根成员会话而不是子智能体文件中运行时非常有用",
"startedAt": "开始 {{time}}",
- "active": "活动",
- "showDetails": "显示细节",
- "hideDetails": "隐藏细节",
- "loadingDetails": "正在装入细节...",
- "failedToLoadDetails": "装入细节失败"
+ "active": "活跃",
+ "showDetails": "显示详情",
+ "hideDetails": "隐藏详细信息",
+ "loadingDetails": "正在加载详细信息…",
+ "failedToLoadDetails": "无法加载详细信息"
},
"detail": {
"relaunchOpenCode": "重新启动 OpenCode",
- "restart": "重新开始",
- "legacyLogsFallback": "遗留日志倒计时",
+ "restart": "重启",
+ "legacyLogsFallback": "旧日志回退",
"copyDiagnostics": "复制诊断",
- "pid": "密码{{pid}}",
- "removedAt": "已删除 {{date}}",
- "failedToRestartMember": "重新启动成员失败",
- "sendMessage": "发送信件",
- "assignTask": "指派任务",
- "remove": "删除"
+ "pid": "PID {{pid}}",
+ "removedAt": "删除了 {{date}}",
+ "failedToRestartMember": "重启成员失败",
+ "sendMessage": "发送消息",
+ "assignTask": "分配任务",
+ "remove": "消除"
},
"list": {
- "loading": "正在装入团队成员",
- "unavailable": "成员名册不详",
- "unavailableDescription": "{{count}}队友从团队元数据中得知,但缺少名册细节.",
- "unavailableDescription_one": "{{count}}队友从团队元数据中得知,但缺少名册细节.",
- "soloLeadOnly": "独奏队 - 仅领队",
- "removedCount": "删除( N){{count}}页:1",
- "unavailableDescription_few": "{{count}}队友从团队元数据中得知,但缺少名册细节.",
- "unavailableDescription_many": "{{count}}队友从团队元数据中得知,但缺少名册细节.",
- "unavailableDescription_other": "{{count}}队友从团队元数据中得知,但缺少名册细节."
+ "loading": "正在加载团队成员",
+ "unavailable": "成员名单不可用",
+ "unavailableDescription": "从团队元数据中可以得知 {{count}} 队友,但缺少名单详细信息。",
+ "unavailableDescription_one": "从团队元数据中可以得知 {{count}} 队友,但缺少名单详细信息。",
+ "soloLeadOnly": "单人团队 - 仅领导",
+ "removedCount": "已删除 ({{count}})",
+ "unavailableDescription_few": "从团队元数据中可以得知 {{count}} 队友,但缺少名单详细信息。",
+ "unavailableDescription_many": "从团队元数据中可以得知 {{count}} 队友,但缺少名单详细信息。",
+ "unavailableDescription_other": "从团队元数据中可以得知 {{count}} 队友,但缺少名单详细信息。"
},
"executionLog": {
- "empty": "没什么可展示的",
- "emptyUserMessage": "{{time}}- (空着)",
- "agentInstructions": "代理指令",
- "memberTurn": "{{member}}转弯",
- "agentTurn": "代理转身",
- "turn": "转弯"
+ "empty": "没有可显示的内容",
+ "emptyUserMessage": "{{time}}-(空)",
+ "agentInstructions": "代理须知",
+ "memberTurn": "{{member}} 转",
+ "agentTurn": "代理转",
+ "turn": "转动"
},
"recentMessages": {
- "latest": "最近消息",
- "latestForMember": "最近的消息 - {{member}}",
- "loadMore": "装入更多",
- "expand": "扩展",
- "collapse": "折叠"
+ "latest": "最新消息",
+ "latestForMember": "最新消息 - {{member}}",
+ "loadMore": "加载更多",
+ "expand": "扩张",
+ "collapse": "坍塌"
},
"leadModel": {
"defaultModel": "默认",
- "providerModelAria": "{{provider}}供应商,{{model}}",
- "leadShort": "牵头",
- "teamLead": "团队领导",
- "syncWithTeammates": "与队友同步模式",
- "anthropicTeamWide": "整个团队",
- "runtimeInheritance": "预赛时间适用于队友,除非他们自行设置提供者或模式.",
- "anthropicContextLimit": "200K上下文限制为本次发射中Anthropic运行时间的全队范围,包括定制的Anthropic队友."
+ "providerModelAria": "{{provider}} 提供商,{{model}}",
+ "leadShort": "带领",
+ "teamLead": "团队负责人",
+ "syncWithTeammates": "与队友同步模型",
+ "anthropicTeamWide": "全团队人性化",
+ "runtimeInheritance": "领先运行时适用于队友,除非他们设置自己的提供商或模型。",
+ "anthropicContextLimit": "此次启动的 Anthropic 运行时的 200K 上下文限制是团队范围内的,包括自定义的 Anthropic 团队成员。"
},
"runtimeLogs": {
- "autoRefresh": "自动更新",
- "wrapLines": "环行",
- "loadingTail": "正在装入进程日志尾巴...",
- "empty": "尚未为这个成员捕获进程日志文件 。",
+ "autoRefresh": "自动刷新",
+ "wrapLines": "换行线",
+ "loadingTail": "正在加载进程日志尾部…",
+ "empty": "尚未捕获该成员的进程日志文件。",
"copy": "复制",
"fileEmpty": "进程日志文件为空。",
- "showingLast": "显示最后 {{bytes}}。",
+ "showingLast": "显示最后一个 {{bytes}}。",
"showing": "显示 {{bytes}}。"
},
"tasks": {
- "empty": "没有指派给该成员的任务"
+ "empty": "没有任务分配给该成员"
},
"messages": {
- "loadOlder": "装入旧信件",
+ "loadOlder": "加载旧消息",
"filters": {
- "all": "全体",
- "messages": "信件",
+ "all": "全部",
+ "messages": "消息",
"comments": "评论"
},
"empty": {
- "loading": "正在装入活动...",
- "noComments": "该成员没有评论意见",
- "noLoadedMessages": "尚未为该成员装入信件",
- "noMessages": "没有给这个成员留言",
- "noLoadedActivity": "此成员的未加载活动",
- "noActivity": "与该成员没有活动"
+ "loading": "正在加载活动…",
+ "noComments": "该成员没有评论",
+ "noLoadedMessages": "该成员尚未加载消息",
+ "noMessages": "没有与该成员消息",
+ "noLoadedActivity": "该成员尚未加载任何活动",
+ "noActivity": "该成员没有任何活动"
}
},
"actions": {
- "openProfile": "打开配置文件",
+ "openProfile": "打开个人资料",
"editRole": "编辑角色",
"sendMessage": "发送消息",
- "assignTask": "指派任务"
+ "assignTask": "分配任务"
},
"roleSelect": {
- "customRolePlaceholder": "输入自定义角色..."
+ "customRolePlaceholder": "输入自定义角色…"
}
},
"schedule": {
- "count": "{{count}}时间表",
- "count_one": "{{count}} 时间表",
- "count_other": "{{count}}时间表",
- "nextRun": "下一个:{{next}}",
+ "count": "{{count}} 计划任务",
+ "count_one": "{{count}} 计划任务",
+ "count_other": "{{count}} 计划任务",
+ "nextRun": "下次运行: {{next}}",
"actions": {
- "runNow": "快点跑",
+ "runNow": "立即运行",
"edit": "编辑",
"pause": "暂停",
- "resume": "继续",
+ "resume": "恢复",
"delete": "删除",
- "addSchedule": "添加计划"
+ "addSchedule": "添加计划任务"
},
"runHistory": {
- "loading": "正在装入运行历史...",
- "empty": "尚未运行"
+ "loading": "正在加载运行历史记录…",
+ "empty": "尚无运行记录"
},
- "count_few": "{{count}}时间表",
- "count_many": "{{count}}时间表",
+ "count_few": "{{count}} 计划任务",
+ "count_many": "{{count}} 计划任务",
"runLog": {
"title": "运行日志",
- "exitCode": "出口 {{code}}",
+ "exitCode": "退出 {{code}}",
"retryCount": "重试 {{count}}/{{max}}",
- "stillRunning": "任务仍在运行...",
- "loadingLogs": "正在装入日志...",
+ "stillRunning": "任务仍在运行…",
+ "loadingLogs": "正在加载日志…",
"errors": "错误",
"close": "关闭"
},
"cron": {
- "expression": "线性表达式",
- "highFrequencyWarning": "高频时间表(小于5分钟间隔)",
- "nextRuns": "下一个运行:",
+ "expression": "克朗表达式",
+ "highFrequencyWarning": "高频率计划任务(间隔少于 5 分钟)",
+ "nextRuns": "接下来运行:",
"timezone": "时区",
"selectTimezone": "选择时区",
- "warmUpTime": "温暖时间",
- "warmUpDescription": "在预定执行前准备选定的提供者",
+ "warmUpTime": "预热时间",
+ "warmUpDescription": "在计划执行之前准备选定的提供商",
"errors": {
"enterExpression": "输入 cron 表达式",
"invalidExpression": "无效的 cron 表达式"
},
"presets": {
"everyHour": "每小时",
- "everySixHours": "每6小时",
- "dailyAtNine": "每日9点",
- "weekdaysAtNine": "上午9时的周日",
- "mondayAtNine": "星期一上午9时",
- "everyThirtyMinutes": "每30分钟"
+ "everySixHours": "每 6 小时",
+ "dailyAtNine": "每天上午 9 点",
+ "weekdaysAtNine": "工作日上午 9 点",
+ "mondayAtNine": "周一上午 9 点",
+ "everyThirtyMinutes": "每30 分钟一班"
},
"warmUpOptions": {
- "none": "没有热身",
- "fiveMinutes": "5分钟",
- "tenMinutes": "10分钟",
- "fifteenMinutes": "15分钟",
- "thirtyMinutes": "30分钟"
+ "none": "无需热身",
+ "fiveMinutes": "5 分钟",
+ "tenMinutes": "10 分钟",
+ "fifteenMinutes": "15 分钟",
+ "thirtyMinutes": "30 分钟"
}
},
"empty": {
- "title": "尚无时间表",
- "description": "创建一个运行 Claude 任务在 cron 时间表上自动运行的调度 。"
+ "title": "还没有计划任务",
+ "description": "创建一个计划以按照 cron 计划自动运行 Claude 任务。"
},
- "title": "附表",
+ "title": "计划任务",
"status": {
- "active": "活动",
- "paused": "暂停",
- "disabled": "已禁用"
+ "active": "活跃",
+ "paused": "已暂停",
+ "disabled": "已停用"
},
"runStatus": {
- "pending": "待决",
- "warmingUp": "暖和起来",
- "warm": "温暖",
- "running": "运行",
- "completed": "已完成",
+ "pending": "待办的",
+ "warmingUp": "热身",
+ "warm": "温暖的",
+ "running": "运行中",
+ "completed": "完全的",
"failed": "失败",
- "interrupted": "中断",
- "cancelled": "已取消"
+ "interrupted": "被打断",
+ "cancelled": "取消"
}
},
"openCodeContextConfigHint": {
- "summary": "OpenCode本地模型可以使用OpenCode上下文预算,而不是只使用即时限制.",
- "description": "为这个队友使用的提供者和模型添加匹配限制到OpenCode配置中. 这有助于在本地模型溢出上下文窗口之前, OpenCode 压缩和调色板 。",
- "replacePrefix": "替换",
- "and": "联合国",
- "replaceSuffix": "与您的 OpenCode 设置中的提供者和模式标识。 提示类",
- "promptInstructionsSuffix": "更弱,因为请求是在模型读完之前组装的。",
- "providerLimits": "供应商限额",
+ "summary": "OpenCode 本地模型可以使用 OpenCode 上下文预算,而不是仅限提示词的限制。",
+ "description": "向该队友使用的提供商和模型的 OpenCode 配置添加匹配限制。这有助于 OpenCode 在本地模型溢出其上下文窗口之前进行压缩和修剪。",
+ "replacePrefix": "代替",
+ "and": "和",
+ "replaceSuffix": "使用 OpenCode 设置中的提供商和模型 ID。提示词说明如",
+ "promptInstructionsSuffix": "较弱,因为请求是在模型读取它们之前组装的。",
+ "providerLimits": "提供商限制",
"compactionConfig": "压缩配置"
},
"sessions": {
- "noProjectPath": "没有工程路径链接",
- "provisioningHint": "会话将在团队提供后出现",
- "projectNotFound": "未找到工程",
- "loading": "正在装入会话...",
+ "noProjectPath": "没有链接项目路径",
+ "provisioningHint": "会话将在团队配置后出现",
+ "projectNotFound": "未找到项目",
+ "loading": "正在加载会话…",
"empty": "未找到会话",
- "showAllSessions": "显示全部会话",
- "lead": "牵头",
- "removeFilter": "删除过滤器",
- "filterBySession": "此会话过滤",
- "openSession": "公开会议",
- "title": "会议"
+ "showAllSessions": "显示所有会话",
+ "lead": "带领",
+ "removeFilter": "移除过滤器",
+ "filterBySession": "按此会话过滤",
+ "openSession": "公开会话",
+ "title": "会话"
},
"provisioning": {
- "pid": "密码{{pid}}",
+ "pid": "PID {{pid}}",
"cancel": "取消",
- "moreWarningsHidden": "{{count}} 隐藏更多的警告",
+ "moreWarningsHidden": "{{count}} 隐藏更多警告",
"diagnostics": "诊断",
"liveOutput": "实时输出",
- "diagnosticsCopied": "复制的诊断",
+ "diagnosticsCopied": "已复制诊断信息",
"copyDiagnostics": "复制诊断",
- "copied": "复制",
- "noOutput": "尚未捕获输出 。",
- "cliLogs": "CLI日志",
+ "copied": "已复制",
+ "noOutput": "尚未捕获任何输出。",
+ "cliLogs": "CLI 日志",
"steps": {
"starting": "开始",
"configuring": "团队设置",
"assembling": "成员加入",
- "finalizing": "最后敲定"
+ "finalizing": "敲定"
},
"providerStatus": {
"status": {
- "checking": "检查...",
- "ready": "还好",
- "notes": "OK( 注释)",
- "failed": "ERR 苏维埃社会主义共和国",
- "pending": "等待时"
+ "checking": "检查…",
+ "ready": "好的",
+ "notes": "好的(注释)",
+ "failed": "犯错",
+ "pending": "等待"
},
"detailSummary": {
- "cliBinaryMissing": "ZXCVKEN0ZXCV 二进制缺失",
- "openCodeRuntimeMissing": "OpenCode 运行时间缺失",
- "openCodeWindowsAccessBlocked": "OpenCode Windows 访问受阻",
- "openCodeNoOutput": "OpenCode 运行时检查未返回输出",
- "openCodeMcpUnreachable": "OpenCode app MCP 无法访问",
- "workingDirectoryMissing": "缺少工作目录",
- "cliBinaryCouldNotStart": "CLI 二进制无法启动",
- "cliPreflightIncomplete": "CLI 飞行前未完成",
+ "cliBinaryMissing": "CLI 二进制文件缺失",
+ "openCodeRuntimeMissing": "OpenCode 运行时缺失",
+ "openCodeWindowsAccessBlocked": "OpenCode Windows 访问被阻止",
+ "openCodeNoOutput": "OpenCode 运行时检查未返回任何输出",
+ "openCodeMcpUnreachable": "OpenCode 应用 MCP 无法访问",
+ "workingDirectoryMissing": "工作目录丢失",
+ "cliBinaryCouldNotStart": "CLI 二进制文件无法启动",
+ "cliPreflightIncomplete": "CLI 预检未完成",
"authenticationRequired": "需要认证",
- "runtimeProviderNotConfigured": "未配置运行时间提供者",
- "cliPreflightFailed": "ZXCVKEN0ZXCV 飞行前失败",
- "selectedModelCompatible": "选中模式兼容",
- "selectedModelCompatibilityPending": "选定模式的兼容性待定",
- "selectedModelAvailable": "可选模式",
- "selectedModelVerified": "选中模式已验证",
- "selectedModelUnavailable": "无法选择模式",
- "selectedModelTimedOut": "选中模式验证超时",
- "selectedModelCheckFailed": "选中模式检查失败",
- "selectedModelDeferred": "推迟选定模式核查",
- "selectedModelPingNotConfirmed": "未确认选中模式",
- "readyWithNotes": "备注",
- "needsAttention": "需要关注"
+ "runtimeProviderNotConfigured": "未配置运行时提供商",
+ "cliPreflightFailed": "CLI 预检失败",
+ "selectedModelCompatible": "所选模型兼容",
+ "selectedModelCompatibilityPending": "所选模型兼容性待定",
+ "selectedModelAvailable": "可选模型",
+ "selectedModelVerified": "所选模型已验证",
+ "selectedModelUnavailable": "所选模型不可用",
+ "selectedModelTimedOut": "所选模型验证超时",
+ "selectedModelCheckFailed": "所选模型检查失败",
+ "selectedModelDeferred": "选定的模型验证推迟",
+ "selectedModelPingNotConfirmed": "所选模型 ping 未确认",
+ "readyWithNotes": "准备好备注",
+ "needsAttention": "需要注意"
},
- "modelChecksSummary": "选定型号检查 - {{details}}",
+ "modelChecksSummary": "选定模型检查 - {{details}}",
"modelParts": {
- "unavailable": "{{count}}无法使用模型",
- "unavailable_one": "{{count}}无法使用模型",
- "unavailable_other": "无法使用 {{count}} 模型",
+ "unavailable": "{{count}} 模型不可用",
+ "unavailable_one": "{{count}} 模型不可用",
+ "unavailable_other": "{{count}} 模型不可用",
"checkFailed": "{{count}} 模型检查失败",
"checkFailed_one": "{{count}} 模型检查失败",
"checkFailed_other": "{{count}} 模型检查失败",
"timedOut": "{{count}} 模型超时",
"timedOut_one": "{{count}} 模型超时",
"timedOut_other": "{{count}} 模型超时",
- "deferred": "{{count}}推迟核查",
- "deferred_one": "{{count}}推迟核查",
- "deferred_other": "{{count}}推迟核查",
- "pingNotConfirmed": "{{count}} Ping未确认",
- "pingNotConfirmed_one": "{{count}} Ping未确认",
- "pingNotConfirmed_other": "{{count}} Ping未确认",
- "compatibilityPending": "{{count}}兼容、深入核查",
- "compatibilityPending_one": "{{count}}兼容、深入核查",
- "compatibilityPending_other": "{{count}}兼容、深入核查",
- "compatible": "{{count}}兼容",
- "compatible_one": "{{count}}兼容",
- "compatible_other": "{{count}}兼容",
- "checking": "{{count}}检查",
- "checking_one": "{{count}}检查",
- "checking_other": "{{count}}检查",
- "available": "{{count}}可用",
- "available_one": "{{count}}可用",
- "available_other": "{{count}}可用",
- "verified": "{{count}} 经核查",
- "verified_one": "{{count}} 经核查",
- "verified_other": "{{count}} 经核查",
- "unavailable_few": "无法使用 {{count}} 模型",
- "unavailable_many": "无法使用 {{count}} 模型",
+ "deferred": "{{count}} 验证延迟",
+ "deferred_one": "{{count}} 验证延迟",
+ "deferred_other": "{{count}} 验证延迟",
+ "pingNotConfirmed": "{{count}} ping 未确认",
+ "pingNotConfirmed_one": "{{count}} ping 未确认",
+ "pingNotConfirmed_other": "{{count}} ping 未确认",
+ "compatibilityPending": "{{count}} 兼容,深度验证待定",
+ "compatibilityPending_one": "{{count}} 兼容,深度验证待定",
+ "compatibilityPending_other": "{{count}} 兼容,深度验证待定",
+ "compatible": "{{count}} 兼容",
+ "compatible_one": "{{count}} 兼容",
+ "compatible_other": "{{count}} 兼容",
+ "checking": "{{count}} 检查",
+ "checking_one": "{{count}} 检查",
+ "checking_other": "{{count}} 检查",
+ "available": "{{count}} 可用",
+ "available_one": "{{count}} 可用",
+ "available_other": "{{count}} 可用",
+ "verified": "{{count}} 已验证",
+ "verified_one": "{{count}} 已验证",
+ "verified_other": "{{count}} 已验证",
+ "unavailable_few": "{{count}} 模型不可用",
+ "unavailable_many": "{{count}} 模型不可用",
"checkFailed_few": "{{count}} 模型检查失败",
"checkFailed_many": "{{count}} 模型检查失败",
"timedOut_few": "{{count}} 模型超时",
"timedOut_many": "{{count}} 模型超时",
- "deferred_few": "{{count}}推迟核查",
- "deferred_many": "{{count}}推迟核查",
- "pingNotConfirmed_few": "{{count}} Ping未确认",
- "pingNotConfirmed_many": "{{count}} Ping未确认",
- "compatibilityPending_few": "{{count}}兼容、深入核查",
- "compatibilityPending_many": "{{count}}兼容、深入核查",
- "compatible_few": "{{count}}兼容",
- "compatible_many": "{{count}}兼容",
- "checking_few": "{{count}}检查",
- "checking_many": "{{count}}检查",
- "available_few": "{{count}}可用",
- "available_many": "{{count}}可用",
- "verified_few": "{{count}} 经核查",
- "verified_many": "{{count}} 经核查"
+ "deferred_few": "{{count}} 验证延迟",
+ "deferred_many": "{{count}} 验证延迟",
+ "pingNotConfirmed_few": "{{count}} ping 未确认",
+ "pingNotConfirmed_many": "{{count}} ping 未确认",
+ "compatibilityPending_few": "{{count}} 兼容,深度验证待定",
+ "compatibilityPending_many": "{{count}} 兼容,深度验证待定",
+ "compatible_few": "{{count}} 兼容",
+ "compatible_many": "{{count}} 兼容",
+ "checking_few": "{{count}} 检查",
+ "checking_many": "{{count}} 检查",
+ "available_few": "{{count}} 可用",
+ "available_many": "{{count}} 可用",
+ "verified_few": "{{count}} 已验证",
+ "verified_many": "{{count}} 已验证"
},
"openProviderSettings": "打开 {{provider}} 设置",
- "copied": "复制",
+ "copied": "已复制",
"copyDiagnostics": "复制诊断",
- "deepVerificationPending": "深度核查仍在进行中。 OpenCode自由模型可能需要20秒左右.",
+ "deepVerificationPending": "深度验证仍在运行。 OpenCode 免费模型可能需要大约 20 秒。",
"progress": {
- "checkingSelectedProviders": "正在并行检查选中的提供者...",
- "checkingProvider": "正在检查 {{provider}} 供应商...",
- "checkingProviders": "正在检查 {{providers}} 供应商..."
+ "checkingSelectedProviders": "并行检查选定的提供商…",
+ "checkingProvider": "正在检查 {{provider}} 提供商…",
+ "checkingProviders": "正在检查 {{providers}} 提供商…"
},
"failureHints": {
- "openCodeAccessDenied": "修改文件夹权限或将工程移动到一个用户可写入的文件夹 。 作为管理员运行只是临时工作。",
- "openCodeBridgeNoOutput": "重新启动应用程序和 OpenCode 运行时间, 然后重试 。 如果重复,复制诊断。",
- "workingDirectoryMissing": "选择已有的工作目录, 然后重新打开此对话框 。",
- "authenticationRequired": "在 Claude CLI 中认证所需的提供者, 然后重新打开此对话框 。",
- "runtimeProviderNotConfigured": "配置选中的提供者运行时间, 然后重新打开此对话框 。",
- "openCodeRuntimeMissing": "从提供者状态卡安装或重试 OpenCode 运行时间, 然后重新打开此对话框 。",
- "openCodeAppMcpUnreachable": "重试发射刷新OpenCodeapp MCP桥. 如果重复,请重新启动应用程序和 OpenCode 运行时间 。",
- "cliBinaryMissing": "确保本地的 Claude CLI 二进制存在并可以启动,然后重新打开此对话框 。",
- "default": "解决上面的问题, 然后重开这个对话框 。",
- "openCodeNodeModulesSymlinkPermission": "以管理员身份运行 Agent Teams AI, 然后重试启动。"
+ "openCodeAccessDenied": "修复文件夹权限或将项目移动到用户可写的文件夹。以管理员身份运行只是一个临时解决方法。",
+ "openCodeBridgeNoOutput": "重新启动应用和 OpenCode 运行时,然后重试。如果重复出现,请复制诊断信息。",
+ "workingDirectoryMissing": "选择现有的工作目录,然后重新打开此对话框。",
+ "authenticationRequired": "在 Claude CLI 中验证所需的提供商,然后重新打开此对话框。",
+ "runtimeProviderNotConfigured": "配置选定的提供商运行时,然后重新打开此对话框。",
+ "openCodeRuntimeMissing": "从提供商状态卡安装或重试 OpenCode 运行时,然后重新打开此对话框。",
+ "openCodeAppMcpUnreachable": "重试启动以刷新 OpenCode 应用 MCP 桥。如果重复出现,请重新启动应用和 OpenCode 运行时。",
+ "cliBinaryMissing": "确保本地 Claude CLI 二进制文件存在并且可以启动,然后重新打开此对话框。",
+ "default": "解决上述问题,然后重新打开此对话框。",
+ "openCodeNodeModulesSymlinkPermission": "以管理员身份运行 Agent Teams AI,然后重试启动。"
}
},
"presentation": {
- "awaitingPermission": "{{count}}队友等待批准",
- "nameListWithMore": "{{names}}+ 组合键{{count}}更多",
- "waitingForOpenCode": "正在等待 OpenCode: {{names}}",
- "bootstrapStalled": "靴子档停: {{names}}",
- "bootstrapStalledWithOpenCodeWait": "{{stalled}}; 等待开放代码: {{names}}",
- "namedPendingDiagnostic": "{{label}}编号:{{names}}",
- "countPendingDiagnostic": "{{count}} 个{{label}}",
+ "awaitingPermission": "{{count}} 队友正在等待权限批准",
+ "nameListWithMore": "{{names}}, +{{count}} 更多",
+ "waitingForOpenCode": "等待 OpenCode:{{names}}",
+ "bootstrapStalled": "引导程序停止:{{names}}",
+ "bootstrapStalledWithOpenCodeWait": "{{stalled}};等待 OpenCode:{{names}}",
+ "namedPendingDiagnostic": "{{label}}: {{names}}",
+ "countPendingDiagnostic": "{{count}} {{label}}",
"pendingLabels": {
- "bootstrapStalled": "靴子绑住了",
- "shellOnly": "只有贝壳",
- "waitingForBootstrap": "等待靴子夹",
- "bootstrapUnconfirmed": "未证实的靴子陷阱",
+ "bootstrapStalled": "引导程序停滞",
+ "shellOnly": "仅外壳",
+ "waitingForBootstrap": "等待引导程序",
+ "bootstrapUnconfirmed": "引导程序未经确认",
"awaitingPermission": "等待许可",
- "waitingForRuntime": "等待运行时间",
- "shellOnlyLower": "只有外壳",
- "waitingForBootstrapLower": "等待靴子夹",
- "bootstrapUnconfirmedLower": "未证实的靴子",
+ "waitingForRuntime": "等待运行时",
+ "shellOnlyLower": "仅外壳",
+ "waitingForBootstrapLower": "等待引导程序",
+ "bootstrapUnconfirmedLower": "引导程序未确认",
"awaitingPermissionLower": "等待许可",
- "waitingForRuntimeLower": "等待运行时间"
+ "waitingForRuntimeLower": "等待运行时"
},
"failed": {
- "memberFailedToStart": "{{name}}启动失败",
- "teammatesFailedToStart": "{{count}}队友未能启动",
- "teammatesFailedRatio": "{{count}}页:1{{total}}队友启动失败"
+ "memberFailedToStart": "{{name}} 启动失败",
+ "teammatesFailedToStart": "{{count}} 队友启动失败",
+ "teammatesFailedRatio": "{{count}}/{{total}} 队友启动失败"
},
"skipped": {
- "memberSkipped": "{{name}}跳过这次发射",
- "memberSkippedWithReason": "{{name}}跳过这次发射...{{reason}}",
- "memberSkippedCompact": "{{name}}跳过",
- "teammatesSkipped": "{{count}}队友跳伞",
- "teammatesSkippedList": "跳过队友: {{list}}",
- "teammatesSkippedRatio": "{{count}}页:1{{total}}队友跳过这次发射"
+ "memberSkipped": "{{name}} 跳过本次启动",
+ "memberSkippedWithReason": "本次启动跳过了 {{name}} - {{reason}}",
+ "memberSkippedCompact": "{{name}} 已跳过",
+ "teammatesSkipped": "{{count}} 队友跳过",
+ "teammatesSkippedList": "跳过的队友:{{list}}",
+ "teammatesSkippedRatio": "{{count}}/{{total}} 队友跳过了本次启动"
},
"joining": {
- "teammatesStillJoining": "{{count}}队友仍然参加",
- "teammatesStillJoining_one": "{{count}}队友仍然参加",
- "teammatesStillJoining_few": "{{count}}队友仍然参加",
- "teammatesStillJoining_many": "{{count}}队友仍然参加",
- "teammatesStillJoining_other": "{{count}}队友仍然参加",
- "teammatesConfirmedRatio": "{{count}}页:1{{total}}队友确认"
+ "teammatesStillJoining": "{{count}} 队友仍在加入",
+ "teammatesStillJoining_one": "{{count}} 队友仍在加入",
+ "teammatesStillJoining_few": "{{count}} 队友仍在加入",
+ "teammatesStillJoining_many": "{{count}} 队友仍在加入",
+ "teammatesStillJoining_other": "{{count}} 队友仍在加入",
+ "teammatesConfirmedRatio": "{{count}}/{{total}} 队友确认"
},
"ready": {
- "leadOnline": "在线领导",
- "allTeammatesJoined": "所有{{count}}队友加入",
- "teamProvisionedLeadOnline": "提供团队-在线领导",
- "teamProvisionedAllJoined": "提供团队-所有{{count}}队友加入",
- "teamProvisionedStillJoining": "团队提供 - 队友仍在加入",
- "launchFinishedWithErrors": "发射完成错误 - {{count}}/{{total}}队友未能启动",
- "launchContinuedSkipped": "继续发射 - {{count}}/{{total}}队友跳伞",
- "teamLaunchedLeadOnline": "启动团队 - 领导在线",
- "teamLaunchedAllJoined": "团队启动 - {{count}}全体队友加入"
+ "leadOnline": "在线引导",
+ "allTeammatesJoined": "{{count}} 队友全部加入",
+ "teamProvisionedLeadOnline": "团队配置 - 在线领导",
+ "teamProvisionedAllJoined": "团队配置 - 所有 {{count}} 队友都加入",
+ "teamProvisionedStillJoining": "团队已配置 - 队友仍在加入",
+ "launchFinishedWithErrors": "启动完成但出现错误 - {{count}}/{{total}} 队友启动失败",
+ "launchContinuedSkipped": "启动继续 - {{count}}/{{total}} 队友跳过",
+ "teamLaunchedLeadOnline": "团队启动 - 在线领导",
+ "teamLaunchedAllJoined": "团队启动 - 所有 {{count}} 队友加入"
},
"panel": {
- "launchFailed": "发射失败",
- "launchDetails": "发射详情",
- "launchFinishedWithErrors": "发射完成时有错误",
- "launchContinuedSkipped": "与跳过队友继续发射",
- "coreTeamReady": "核心小组准备就绪",
- "finishingLaunch": "完成发射",
- "teamLaunched": "启动小组",
- "launchingTeam": "发射队"
+ "launchFailed": "启动失败",
+ "launchDetails": "启动详情",
+ "launchFinishedWithErrors": "启动完成但有错误",
+ "launchContinuedSkipped": "与跳过的队友一起继续启动",
+ "coreTeamReady": "核心团队准备就绪",
+ "finishingLaunch": "正在完成启动",
+ "teamLaunched": "团队启动",
+ "launchingTeam": "启动团队"
}
}
},
"liveRuntimeStatus": {
- "title": "实时运行状态",
- "description": "只显示心跳和发射状态 。 流程控制仍低于此。",
- "source": "资料来源:{{source}}",
- "lane": "{{lane}}车道",
- "diagnosticOnly": "仅诊断",
- "updated": "更新的 {{value}}",
+ "title": "实时运行时状态",
+ "description": "仅显示心跳和启动状态。过程控制保留在下面。",
+ "source": "来源:{{source}}",
+ "lane": "{{lane}} 车道",
+ "diagnosticOnly": "仅用于诊断",
+ "updated": "更新 {{value}}",
"states": {
- "running": "运行",
+ "running": "运行中",
"starting": "开始",
- "waiting": "等待时",
- "degraded": "需要关注",
+ "waiting": "等待",
+ "degraded": "需要注意",
"stopped": "已停止",
- "unknown": "未知数"
+ "unknown": "未知"
}
},
"taskLogs": {
"exact": {
- "title": "精确的任务日志",
- "loading": "正在装入精确的任务日志...",
- "description": "精确的笔录切片,与Logs中使用的相同执行记录组件.",
- "emptyTitle": "还没有精确的任务日志",
- "emptyDescription": "在有与任务相关的明确记录元数据时,准确的记录稿捆绑将在此出现。",
+ "title": "确切的任务日志",
+ "loading": "正在加载准确的任务日志…",
+ "description": "使用日志中使用的相同执行日志组件呈现的精确转录切片。",
+ "emptyTitle": "尚无确切的任务日志",
+ "emptyDescription": "当明确的任务链接转录本元数据可用时,精确的转录本包将出现在此处。",
"summaryOnly": "仅摘要"
},
"executionSessions": {
"title": "执行会话",
- "online": "在线",
- "updating": "更新...",
- "description": "以会话为中心的语录浏览和预览."
+ "online": "在线的",
+ "updating": "更新中…",
+ "description": "传统的以会话为中心的成绩单浏览和预览。"
},
"stream": {
"title": "任务日志流"
@@ -2180,73 +2180,73 @@
"taskCard": {
"cancelTask": "取消任务 {{taskId}}",
"cancel": "取消",
- "moveBackToTodoConfirm": "把这个任务移回TODO并通知小组?",
+ "moveBackToTodoConfirm": "将此任务移回 TODO 并通知团队?",
"confirm": "确认",
- "keep": "保留",
- "changesNeedAttention": "变化需要注意",
- "changes": "变动",
+ "keep": "保持",
+ "changesNeedAttention": "变化需要关注",
+ "changes": "变化",
"deleteTask": "删除任务",
- "taskLogsActive": "任务日志活动",
- "newTaskLogsArriving": "到达新任务日志",
+ "taskLogsActive": "任务日志处于活动状态",
+ "newTaskLogsArriving": "新任务日志到达",
"awaitingUser": "等待用户",
- "awaitingLead": "等待领先",
- "blockedBy": "被封锁",
- "blocks": "块",
+ "awaitingLead": "等待线索",
+ "blockedBy": "被阻止",
+ "blocks": "积木",
"start": "开始",
- "complete": "完成",
- "approve": "核准",
- "requestReview": "请求审查",
- "manualReview": "手工审查",
+ "complete": "完全的",
+ "approve": "批准",
+ "requestReview": "请求审核",
+ "manualReview": "人工审核",
"requestChanges": "请求更改"
},
"filter": {
"title": "过滤任务",
- "session": "会议",
- "allSessions": "所有会议",
+ "session": "会话",
+ "allSessions": "所有会话",
"teammate": "队友",
- "unassigned": "(未签名)",
- "column": "栏",
+ "unassigned": "(未分配)",
+ "column": "柱子",
"clearAll": "全部清除"
},
"board": {
"addTask": "添加任务",
- "noTasks": "无任务",
- "showMore": "显示{{count}}更多",
- "hiddenCount": "{{count}}隐藏",
- "trash": "废物",
+ "noTasks": "没有任务",
+ "showMore": "再显示 {{count}} 条",
+ "hiddenCount": "{{count}} 隐藏",
+ "trash": "垃圾",
"gridView": "网格视图",
"columnsView": "列视图"
},
"trash": {
- "title": "废物",
+ "title": "垃圾",
"empty": "没有删除的任务",
- "subject": "议题",
- "owner": "拥有者",
- "deleted": "删除",
- "unassigned": "未指定",
+ "subject": "主题",
+ "owner": "所有者",
+ "deleted": "已删除",
+ "unassigned": "未分配",
"restoreTask": "恢复任务",
"restore": "恢复",
"close": "关闭"
},
"sort": {
- "title": "排序任务",
- "sortBy": "排序为",
- "reset": "重设",
+ "title": "对任务进行排序",
+ "sortBy": "排序方式",
+ "reset": "重置",
"options": {
"updatedAt": {
- "label": "上次更新",
- "description": "最近第一次更新"
+ "label": "最后更新",
+ "description": "最近更新的先"
},
"createdAt": {
"label": "已创建",
- "description": "最新的第一个"
+ "description": "最新的优先"
},
"owner": {
- "label": "拥有者",
+ "label": "所有者",
"description": "按受让人字母顺序排列"
},
"manual": {
- "label": "手动",
+ "label": "手动的",
"description": "拖放顺序"
}
}
@@ -2254,88 +2254,88 @@
"search": {
"clearSearch": "清除搜索",
"tasks": "任务",
- "createdAgo": "创建 {{time}}",
- "updatedAgo": "更新的 {{time}}",
- "placeholder": "搜索任务... (#id 或文本)"
+ "createdAgo": "创建了 {{time}}",
+ "updatedAgo": "更新 {{time}}",
+ "placeholder": "搜索任务…(#id 或文本)"
},
"grid": {
"addTask": "添加任务",
- "noTasks": "无任务"
+ "noTasks": "没有任务"
},
- "title": "坎班语Name",
+ "title": "看板",
"columns": {
- "todo": "TODO 苏维埃社会主义共和国",
- "inProgress": "在ZXCVKEN0ZXCV",
- "review": "REVIEW 苏维埃社会主义共和国",
- "done": "DONE 苏维埃社会主义共和国",
- "approved": "APPROVED 苏维埃社会主义共和国"
+ "todo": "待办事项",
+ "inProgress": "进行中",
+ "review": "审核",
+ "done": "完毕",
+ "approved": "已批准"
}
},
"worktreeGitReadiness": {
- "checking": "正在检查 Git 仓库状态 。",
- "ready": "吉特工作树准备好了",
- "readyOnBranch": "在{{branch}}的分支上,Git工作树已经准备就绪.",
- "needsSetup": "工作树隔离需要设置",
- "initialCommitNotice": "初始承诺动作阶段, 并用信件输入所有当前文件",
+ "checking": "正在检查队友工作树的 Git 仓库状态…",
+ "ready": "Git 工作树已准备就绪。",
+ "readyOnBranch": "Git 工作树已在分支 {{branch}} 上准备就绪。",
+ "needsSetup": "工作树隔离需要 Git 设置",
+ "initialCommitNotice": "初始提交操作阶段并提交带有消息的所有当前文件",
"initializeRepository": "初始化 Git 仓库",
- "createInitialCommit": "创建初始承诺",
- "initialCommitMessage": "胆量:首次承诺"
+ "createInitialCommit": "创建初始提交",
+ "initialCommitMessage": "杂务:初始提交"
},
"toolApproval": {
"settings": "设置",
- "autoAllowAllTools": "自动启用所有工具",
- "autoAllowFileEdits": "自动启用文件编辑( 编辑、 写入、 笔记本编辑)",
- "autoAllowSafeCommands": "自动报警安全命令( git, pnpm, npm, Is...)",
- "onTimeout": "超时:",
- "after": "之后",
- "secondsShort": "秒数",
+ "autoAllowAllTools": "自动允许所有工具",
+ "autoAllowFileEdits": "自动允许文件编辑(编辑、写入、NotebookEdit)",
+ "autoAllowSafeCommands": "自动允许安全命令(git、pnpm、npm、ls…)",
+ "onTimeout": "超时时:",
+ "after": "后",
+ "secondsShort": "秒",
"timeoutActions": {
"wait": "永远等待",
"allow": "允许",
- "deny": "拒绝"
+ "deny": "否定"
},
"submit": "提交",
"allow": "允许",
- "deny": "拒绝",
+ "deny": "否定",
"allowAll": "允许全部",
- "pendingCount": "{{count}}待处理",
- "autoActionIn": "自动{{action}}输入{{time}}",
+ "pendingCount": "{{count}} 待定",
+ "autoActionIn": "将在 {{time}} 后自动{{action}}",
"diff": {
"previewChanges": "预览更改",
- "readingFile": "正在读取文件...",
+ "readingFile": "正在读取文件…",
"binaryFile": "二进制文件 - 无法预览",
- "truncated": "在 2MB - diff 上切换的文件可能不完整",
+ "truncated": "文件被截断为 2MB - diff 可能不完整",
"newFile": "新文件"
}
},
"memberWorkSync": {
"details": {
"title": "成员工作同步",
- "actionableItems": "可采取行动的项目",
+ "actionableItems": "可操作的项目",
"fingerprint": "指纹",
"report": "报告",
"none": "无",
- "shadowWouldNudge": "阴影会冲动",
- "yes": "对",
- "no": "无",
- "moreActionableItems": "{{count}}更多可操作项目",
- "diagnostics": "诊断: {{diagnostics}}"
+ "shadowWouldNudge": "影子会轻推",
+ "yes": "是的",
+ "no": "不",
+ "moreActionableItems": "{{count}} 更多可操作项目",
+ "diagnostics": "诊断:{{diagnostics}}"
},
"title": "成员工作同步",
- "loadingDiagnostics": "装入成员的工作同步诊断。",
- "diagnosticsUnavailable": "成员工作同步诊断不可用."
+ "loadingDiagnostics": "正在加载成员工作同步诊断。",
+ "diagnosticsUnavailable": "成员工作同步诊断不可用。"
},
"advancedCli": {
"title": "高级",
"useWorktree": "使用工作树",
- "recent": "近期",
+ "recent": "最近的",
"commandPreview": "命令预览",
"customArguments": "自定义参数",
- "validate": "校验",
+ "validate": "证实",
"validation": {
- "allFlagsValid": "所有旗帜都有效",
- "unknownFlags": "未知数: {{flags}}",
- "protectedFlags": "保护:{{flags}}",
+ "allFlagsValid": "所有标志均有效",
+ "unknownFlags": "未知:{{flags}}",
+ "protectedFlags": "密码保护:{{flags}}",
"failed": "验证失败"
},
"placeholders": {
@@ -2343,108 +2343,108 @@
}
},
"processes": {
- "ago": "{{time}}刚才",
- "stoppedAgo": "在{{time}}之前停了下来",
- "running": "运行",
+ "ago": "{{time}} 前",
+ "stoppedAgo": "之前停止过 {{time}}",
+ "running": "运行中",
"stopped": "已停止",
- "stopProcess": "停止进程( SIGTERM)",
+ "stopProcess": "停止进程(SIGTERM)",
"kill": "杀",
"openInBrowser": "在浏览器中打开",
"open": "打开",
- "pid": "密码{{pid}}",
- "title": "CLI 苏维埃社会主义共和国 进程"
+ "pid": "PID{{pid}}",
+ "title": "CLI 进程"
},
"taskActivity": {
- "loadingDetails": "正在装入活动细节...",
- "contextUnavailable": "这项活动不再有详细的笔录背景。",
- "loading": "正在装入任务活动...",
- "lowSignalOnly": "尚未找到关键任务活动 。 低级执行详情见以下\"任务日志流\".",
- "empty": "现有记录稿中尚未找到明确的任务活动。 下面的执行会话中可能还会有旧的或休眠的会话记录。",
+ "loadingDetails": "正在加载活动详细信息…",
+ "contextUnavailable": "此活动不再提供详细的文字记录上下文。",
+ "loading": "正在加载任务活动…",
+ "lowSignalOnly": "尚未找到关键任务活动。下面的任务日志流中提供了低级执行详细信息。",
+ "empty": "在可用的记录中尚未发现明确的任务活动。旧的或启发式会话日志可能仍可在下面的执行会话中使用。",
"title": "任务活动",
- "description": "从元数据记录中与这项任务相关的明确的关键运行时间活动。"
+ "description": "从转录元数据链接到此任务的关键显式运行时活动。"
},
"sendMessage": {
- "title": "发送信件",
- "description": "直接给一名队员发信息",
- "recipientLabel": "收件人",
- "selectMemberPlaceholder": "选择成员...",
- "messageLabel": "消息",
- "placeholder": "写入信件... (输入发送)",
+ "title": "发送消息",
+ "description": "向团队成员发送直接消息。",
+ "recipientLabel": "接受者",
+ "selectMemberPlaceholder": "选择成员…",
+ "messageLabel": "信息",
+ "placeholder": "写下您的信息…(输入以发送)",
"send": "发送",
- "sending": "正在发送...",
- "charsLeft": "{{count}}左边的字符",
+ "sending": "正在发送…",
+ "charsLeft": "{{count}} 剩余字符数",
"saved": "已保存",
"attachments": {
- "teamOnlineRequired": "团队必须在线附加文件",
+ "teamOnlineRequired": "团队必须在线才能附加文件",
"recipientUnsupported": "文件可以发送给团队领导或 OpenCode 队友",
- "openCodeOnlineRequired": "团队必须在线为 OpenCode 队友附加文件",
- "disabledHint": "为在线团队领队和在线OpenCode队友提供文件附件支持. 删除附件或切换收件人。",
- "attachFiles": "附加文件( 粘贴或拖放) (D)",
- "unavailable": "无附件"
+ "openCodeOnlineRequired": "团队必须在线才能为 OpenCode 队友附加文件",
+ "disabledHint": "在线团队领导和在线 OpenCode 队友支持文件附件。删除附件或切换收件人。",
+ "attachFiles": "附加文件(粘贴或拖放)",
+ "unavailable": "附件不可用"
},
"quote": {
- "remove": "删除引用",
- "replyingTo": "答复"
+ "remove": "删除报价",
+ "replyingTo": "正在回复"
}
},
"taskComments": {
"cancelReply": "取消回复",
- "replyingTo": "答复",
- "placeholder": "添加注释... (输入要发送)",
- "attachFile": "附加文件( 或粘贴)",
- "voiceToText": "语音对文本",
- "comment": "注释",
- "charsLeft": "{{count}}左边的字符",
+ "replyingTo": "正在回复",
+ "placeholder": "添加评论…(输入发送)",
+ "attachFile": "附加文件(或粘贴)",
+ "voiceToText": "语音转文字",
+ "comment": "评论",
+ "charsLeft": "{{count}} 剩余字符数",
"saved": "已保存",
- "awaitingReplyFrom": "等待回复",
+ "awaitingReplyFrom": "正在等待回复",
"or": "或"
},
"taskAttachments": {
- "dropImageHere": "在此丢弃图像",
- "attachImage": "附加图像",
+ "dropImageHere": "将图像拖放到此处",
+ "attachImage": "附上图片",
"pasteOrDragDrop": "或粘贴/拖放",
- "fromOriginalMessage": "从原始消息",
- "dropFilesHere": "在此丢弃文件",
- "loading": "正在装入附件..."
+ "fromOriginalMessage": "来自原始消息",
+ "dropFilesHere": "将文件拖放到此处",
+ "loading": "正在加载附件…"
},
"permissions": {
"autoApproveAllTools": "自动批准所有工具",
- "autonomousModeDescription": "自主模式:团队工具未经确认即执行. 谨慎行事,不要相信密码",
- "manualModeDescription": "手动模式:您会实时批准或拒绝每个工具调用."
+ "autonomousModeDescription": "自主模式:团队工具无需确认即可执行。对不受信任的代码要小心。",
+ "manualModeDescription": "手动模式:您将实时批准或拒绝每个工具调用。"
},
"memberLogStream": {
"tabs": {
"execution": "执行",
- "process": "进程"
+ "process": "过程"
},
"filters": {
- "all": "全体"
+ "all": "全部"
},
"logs": {
"title": "日志",
- "loading": "正在装入成员日志流...",
- "emptyTitle": "此成员的日志流条目尚未找到 。",
- "emptyDescription": "成员范围记录或运行时间记录将在此备有。"
+ "loading": "正在加载成员日志流…",
+ "emptyTitle": "尚未找到该成员的日志流条目。",
+ "emptyDescription": "成员范围的记录或运行时日志将在可用时显示在此处。"
}
},
"reviewDialog": {
- "placeholder": "描述需要改变什么... (输入以提交)",
+ "placeholder": "描述需要更改的内容…(输入提交)",
"submit": "提交",
- "charsLeft": "{{count}}左边的字符",
+ "charsLeft": "{{count}} 剩余字符数",
"saved": "已保存",
- "title": "请求更改"
+ "title": "请求变更"
},
"dialogs": {
"actions": {
- "openDashboard": "打开挂板",
- "openTeam": "开放团队",
+ "openDashboard": "打开控制台",
+ "openTeam": "开放组队",
"cancel": "取消"
},
"membersJson": {
"hide": "隐藏 JSON"
},
"optional": {
- "badge": "可选"
+ "badge": "选修的"
}
},
"runningTeams": {
@@ -2454,34 +2454,34 @@
"provisioning": "启动中",
"idle": "运行中"
},
- "noProject": "无项目"
+ "noProject": "没有项目"
},
"layout": {
- "maxPanesReached": "达到的最大 {{count}} 面板"
+ "maxPanesReached": "已达到 {{count}} 窗格的最大值"
},
"codexReconnect": {
- "description": "您的编码会话似乎已停滞 。 重新连接继续 。",
+ "description": "您的 Codex 会话似乎已过时。重新连接以继续。",
"useCode": "使用代码",
- "generating": "正在生成...",
+ "generating": "生成…",
"openLogin": "打开登录",
"generateLink": "生成链接"
},
"effortLevel": {
- "label": "努力级别( 可选)",
- "maxDescription": "马克斯给模型提供了困难任务的最推理时间."
+ "label": "努力程度(可选)",
+ "maxDescription": "Max 为模型提供了完成困难任务最多的推理时间。"
},
"contextLimit": {
- "limitTo200k": "上下文限制为 200K 令牌",
- "always200k": "(这个型号总是200K)",
- "tooltipContent": "支持时, 将发射保留在一个 200K 上下文窗口内 。",
+ "limitTo200k": "将上下文限制为 200K Token",
+ "always200k": "(此模型始终为 200K)",
+ "tooltipContent": "如果支持,将启动保持在 200K Token 上下文窗口内。",
"tooltipTitle": "上下文限制"
},
"roleSelect": {
- "noRole": "无角色",
- "customRole": "自定义角色...",
- "searchPlaceholder": "搜索角色...",
- "empty": "没有找到角色 。",
- "reservedRole": "这个角色是保留下来的",
+ "noRole": "没有角色",
+ "customRole": "自定义角色…",
+ "searchPlaceholder": "搜索角色…",
+ "empty": "未找到角色。",
+ "reservedRole": "该角色已保留",
"emptyCustomRole": "角色不能为空"
}
}
diff --git a/src/features/member-log-stream/renderer/ui/MemberRuntimeProcessLogsPanel.tsx b/src/features/member-log-stream/renderer/ui/MemberRuntimeProcessLogsPanel.tsx
index 7559057b..6f7ce81d 100644
--- a/src/features/member-log-stream/renderer/ui/MemberRuntimeProcessLogsPanel.tsx
+++ b/src/features/member-log-stream/renderer/ui/MemberRuntimeProcessLogsPanel.tsx
@@ -34,12 +34,20 @@ function formatBytes(bytes: number | undefined): string {
return `${mb.toFixed(mb >= 10 ? 0 : 1)} MB`;
}
-function buildStatusText(log: MemberRuntimeLogTailResponse | null): string | null {
+function buildStatusText(
+ log: MemberRuntimeLogTailResponse | null,
+ labels: {
+ empty: string;
+ fileEmpty: string;
+ showingLast: (bytes: string) => string;
+ showing: (bytes: string) => string;
+ }
+): string | null {
if (!log) return null;
- if (log.missing) return 'No process log file captured for this member yet.';
- if (!log.content) return 'Process log file is empty.';
- if (log.truncated) return `Showing last ${formatBytes(log.bytesRead)}.`;
- return `Showing ${formatBytes(log.bytesRead)}.`;
+ if (log.missing) return labels.empty;
+ if (!log.content) return labels.fileEmpty;
+ if (log.truncated) return labels.showingLast(formatBytes(log.bytesRead));
+ return labels.showing(formatBytes(log.bytesRead));
}
function ProcessLogKindTabs({
@@ -204,7 +212,12 @@ export function MemberRuntimeProcessLogsPanel({
}
}, [log?.content]);
- const statusText = buildStatusText(log);
+ const statusText = buildStatusText(log, {
+ empty: t('members.runtimeLogs.empty'),
+ fileEmpty: t('members.runtimeLogs.fileEmpty'),
+ showingLast: (bytes) => t('members.runtimeLogs.showingLast', { bytes }),
+ showing: (bytes) => t('members.runtimeLogs.showing', { bytes }),
+ });
const hasContent = Boolean(log?.content);
return (
@@ -252,7 +265,7 @@ export function MemberRuntimeProcessLogsPanel({
disabled={!hasContent}
>
{copied ? : }
- {copied ? 'Copied' : 'Copy'}
+ {copied ? tCommon('actions.copied') : t('members.runtimeLogs.copy')}
diff --git a/src/features/member-work-sync/core/application/MemberWorkSyncDiagnosticsReader.ts b/src/features/member-work-sync/core/application/MemberWorkSyncDiagnosticsReader.ts
index c329abe1..748cf1c8 100644
--- a/src/features/member-work-sync/core/application/MemberWorkSyncDiagnosticsReader.ts
+++ b/src/features/member-work-sync/core/application/MemberWorkSyncDiagnosticsReader.ts
@@ -1,6 +1,9 @@
import { decideMemberWorkSyncStatus } from '../domain';
-import { finalizeMemberWorkSyncAgenda } from './MemberWorkSyncReconciler';
+import {
+ attachMemberWorkSyncReportToken,
+ finalizeMemberWorkSyncAgenda,
+} from './MemberWorkSyncReconciler';
import { resolveMemberWorkSyncRuntimeActivity } from './MemberWorkSyncRuntimeActivity';
import type { MemberWorkSyncStatus, MemberWorkSyncStatusRequest } from '../../contracts';
@@ -28,7 +31,7 @@ export class MemberWorkSyncDiagnosticsReader {
inactive: source.inactive || runtimeActivity.inactive,
});
- return {
+ return attachMemberWorkSyncReportToken(this.deps, {
teamName: agenda.teamName,
memberName: agenda.memberName,
state: decision.state,
@@ -46,6 +49,6 @@ export class MemberWorkSyncDiagnosticsReader {
'status_snapshot_not_persisted',
],
...(source.providerId ? { providerId: source.providerId } : {}),
- };
+ });
}
}
diff --git a/src/features/member-work-sync/core/application/MemberWorkSyncNudgeOutboxPlanner.ts b/src/features/member-work-sync/core/application/MemberWorkSyncNudgeOutboxPlanner.ts
index 310c2f14..4b2caad8 100644
--- a/src/features/member-work-sync/core/application/MemberWorkSyncNudgeOutboxPlanner.ts
+++ b/src/features/member-work-sync/core/application/MemberWorkSyncNudgeOutboxPlanner.ts
@@ -155,6 +155,21 @@ function shouldPlanDeliveredStillStuckRecovery(input: {
);
}
+function shouldRepairDeliveredAgendaSyncNudge(input: {
+ status: MemberWorkSyncStatus;
+ requestedInput: MemberWorkSyncOutboxEnsureInput;
+ existingItem: MemberWorkSyncOutboxItem;
+}): boolean {
+ return (
+ input.status.state === 'needs_sync' &&
+ input.requestedInput.payload.workSyncIntent === 'agenda_sync' &&
+ input.existingItem.status === 'delivered' &&
+ input.existingItem.agendaFingerprint === input.requestedInput.agendaFingerprint &&
+ input.existingItem.payloadHash === input.requestedInput.payloadHash &&
+ !hasActiveAcceptedWorkLease(input.status)
+ );
+}
+
function isOutboxItemAwaitingDelivery(item: MemberWorkSyncOutboxItem): boolean {
return item.status !== 'delivered' && item.status !== 'failed_terminal';
}
@@ -296,6 +311,7 @@ export class MemberWorkSyncNudgeOutboxPlanner {
await this.appendPlanAudit(status, { planned: false, code: 'payload_conflict' });
return { planned: false, code: 'payload_conflict' };
}
+ await this.repairDeliveredAgendaSyncNudgeIfNeeded(status, recoveryInput, recoveryResult.item);
if (activationReason) {
const deliveredStillStuckRecovery = await this.planDeliveredStillStuckRecovery(
@@ -371,6 +387,7 @@ export class MemberWorkSyncNudgeOutboxPlanner {
await this.appendPlanAudit(status, { planned: false, code: 'payload_conflict' });
return { planned: false, code: 'payload_conflict' };
}
+ await this.repairDeliveredAgendaSyncNudgeIfNeeded(status, recoveryInput, recoveryResult.item);
const recoveryPlanned = isOutboxItemAwaitingDelivery(recoveryResult.item);
const recoveryPlanResult = {
@@ -491,6 +508,11 @@ export class MemberWorkSyncNudgeOutboxPlanner {
await this.appendPlanAudit(status, { planned: false, code: 'payload_conflict' });
return { planned: false, code: 'payload_conflict' };
}
+ await this.repairDeliveredAgendaSyncNudgeIfNeeded(
+ status,
+ recoveryInput,
+ recoveryResult.item
+ );
if (
shouldPlanStatusOnlyRecovery({
status,
@@ -544,6 +566,7 @@ export class MemberWorkSyncNudgeOutboxPlanner {
await this.appendPlanAudit(status, { planned: false, code });
return { planned: false, code };
}
+ await this.repairDeliveredAgendaSyncNudgeIfNeeded(status, input, result.item);
if (
shouldPlanStatusOnlyRecovery({
status,
@@ -580,6 +603,37 @@ export class MemberWorkSyncNudgeOutboxPlanner {
return planResult;
}
+ private async repairDeliveredAgendaSyncNudgeIfNeeded(
+ status: MemberWorkSyncStatus,
+ requestedInput: MemberWorkSyncOutboxEnsureInput,
+ existingItem: MemberWorkSyncOutboxItem
+ ): Promise {
+ const inboxNudge = this.deps.inboxNudge;
+ if (
+ !inboxNudge?.repairIfPresent ||
+ !shouldRepairDeliveredAgendaSyncNudge({ status, requestedInput, existingItem })
+ ) {
+ return;
+ }
+
+ try {
+ await inboxNudge.repairIfPresent({
+ teamName: status.teamName,
+ memberName: status.memberName,
+ messageId: existingItem.deliveredMessageId ?? existingItem.id,
+ payloadHash: existingItem.payloadHash,
+ payload: existingItem.payload,
+ });
+ } catch (error) {
+ this.deps.logger?.warn('member work sync delivered nudge repair failed', {
+ teamName: status.teamName,
+ memberName: status.memberName,
+ outboxId: existingItem.id,
+ error: String(error),
+ });
+ }
+ }
+
private async appendReviewPickupEscalationAudit(
status: MemberWorkSyncStatus,
reason: string
diff --git a/src/features/member-work-sync/core/application/MemberWorkSyncPendingReportIntentReplayer.ts b/src/features/member-work-sync/core/application/MemberWorkSyncPendingReportIntentReplayer.ts
index a878e463..d1913dc9 100644
--- a/src/features/member-work-sync/core/application/MemberWorkSyncPendingReportIntentReplayer.ts
+++ b/src/features/member-work-sync/core/application/MemberWorkSyncPendingReportIntentReplayer.ts
@@ -1,6 +1,10 @@
import { MemberWorkSyncReporter } from './MemberWorkSyncReporter';
-import type { MemberWorkSyncReportIntentStatus } from '../../contracts';
+import type {
+ MemberWorkSyncReportIntent,
+ MemberWorkSyncReportIntentStatus,
+ MemberWorkSyncReportResult,
+} from '../../contracts';
import type { MemberWorkSyncUseCaseDeps } from './ports';
export interface MemberWorkSyncPendingReportReplaySummary {
@@ -52,10 +56,7 @@ export class MemberWorkSyncPendingReportIntentReplayer {
let status: MemberWorkSyncReportIntentStatus = 'rejected';
let resultCode = 'replay_failed';
try {
- const result = await this.reporter.execute({
- ...intent.request,
- source: intent.request.source ?? 'mcp',
- });
+ const result = await this.executeReplay(intent);
status = statusForResult(result);
resultCode = result.code;
} catch (error) {
@@ -83,4 +84,56 @@ export class MemberWorkSyncPendingReportIntentReplayer {
return summary;
}
+
+ private async executeReplay(
+ intent: MemberWorkSyncReportIntent
+ ): Promise {
+ const result = await this.reporter.execute({
+ ...intent.request,
+ source: intent.request.source ?? 'mcp',
+ });
+ const freshToken = await this.getFreshTokenForExpiredFallbackReport(intent, result);
+ if (!freshToken) {
+ return result;
+ }
+ return this.reporter.execute({
+ ...intent.request,
+ agendaFingerprint: freshToken.agendaFingerprint,
+ reportToken: freshToken.reportToken,
+ source: intent.request.source ?? 'mcp',
+ });
+ }
+
+ private async getFreshTokenForExpiredFallbackReport(
+ intent: MemberWorkSyncReportIntent,
+ result: MemberWorkSyncReportResult
+ ): Promise<{ agendaFingerprint: string; reportToken: string } | null> {
+ if (
+ result.accepted ||
+ result.code !== 'invalid_report_token' ||
+ intent.reason !== 'control_api_unavailable' ||
+ !intent.request.reportToken ||
+ !result.status.reportToken ||
+ result.status.agenda.fingerprint !== intent.request.agendaFingerprint ||
+ !this.deps.reportToken
+ ) {
+ return null;
+ }
+
+ const validation = await this.deps.reportToken.verify({
+ token: intent.request.reportToken,
+ teamName: result.status.teamName,
+ memberName: result.status.memberName,
+ agendaFingerprint: result.status.agenda.fingerprint,
+ nowIso: this.deps.clock.now().toISOString(),
+ });
+ if (validation.ok || validation.reason !== 'expired') {
+ return null;
+ }
+
+ return {
+ agendaFingerprint: result.status.agenda.fingerprint,
+ reportToken: result.status.reportToken,
+ };
+ }
}
diff --git a/src/features/member-work-sync/core/application/ports.ts b/src/features/member-work-sync/core/application/ports.ts
index 58400bce..a7ac4a5f 100644
--- a/src/features/member-work-sync/core/application/ports.ts
+++ b/src/features/member-work-sync/core/application/ports.ts
@@ -190,6 +190,13 @@ export interface MemberWorkSyncInboxNudgePort {
payload: MemberWorkSyncOutboxItem['payload'];
timestamp: string;
}): Promise<{ inserted: boolean; messageId: string; conflict?: boolean }>;
+ repairIfPresent?(input: {
+ teamName: string;
+ memberName: string;
+ messageId: string;
+ payloadHash: string;
+ payload: MemberWorkSyncOutboxItem['payload'];
+ }): Promise<{ found: boolean; repaired: boolean; conflict?: boolean }>;
}
export interface MemberWorkSyncWatchdogCooldownPort {
diff --git a/src/features/member-work-sync/main/adapters/output/TeamInboxMemberWorkSyncNudgeSink.ts b/src/features/member-work-sync/main/adapters/output/TeamInboxMemberWorkSyncNudgeSink.ts
index d3c0c949..f3447230 100644
--- a/src/features/member-work-sync/main/adapters/output/TeamInboxMemberWorkSyncNudgeSink.ts
+++ b/src/features/member-work-sync/main/adapters/output/TeamInboxMemberWorkSyncNudgeSink.ts
@@ -3,24 +3,48 @@ import { TeamInboxWriter } from '@main/services/team/TeamInboxWriter';
import type { MemberWorkSyncInboxNudgePort } from '../../../core/application';
+type TeamInboxMemberWorkSyncNudgeInput = Parameters<
+ MemberWorkSyncInboxNudgePort['insertIfAbsent']
+>[0];
+type TeamInboxMemberWorkSyncNudgeRepairInput = Parameters<
+ NonNullable
+>[0];
+
+type TeamInboxMemberWorkSyncNudgeWriter = Pick &
+ Partial>;
+
+function isStoredMemberWorkSyncNudge(
+ message: Awaited>[number]
+): boolean {
+ return message.messageKind === 'member_work_sync_nudge';
+}
+
export class TeamInboxMemberWorkSyncNudgeSink implements MemberWorkSyncInboxNudgePort {
constructor(
private readonly inboxReader: Pick = new TeamInboxReader(),
- private readonly inboxWriter: Pick = new TeamInboxWriter(),
+ private readonly inboxWriter: TeamInboxMemberWorkSyncNudgeWriter = new TeamInboxWriter(),
private readonly controlUrlResolver?: () => Promise | string | null
) {}
- async insertIfAbsent(input: Parameters[0]) {
+ async insertIfAbsent(input: TeamInboxMemberWorkSyncNudgeInput) {
const existing = await this.inboxReader.getMessagesFor(input.teamName, input.memberName);
const existingMessage = existing.find((message) => message.messageId === input.messageId);
if (existingMessage) {
- if (existingMessage.workSyncPayloadHash !== input.payloadHash) {
+ if (
+ existingMessage.workSyncPayloadHash !== input.payloadHash ||
+ !isStoredMemberWorkSyncNudge(existingMessage)
+ ) {
return { inserted: false, messageId: input.messageId, conflict: true };
}
+ await this.repairExistingControlUrlIfNeeded(input, existingMessage.text, {
+ required: Boolean(this.controlUrlResolver),
+ });
return { inserted: false, messageId: input.messageId };
}
- const controlUrl = await this.resolveControlUrl();
+ const controlUrl = await this.resolveControlUrl({
+ required: Boolean(this.controlUrlResolver),
+ });
const text = controlUrl
? this.withControlUrl(input.payload.text, controlUrl)
: input.payload.text;
@@ -48,27 +72,89 @@ export class TeamInboxMemberWorkSyncNudgeSink implements MemberWorkSyncInboxNudg
};
}
- private async resolveControlUrl(): Promise {
+ async repairIfPresent(input: TeamInboxMemberWorkSyncNudgeRepairInput) {
+ const existing = await this.inboxReader.getMessagesFor(input.teamName, input.memberName);
+ const existingMessage = existing.find((message) => message.messageId === input.messageId);
+ if (!existingMessage) {
+ return { found: false, repaired: false };
+ }
+ if (
+ existingMessage.workSyncPayloadHash !== input.payloadHash ||
+ !isStoredMemberWorkSyncNudge(existingMessage)
+ ) {
+ return { found: true, repaired: false, conflict: true };
+ }
+ const repaired = await this.repairExistingControlUrlIfNeeded(input, existingMessage.text, {
+ required: Boolean(this.controlUrlResolver),
+ });
+ return { found: true, repaired };
+ }
+
+ private async repairExistingControlUrlIfNeeded(
+ input: TeamInboxMemberWorkSyncNudgeRepairInput,
+ existingText: string | undefined,
+ options: { required?: boolean } = {}
+ ): Promise {
+ const controlUrl = await this.resolveControlUrl(options);
+ if (!controlUrl) {
+ return false;
+ }
+ const currentText = existingText ?? input.payload.text;
+ const repairedText = this.withControlUrl(currentText, controlUrl);
+ if (repairedText === currentText) {
+ return false;
+ }
+ if (typeof this.inboxWriter.updateMessageText !== 'function') {
+ if (options.required) {
+ throw new Error('member work sync inbox text update unavailable');
+ }
+ return false;
+ }
+ const result = await this.inboxWriter.updateMessageText(input.teamName, {
+ member: input.memberName,
+ messageId: input.messageId,
+ text: repairedText,
+ expectedMessageKind: 'member_work_sync_nudge',
+ expectedWorkSyncPayloadHash: input.payloadHash,
+ });
+ return result.updated;
+ }
+
+ private async resolveControlUrl(options: { required?: boolean } = {}): Promise {
if (!this.controlUrlResolver) {
return null;
}
+ let value: string | null | undefined;
try {
- const value = await this.controlUrlResolver();
- const trimmed = value?.trim();
- return trimmed ? trimmed : null;
- } catch {
+ value = await this.controlUrlResolver();
+ } catch (error) {
+ if (options.required) {
+ throw new Error(`member work sync control URL unavailable: ${String(error)}`);
+ }
return null;
}
+
+ const trimmed = value?.trim();
+ if (trimmed) {
+ return trimmed;
+ }
+ if (options.required) {
+ throw new Error('member work sync control URL unavailable');
+ }
+ return null;
}
private withControlUrl(text: string, controlUrl: string): string {
- if (text.includes('controlUrl')) {
+ const controlLine = `Required control API: pass controlUrl "${controlUrl}" in both member_work_sync_status and member_work_sync_report.`;
+ const existingControlLine =
+ /^Required control API: pass controlUrl "[^"\n]+" in both member_work_sync_status and member_work_sync_report\.$/m;
+ if (existingControlLine.test(text)) {
+ return text.replace(existingControlLine, controlLine);
+ }
+ if (text.includes(`controlUrl "${controlUrl}"`)) {
return text;
}
- return [
- text,
- `Required control API: pass controlUrl "${controlUrl}" in both member_work_sync_status and member_work_sync_report.`,
- ].join('\n');
+ return [text, controlLine].join('\n');
}
}
diff --git a/src/features/member-work-sync/main/adapters/output/TeamRuntimeTurnSettledTargetResolver.ts b/src/features/member-work-sync/main/adapters/output/TeamRuntimeTurnSettledTargetResolver.ts
index 87c31205..1a4dfb29 100644
--- a/src/features/member-work-sync/main/adapters/output/TeamRuntimeTurnSettledTargetResolver.ts
+++ b/src/features/member-work-sync/main/adapters/output/TeamRuntimeTurnSettledTargetResolver.ts
@@ -7,6 +7,8 @@ import path from 'path';
import { isReservedMemberName, normalizeMemberName } from '../../../core/domain';
+import { mergeTeamMembers } from './mergeTeamMembers';
+
import type {
RuntimeTurnSettledTargetResolution,
RuntimeTurnSettledTargetResolverPort,
@@ -14,7 +16,7 @@ import type {
import type { RuntimeTurnSettledEvent } from '../../../core/domain';
import type { TeamConfigReader } from '@main/services/team/TeamConfigReader';
import type { TeamMembersMetaStore } from '@main/services/team/TeamMembersMetaStore';
-import type { TeamMember, TeamSummary } from '@shared/types';
+import type { TeamMember, TeamProviderId, TeamSummary } from '@shared/types';
export interface RuntimeTurnSettledTeamSource {
listTeams(): Promise;
@@ -38,26 +40,21 @@ function memberKey(member: Pick): string {
return normalizeMemberName(member.name);
}
-function mergeMembers(configMembers: TeamMember[], metaMembers: TeamMember[]): TeamMember[] {
- const byName = new Map();
- for (const member of configMembers) {
- const key = memberKey(member);
- if (key) {
- byName.set(key, member);
- }
+function providerIdFromBackend(providerBackendId: unknown): TeamProviderId | undefined {
+ const normalized = typeof providerBackendId === 'string' ? providerBackendId.trim() : '';
+ if (normalized === 'codex-native') {
+ return 'codex';
}
- for (const member of metaMembers) {
- const key = memberKey(member);
- if (key) {
- byName.set(key, { ...byName.get(key), ...member });
- }
+ if (normalized === 'opencode-cli') {
+ return 'opencode';
}
- return [...byName.values()];
+ return undefined;
}
-function providerForMember(member: TeamMember | undefined): string | undefined {
+function providerForMember(member: TeamMember | undefined): TeamProviderId | undefined {
return (
normalizeOptionalTeamProviderId(member?.providerId) ??
+ providerIdFromBackend(member?.providerBackendId) ??
inferTeamProviderIdFromModel(member?.model)
);
}
@@ -202,7 +199,7 @@ export class TeamRuntimeTurnSettledTargetResolver implements RuntimeTurnSettledT
const normalizedTarget = normalizeMemberName(memberName);
return (
- mergeMembers(config.members ?? [], metaMembers).find(
+ mergeTeamMembers(config.members ?? [], metaMembers).find(
(member) => !member.removedAt && memberKey(member) === normalizedTarget
) ?? null
);
diff --git a/src/features/member-work-sync/main/adapters/output/TeamTaskAgendaSource.ts b/src/features/member-work-sync/main/adapters/output/TeamTaskAgendaSource.ts
index 5550c5d5..cadd50ec 100644
--- a/src/features/member-work-sync/main/adapters/output/TeamTaskAgendaSource.ts
+++ b/src/features/member-work-sync/main/adapters/output/TeamTaskAgendaSource.ts
@@ -10,6 +10,8 @@ import {
normalizeMemberName,
} from '../../../core/domain';
+import { mergeTeamMembers } from './mergeTeamMembers';
+
import type {
MemberWorkSyncAgendaSourcePort,
MemberWorkSyncAgendaSourceResult,
@@ -19,7 +21,7 @@ import type { TeamConfigReader } from '@main/services/team/TeamConfigReader';
import type { TeamKanbanManager } from '@main/services/team/TeamKanbanManager';
import type { TeamMembersMetaStore } from '@main/services/team/TeamMembersMetaStore';
import type { TeamTaskReader } from '@main/services/team/TeamTaskReader';
-import type { TeamMember } from '@shared/types';
+import type { TeamMember, TeamProviderId } from '@shared/types';
export interface TeamTaskAgendaSourceDeps {
configReader: Pick;
@@ -34,26 +36,21 @@ function memberKey(member: Pick): string {
return normalizeMemberName(member.name);
}
-function mergeMembers(configMembers: TeamMember[], metaMembers: TeamMember[]): TeamMember[] {
- const byName = new Map();
- for (const member of configMembers) {
- const key = memberKey(member);
- if (key) {
- byName.set(key, member);
- }
+function providerIdFromBackend(providerBackendId: unknown): TeamProviderId | undefined {
+ const normalized = typeof providerBackendId === 'string' ? providerBackendId.trim() : '';
+ if (normalized === 'codex-native') {
+ return 'codex';
}
- for (const member of metaMembers) {
- const key = memberKey(member);
- if (key) {
- byName.set(key, { ...byName.get(key), ...member });
- }
+ if (normalized === 'opencode-cli') {
+ return 'opencode';
}
- return [...byName.values()];
+ return undefined;
}
function toMemberLike(member: TeamMember): MemberWorkSyncMemberLike {
const providerId =
normalizeOptionalTeamProviderId(member.providerId) ??
+ providerIdFromBackend(member.providerBackendId) ??
inferTeamProviderIdFromModel(member.model);
return {
name: member.name,
@@ -74,7 +71,7 @@ export class TeamTaskAgendaSource implements MemberWorkSyncAgendaSourcePort {
}
const metaMembers = await this.deps.membersMetaStore.getMembers(teamName);
- return mergeMembers(config.members ?? [], metaMembers)
+ return mergeTeamMembers(config.members ?? [], metaMembers)
.filter((member) => !member.removedAt)
.map((member) => normalizeMemberName(member.name))
.filter((memberName) => memberName.length > 0 && !isReservedMemberName(memberName))
@@ -107,7 +104,7 @@ export class TeamTaskAgendaSource implements MemberWorkSyncAgendaSourcePort {
this.deps.kanbanManager.getState(input.teamName),
this.deps.membersMetaStore.getMembers(input.teamName),
]);
- const members = mergeMembers(config.members ?? [], metaMembers);
+ const members = mergeTeamMembers(config.members ?? [], metaMembers);
const activeMemberNames = members
.filter((member) => !member.removedAt)
.map((member) => normalizeMemberName(member.name))
@@ -116,6 +113,7 @@ export class TeamTaskAgendaSource implements MemberWorkSyncAgendaSourcePort {
const member = members.find((candidate) => memberKey(candidate) === normalizedMemberName);
const providerId =
normalizeOptionalTeamProviderId(member?.providerId) ??
+ providerIdFromBackend(member?.providerBackendId) ??
inferTeamProviderIdFromModel(member?.model);
const agenda = buildActionableWorkAgenda({
diff --git a/src/features/member-work-sync/main/adapters/output/TeamTaskStallJournalWorkSyncCooldown.ts b/src/features/member-work-sync/main/adapters/output/TeamTaskStallJournalWorkSyncCooldown.ts
index c5dd44b4..654519db 100644
--- a/src/features/member-work-sync/main/adapters/output/TeamTaskStallJournalWorkSyncCooldown.ts
+++ b/src/features/member-work-sync/main/adapters/output/TeamTaskStallJournalWorkSyncCooldown.ts
@@ -12,7 +12,10 @@ interface StallJournalEntry {
alertedAt?: string;
}
-type WatchdogCooldownResult = { active: boolean; retryAfterIso?: string };
+interface WatchdogCooldownResult {
+ active: boolean;
+ retryAfterIso?: string;
+}
function parseTime(value: string | undefined): number | null {
if (!value) {
diff --git a/src/features/member-work-sync/main/adapters/output/mergeTeamMembers.ts b/src/features/member-work-sync/main/adapters/output/mergeTeamMembers.ts
new file mode 100644
index 00000000..2ae3dbc1
--- /dev/null
+++ b/src/features/member-work-sync/main/adapters/output/mergeTeamMembers.ts
@@ -0,0 +1,110 @@
+import {
+ inferTeamProviderIdFromModel,
+ normalizeOptionalTeamProviderId,
+} from '@shared/utils/teamProvider';
+
+import { normalizeMemberName } from '../../../core/domain';
+
+import type { TeamMember, TeamProviderId } from '@shared/types';
+
+function memberKey(member: Pick): string {
+ return normalizeMemberName(member.name);
+}
+
+const PROVIDER_SCOPED_MEMBER_FIELDS = new Set([
+ 'providerId',
+ 'providerBackendId',
+ 'model',
+ 'effort',
+ 'fastMode',
+]);
+
+const PROVIDER_SETTING_MEMBER_FIELDS = new Set(['effort', 'fastMode']);
+
+function hasProviderIdentityFields(member: TeamMember | undefined): boolean {
+ return providerIdForMember(member) !== undefined;
+}
+
+function inferProviderIdFromBackend(providerBackendId: unknown): TeamProviderId | undefined {
+ const normalized = typeof providerBackendId === 'string' ? providerBackendId.trim() : '';
+ if (normalized === 'codex-native') {
+ return 'codex';
+ }
+ if (normalized === 'opencode-cli') {
+ return 'opencode';
+ }
+ return undefined;
+}
+
+function providerIdForMember(member: TeamMember | undefined): TeamProviderId | undefined {
+ return (
+ normalizeOptionalTeamProviderId(member?.providerId) ??
+ inferProviderIdFromBackend(member?.providerBackendId) ??
+ inferTeamProviderIdFromModel(member?.model)
+ );
+}
+
+function shouldPreserveBaseProviderScopedField(
+ base: TeamMember | undefined,
+ key: keyof TeamMember
+): boolean {
+ if (!base || !PROVIDER_SCOPED_MEMBER_FIELDS.has(key)) {
+ return false;
+ }
+ if (hasProviderIdentityFields(base)) {
+ return true;
+ }
+ return PROVIDER_SETTING_MEMBER_FIELDS.has(key) && base[key] !== undefined;
+}
+
+function mergeDefinedMemberFields(base: TeamMember | undefined, overlay: TeamMember): TeamMember {
+ const merged: TeamMember = { ...(base ?? { name: overlay.name }) };
+ const overlayProviderId = normalizeOptionalTeamProviderId(overlay.providerId);
+ const overlayHasProviderId = overlayProviderId !== undefined;
+ const baseProviderId = providerIdForMember(base);
+ const providerChanged =
+ overlayHasProviderId && baseProviderId !== undefined && overlayProviderId !== baseProviderId;
+ if (providerChanged) {
+ for (const key of PROVIDER_SCOPED_MEMBER_FIELDS) {
+ delete merged[key];
+ }
+ }
+ for (const [key, value] of Object.entries(overlay) as [
+ keyof TeamMember,
+ TeamMember[keyof TeamMember],
+ ][]) {
+ if (value !== undefined) {
+ if (!overlayHasProviderId && shouldPreserveBaseProviderScopedField(base, key)) {
+ continue;
+ }
+ merged[key] = value as never;
+ }
+ }
+ if (
+ Object.prototype.hasOwnProperty.call(overlay, 'removedAt') &&
+ overlay.removedAt === undefined
+ ) {
+ delete merged.removedAt;
+ }
+ return merged;
+}
+
+export function mergeTeamMembers(
+ configMembers: TeamMember[],
+ metaMembers: TeamMember[]
+): TeamMember[] {
+ const byName = new Map();
+ for (const member of configMembers) {
+ const key = memberKey(member);
+ if (key) {
+ byName.set(key, member);
+ }
+ }
+ for (const member of metaMembers) {
+ const key = memberKey(member);
+ if (key) {
+ byName.set(key, mergeDefinedMemberFields(byName.get(key), member));
+ }
+ }
+ return [...byName.values()];
+}
diff --git a/src/features/member-work-sync/main/composition/__tests__/memberWorkSyncTeamActivity.test.ts b/src/features/member-work-sync/main/composition/__tests__/memberWorkSyncTeamActivity.test.ts
index 7079c5e2..51b543a9 100644
--- a/src/features/member-work-sync/main/composition/__tests__/memberWorkSyncTeamActivity.test.ts
+++ b/src/features/member-work-sync/main/composition/__tests__/memberWorkSyncTeamActivity.test.ts
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
import {
hasUncertainWorkSyncRuntimeActivity,
hasWorkSyncActiveRuntime,
+ hasWorkSyncReachableRuntime,
isRuntimeEntryActiveForWorkSync,
isRuntimeMemberActiveForWorkSync,
isRuntimeMemberActivityUncertainForWorkSync,
@@ -87,6 +88,60 @@ describe('member work sync team activity', () => {
).toBe(false);
});
+ it('does not treat lead process evidence as active for ordinary teammates', () => {
+ for (const livenessKind of [undefined, 'runtime_process', 'confirmed_bootstrap'] as const) {
+ const snapshot = createRuntimeSnapshot({
+ alice: createRuntimeEntry({
+ memberName: 'alice',
+ backendType: 'process',
+ livenessKind,
+ pidSource: 'lead_process',
+ }),
+ });
+
+ expect(isRuntimeEntryActiveForWorkSync(snapshot.members.alice)).toBe(false);
+ expect(hasWorkSyncActiveRuntime(snapshot)).toBe(false);
+ expect(hasWorkSyncReachableRuntime(snapshot)).toBe(false);
+ expect(isRuntimeMemberActiveForWorkSync(snapshot, 'alice')).toBe(false);
+ }
+ });
+
+ it('keeps active lead processes reachable for targeted lead work-sync', () => {
+ const snapshot = createRuntimeSnapshot({
+ 'team-lead': createRuntimeEntry({
+ memberName: 'team-lead',
+ backendType: 'lead',
+ livenessKind: undefined,
+ pidSource: 'lead_process',
+ }),
+ alice: createRuntimeEntry({
+ memberName: 'alice',
+ alive: false,
+ livenessKind: 'stale_metadata',
+ }),
+ });
+
+ expect(hasWorkSyncActiveRuntime(snapshot)).toBe(false);
+ expect(hasWorkSyncReachableRuntime(snapshot)).toBe(true);
+ expect(isRuntimeMemberActiveForWorkSync(snapshot, 'team-lead')).toBe(true);
+ expect(isRuntimeMemberActiveForWorkSync(snapshot, 'alice')).toBe(false);
+ });
+
+ it('keeps ordinary teammates named lead active from normal agent process evidence', () => {
+ const snapshot = createRuntimeSnapshot({
+ lead: createRuntimeEntry({
+ memberName: 'lead',
+ backendType: 'process',
+ livenessKind: 'confirmed_bootstrap',
+ pidSource: 'agent_process_table',
+ }),
+ });
+
+ expect(hasWorkSyncActiveRuntime(snapshot)).toBe(true);
+ expect(hasWorkSyncReachableRuntime(snapshot)).toBe(true);
+ expect(isRuntimeMemberActiveForWorkSync(snapshot, 'lead')).toBe(true);
+ });
+
it('does not treat inactive liveness diagnostics as active by themselves', () => {
for (const livenessKind of [
'permission_blocked',
diff --git a/src/features/member-work-sync/main/composition/createMemberWorkSyncFeature.ts b/src/features/member-work-sync/main/composition/createMemberWorkSyncFeature.ts
index c617e653..06ef962e 100644
--- a/src/features/member-work-sync/main/composition/createMemberWorkSyncFeature.ts
+++ b/src/features/member-work-sync/main/composition/createMemberWorkSyncFeature.ts
@@ -88,6 +88,22 @@ function getAcceptedWorkLeaseStaleness(
return reportExpiresAtMs <= nowMs ? 'expired' : null;
}
+function getReportTokenStaleness(
+ status: MemberWorkSyncStatus,
+ nowMs: number
+): 'missing' | 'expired' | null {
+ if (!status.reportToken?.trim()) {
+ return 'missing';
+ }
+
+ const tokenExpiresAtMs = Date.parse(status.reportTokenExpiresAt ?? '');
+ if (!Number.isFinite(tokenExpiresAtMs) || !Number.isFinite(nowMs)) {
+ return 'missing';
+ }
+
+ return tokenExpiresAtMs <= nowMs ? 'expired' : null;
+}
+
function isEmptyAgendaStaleState(status: MemberWorkSyncStatus): boolean {
return (
status.agenda.items.length === 0 &&
@@ -99,6 +115,10 @@ function isEmptyAgendaStaleState(status: MemberWorkSyncStatus): boolean {
}
function statusNeedsBackgroundRefresh(status: MemberWorkSyncStatus, nowMs: number): boolean {
+ if (getReportTokenStaleness(status, nowMs) !== null) {
+ return true;
+ }
+
if (isEmptyAgendaStaleState(status)) {
return true;
}
@@ -125,6 +145,13 @@ function statusNeedsBackgroundRefresh(status: MemberWorkSyncStatus, nowMs: numbe
function getStatusStalenessDiagnostics(status: MemberWorkSyncStatus, nowMs: number): string[] {
const diagnostics: string[] = [];
+ const tokenStaleness = getReportTokenStaleness(status, nowMs);
+ if (tokenStaleness === 'missing') {
+ diagnostics.push('report_token_missing_refresh_enqueued');
+ } else if (tokenStaleness === 'expired') {
+ diagnostics.push('report_token_expired_refresh_enqueued');
+ }
+
const evaluatedAtMs = Date.parse(status.evaluatedAt);
if (!Number.isFinite(evaluatedAtMs)) {
diagnostics.push('status_evaluated_at_invalid');
@@ -150,6 +177,12 @@ function getStatusStalenessDiagnostics(status: MemberWorkSyncStatus, nowMs: numb
return [...new Set(diagnostics)];
}
+function shouldRefreshStatusSynchronously(stalenessDiagnostics: string[]): boolean {
+ return stalenessDiagnostics.some(
+ (diagnostic) => diagnostic !== 'caught_up_stale_refresh_enqueued'
+ );
+}
+
export function buildMemberWorkSyncRuntimeTurnSettledEnvironment(input: {
teamsBasePath: string;
provider: RuntimeTurnSettledProvider;
@@ -505,6 +538,21 @@ export function createMemberWorkSyncFeature(deps: {
if (stalenessDiagnostics.length === 0) {
return status;
}
+ if (shouldRefreshStatusSynchronously(stalenessDiagnostics)) {
+ try {
+ return await reconciler.execute(request, {
+ reconciledBy: 'request',
+ triggerReasons: ['manual_refresh'],
+ });
+ } catch (error) {
+ deps.logger?.warn('member work sync synchronous status refresh failed', {
+ teamName: status.teamName,
+ memberName: status.memberName,
+ diagnostics: stalenessDiagnostics,
+ error: String(error),
+ });
+ }
+ }
queue.enqueue({
teamName: status.teamName,
memberName: status.memberName,
diff --git a/src/features/member-work-sync/main/composition/memberWorkSyncTeamActivity.ts b/src/features/member-work-sync/main/composition/memberWorkSyncTeamActivity.ts
index a739f201..e4672d97 100644
--- a/src/features/member-work-sync/main/composition/memberWorkSyncTeamActivity.ts
+++ b/src/features/member-work-sync/main/composition/memberWorkSyncTeamActivity.ts
@@ -26,11 +26,46 @@ const WORK_SYNC_BOOTSTRAP_ONLY_PID_SOURCES = new Set(
'persisted_metadata',
]);
-const WORK_SYNC_CONFIRMED_BOOTSTRAP_ACTIVE_PID_SOURCES = new Set([
+const WORK_SYNC_MEMBER_CONFIRMED_BOOTSTRAP_ACTIVE_PID_SOURCES = new Set([
'agent_process_table',
'opencode_bridge',
]);
+const WORK_SYNC_LEAD_CONFIRMED_BOOTSTRAP_ACTIVE_PID_SOURCES = new Set([
+ 'lead_process',
+]);
+
+function isWorkSyncLeadLikeMemberName(memberName: string): boolean {
+ const normalized = normalizeMemberName(memberName).replace(/[\s_]+/g, '-');
+ return (
+ normalized === 'lead' ||
+ normalized === 'team-lead' ||
+ normalized === 'teamlead' ||
+ normalized === 'team-leader'
+ );
+}
+
+function hasActiveWorkSyncProcessEvidence(
+ entry: Pick | null | undefined,
+ confirmedBootstrapActivePidSources: ReadonlySet
+): boolean {
+ if (entry?.alive !== true) {
+ return false;
+ }
+ if (
+ entry.livenessKind === 'confirmed_bootstrap' &&
+ (!entry.pidSource ||
+ WORK_SYNC_BOOTSTRAP_ONLY_PID_SOURCES.has(entry.pidSource) ||
+ !confirmedBootstrapActivePidSources.has(entry.pidSource))
+ ) {
+ return false;
+ }
+ if (!entry.livenessKind) {
+ return true;
+ }
+ return !WORK_SYNC_INACTIVE_LIVENESS_KINDS.has(entry.livenessKind);
+}
+
export function isRuntimeEntryActiveForWorkSync(
entry:
| Pick<
@@ -40,7 +75,7 @@ export function isRuntimeEntryActiveForWorkSync(
| null
| undefined
): boolean {
- if (entry?.alive !== true) {
+ if (!entry) {
return false;
}
if (
@@ -50,17 +85,33 @@ export function isRuntimeEntryActiveForWorkSync(
return false;
}
if (
- entry.livenessKind === 'confirmed_bootstrap' &&
- (!entry.pidSource ||
- WORK_SYNC_BOOTSTRAP_ONLY_PID_SOURCES.has(entry.pidSource) ||
- !WORK_SYNC_CONFIRMED_BOOTSTRAP_ACTIVE_PID_SOURCES.has(entry.pidSource))
+ entry.pidSource &&
+ WORK_SYNC_LEAD_CONFIRMED_BOOTSTRAP_ACTIVE_PID_SOURCES.has(entry.pidSource)
) {
return false;
}
- if (!entry.livenessKind) {
- return true;
+ return hasActiveWorkSyncProcessEvidence(
+ entry,
+ WORK_SYNC_MEMBER_CONFIRMED_BOOTSTRAP_ACTIVE_PID_SOURCES
+ );
+}
+
+function isRuntimeLeadEntryActiveForWorkSync(
+ entry:
+ | Pick<
+ TeamAgentRuntimeEntry,
+ 'alive' | 'backendType' | 'livenessKind' | 'memberName' | 'pidSource'
+ >
+ | null
+ | undefined
+): boolean {
+ if (!entry || !isWorkSyncLeadLikeMemberName(entry.memberName)) {
+ return false;
}
- return !WORK_SYNC_INACTIVE_LIVENESS_KINDS.has(entry.livenessKind);
+ return (
+ entry.backendType === 'lead' &&
+ hasActiveWorkSyncProcessEvidence(entry, WORK_SYNC_LEAD_CONFIRMED_BOOTSTRAP_ACTIVE_PID_SOURCES)
+ );
}
function isRuntimeEntryRelevantForWorkSync(
@@ -95,6 +146,14 @@ export function hasWorkSyncActiveRuntime(
return Object.values(snapshot?.members ?? {}).some(isRuntimeEntryActiveForWorkSync);
}
+export function hasWorkSyncReachableRuntime(
+ snapshot: Pick | null | undefined
+): boolean {
+ return Object.values(snapshot?.members ?? {}).some(
+ (entry) => isRuntimeEntryActiveForWorkSync(entry) || isRuntimeLeadEntryActiveForWorkSync(entry)
+ );
+}
+
export function isRuntimeMemberActiveForWorkSync(
snapshot: Pick | null | undefined,
memberName: string
@@ -106,7 +165,9 @@ export function isRuntimeMemberActiveForWorkSync(
return Object.values(snapshot?.members ?? {}).some(
(entry) =>
normalizeMemberName(entry.memberName) === normalizedMemberName &&
- isRuntimeEntryActiveForWorkSync(entry)
+ (isRuntimeEntryActiveForWorkSync(entry) ||
+ (isWorkSyncLeadLikeMemberName(normalizedMemberName) &&
+ isRuntimeLeadEntryActiveForWorkSync(entry)))
);
}
diff --git a/src/features/member-work-sync/main/index.ts b/src/features/member-work-sync/main/index.ts
index f451680f..fa0ac4e4 100644
--- a/src/features/member-work-sync/main/index.ts
+++ b/src/features/member-work-sync/main/index.ts
@@ -11,6 +11,7 @@ export {
export {
hasUncertainWorkSyncRuntimeActivity,
hasWorkSyncActiveRuntime,
+ hasWorkSyncReachableRuntime,
isRuntimeEntryActiveForWorkSync,
isRuntimeMemberActiveForWorkSync,
isRuntimeMemberActivityUncertainForWorkSync,
diff --git a/src/features/running-teams/renderer/adapters/RunningTeamsSectionAdapter.ts b/src/features/running-teams/renderer/adapters/RunningTeamsSectionAdapter.ts
index 504dc9f7..2e0dc79e 100644
--- a/src/features/running-teams/renderer/adapters/RunningTeamsSectionAdapter.ts
+++ b/src/features/running-teams/renderer/adapters/RunningTeamsSectionAdapter.ts
@@ -17,36 +17,54 @@ export interface RunningTeamRowModel {
taskCounts?: TaskStatusCounts;
}
-function getStatusLabel(status: RunningTeamDashboardEntry['status']): string {
+export interface RunningTeamsSectionText {
+ status: Record;
+ noProject: string;
+}
+
+const DEFAULT_TEXT: RunningTeamsSectionText = {
+ status: {
+ active: 'Active',
+ provisioning: 'Launching',
+ idle: 'Running',
+ },
+ noProject: 'No project',
+};
+
+function getStatusLabel(
+ status: RunningTeamDashboardEntry['status'],
+ text: RunningTeamsSectionText
+): string {
switch (status) {
case 'active':
- return 'Active';
+ return text.status.active;
case 'provisioning':
- return 'Launching';
+ return text.status.provisioning;
case 'idle':
- return 'Running';
+ return text.status.idle;
}
}
-function getProjectLabel(projectPath?: string): string {
+function getProjectLabel(projectPath: string | undefined, text: RunningTeamsSectionText): string {
if (!projectPath) {
- return 'No project';
+ return text.noProject;
}
return getBaseName(projectPath) || projectPath;
}
export function adaptRunningTeamsSection(
- teams: RunningTeamDashboardEntry[]
+ teams: RunningTeamDashboardEntry[],
+ text: RunningTeamsSectionText = DEFAULT_TEXT
): RunningTeamRowModel[] {
return teams.map((team) => ({
id: team.teamName,
teamName: team.teamName,
displayName: team.displayName,
projectPath: team.projectPath,
- projectLabel: getProjectLabel(team.projectPath),
+ projectLabel: getProjectLabel(team.projectPath, text),
status: team.status,
- statusLabel: getStatusLabel(team.status),
+ statusLabel: getStatusLabel(team.status, text),
iconColor: team.color
? getTeamColorSet(team.color).border
: nameColorSet(team.displayName).border,
diff --git a/src/features/running-teams/renderer/hooks/useRunningTeamsSection.ts b/src/features/running-teams/renderer/hooks/useRunningTeamsSection.ts
index 7f64e91f..c1023e17 100644
--- a/src/features/running-teams/renderer/hooks/useRunningTeamsSection.ts
+++ b/src/features/running-teams/renderer/hooks/useRunningTeamsSection.ts
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useAppTranslation } from '@features/localization/renderer';
import { api } from '@renderer/api';
import { useStore } from '@renderer/store';
import {
@@ -58,6 +59,7 @@ function toCandidate(input: {
}
export function useRunningTeamsSection(searchQuery: string): RunningTeamsSectionState {
+ const { t } = useAppTranslation('team');
const {
teams,
globalTasks,
@@ -172,7 +174,14 @@ export function useRunningTeamsSection(searchQuery: string): RunningTeamsSection
),
});
- return adaptRunningTeamsSection(runningTeams);
+ return adaptRunningTeamsSection(runningTeams, {
+ status: {
+ active: t('runningTeams.status.active'),
+ provisioning: t('runningTeams.status.provisioning'),
+ idle: t('runningTeams.status.idle'),
+ },
+ noProject: t('runningTeams.noProject'),
+ });
}, [
aliveTeams,
globalTasks,
@@ -182,6 +191,7 @@ export function useRunningTeamsSection(searchQuery: string): RunningTeamsSection
provisioningTeamNames,
searchActive,
teams,
+ t,
]);
const openRunningTeam = useCallback(
diff --git a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx
index 6b098ed0..c01e5242 100644
--- a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx
+++ b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx
@@ -26,6 +26,7 @@ import {
getOpenCodeTeamModelRecommendation,
isOpenCodeTeamModelRecommended,
} from '@renderer/utils/openCodeModelRecommendations';
+import { isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic } from '@shared/utils/openCodeWindowsAccessDenied';
import {
AlertTriangle,
Check,
@@ -785,6 +786,20 @@ function getRuntimeProviderDiagnosticRows(
.map(([label, value]) => [label, String(value)]);
}
+function isOpenCodeWindowsNodeModulesSymlinkPermissionError(
+ message: string,
+ diagnostics: RuntimeProviderManagementErrorDiagnosticsDto | null | undefined
+): boolean {
+ const value = [
+ message,
+ diagnostics?.stderrPreview ?? '',
+ diagnostics?.stdoutPreview ?? '',
+ diagnostics?.likelyCause ?? '',
+ ...(diagnostics?.hints ?? []),
+ ].join('\n');
+ return isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic(value);
+}
+
async function writeRuntimeProviderDiagnosticsToClipboard(text: string): Promise {
if (navigator.clipboard?.writeText) {
try {
@@ -826,6 +841,10 @@ const RuntimeProviderErrorAlert = ({
const [headline = message, ...detailLines] = message.trim().split(/\r?\n/);
const fallbackDetails = detailLines.join('\n').trim();
const hints = diagnostics?.hints ?? [];
+ const showWindowsSymlinkPermissionHint = isOpenCodeWindowsNodeModulesSymlinkPermissionError(
+ message,
+ diagnostics
+ );
const copyText = useMemo(
() => formatRuntimeProviderDiagnosticsCopyText(message, diagnostics),
[diagnostics, message]
@@ -859,6 +878,11 @@ const RuntimeProviderErrorAlert = ({
{headline || message}
+ {showWindowsSymlinkPermissionHint ? (
+
+ {t('runtimeProvider.diagnostics.windowsSymlinkAdminHint')}
+
+ ) : null}
{
});
});
+ it('creates worktree-root OpenCode lanes for pure OpenCode teams with isolated members', () => {
+ const result = planTeamRuntimeLanes({
+ leadProviderId: 'opencode',
+ baseCwd: '/repo',
+ members: [
+ {
+ name: 'bob',
+ providerId: 'opencode',
+ model: 'minimax-m2.5-free',
+ cwd: '/repo/.worktrees/bob',
+ },
+ {
+ name: 'tom',
+ providerId: 'opencode',
+ model: 'nemotron-3-super-free',
+ cwd: '/repo/.worktrees/tom',
+ },
+ ],
+ });
+
+ expect(result).toMatchObject({
+ ok: true,
+ plan: {
+ mode: 'pure_opencode_worktree_root_lanes',
+ primaryMembers: [],
+ sideLanes: [
+ {
+ laneId: 'secondary:opencode:bob',
+ providerId: 'opencode',
+ member: expect.objectContaining({
+ name: 'bob',
+ providerId: 'opencode',
+ cwd: '/repo/.worktrees/bob',
+ }),
+ },
+ {
+ laneId: 'secondary:opencode:tom',
+ providerId: 'opencode',
+ member: expect.objectContaining({
+ name: 'tom',
+ providerId: 'opencode',
+ cwd: '/repo/.worktrees/tom',
+ }),
+ },
+ ],
+ },
+ });
+ });
+
+ it('keeps base-cwd OpenCode members on primary and isolated members on worktree lanes', () => {
+ const result = planTeamRuntimeLanes({
+ leadProviderId: 'opencode',
+ baseCwd: '/repo',
+ members: [
+ { name: 'lead-dev', providerId: 'opencode', model: 'big-pickle' },
+ {
+ name: 'bob',
+ providerId: 'opencode',
+ model: 'minimax-m2.5-free',
+ cwd: '/repo/.worktrees/bob',
+ },
+ ],
+ });
+
+ expect(result).toMatchObject({
+ ok: true,
+ plan: {
+ mode: 'pure_opencode_worktree_root_lanes',
+ primaryMembers: [expect.objectContaining({ name: 'lead-dev', providerId: 'opencode' })],
+ sideLanes: [
+ {
+ laneId: 'secondary:opencode:bob',
+ providerId: 'opencode',
+ member: expect.objectContaining({
+ name: 'bob',
+ providerId: 'opencode',
+ cwd: '/repo/.worktrees/bob',
+ }),
+ },
+ ],
+ },
+ });
+ });
+
it('creates a secondary OpenCode lane for an Anthropic-led mixed team', () => {
const result = planTeamRuntimeLanes({
leadProviderId: 'anthropic',
diff --git a/src/features/team-runtime-lanes/core/domain/planTeamRuntimeLanes.ts b/src/features/team-runtime-lanes/core/domain/planTeamRuntimeLanes.ts
index 63b1e011..8581cd0d 100644
--- a/src/features/team-runtime-lanes/core/domain/planTeamRuntimeLanes.ts
+++ b/src/features/team-runtime-lanes/core/domain/planTeamRuntimeLanes.ts
@@ -44,6 +44,16 @@ export type TeamRuntimeLanePlan =
allMembers: PlannedRuntimeMember[];
sideLanes: [];
}
+ | {
+ mode: 'pure_opencode_worktree_root_lanes';
+ primaryMembers: PlannedRuntimeMember[];
+ allMembers: PlannedRuntimeMember[];
+ sideLanes: {
+ laneId: string;
+ providerId: 'opencode';
+ member: PlannedRuntimeMember;
+ }[];
+ }
| {
mode: 'mixed_opencode_side_lanes';
primaryMembers: PlannedRuntimeMember[];
@@ -111,9 +121,16 @@ export function buildPlannedMemberLaneIdentity(params: {
};
}
+export function buildOpenCodeSecondaryLaneId(
+ member: Pick
+): string {
+ return `secondary:opencode:${member.name.trim()}`;
+}
+
export function planTeamRuntimeLanes(params: {
leadProviderId?: TeamProviderId;
members: readonly RuntimeLanePlannerMemberInput[];
+ baseCwd?: string;
}): TeamRuntimeLanePlanResult {
const leadProviderId = normalizeLeadProviderId(params.leadProviderId);
const allMembers = normalizePlannedMembers(params.members, leadProviderId);
@@ -129,6 +146,27 @@ export function planTeamRuntimeLanes(params: {
'Mixed teams with an OpenCode lead are not supported in this phase. Keep the team lead on Anthropic or Codex when you mix OpenCode with other providers.',
};
}
+ const normalizedBaseCwd = params.baseCwd?.trim();
+ const worktreeRootMembers = allMembers.filter((member) => {
+ const memberCwd = member.cwd?.trim();
+ return Boolean(memberCwd && (!normalizedBaseCwd || memberCwd !== normalizedBaseCwd));
+ });
+ if (worktreeRootMembers.length > 0 && allMembers.length > 1) {
+ const worktreeRootMemberNames = new Set(worktreeRootMembers.map((member) => member.name));
+ return {
+ ok: true,
+ plan: {
+ mode: 'pure_opencode_worktree_root_lanes',
+ primaryMembers: allMembers.filter((member) => !worktreeRootMemberNames.has(member.name)),
+ allMembers,
+ sideLanes: worktreeRootMembers.map((member) => ({
+ laneId: buildOpenCodeSecondaryLaneId(member),
+ providerId: 'opencode',
+ member,
+ })),
+ },
+ };
+ }
return {
ok: true,
plan: {
@@ -175,18 +213,37 @@ export function isMixedOpenCodeSideLanePlan(
return plan.mode === 'mixed_opencode_side_lanes';
}
+export function isOpenCodeSideLanePlan(
+ plan: TeamRuntimeLanePlan
+): plan is Extract<
+ TeamRuntimeLanePlan,
+ { mode: 'mixed_opencode_side_lanes' | 'pure_opencode_worktree_root_lanes' }
+> {
+ return (
+ plan.mode === 'mixed_opencode_side_lanes' || plan.mode === 'pure_opencode_worktree_root_lanes'
+ );
+}
+
export function isPureOpenCodeLanePlan(
plan: TeamRuntimeLanePlan
): plan is Extract {
return plan.mode === 'pure_opencode';
}
+export function isPureOpenCodeWorktreeRootLanePlan(
+ plan: TeamRuntimeLanePlan
+): plan is Extract {
+ return plan.mode === 'pure_opencode_worktree_root_lanes';
+}
+
export function fromProvisioningMembers(
leadProviderId: TeamProviderId | undefined,
- members: readonly TeamProvisioningMemberInput[]
+ members: readonly TeamProvisioningMemberInput[],
+ options: { baseCwd?: string } = {}
): TeamRuntimeLanePlanResult {
return planTeamRuntimeLanes({
leadProviderId,
+ baseCwd: options.baseCwd,
members: members.map((member) => ({
name: member.name,
role: member.role,
diff --git a/src/features/team-runtime-lanes/index.ts b/src/features/team-runtime-lanes/index.ts
index e8ae757b..bdd3e8da 100644
--- a/src/features/team-runtime-lanes/index.ts
+++ b/src/features/team-runtime-lanes/index.ts
@@ -9,9 +9,12 @@ export type {
TeamRuntimeLanePlanSuccess,
} from './core/domain/planTeamRuntimeLanes';
export {
+ buildOpenCodeSecondaryLaneId,
buildPlannedMemberLaneIdentity,
fromProvisioningMembers,
isMixedOpenCodeSideLanePlan,
+ isOpenCodeSideLanePlan,
isPureOpenCodeLanePlan,
+ isPureOpenCodeWorktreeRootLanePlan,
planTeamRuntimeLanes,
} from './core/domain/planTeamRuntimeLanes';
diff --git a/src/features/team-runtime-lanes/main/composition/__tests__/createTeamRuntimeLaneCoordinator.test.ts b/src/features/team-runtime-lanes/main/composition/__tests__/createTeamRuntimeLaneCoordinator.test.ts
index a5875ba9..c09ddfd6 100644
--- a/src/features/team-runtime-lanes/main/composition/__tests__/createTeamRuntimeLaneCoordinator.test.ts
+++ b/src/features/team-runtime-lanes/main/composition/__tests__/createTeamRuntimeLaneCoordinator.test.ts
@@ -45,7 +45,7 @@ describe('createTeamRuntimeLaneCoordinator', () => {
{ name: 'tom', providerId: 'opencode', model: 'minimax-m2.5-free' },
],
})
- ).toThrow('Mixed teams with OpenCode side lanes require the OpenCode runtime adapter');
+ ).toThrow('OpenCode side lanes require the OpenCode runtime adapter');
});
it('drops stale hard-failure reasons when secondary OpenCode evidence later confirms alive', () => {
diff --git a/src/features/team-runtime-lanes/main/composition/createTeamRuntimeLaneCoordinator.ts b/src/features/team-runtime-lanes/main/composition/createTeamRuntimeLaneCoordinator.ts
index 3b05e370..bb51019e 100644
--- a/src/features/team-runtime-lanes/main/composition/createTeamRuntimeLaneCoordinator.ts
+++ b/src/features/team-runtime-lanes/main/composition/createTeamRuntimeLaneCoordinator.ts
@@ -1,7 +1,7 @@
import { buildMixedPersistedLaunchSnapshot } from '@features/team-runtime-lanes/core/domain/buildMixedPersistedLaunchSnapshot';
import {
fromProvisioningMembers,
- isMixedOpenCodeSideLanePlan,
+ isOpenCodeSideLanePlan,
type TeamRuntimeLanePlan,
} from '@features/team-runtime-lanes/core/domain/planTeamRuntimeLanes';
@@ -11,6 +11,7 @@ export interface TeamRuntimeLaneCoordinator {
planProvisioningMembers(params: {
leadProviderId?: TeamProviderId;
members: TeamCreateRequest['members'];
+ baseCwd?: string;
hasOpenCodeRuntimeAdapter: boolean;
}): TeamRuntimeLanePlan;
buildAggregateLaunchSnapshot(
@@ -22,13 +23,15 @@ export interface TeamRuntimeLaneCoordinator {
export function createTeamRuntimeLaneCoordinator(): TeamRuntimeLaneCoordinator {
return {
planProvisioningMembers(params) {
- const lanePlan = fromProvisioningMembers(params.leadProviderId, params.members);
+ const lanePlan = fromProvisioningMembers(params.leadProviderId, params.members, {
+ baseCwd: params.baseCwd,
+ });
if (!lanePlan.ok) {
throw new Error(lanePlan.message);
}
- if (isMixedOpenCodeSideLanePlan(lanePlan.plan) && !params.hasOpenCodeRuntimeAdapter) {
+ if (isOpenCodeSideLanePlan(lanePlan.plan) && !params.hasOpenCodeRuntimeAdapter) {
throw new Error(
- 'Mixed teams with OpenCode side lanes require the OpenCode runtime adapter to be registered.'
+ 'OpenCode side lanes require the OpenCode runtime adapter to be registered.'
);
}
return lanePlan.plan;
@@ -37,7 +40,7 @@ export function createTeamRuntimeLaneCoordinator(): TeamRuntimeLaneCoordinator {
return buildMixedPersistedLaunchSnapshot(params);
},
isMixedSideLanePlan(plan) {
- return isMixedOpenCodeSideLanePlan(plan);
+ return isOpenCodeSideLanePlan(plan);
},
};
}
diff --git a/src/main/index.ts b/src/main/index.ts
index 46d16e8d..f19e0349 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -41,7 +41,7 @@ import {
buildMemberWorkSyncRuntimeTurnSettledEnvironment,
createMemberWorkSyncFeature,
hasUncertainWorkSyncRuntimeActivity,
- hasWorkSyncActiveRuntime,
+ hasWorkSyncReachableRuntime,
isRuntimeMemberActivityUncertainForWorkSync,
isRuntimeMemberActiveForWorkSync,
type MemberWorkSyncFeatureFacade,
@@ -1919,7 +1919,7 @@ async function initializeServices(): Promise {
if (!snapshot) {
return null;
}
- const active = hasWorkSyncActiveRuntime(snapshot);
+ const active = hasWorkSyncReachableRuntime(snapshot);
if (!active && hasUncertainWorkSyncRuntimeActivity(snapshot)) {
return null;
}
@@ -2037,7 +2037,12 @@ async function initializeServices(): Promise {
isBusy: (input) => teamProvisioningService.getOpenCodeMemberDeliveryBusyStatus(input),
},
],
- resolveControlUrl: async () => getTeamControlApiBaseUrl(),
+ resolveControlUrl: async () => {
+ if (!httpServer.isRunning()) {
+ await startHttpServer(handleModeSwitch);
+ }
+ return getTeamControlApiBaseUrl();
+ },
proofMissingRecoveryGuard: {
shouldDispatch: async (input) => {
const isOpenCodeRecipient = await teamProvisioningService
diff --git a/src/main/ipc/teams.ts b/src/main/ipc/teams.ts
index 6e404075..f4336542 100644
--- a/src/main/ipc/teams.ts
+++ b/src/main/ipc/teams.ts
@@ -240,7 +240,17 @@ import type {
import type { CliArgsValidationResult } from '@shared/utils/cliArgsParser';
const logger = createLogger('IPC:teams');
-const OPENCODE_RUNTIME_DELIVERY_UI_TIMEOUT_MS = 12_000;
+// Runtime relay continues in the background after this race; keep sendMessage IPC off the
+// 25s OpenCode turn-settled guard while still giving prompt acceptance/reconcile time.
+const OPENCODE_RUNTIME_DELIVERY_UI_TIMEOUT_MS = 6_000;
+const OPENCODE_RUNTIME_DELIVERY_STATUS_AFTER_UI_TIMEOUT_MS = 1_000;
+const OPENCODE_RUNTIME_DELIVERY_UI_TIMEOUT_PENDING_REASON =
+ 'opencode_runtime_delivery_ui_timeout_pending';
+
+type OpenCodeMemberInboxRelayResult = Awaited<
+ ReturnType
+>;
+type OpenCodeMemberInboxDelivery = NonNullable;
type VisibleDirectReplyProtocol = 'send_message' | 'agent_teams_message_send';
@@ -318,6 +328,158 @@ async function withTimeoutValue(
}
}
+async function waitForOpenCodeRuntimeRelayForUi(input: {
+ provisioning: TeamProvisioningService;
+ teamName: string;
+ memberName: string;
+ messageId: string;
+ relayPromise: Promise;
+ timeoutMs?: number;
+}): Promise {
+ let timer: ReturnType | null = null;
+ let timedOut = false;
+
+ void input.relayPromise.then(
+ (relay) => {
+ if (!timedOut) return;
+ const delivery = relay.lastDelivery;
+ if (delivery && !delivery.delivered && delivery.reason !== 'recipient_is_not_opencode') {
+ logger.warn(
+ `OpenCode runtime delivery after sendMessage completed after UI timeout for teammate "${input.memberName}" with failure: ${
+ delivery.reason ?? 'unknown error'
+ }`
+ );
+ }
+ },
+ (error: unknown) => {
+ if (!timedOut) return;
+ logger.warn(
+ `OpenCode runtime delivery after sendMessage rejected after UI timeout for teammate "${input.memberName}": ${getErrorMessage(error)}`
+ );
+ }
+ );
+
+ try {
+ const outcome = await Promise.race<
+ { kind: 'relay'; relay: OpenCodeMemberInboxRelayResult } | { kind: 'timeout' }
+ >([
+ input.relayPromise.then((relay) => ({ kind: 'relay' as const, relay })),
+ new Promise<{ kind: 'timeout' }>((resolve) => {
+ timer = setTimeout(() => {
+ timedOut = true;
+ resolve({ kind: 'timeout' });
+ }, input.timeoutMs ?? OPENCODE_RUNTIME_DELIVERY_UI_TIMEOUT_MS);
+ timer.unref?.();
+ }),
+ ]);
+
+ if (outcome.kind === 'relay') {
+ return outcome.relay;
+ }
+
+ try {
+ const status = await withTimeoutValue(
+ input.provisioning.getOpenCodeRuntimeDeliveryStatus(input.teamName, input.messageId),
+ OPENCODE_RUNTIME_DELIVERY_STATUS_AFTER_UI_TIMEOUT_MS,
+ null
+ );
+ if (status) {
+ return openCodeRuntimeDeliveryStatusToRelayResult(status);
+ }
+ } catch (error) {
+ const reason = getErrorMessage(error);
+ logger.warn(
+ `OpenCode runtime delivery status after UI timeout failed for teammate "${input.memberName}": ${reason}`
+ );
+ return buildOpenCodeRuntimeDeliveryUiTimeoutRelayResult([
+ `${OPENCODE_RUNTIME_DELIVERY_UI_TIMEOUT_PENDING_REASON}: status lookup failed: ${reason}`,
+ ]);
+ }
+
+ return buildOpenCodeRuntimeDeliveryUiTimeoutRelayResult();
+ } finally {
+ if (timer) {
+ clearTimeout(timer);
+ }
+ }
+}
+
+function openCodeRuntimeDeliveryStatusToRelayResult(
+ status: OpenCodeRuntimeDeliveryStatus
+): OpenCodeMemberInboxRelayResult {
+ const lastDelivery: OpenCodeMemberInboxDelivery = {
+ delivered: status.delivered,
+ ...(typeof status.responsePending === 'boolean'
+ ? { responsePending: status.responsePending }
+ : {}),
+ ...(typeof status.acceptanceUnknown === 'boolean'
+ ? { acceptanceUnknown: status.acceptanceUnknown }
+ : {}),
+ ...(status.responseState ? { responseState: status.responseState } : {}),
+ ...(status.ledgerStatus ? { ledgerStatus: status.ledgerStatus } : {}),
+ ...(status.visibleReplyMessageId
+ ? { visibleReplyMessageId: status.visibleReplyMessageId }
+ : {}),
+ ...(status.visibleReplyCorrelation
+ ? { visibleReplyCorrelation: status.visibleReplyCorrelation }
+ : {}),
+ ...(status.queuedBehindMessageId
+ ? { queuedBehindMessageId: status.queuedBehindMessageId }
+ : {}),
+ ...(status.reason ? { reason: status.reason } : {}),
+ ...(status.diagnostics ? { diagnostics: status.diagnostics } : {}),
+ ...(shouldPreserveOpenCodeRuntimeDeliveryStatusImpact(status)
+ ? { userVisibleImpact: status.userVisibleImpact }
+ : {}),
+ };
+ return {
+ relayed: 0,
+ attempted: 1,
+ delivered: status.delivered && status.responsePending !== true ? 1 : 0,
+ failed: status.delivered ? 0 : 1,
+ lastDelivery,
+ diagnostics: status.diagnostics,
+ };
+}
+
+function shouldPreserveOpenCodeRuntimeDeliveryStatusImpact(
+ status: OpenCodeRuntimeDeliveryStatus
+): boolean {
+ if (!status.userVisibleImpact) {
+ return false;
+ }
+ if (
+ status.userVisibleImpact.state === 'none' &&
+ (status.responsePending === true ||
+ status.acceptanceUnknown === true ||
+ Boolean(status.queuedBehindMessageId))
+ ) {
+ return false;
+ }
+ return true;
+}
+
+function buildOpenCodeRuntimeDeliveryUiTimeoutRelayResult(
+ extraDiagnostics: string[] = []
+): OpenCodeMemberInboxRelayResult {
+ const diagnostics = [OPENCODE_RUNTIME_DELIVERY_UI_TIMEOUT_PENDING_REASON, ...extraDiagnostics];
+ return {
+ relayed: 0,
+ attempted: 1,
+ delivered: 0,
+ failed: 1,
+ lastDelivery: {
+ delivered: true,
+ accepted: false,
+ responsePending: true,
+ acceptanceUnknown: true,
+ responseState: 'not_observed',
+ reason: OPENCODE_RUNTIME_DELIVERY_UI_TIMEOUT_PENDING_REASON,
+ diagnostics,
+ },
+ };
+}
+
function noteHeavyTeamDataWorkerFallback(operation: string): void {
if (!app.isPackaged) {
return;
@@ -2866,9 +3028,10 @@ async function handleSendMessage(
: leadName !== null && memberName === leadName;
const actionMode = payload.actionMode;
- const recipientProviderId = !isLeadRecipient
- ? await provisioning.resolveRuntimeRecipientProviderId(tn, memberName)
- : undefined;
+ const recipientProviderId = await provisioning.resolveRuntimeRecipientProviderId(
+ tn,
+ memberName
+ );
const isOpenCodeRecipient = recipientProviderId === 'opencode';
// Attachments are routed through explicit provider transports only.
@@ -2889,7 +3052,7 @@ async function handleSendMessage(
}
// Smart routing: lead + alive → stdin direct, else → inbox
- if (isLeadRecipient && isAlive) {
+ if (isLeadRecipient && isAlive && !isOpenCodeRecipient) {
const resolvedLeadName = leadName ?? memberName;
const teammateRoster = await getDurableLeadTeammateRoster(tn, resolvedLeadName);
const rosterContextBlock = buildLeadRosterContextBlock(tn, resolvedLeadName, teammateRoster);
@@ -3083,8 +3246,12 @@ async function handleSendMessage(
// }
if (isOpenCodeRecipient) {
try {
- const relay = await withTimeoutValue(
- provisioning.relayOpenCodeMemberInboxMessages(tn, memberName, {
+ const relay = await waitForOpenCodeRuntimeRelayForUi({
+ provisioning,
+ teamName: tn,
+ memberName,
+ messageId: result.messageId,
+ relayPromise: provisioning.relayOpenCodeMemberInboxMessages(tn, memberName, {
onlyMessageId: result.messageId,
source: 'ui-send',
deliveryMetadata: {
@@ -3093,23 +3260,7 @@ async function handleSendMessage(
taskRefs: validatedTaskRefs.value,
},
}),
- OPENCODE_RUNTIME_DELIVERY_UI_TIMEOUT_MS,
- {
- relayed: 0,
- attempted: 1,
- delivered: 0,
- failed: 1,
- lastDelivery: {
- delivered: true,
- accepted: false,
- responsePending: true,
- acceptanceUnknown: true,
- responseState: 'not_observed',
- reason: 'opencode_runtime_delivery_ui_timeout_pending',
- diagnostics: ['opencode_runtime_delivery_ui_timeout_pending'],
- },
- }
- );
+ });
const delivery = relay.lastDelivery ?? {
delivered: relay.relayed > 0,
reason: relay.relayed > 0 ? undefined : 'opencode_message_delivery_not_attempted',
diff --git a/src/main/services/runtime/ClaudeMultimodelBridgeService.ts b/src/main/services/runtime/ClaudeMultimodelBridgeService.ts
index 9d45cc33..c9adade2 100644
--- a/src/main/services/runtime/ClaudeMultimodelBridgeService.ts
+++ b/src/main/services/runtime/ClaudeMultimodelBridgeService.ts
@@ -11,7 +11,11 @@ import { tmpdir } from 'os';
import path from 'path';
import { resolveGeminiRuntimeAuth } from './geminiRuntimeAuth';
-import { buildProviderAwareCliEnv } from './providerAwareCliEnv';
+import {
+ buildProviderAwareCliEnv,
+ getAggregateProviderStatusStoredCredentialAllowlist,
+ getProviderStatusStoredCredentialAllowlist,
+} from './providerAwareCliEnv';
import { providerConnectionService } from './ProviderConnectionService';
import type {
@@ -839,12 +843,15 @@ export class ClaudeMultimodelBridgeService {
}
private async buildCliEnv(
- binaryPath: string
+ binaryPath: string,
+ options: { allowedStoredApiKeyEnvVarNames?: readonly string[] } = {}
): Promise>> {
return buildProviderAwareCliEnv({
binaryPath,
allowStoredApiKeyDecryption: false,
- allowedStoredApiKeyEnvVarNames: ['ANTHROPIC_AUTH_TOKEN'],
+ allowedStoredApiKeyEnvVarNames: options.allowedStoredApiKeyEnvVarNames ?? [
+ 'ANTHROPIC_AUTH_TOKEN',
+ ],
});
}
@@ -856,8 +863,7 @@ export class ClaudeMultimodelBridgeService {
binaryPath,
providerId,
allowStoredApiKeyDecryption: false,
- allowedStoredApiKeyEnvVarNames:
- providerId === 'anthropic' ? ['ANTHROPIC_AUTH_TOKEN'] : undefined,
+ allowedStoredApiKeyEnvVarNames: getProviderStatusStoredCredentialAllowlist(providerId),
});
}
@@ -1747,7 +1753,9 @@ export class ClaudeMultimodelBridgeService {
);
}
- const { env, connectionIssues } = await this.buildCliEnv(binaryPath);
+ const { env, connectionIssues } = await this.buildCliEnv(binaryPath, {
+ allowedStoredApiKeyEnvVarNames: getAggregateProviderStatusStoredCredentialAllowlist(),
+ });
try {
const { stdout } = await execCli(binaryPath, ['runtime', 'status', '--json'], {
diff --git a/src/main/services/runtime/CliProviderModelAvailabilityService.ts b/src/main/services/runtime/CliProviderModelAvailabilityService.ts
index b9c8b520..4c23438c 100644
--- a/src/main/services/runtime/CliProviderModelAvailabilityService.ts
+++ b/src/main/services/runtime/CliProviderModelAvailabilityService.ts
@@ -3,7 +3,10 @@ import { getErrorMessage } from '@shared/utils/errorHandling';
import { createLogger } from '@shared/utils/logger';
import { filterVisibleProviderRuntimeModels } from '@shared/utils/providerModelVisibility';
-import { buildProviderAwareCliEnv } from './providerAwareCliEnv';
+import {
+ buildProviderAwareCliEnv,
+ getProviderStatusStoredCredentialAllowlist,
+} from './providerAwareCliEnv';
import {
buildProviderModelProbeArgs,
classifyProviderModelProbeFailure,
@@ -194,8 +197,9 @@ export class CliProviderModelAvailabilityService {
binaryPath: context.binaryPath,
providerId: context.provider.providerId,
allowStoredApiKeyDecryption: false,
- allowedStoredApiKeyEnvVarNames:
- context.provider.providerId === 'anthropic' ? ['ANTHROPIC_AUTH_TOKEN'] : undefined,
+ allowedStoredApiKeyEnvVarNames: getProviderStatusStoredCredentialAllowlist(
+ context.provider.providerId
+ ),
}).then((result) => ({
env: result.env,
providerArgs: result.providerArgs ?? [],
diff --git a/src/main/services/runtime/providerAwareCliEnv.ts b/src/main/services/runtime/providerAwareCliEnv.ts
index a6bdd0ae..7dfa53dd 100644
--- a/src/main/services/runtime/providerAwareCliEnv.ts
+++ b/src/main/services/runtime/providerAwareCliEnv.ts
@@ -12,6 +12,14 @@ import type { CliProviderId, TeamProviderId } from '@shared/types';
type ProviderEnvTargetId = CliProviderId | TeamProviderId | undefined;
const ELECTRON_RUN_AS_NODE_ENV = 'ELECTRON_RUN_AS_NODE';
+const PROVIDER_STATUS_STORED_CREDENTIAL_ALLOWLIST = {
+ anthropic: ['ANTHROPIC_AUTH_TOKEN'],
+ codex: ['OPENAI_API_KEY'],
+} as const;
+const AGGREGATE_PROVIDER_STATUS_STORED_CREDENTIAL_ALLOWLIST = [
+ 'ANTHROPIC_AUTH_TOKEN',
+ 'OPENAI_API_KEY',
+] as const;
export interface ProviderAwareCliEnvOptions {
binaryPath?: string | null;
@@ -30,6 +38,20 @@ export interface ProviderAwareCliEnvResult {
providerArgs: string[];
}
+export function getProviderStatusStoredCredentialAllowlist(
+ providerId: ProviderEnvTargetId
+): readonly string[] | undefined {
+ if (providerId === 'anthropic' || providerId === 'codex') {
+ return PROVIDER_STATUS_STORED_CREDENTIAL_ALLOWLIST[providerId];
+ }
+
+ return undefined;
+}
+
+export function getAggregateProviderStatusStoredCredentialAllowlist(): readonly string[] {
+ return AGGREGATE_PROVIDER_STATUS_STORED_CREDENTIAL_ALLOWLIST;
+}
+
function removeGlobalElectronRunAsNodeEnv(env: NodeJS.ProcessEnv): void {
delete env[ELECTRON_RUN_AS_NODE_ENV];
}
diff --git a/src/main/services/schedule/ScheduledTaskExecutor.ts b/src/main/services/schedule/ScheduledTaskExecutor.ts
index 01577b0d..43cabf0b 100644
--- a/src/main/services/schedule/ScheduledTaskExecutor.ts
+++ b/src/main/services/schedule/ScheduledTaskExecutor.ts
@@ -55,15 +55,6 @@ function buildProviderFastModeArgs(config: ScheduleLaunchConfig): string[] {
}
function validateFastModeLaunchConfig(config: ScheduleLaunchConfig): void {
- if (
- config.providerId === 'codex' &&
- config.fastMode === 'on' &&
- config.resolvedFastMode !== true
- ) {
- throw new Error(
- 'Codex Fast mode was requested for this schedule, but the saved launch profile is not Fast-eligible. Reopen the schedule and save it again with a supported ChatGPT account configuration.'
- );
- }
if (config.providerId !== 'codex' || config.resolvedFastMode !== true) {
return;
}
diff --git a/src/main/services/team/TaskChangeLedgerReader.ts b/src/main/services/team/TaskChangeLedgerReader.ts
index 3982cee2..93d990e4 100644
--- a/src/main/services/team/TaskChangeLedgerReader.ts
+++ b/src/main/services/team/TaskChangeLedgerReader.ts
@@ -139,6 +139,9 @@ interface LedgerEvent {
sourceImportKey?: string;
evidenceProof?: string;
supersedesEventId?: string;
+ suppressed?: true;
+ suppressionReason?: string;
+ suppressedAt?: string;
snapshotId?: string;
snapshotSource?: string;
}
@@ -1209,10 +1212,12 @@ export class TaskChangeLedgerReader {
events.forEach((event, index) => {
const sourceImportKey = this.sourceImportKeyForEvent(event);
if (!sourceImportKey) {
- passthrough.push({ event, index });
+ if (event.suppressed !== true) {
+ passthrough.push({ event, index });
+ }
return;
}
- const rank = this.evidenceRankForEvent(event);
+ const rank = this.projectionRankForEvent(event);
const existing = selectedBySourceImportKey.get(sourceImportKey);
if (!existing || rank >= existing.rank) {
selectedBySourceImportKey.set(sourceImportKey, { event, index, rank });
@@ -1221,7 +1226,9 @@ export class TaskChangeLedgerReader {
return [
...passthrough,
- ...[...selectedBySourceImportKey.values()].map(({ event, index }) => ({ event, index })),
+ ...[...selectedBySourceImportKey.values()]
+ .filter(({ event }) => event.suppressed !== true)
+ .map(({ event, index }) => ({ event, index })),
]
.sort((left, right) => left.index - right.index)
.map(({ event }) => event);
@@ -1241,6 +1248,10 @@ export class TaskChangeLedgerReader {
return null;
}
+ private projectionRankForEvent(event: LedgerEvent): number {
+ return event.suppressed === true ? Number.MAX_SAFE_INTEGER : this.evidenceRankForEvent(event);
+ }
+
private evidenceRankForEvent(event: LedgerEvent): number {
const hasFullText = this.hasFullTextEvidence(event);
diff --git a/src/main/services/team/TeamInboxWriter.ts b/src/main/services/team/TeamInboxWriter.ts
index 63326acf..c26a1e61 100644
--- a/src/main/services/team/TeamInboxWriter.ts
+++ b/src/main/services/team/TeamInboxWriter.ts
@@ -6,9 +6,23 @@ import * as path from 'path';
import { atomicWriteAsync } from './atomicWrite';
import { withFileLock } from './fileLock';
import { withInboxLock } from './inboxLock';
+import { getEffectiveInboxMessageId } from './inboxMessageIdentity';
import type { InboxMessage, SendMessageRequest, SendMessageResult, TaskRef } from '@shared/types';
+export interface UpdateInboxMessageTextRequest {
+ member: string;
+ messageId: string;
+ text: string;
+ expectedMessageKind?: InboxMessage['messageKind'];
+ expectedWorkSyncPayloadHash?: string;
+}
+
+export interface UpdateInboxMessageTextResult {
+ found: boolean;
+ updated: boolean;
+}
+
export interface MergeRuntimeDeliveryTaskRefsRequest {
inboxName: string;
messageId: string;
@@ -137,6 +151,78 @@ export class TeamInboxWriter {
};
}
+ async updateMessageText(
+ teamName: string,
+ request: UpdateInboxMessageTextRequest
+ ): Promise {
+ const messageId = request.messageId.trim();
+ if (!messageId) {
+ return { found: false, updated: false };
+ }
+
+ const inboxPath = path.join(getTeamsBasePath(), teamName, 'inboxes', `${request.member}.json`);
+ let result: UpdateInboxMessageTextResult = { found: false, updated: false };
+
+ await withFileLock(inboxPath, async () => {
+ await withInboxLock(inboxPath, async () => {
+ let raw: string;
+ try {
+ raw = await fs.promises.readFile(inboxPath, 'utf8');
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
+ return;
+ }
+ throw error;
+ }
+
+ let parsed: unknown;
+ try {
+ parsed = JSON.parse(raw) as unknown;
+ } catch {
+ return;
+ }
+ if (!Array.isArray(parsed)) {
+ return;
+ }
+
+ let changed = false;
+ for (const item of parsed) {
+ if (!item || typeof item !== 'object') {
+ continue;
+ }
+ const row = item as Record;
+ const rowMessageId = getEffectiveInboxMessageId(row);
+ if (rowMessageId !== messageId) {
+ continue;
+ }
+ result = { found: true, updated: changed };
+ if (request.expectedMessageKind && row.messageKind !== request.expectedMessageKind) {
+ continue;
+ }
+ if (
+ request.expectedWorkSyncPayloadHash &&
+ row.workSyncPayloadHash !== request.expectedWorkSyncPayloadHash
+ ) {
+ continue;
+ }
+ if (row.text === request.text) {
+ continue;
+ }
+ row.text = request.text;
+ changed = true;
+ result = { found: true, updated: true };
+ }
+
+ if (!changed) {
+ return;
+ }
+ await atomicWriteAsync(inboxPath, JSON.stringify(parsed, null, 2));
+ });
+ });
+
+ return result;
+ }
+
async mergeRuntimeDeliveryTaskRefs(
teamName: string,
request: MergeRuntimeDeliveryTaskRefsRequest
diff --git a/src/main/services/team/TeamLaunchStateEvaluator.ts b/src/main/services/team/TeamLaunchStateEvaluator.ts
index 83711fdc..424a56fd 100644
--- a/src/main/services/team/TeamLaunchStateEvaluator.ts
+++ b/src/main/services/team/TeamLaunchStateEvaluator.ts
@@ -712,24 +712,23 @@ export function createPersistedLaunchSnapshot(params: {
teamName: string;
expectedMembers: readonly string[];
bootstrapExpectedMembers?: readonly string[];
+ includeLeadMembers?: boolean;
leadSessionId?: string;
launchPhase?: PersistedTeamLaunchPhase;
members?: Record;
updatedAt?: string;
}): PersistedTeamLaunchSnapshot {
const updatedAt = params.updatedAt ?? new Date().toISOString();
+ const shouldKeepExpectedMemberName = (name: string): boolean =>
+ name.length > 0 && name !== 'user' && (params.includeLeadMembers || !isLeadMember({ name }));
const expectedMembers = Array.from(
- new Set(
- params.expectedMembers
- .map(normalizeMemberName)
- .filter((name) => name.length > 0 && name !== 'user' && !isLeadMember({ name }))
- )
+ new Set(params.expectedMembers.map(normalizeMemberName).filter(shouldKeepExpectedMemberName))
);
const bootstrapExpectedMembers = Array.from(
new Set(
(params.bootstrapExpectedMembers ?? expectedMembers)
.map(normalizeMemberName)
- .filter((name) => name.length > 0 && name !== 'user' && !isLeadMember({ name }))
+ .filter(shouldKeepExpectedMemberName)
)
);
const members = params.members ?? {};
diff --git a/src/main/services/team/TeamMessageFeedService.ts b/src/main/services/team/TeamMessageFeedService.ts
index fd78b910..d314d690 100644
--- a/src/main/services/team/TeamMessageFeedService.ts
+++ b/src/main/services/team/TeamMessageFeedService.ts
@@ -85,18 +85,21 @@ function resolveLeadName(config: TeamConfig): string {
return lead?.name?.trim() || 'team-lead';
}
-function resolveSyntheticBootstrapTimestamp(config: TeamConfig, member: TeamConfigMember): string {
+function resolveSyntheticBootstrapTimestamp(
+ config: TeamConfig,
+ member: TeamConfigMember
+): string | null {
const raw = member.joinedAt ?? (config as { createdAt?: unknown }).createdAt;
- if (typeof raw === 'number' && Number.isFinite(raw)) {
+ if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) {
return new Date(raw).toISOString();
}
if (typeof raw === 'string') {
const parsed = Date.parse(raw);
- if (Number.isFinite(parsed)) {
+ if (Number.isFinite(parsed) && parsed > 0) {
return new Date(parsed).toISOString();
}
}
- return new Date(0).toISOString();
+ return null;
}
function buildSyntheticBootstrapDisplayPrompt(
@@ -122,7 +125,10 @@ Call member_briefing directly yourself. Do NOT use Agent, any subagent, or a del
After member_briefing succeeds, wait for instructions from the lead and use team mailbox/task tools normally.`;
}
-function buildSyntheticBootstrapMessages(config: TeamConfig): InboxMessage[] {
+function buildSyntheticBootstrapMessages(
+ config: TeamConfig,
+ fallbackTimestampForMessage: (messageId: string) => string
+): InboxMessage[] {
const members = Array.isArray(config.members) ? config.members : [];
const leadName = resolveLeadName(config);
const normalizedLeadName = leadName.trim().toLowerCase();
@@ -134,15 +140,20 @@ function buildSyntheticBootstrapMessages(config: TeamConfig): InboxMessage[] {
member.name.trim().toLowerCase() !== normalizedLeadName &&
member.removedAt == null
)
- .map((member) => ({
- from: leadName,
- to: member.name,
- text: buildSyntheticBootstrapDisplayPrompt(config, member),
- timestamp: resolveSyntheticBootstrapTimestamp(config, member),
- read: true,
- source: 'system_notification' as const,
- messageId: `bootstrap-start:${config.name}:${member.name}`,
- }));
+ .map((member) => {
+ const messageId = `bootstrap-start:${config.name}:${member.name}`;
+ return {
+ from: leadName,
+ to: member.name,
+ text: buildSyntheticBootstrapDisplayPrompt(config, member),
+ timestamp:
+ resolveSyntheticBootstrapTimestamp(config, member) ??
+ fallbackTimestampForMessage(messageId),
+ read: true,
+ source: 'system_notification' as const,
+ messageId,
+ };
+ });
}
function isVisibleTeamMessage(message: InboxMessage): boolean {
@@ -429,6 +440,7 @@ export class TeamMessageFeedService {
private readonly dirtyTeams = new Set();
private readonly inFlightByTeam = new Map();
private readonly generationByTeam = new Map();
+ private readonly syntheticBootstrapTimestampByMessageId = new Map();
constructor(private readonly deps: TeamMessageFeedDeps) {}
@@ -487,6 +499,17 @@ export class TeamMessageFeedService {
return this.generationByTeam.get(teamName) ?? 0;
}
+ private getSyntheticBootstrapFallbackTimestamp(messageId: string): string {
+ const existing = this.syntheticBootstrapTimestampByMessageId.get(messageId);
+ if (existing) {
+ return existing;
+ }
+
+ const timestamp = new Date(Date.now()).toISOString();
+ this.syntheticBootstrapTimestampByMessageId.set(messageId, timestamp);
+ return timestamp;
+ }
+
private refreshCleanExpiredCacheInBackground(
teamName: string,
cached: TeamMessageFeedCacheEntry,
@@ -554,7 +577,9 @@ export class TeamMessageFeedService {
const sourceMs = Date.now() - sourceStartedAt;
const normalizeStartedAt = Date.now();
- const syntheticMessages = buildSyntheticBootstrapMessages(config);
+ const syntheticMessages = buildSyntheticBootstrapMessages(config, (messageId) =>
+ this.getSyntheticBootstrapFallbackTimestamp(messageId)
+ );
let messages = [...inboxMessages, ...leadTexts, ...sentMessages, ...syntheticMessages].filter(
isVisibleTeamMessage
);
diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts
index 856da524..8071d99f 100644
--- a/src/main/services/team/TeamProvisioningService.ts
+++ b/src/main/services/team/TeamProvisioningService.ts
@@ -17,8 +17,10 @@ import {
resolveCodexRuntimeSelection,
} from '@features/codex-runtime-profile/main';
import {
+ buildOpenCodeSecondaryLaneId,
buildPlannedMemberLaneIdentity,
- isMixedOpenCodeSideLanePlan,
+ isOpenCodeSideLanePlan,
+ isPureOpenCodeWorktreeRootLanePlan,
type TeamRuntimeLanePlan,
} from '@features/team-runtime-lanes';
import { createTeamRuntimeLaneCoordinator } from '@features/team-runtime-lanes/main';
@@ -231,6 +233,7 @@ import {
import {
deriveMemberLaunchState,
isAutoClearableLaunchFailureReason,
+ isBootstrapCheckInTimeoutFailureReason,
isCliProvisionedButNotAliveFailureReason,
isNeverSpawnedDuringLaunchReason,
isProvisionedButNotAliveFailureReason,
@@ -1120,7 +1123,9 @@ function isConfirmedBootstrapStaleRuntimeDiagnostic(reason?: string): boolean {
function isBootstrapProofClearableLaunchFailureReason(reason?: string): boolean {
return (
- isAutoClearableLaunchFailureReason(reason) || isProvisionedButNotAliveFailureReason(reason)
+ isAutoClearableLaunchFailureReason(reason) ||
+ isProvisionedButNotAliveFailureReason(reason) ||
+ isConfirmedBootstrapStaleRuntimeDiagnostic(reason)
);
}
@@ -3342,12 +3347,7 @@ interface OpenCodeMemberInboxRelayResult {
}
interface LiveInboxRelayResult {
- kind:
- | 'ignored'
- | 'native_lead'
- | 'native_member_noop'
- | 'opencode_member'
- | 'opencode_lead_unsupported';
+ kind: 'ignored' | 'native_lead' | 'native_member_noop' | 'opencode_member';
relayed: number;
diagnostics?: string[];
lastDelivery?: OpenCodeMemberInboxDelivery;
@@ -3469,10 +3469,7 @@ function getMemberInboxRelayPriority(
}
function getLeadInboxRelayPriority(message: Pick): number {
- if (message.messageKind === 'member_work_sync_nudge') {
- return 30;
- }
- return 0;
+ return message.messageKind === 'member_work_sync_nudge' ? 30 : 0;
}
function compareInboxRelayMessages(
@@ -4040,6 +4037,26 @@ export class TeamProvisioningService {
const canonicalMemberName =
metaMember?.name?.trim() || configMember?.name?.trim() || normalizedMemberName;
+ const secondaryRuntimeRun = this.getSecondaryRuntimeRuns(teamName).find((run) =>
+ matchesTeamMemberIdentity(run.memberName, canonicalMemberName)
+ );
+ if (secondaryRuntimeRun) {
+ const memberRuntimeCwd =
+ secondaryRuntimeRun.cwd?.trim() || metaMember?.cwd?.trim() || configMember?.cwd?.trim();
+ return {
+ ok: true,
+ canonicalMemberName,
+ laneId: secondaryRuntimeRun.laneId,
+ laneIdentity: {
+ laneId: secondaryRuntimeRun.laneId,
+ laneKind: 'secondary',
+ laneOwnerProviderId: 'opencode',
+ },
+ ...(configMember ? { configMember } : {}),
+ ...(metaMember ? { metaMember } : {}),
+ ...(memberRuntimeCwd ? { memberRuntimeCwd } : {}),
+ };
+ }
const runtimeRun = this.runtimeAdapterRunByTeam.get(teamName);
if (runtimeRun?.providerId === 'opencode') {
const laneIdentity = buildPlannedMemberLaneIdentity({
@@ -4985,22 +5002,8 @@ export class TeamProvisioningService {
);
}
- const codexSelection = resolveCodexSelectionFromFacts({
- selectedModel: params.model,
- facts: params.facts,
- });
- const codexFastResolution = resolveCodexFastMode({
- selection: codexSelection,
- selectedFastMode: params.fastMode,
- });
- if ((params.fastMode ?? 'inherit') === 'on' && !codexFastResolution.selectable) {
- throw new Error(
- `${params.actorLabel} enables Codex Fast mode, but ${
- codexFastResolution.disabledReason ??
- 'it is unavailable for the selected runtime, model, or auth mode.'
- }`
- );
- }
+ // Codex Fast is optional acceleration. If it is no longer eligible, the launch identity
+ // resolves it to normal Codex mode instead of blocking an otherwise launch-ready model.
if (!explicitModel || params.facts.modelIds.has(explicitModel)) {
return;
@@ -9877,11 +9880,13 @@ export class TeamProvisioningService {
private planRuntimeLanesOrThrow(
leadProviderId: TeamProviderId | undefined,
- members: TeamCreateRequest['members']
+ members: TeamCreateRequest['members'],
+ baseCwd?: string
): TeamRuntimeLanePlan {
return this.runtimeLaneCoordinator.planProvisioningMembers({
leadProviderId,
members,
+ baseCwd,
hasOpenCodeRuntimeAdapter: this.getOpenCodeRuntimeAdapter() !== null,
});
}
@@ -9889,7 +9894,7 @@ export class TeamProvisioningService {
private createMixedSecondaryLaneStates(
plan: TeamRuntimeLanePlan
): MixedSecondaryRuntimeLaneState[] {
- if (!isMixedOpenCodeSideLanePlan(plan)) {
+ if (!isOpenCodeSideLanePlan(plan)) {
return [];
}
return plan.sideLanes.map((sideLane) => ({
@@ -9907,11 +9912,42 @@ export class TeamProvisioningService {
}
private createMixedSecondaryLaneStateForMember(
- run: Pick,
+ run: Pick,
member: TeamCreateRequest['members'][number]
): MixedSecondaryRuntimeLaneState {
+ const leadProviderId = resolveTeamProviderId(run.request.providerId);
+ const existingLane = (run.mixedSecondaryLanes ?? []).find((lane) =>
+ matchesTeamMemberIdentity(lane.member.name, member.name)
+ );
+ if (leadProviderId === 'opencode') {
+ const memberCwd = member.cwd?.trim();
+ const baseCwd = run.request.cwd?.trim();
+ const laneId =
+ existingLane?.laneId ??
+ (memberCwd && (!baseCwd || memberCwd !== baseCwd)
+ ? buildOpenCodeSecondaryLaneId(member)
+ : null);
+ if (!laneId) {
+ throw new Error(
+ `Member "${member.name}" is not eligible for an OpenCode secondary runtime lane`
+ );
+ }
+ return {
+ laneId,
+ providerId: 'opencode',
+ member: {
+ ...member,
+ },
+ runId: null,
+ state: 'queued',
+ result: null,
+ warnings: [],
+ diagnostics: [],
+ };
+ }
+
const laneIdentity = buildPlannedMemberLaneIdentity({
- leadProviderId: resolveTeamProviderId(run.request.providerId),
+ leadProviderId,
member: {
name: member.name,
providerId: normalizeOptionalTeamProviderId(member.providerId),
@@ -14871,7 +14907,13 @@ export class TeamProvisioningService {
if (!memberName) continue;
const isLead = isLeadMember({ name: memberName, agentType: member.agentType });
- if (isLead) {
+ const candidateLaunchMember = launchSnapshot?.members[memberName];
+ const candidateRuntimeAdapterEvidence = currentRuntimeAdapterRun?.members?.[memberName];
+ const leadRuntimeProviderId =
+ normalizeOptionalTeamProviderId(candidateRuntimeAdapterEvidence?.providerId) ??
+ normalizeOptionalTeamProviderId(candidateLaunchMember?.providerId) ??
+ normalizeOptionalTeamProviderId(member.providerId);
+ if (isLead && leadRuntimeProviderId !== 'opencode') {
const pid = run?.child?.pid;
const usageStats = pid
? this.buildRuntimeProcessLoadStatsSafely(teamName, memberName, {
@@ -14941,6 +14983,7 @@ export class TeamProvisioningService {
const liveRuntimeMember = getLiveRuntimeMember(memberName);
const spawnStatusMember = getSpawnStatusMember(memberName);
const launchMember = launchSnapshot?.members[memberName];
+ const runtimeAdapterEvidence = currentRuntimeAdapterRun?.members?.[memberName];
const activeRunMember = activeRunMemberByName.get(memberName);
const activeRunModel = activeRunMember?.model?.trim();
const activeRunProviderId =
@@ -14981,6 +15024,16 @@ export class TeamProvisioningService {
member.providerBackendId
);
const isOpenCodeMember = memberProviderId === 'opencode';
+ const runtimeAdapterSessionId =
+ typeof runtimeAdapterEvidence?.sessionId === 'string'
+ ? runtimeAdapterEvidence.sessionId.trim()
+ : '';
+ const runtimeAdapterPid =
+ typeof runtimeAdapterEvidence?.runtimePid === 'number' &&
+ Number.isFinite(runtimeAdapterEvidence.runtimePid) &&
+ runtimeAdapterEvidence.runtimePid > 0
+ ? runtimeAdapterEvidence.runtimePid
+ : undefined;
const configuredCwd =
typeof activeRunMember?.cwd === 'string'
? activeRunMember.cwd.trim()
@@ -14997,7 +15050,9 @@ export class TeamProvisioningService {
metricsPid > 0 &&
liveRuntimeMember?.pidSource !== 'agent_process_table';
const rssPid = isSharedOpenCodeHost ? metricsPid : (liveRuntimeMember?.pid ?? metricsPid);
- const displayPid = isSharedOpenCodeHost ? rssPid : liveRuntimeMember?.pid;
+ const displayPid = isSharedOpenCodeHost
+ ? rssPid
+ : (liveRuntimeMember?.pid ?? runtimeAdapterPid);
const restartable = isOpenCodeMember
? !isSharedOpenCodeHost && Boolean(liveRuntimeMember?.pid)
: isSharedOpenCodeHost
@@ -15006,6 +15061,8 @@ export class TeamProvisioningService {
const historicalBootstrapConfirmed =
launchMember?.bootstrapConfirmed === true ||
launchMember?.launchState === 'confirmed_alive' ||
+ runtimeAdapterEvidence?.bootstrapConfirmed === true ||
+ runtimeAdapterEvidence?.launchState === 'confirmed_alive' ||
spawnStatusMember?.bootstrapConfirmed === true ||
spawnStatusMember?.launchState === 'confirmed_alive';
const spawnStatusConfirmsBootstrap =
@@ -15015,7 +15072,9 @@ export class TeamProvisioningService {
isOpenCodeMember &&
(typeof liveRuntimeMember?.pid === 'number' ||
typeof liveRuntimeMember?.metricsPid === 'number' ||
- typeof liveRuntimeMember?.runtimeSessionId === 'string');
+ typeof liveRuntimeMember?.runtimeSessionId === 'string' ||
+ typeof runtimeAdapterPid === 'number' ||
+ runtimeAdapterSessionId.length > 0);
const confirmedOpenCodeRuntimeAlive =
isOpenCodeMember &&
canUseLiveSpawnStatusRuntimeTruth &&
@@ -15024,6 +15083,12 @@ export class TeamProvisioningService {
spawnStatusMember?.hardFailure !== true &&
spawnStatusMember?.launchState !== 'failed_to_start' &&
spawnStatusMember?.launchState !== 'runtime_pending_permission';
+ const confirmedOpenCodeRuntimeAdapterAlive =
+ isOpenCodeMember &&
+ runtimeAdapterEvidence?.bootstrapConfirmed === true &&
+ runtimeAdapterEvidence.runtimeAlive === true &&
+ runtimeAdapterEvidence.hardFailure !== true &&
+ hasOpenCodeRuntimeHandle;
const confirmedSpawnRuntimeFallback =
!isOpenCodeMember &&
spawnStatusConfirmsBootstrap &&
@@ -15038,6 +15103,7 @@ export class TeamProvisioningService {
const effectiveAlive =
liveRuntimeMember?.alive === true ||
confirmedOpenCodeRuntimeAlive ||
+ confirmedOpenCodeRuntimeAdapterAlive ||
confirmedSpawnRuntimeFallback;
const effectiveLivenessKind =
confirmedOpenCodeRuntimeAlive &&
@@ -15166,7 +15232,9 @@ export class TeamProvisioningService {
...(liveRuntimeMember?.metricsPid ? { runtimePid: liveRuntimeMember.metricsPid } : {}),
...(liveRuntimeMember?.runtimeSessionId
? { runtimeSessionId: liveRuntimeMember.runtimeSessionId }
- : {}),
+ : runtimeAdapterSessionId
+ ? { runtimeSessionId: runtimeAdapterSessionId }
+ : {}),
...(liveRuntimeMember?.runtimeLastSeenAt
? { runtimeLastSeenAt: liveRuntimeMember.runtimeLastSeenAt }
: {}),
@@ -17084,9 +17152,11 @@ export class TeamProvisioningService {
): Promise {
const teamName = run.teamName;
const leadProviderId = resolveTeamProviderId(run.request.providerId);
- if (leadProviderId === 'opencode') {
+ const isOpenCodeAggregateRun =
+ leadProviderId === 'opencode' && (run.mixedSecondaryLanes?.length ?? 0) > 0;
+ if (leadProviderId === 'opencode' && !isOpenCodeAggregateRun) {
throw new Error(
- 'Retrying OpenCode secondary lanes is only supported for mixed teams with a non-OpenCode lead.'
+ 'Retrying OpenCode secondary lanes requires an active OpenCode worktree lane run.'
);
}
if (!this.getOpenCodeRuntimeAdapter()) {
@@ -17143,35 +17213,49 @@ export class TeamProvisioningService {
if (isLeadMember({ name: memberName, agentType: configuredMember.agentType })) {
continue;
}
- if (normalizeOptionalTeamProviderId(configuredMember.providerId) !== 'opencode') {
+ const desiredProviderId =
+ normalizeOptionalTeamProviderId(configuredMember.providerId) ?? leadProviderId;
+ if (desiredProviderId !== 'opencode') {
continue;
}
- const laneIdentity = buildPlannedMemberLaneIdentity({
- leadProviderId,
- member: {
- name: memberName,
- providerId: 'opencode',
- },
- });
- if (
- laneIdentity.laneKind !== 'secondary' ||
- laneIdentity.laneOwnerProviderId !== 'opencode'
- ) {
- continue;
- }
-
- const existingLane = (run.mixedSecondaryLanes ?? []).find(
- (lane) =>
- lane.laneId === laneIdentity.laneId ||
- matchesTeamMemberIdentity(lane.member.name, memberName)
+ const existingLane = (run.mixedSecondaryLanes ?? []).find((lane) =>
+ matchesTeamMemberIdentity(lane.member.name, memberName)
);
const liveEntry = run.memberSpawnStatuses.get(memberName);
- const persistedMember =
+ const persistedMemberByName =
persistedSnapshot?.members[memberName] ??
- Object.values(persistedSnapshot?.members ?? {}).find(
- (member) => member.laneId === laneIdentity.laneId
+ Object.values(persistedSnapshot?.members ?? {}).find((member) =>
+ matchesTeamMemberIdentity(member.name, memberName)
);
+ let laneId: string | null = null;
+ if (leadProviderId === 'opencode') {
+ const persistedLaneId = persistedMemberByName?.laneId?.startsWith('secondary:opencode:')
+ ? persistedMemberByName.laneId
+ : null;
+ laneId = existingLane?.laneId ?? persistedLaneId;
+ if (!laneId) {
+ continue;
+ }
+ } else {
+ const laneIdentity = buildPlannedMemberLaneIdentity({
+ leadProviderId,
+ member: {
+ name: memberName,
+ providerId: 'opencode',
+ },
+ });
+ if (
+ laneIdentity.laneKind !== 'secondary' ||
+ laneIdentity.laneOwnerProviderId !== 'opencode'
+ ) {
+ continue;
+ }
+ laneId = laneIdentity.laneId;
+ }
+ const persistedMember =
+ persistedMemberByName ??
+ Object.values(persistedSnapshot?.members ?? {}).find((member) => member.laneId === laneId);
if (
this.isRetryableFailedOpenCodeSecondaryLane({
@@ -17180,7 +17264,7 @@ export class TeamProvisioningService {
existingLane,
})
) {
- candidates.push({ memberName, laneId: laneIdentity.laneId });
+ candidates.push({ memberName, laneId });
}
}
return candidates;
@@ -17514,9 +17598,9 @@ export class TeamProvisioningService {
): Promise {
const run = this.getMutableAliveRunOrThrow(teamName);
const leadProviderId = resolveTeamProviderId(run.request.providerId);
- if (leadProviderId === 'opencode') {
+ if (leadProviderId === 'opencode' && (run.mixedSecondaryLanes?.length ?? 0) === 0) {
throw new Error(
- 'OpenCode-led mixed teams are not supported in this phase. Stop the team and relaunch with a non-OpenCode lead.'
+ 'OpenCode secondary lane reattach requires an active OpenCode worktree lane run.'
);
}
if (!this.getOpenCodeRuntimeAdapter()) {
@@ -17547,7 +17631,8 @@ export class TeamProvisioningService {
if (isLeadMember({ name: configuredMember.name, agentType: configuredMember.agentType })) {
throw new Error('Lead lane reattach is not supported');
}
- const desiredProviderId = normalizeOptionalTeamProviderId(configuredMember.providerId);
+ const desiredProviderId =
+ normalizeOptionalTeamProviderId(configuredMember.providerId) ?? leadProviderId;
if (desiredProviderId !== 'opencode') {
throw new Error(
`Controlled reattach is only supported for OpenCode-owned members. "${memberName}" remains on the primary runtime owner.`
@@ -19791,7 +19876,7 @@ export class TeamProvisioningService {
return memberCwds[0];
}
throw new Error(
- 'OpenCode runtime lanes support exactly one project path in this release. Use mixed-team OpenCode side lanes for per-teammate worktree isolation.'
+ 'OpenCode runtime lanes support exactly one project path per lane. Use separate OpenCode worktree-root lanes for per-teammate worktree isolation.'
);
}
@@ -19900,6 +19985,29 @@ export class TeamProvisioningService {
};
}
+ private buildOpenCodeRuntimeAdapterLaunchMembers(
+ request: TeamCreateRequest | TeamLaunchRequest,
+ members: TeamCreateRequest['members']
+ ): TeamCreateRequest['members'] {
+ if (resolveTeamProviderId(request.providerId) !== 'opencode') {
+ return members;
+ }
+ if (members.some((member) => isLeadMember(member))) {
+ return members;
+ }
+
+ return [
+ {
+ name: 'team-lead',
+ role: 'Team Lead',
+ providerId: 'opencode',
+ model: request.model,
+ effort: request.effort,
+ },
+ ...members,
+ ];
+ }
+
private async resolveOpenCodeMemberWorkspacesForRuntime(params: {
teamName: string;
baseCwd: string;
@@ -19914,18 +20022,6 @@ export class TeamProvisioningService {
return params.members;
}
- if (
- isPureOpenCodeProvisioningRequest({
- providerId: params.leadProviderId,
- members: params.members,
- }) &&
- params.members.length > 1
- ) {
- throw new Error(
- 'OpenCode worktree isolation currently supports mixed-team OpenCode side lanes or one-member OpenCode runtime lanes. Multiple OpenCode members in one lane cannot use separate worktrees yet.'
- );
- }
-
const nextMembers: TeamCreateRequest['members'] = [];
for (const member of params.members) {
const providerId = normalizeTeamMemberProviderId(member.providerId);
@@ -20988,7 +21084,11 @@ export class TeamProvisioningService {
allEffectiveMemberSpecs
)
);
- const lanePlan = this.planRuntimeLanesOrThrow(request.providerId, allEffectiveMemberSpecs);
+ const lanePlan = this.planRuntimeLanesOrThrow(
+ request.providerId,
+ allEffectiveMemberSpecs,
+ request.cwd
+ );
const primaryMemberNames = new Set(lanePlan.primaryMembers.map((member) => member.name));
const effectiveMemberSpecs = allEffectiveMemberSpecs.filter((member) =>
primaryMemberNames.has(member.name)
@@ -21542,6 +21642,10 @@ export class TeamProvisioningService {
leadProviderId: launchRequest.providerId,
members: materialized.members,
});
+ const runtimeLaunchMembers = this.buildOpenCodeRuntimeAdapterLaunchMembers(
+ launchRequest,
+ effectiveMembers
+ );
const teamDir = path.join(getTeamsBasePath(), launchRequest.teamName);
const tasksDir = path.join(getTasksBasePath(), launchRequest.teamName);
await fs.promises.mkdir(teamDir, { recursive: true });
@@ -21567,10 +21671,25 @@ export class TeamProvisioningService {
providerBackendId: launchRequest.providerBackendId,
});
await this.writeOpenCodeTeamConfig(launchRequest, effectiveMembers);
+ const lanePlan = this.planRuntimeLanesOrThrow(
+ launchRequest.providerId,
+ effectiveMembers,
+ launchRequest.cwd
+ );
+ if (isPureOpenCodeWorktreeRootLanePlan(lanePlan)) {
+ return this.runOpenCodeWorktreeRootAggregateLaunch({
+ request: launchRequest,
+ members: effectiveMembers,
+ lanePlan,
+ prompt: launchRequest.prompt?.trim() ?? '',
+ sourceWarning: undefined,
+ onProgress,
+ });
+ }
return this.runOpenCodeTeamRuntimeAdapterLaunch({
request: launchRequest,
- members: effectiveMembers,
+ members: runtimeLaunchMembers,
prompt: launchRequest.prompt?.trim() ?? '',
sourceWarning: undefined,
onProgress,
@@ -21606,6 +21725,10 @@ export class TeamProvisioningService {
leadProviderId: launchRequest.providerId,
members: materialized.members,
});
+ const runtimeLaunchMembers = this.buildOpenCodeRuntimeAdapterLaunchMembers(
+ launchRequest,
+ effectiveMembers
+ );
await this.updateConfigProjectPath(launchRequest.teamName, launchRequest.cwd);
let existingTasks: TeamTask[] = [];
@@ -21622,16 +21745,450 @@ export class TeamProvisioningService {
existingTasks,
false
);
+ const lanePlan = this.planRuntimeLanesOrThrow(
+ launchRequest.providerId,
+ effectiveMembers,
+ launchRequest.cwd
+ );
+ if (isPureOpenCodeWorktreeRootLanePlan(lanePlan)) {
+ return this.runOpenCodeWorktreeRootAggregateLaunch({
+ request: launchRequest,
+ members: effectiveMembers,
+ lanePlan,
+ prompt,
+ sourceWarning: warning,
+ onProgress,
+ });
+ }
return this.runOpenCodeTeamRuntimeAdapterLaunch({
request: launchRequest,
- members: effectiveMembers,
+ members: runtimeLaunchMembers,
prompt,
sourceWarning: warning,
onProgress,
});
}
+ private createOpenCodeAggregateProvisioningRun(params: {
+ runId: string;
+ startedAt: string;
+ progress: TeamProvisioningProgress;
+ request: TeamCreateRequest | TeamLaunchRequest;
+ members: TeamCreateRequest['members'];
+ lanePlan: Extract;
+ onProgress: (progress: TeamProvisioningProgress) => void;
+ }): ProvisioningRun {
+ return {
+ runId: params.runId,
+ teamName: params.request.teamName,
+ startedAt: params.startedAt,
+ progress: params.progress,
+ stdoutBuffer: '',
+ stderrBuffer: '',
+ claudeLogLines: [],
+ lastClaudeLogStream: null,
+ stdoutLogLineBuf: '',
+ stderrLogLineBuf: '',
+ stdoutParserCarry: '',
+ stdoutParserCarryIsCompleteJson: false,
+ stdoutParserCarryLooksLikeClaudeJson: false,
+ deterministicBootstrapMemberSpawnSeen: false,
+ deterministicBootstrapMemberResultSeen: false,
+ processKilled: false,
+ finalizingByTimeout: false,
+ cancelRequested: false,
+ teamsBasePathsToProbe: getTeamsBasePathsToProbe(),
+ child: null,
+ timeoutHandle: null,
+ fsMonitorHandle: null,
+ onProgress: params.onProgress,
+ expectedMembers: params.members.map((member) => member.name),
+ request: {
+ ...params.request,
+ members: params.members,
+ } as TeamCreateRequest,
+ allEffectiveMembers: params.members,
+ effectiveMembers: params.lanePlan.primaryMembers,
+ launchIdentity: null,
+ mixedSecondaryLanes: this.createMixedSecondaryLaneStates(params.lanePlan),
+ lastLogProgressAt: 0,
+ lastDataReceivedAt: 0,
+ lastStdoutReceivedAt: 0,
+ stallCheckHandle: null,
+ stallWarningIndex: null,
+ preStallMessage: null,
+ lastRetryAt: 0,
+ apiRetryWarningIndex: null,
+ apiErrorWarningEmitted: false,
+ fsPhase: 'all_files_found',
+ waitingTasksSince: null,
+ provisioningComplete: false,
+ processClosed: false,
+ requiresFirstRealTurnSuccess: false,
+ firstRealTurnSucceeded: false,
+ mcpConfigPath: null,
+ memberMcpConfigPaths: [],
+ bootstrapSpecPath: null,
+ bootstrapUserPromptPath: null,
+ isLaunch: true,
+ launchStateClearedForRun: false,
+ deterministicBootstrap: false,
+ workspaceTrustPlan: null,
+ workspaceTrustExecution: null,
+ workspaceTrustDiagnostics: null,
+ workspaceTrustRetryAttempted: false,
+ leadRelayCapture: null,
+ activeCrossTeamReplyHints: [],
+ leadMsgSeq: 0,
+ liveLeadTextBuffer: null,
+ pendingToolCalls: [],
+ activeToolCalls: new Map(),
+ pendingDirectCrossTeamSendRefresh: false,
+ lastLeadTextEmitMs: 0,
+ silentUserDmForward: null,
+ silentUserDmForwardClearHandle: null,
+ pendingInboxRelayCandidates: [],
+ provisioningOutputParts: [],
+ provisioningTraceLines: [],
+ lastProvisioningTraceKey: null,
+ provisioningOutputIndexByMessageId: new Map(),
+ detectedSessionId: null,
+ leadActivityState: 'active',
+ authFailureRetried: false,
+ authRetryInProgress: false,
+ leadContextUsage: null,
+ spawnContext: null,
+ anthropicApiKeyHelper: null,
+ pendingApprovals: new Map(),
+ processedPermissionRequestIds: new Set(),
+ pendingPostCompactReminder: false,
+ postCompactReminderInFlight: false,
+ suppressPostCompactReminderOutput: false,
+ pendingGeminiPostLaunchHydration: false,
+ geminiPostLaunchHydrationInFlight: false,
+ geminiPostLaunchHydrationSent: false,
+ suppressGeminiPostLaunchHydrationOutput: false,
+ memberSpawnStatuses: new Map(),
+ memberSpawnToolUseIds: new Map(),
+ pendingMemberRestarts: new Map(),
+ memberSpawnLeadInboxCursorByMember: new Map(),
+ lastDeterministicBootstrapSeq: 0,
+ lastMemberSpawnAuditAt: 0,
+ lastMemberSpawnAuditConfigReadWarningAt: 0,
+ lastMemberSpawnAuditMissingWarningAt: new Map(),
+ };
+ }
+
+ private async launchOpenCodeAggregatePrimaryLane(params: {
+ run: ProvisioningRun;
+ adapter: TeamLaunchRuntimeAdapter;
+ prompt: string;
+ previousLaunchState: PersistedTeamLaunchSnapshot | null;
+ }): Promise {
+ if (params.run.effectiveMembers.length === 0) {
+ return null;
+ }
+
+ const teamName = params.run.teamName;
+ const runId = params.run.runId;
+ const launchCwd = this.getOpenCodeRuntimeLaunchCwd(
+ params.run.request.cwd,
+ params.run.effectiveMembers
+ );
+ const migration = await migrateLegacyOpenCodeRuntimeState({
+ teamsBasePath: getTeamsBasePath(),
+ teamName,
+ laneId: 'primary',
+ });
+ await upsertOpenCodeRuntimeLaneIndexEntry({
+ teamsBasePath: getTeamsBasePath(),
+ teamName,
+ laneId: 'primary',
+ state: migration.degraded ? 'degraded' : 'active',
+ diagnostics: migration.diagnostics,
+ });
+ await setOpenCodeRuntimeActiveRunManifest({
+ teamsBasePath: getTeamsBasePath(),
+ teamName,
+ laneId: 'primary',
+ runId,
+ });
+
+ const expectedMembers: TeamRuntimeMemberSpec[] = params.run.effectiveMembers.map((member) => ({
+ name: member.name,
+ role: member.role,
+ workflow: member.workflow,
+ isolation: member.isolation === 'worktree' ? ('worktree' as const) : undefined,
+ providerId: 'opencode',
+ model: member.model ?? params.run.request.model,
+ effort: member.effort ?? params.run.request.effort,
+ cwd: member.cwd?.trim() || launchCwd,
+ }));
+ const launchInput: TeamRuntimeLaunchInput = {
+ runId,
+ laneId: 'primary',
+ teamName,
+ cwd: launchCwd,
+ prompt: params.prompt,
+ providerId: 'opencode',
+ model: params.run.request.model,
+ effort: params.run.request.effort,
+ skipPermissions: params.run.request.skipPermissions !== false,
+ expectedMembers,
+ previousLaunchState: params.previousLaunchState,
+ };
+ const launchResult = await params.adapter.launch(launchInput);
+ const { snapshot, result } = await this.persistOpenCodeRuntimeAdapterLaunchResult(
+ launchResult,
+ launchInput
+ );
+ const snapshotStatuses = snapshotToMemberSpawnStatuses(snapshot);
+ for (const member of expectedMembers) {
+ const status = snapshotStatuses[member.name];
+ if (status) {
+ params.run.memberSpawnStatuses.set(member.name, status);
+ }
+ }
+ this.syncOpenCodeRuntimeToolApprovals({
+ teamName,
+ runId,
+ laneId: 'primary',
+ cwd: launchCwd,
+ members: result.members,
+ expectedMembers,
+ teamColor: params.run.request.color,
+ teamDisplayName: params.run.request.displayName,
+ });
+ if (result.teamLaunchState !== 'partial_failure') {
+ this.runtimeAdapterRunByTeam.set(teamName, {
+ runId,
+ providerId: 'opencode',
+ cwd: launchCwd,
+ members: result.members,
+ });
+ }
+ return result;
+ }
+
+ private summarizeOpenCodeAggregateLaunchState(input: {
+ primaryResult: TeamRuntimeLaunchResult | null;
+ lanes: readonly MixedSecondaryRuntimeLaneState[];
+ }): TeamRuntimeLaunchResult['teamLaunchState'] {
+ const states = [
+ input.primaryResult?.teamLaunchState,
+ ...input.lanes.map((lane) => lane.result?.teamLaunchState),
+ ].filter((state): state is TeamRuntimeLaunchResult['teamLaunchState'] => Boolean(state));
+ if (states.length === 0 || states.some((state) => state === 'partial_failure')) {
+ return 'partial_failure';
+ }
+ if (
+ states.some((state) => state === 'partial_pending') ||
+ input.lanes.some((lane) => !lane.result)
+ ) {
+ return 'partial_pending';
+ }
+ return 'clean_success';
+ }
+
+ private async runOpenCodeWorktreeRootAggregateLaunch(input: {
+ request: TeamCreateRequest | TeamLaunchRequest;
+ members: TeamCreateRequest['members'];
+ lanePlan: Extract;
+ prompt: string;
+ sourceWarning?: string;
+ onProgress: (progress: TeamProvisioningProgress) => void;
+ }): Promise {
+ const adapter = this.getOpenCodeRuntimeAdapter();
+ if (!adapter) {
+ throw new Error('OpenCode runtime adapter is not registered');
+ }
+
+ const stopAllGenerationAtStart = this.stopAllTeamsGeneration;
+ const previousRuntimeRun = this.runtimeAdapterRunByTeam.get(input.request.teamName);
+ if (previousRuntimeRun?.providerId === 'opencode') {
+ await this.stopOpenCodeRuntimeAdapterTeam(input.request.teamName, previousRuntimeRun.runId);
+ }
+ if (this.hasSecondaryRuntimeRuns(input.request.teamName)) {
+ await this.stopMixedSecondaryRuntimeLanes(input.request.teamName);
+ }
+ const previousPendingRunId = this.provisioningRunByTeam.get(input.request.teamName);
+ const previousRuntimeProgress = previousPendingRunId
+ ? this.runtimeAdapterProgressByRunId.get(previousPendingRunId)
+ : null;
+ if (
+ previousPendingRunId &&
+ previousRuntimeProgress &&
+ this.isCancellableRuntimeAdapterProgress(previousRuntimeProgress)
+ ) {
+ await this.cancelRuntimeAdapterProvisioning(previousPendingRunId, previousRuntimeProgress);
+ }
+ if (this.stopAllTeamsGeneration !== stopAllGenerationAtStart) {
+ return this.recordCancelledOpenCodeRuntimeAdapterLaunch(
+ input.request.teamName,
+ input.sourceWarning,
+ input.onProgress
+ );
+ }
+
+ const runId = randomUUID();
+ const startedAt = nowIso();
+ const initialProgress: TeamProvisioningProgress = {
+ runId,
+ teamName: input.request.teamName,
+ state: 'validating',
+ message: 'Validating OpenCode worktree lane launch gate',
+ startedAt,
+ updatedAt: startedAt,
+ warnings: input.sourceWarning ? [input.sourceWarning] : undefined,
+ };
+ this.provisioningRunByTeam.set(input.request.teamName, runId);
+ const initialRuntimeProgress = this.setRuntimeAdapterProgress(
+ initialProgress,
+ input.onProgress
+ );
+ this.resetTeamScopedTransientStateForNewRun(input.request.teamName);
+ const previousLaunchState = await this.launchStateStore.read(input.request.teamName);
+ await this.clearPersistedLaunchState(input.request.teamName);
+
+ const run = this.createOpenCodeAggregateProvisioningRun({
+ runId,
+ startedAt,
+ progress: initialRuntimeProgress,
+ request: input.request,
+ members: input.members,
+ lanePlan: input.lanePlan,
+ onProgress: input.onProgress,
+ });
+ this.runs.set(runId, run);
+ this.invalidateRuntimeSnapshotCaches(input.request.teamName);
+
+ const launching = this.setRuntimeAdapterProgress(
+ {
+ ...initialRuntimeProgress,
+ state: 'spawning',
+ message: 'Starting OpenCode worktree runtime lanes',
+ updatedAt: nowIso(),
+ },
+ input.onProgress
+ );
+ run.progress = launching;
+
+ try {
+ const primaryResult = await this.launchOpenCodeAggregatePrimaryLane({
+ run,
+ adapter,
+ prompt: input.prompt,
+ previousLaunchState,
+ });
+ for (const lane of run.mixedSecondaryLanes) {
+ if (run.cancelRequested || run.processKilled) {
+ break;
+ }
+ await this.launchSingleMixedSecondaryLane(run, lane);
+ }
+
+ run.provisioningComplete = true;
+ const launchState = this.summarizeOpenCodeAggregateLaunchState({
+ primaryResult,
+ lanes: run.mixedSecondaryLanes,
+ });
+ const launchPhase = launchState === 'partial_pending' ? 'active' : 'finished';
+ const snapshot = await this.persistLaunchStateSnapshot(run, launchPhase);
+ if (snapshot) {
+ this.syncRunMemberSpawnStatusesFromSnapshot(run, snapshot);
+ }
+
+ const success = launchState === 'clean_success';
+ const pending = launchState === 'partial_pending';
+ const failed = launchState === 'partial_failure';
+ const finalProgress = this.setRuntimeAdapterProgress(
+ {
+ ...launching,
+ state: success || pending ? 'ready' : 'failed',
+ message: success
+ ? 'OpenCode worktree lanes are ready'
+ : pending
+ ? 'OpenCode worktree lanes are waiting for runtime evidence or permissions'
+ : 'OpenCode worktree lane launch failed readiness gate',
+ messageSeverity: pending ? 'warning' : failed ? 'error' : undefined,
+ updatedAt: nowIso(),
+ error: failed
+ ? run.mixedSecondaryLanes
+ .flatMap((lane) => lane.diagnostics)
+ .filter(Boolean)
+ .join('\n') || 'OpenCode worktree lane launch failed'
+ : undefined,
+ cliLogsTail:
+ run.mixedSecondaryLanes.flatMap((lane) => lane.diagnostics).join('\n') || undefined,
+ configReady: true,
+ },
+ input.onProgress
+ );
+ run.progress = finalProgress;
+ if (success || pending) {
+ this.setAliveRunId(input.request.teamName, runId);
+ } else {
+ this.deleteAliveRunId(input.request.teamName);
+ this.runtimeAdapterRunByTeam.delete(input.request.teamName);
+ }
+ if (this.provisioningRunByTeam.get(input.request.teamName) === runId) {
+ this.provisioningRunByTeam.delete(input.request.teamName);
+ }
+ this.invalidateRuntimeSnapshotCaches(input.request.teamName);
+ this.teamChangeEmitter?.({
+ type: 'process',
+ teamName: input.request.teamName,
+ runId,
+ detail: finalProgress.state,
+ });
+ return { runId };
+ } catch (error) {
+ if (
+ this.cancelledRuntimeAdapterRunIds.delete(runId) ||
+ this.provisioningRunByTeam.get(input.request.teamName) !== runId
+ ) {
+ return { runId };
+ }
+ for (const lane of run.mixedSecondaryLanes) {
+ await clearOpenCodeRuntimeLaneStorage({
+ teamsBasePath: getTeamsBasePath(),
+ teamName: input.request.teamName,
+ laneId: lane.laneId,
+ }).catch(() => undefined);
+ this.deleteSecondaryRuntimeRun(input.request.teamName, lane.laneId);
+ }
+ if (run.effectiveMembers.length > 0) {
+ await clearOpenCodeRuntimeLaneStorage({
+ teamsBasePath: getTeamsBasePath(),
+ teamName: input.request.teamName,
+ laneId: 'primary',
+ }).catch(() => undefined);
+ }
+ const message = error instanceof Error ? error.message : String(error);
+ const failedProgress = this.setRuntimeAdapterProgress(
+ {
+ ...launching,
+ state: 'failed',
+ message: 'OpenCode worktree lane launch failed',
+ messageSeverity: 'error',
+ updatedAt: nowIso(),
+ error: message,
+ cliLogsTail: message,
+ },
+ input.onProgress
+ );
+ run.progress = failedProgress;
+ if (this.provisioningRunByTeam.get(input.request.teamName) === runId) {
+ this.provisioningRunByTeam.delete(input.request.teamName);
+ }
+ this.runtimeAdapterRunByTeam.delete(input.request.teamName);
+ this.deleteAliveRunId(input.request.teamName);
+ this.invalidateRuntimeSnapshotCaches(input.request.teamName);
+ throw error;
+ }
+ }
+
private async runOpenCodeTeamRuntimeAdapterLaunch(input: {
request: TeamCreateRequest | TeamLaunchRequest;
members: TeamCreateRequest['members'];
@@ -21913,6 +22470,7 @@ export class TeamProvisioningService {
teamName: input.teamName,
expectedMembers: input.expectedMembers.map((member) => member.name),
bootstrapExpectedMembers: input.expectedMembers.map((member) => member.name),
+ includeLeadMembers: true,
leadSessionId: result.leadSessionId,
launchPhase: committedResult.launchPhase,
members,
@@ -22276,7 +22834,11 @@ export class TeamProvisioningService {
allEffectiveMemberSpecs
)
);
- const lanePlan = this.planRuntimeLanesOrThrow(request.providerId, allEffectiveMemberSpecs);
+ const lanePlan = this.planRuntimeLanesOrThrow(
+ request.providerId,
+ allEffectiveMemberSpecs,
+ request.cwd
+ );
const primaryMemberNames = new Set(lanePlan.primaryMembers.map((member) => member.name));
const effectiveMemberSpecs = allEffectiveMemberSpecs.filter((member) =>
primaryMemberNames.has(member.name)
@@ -23416,15 +23978,26 @@ export class TeamProvisioningService {
return 0;
}
+ const readCommitBatch: (InboxMessage & { messageId: string })[] = [];
for (const m of batch) {
- relayedIds.add(m.messageId);
+ if (m.messageKind !== 'member_work_sync_nudge') {
+ readCommitBatch.push(m);
+ relayedIds.add(m.messageId);
+ continue;
+ }
+ if (await this.hasAcceptedMemberWorkSyncReport({ teamName, memberName })) {
+ readCommitBatch.push(m);
+ relayedIds.add(m.messageId);
+ }
}
this.relayedMemberInboxMessageIds.set(relayKey, this.trimRelayedSet(relayedIds));
- try {
- await this.markInboxMessagesRead(teamName, memberName, batch);
- } catch {
- // Best-effort: relay succeeded; marking read failed.
+ if (readCommitBatch.length > 0) {
+ try {
+ await this.markInboxMessagesRead(teamName, memberName, readCommitBatch);
+ } catch {
+ // Best-effort: relay succeeded; marking read failed.
+ }
}
return batch.length;
@@ -23474,13 +24047,21 @@ export class TeamProvisioningService {
);
if (inboxName.trim().toLowerCase() === leadName?.toLowerCase()) {
if (isOpenCodeRecipient) {
- const diagnostic =
- 'opencode_lead_runtime_session_missing: OpenCode lead inbox relay is unsupported in v1; leaving inbox unread for durable retry/diagnostics.';
- logger.warn(`[${teamName}] ${diagnostic} inbox=${inboxName}`);
+ const relayOptions: OpenCodeMemberInboxRelayOptions = {
+ source: options.source ?? 'watcher',
+ ...(options.onlyMessageId ? { onlyMessageId: options.onlyMessageId } : {}),
+ ...(options.deliveryMetadata ? { deliveryMetadata: options.deliveryMetadata } : {}),
+ };
+ const relay = await this.relayOpenCodeMemberInboxMessages(
+ teamName,
+ inboxName,
+ relayOptions
+ );
return {
- kind: 'opencode_lead_unsupported',
- relayed: 0,
- diagnostics: [diagnostic],
+ kind: 'opencode_member',
+ relayed: relay.relayed,
+ diagnostics: relay.diagnostics,
+ lastDelivery: relay.lastDelivery,
};
}
return {
@@ -25069,6 +25650,9 @@ export class TeamProvisioningService {
if (!run && this.runtimeAdapterRunByTeam.get(teamName)?.runId === runId) {
return true;
}
+ if (run && this.hasSecondaryRuntimeRuns(teamName)) {
+ return !run.processKilled && !run.cancelRequested;
+ }
return run?.child != null && !run.processKilled && !run.cancelRequested;
}
@@ -29945,7 +30529,7 @@ export class TeamProvisioningService {
const leadProviderId =
normalizeOptionalTeamProviderId(leadLaunchIdentity?.providerId) ??
normalizeOptionalTeamProviderId(teamMeta?.providerId);
- if (!leadProviderId || leadProviderId === 'opencode') {
+ if (!leadProviderId) {
return null;
}
@@ -30023,13 +30607,39 @@ export class TeamProvisioningService {
let recoveredAny = false;
for (const member of activeMembers) {
- const laneIdentity = buildPlannedMemberLaneIdentity({
- leadProviderId,
- member: {
- name: member.name,
- providerId: normalizeOptionalTeamProviderId(member.providerId),
- },
- });
+ const persistedMember =
+ persistedSnapshot?.members?.[member.name] ?? bootstrapSnapshot?.members?.[member.name];
+ const laneIdentity =
+ leadProviderId === 'opencode'
+ ? (() => {
+ const persistedLaneId = persistedMember?.laneId?.startsWith('secondary:opencode:')
+ ? persistedMember.laneId
+ : null;
+ const generatedLaneId = buildOpenCodeSecondaryLaneId(member);
+ const memberCwd = member.cwd?.trim();
+ const projectRoot = projectPath?.trim();
+ const hasWorktreeRoot =
+ Boolean(memberCwd) && (!projectRoot || memberCwd !== projectRoot);
+ if (!persistedLaneId && !laneIndex.lanes[generatedLaneId] && !hasWorktreeRoot) {
+ return {
+ laneId: 'primary',
+ laneKind: 'primary',
+ laneOwnerProviderId: leadProviderId,
+ } as const;
+ }
+ return {
+ laneId: persistedLaneId ?? generatedLaneId,
+ laneKind: 'secondary',
+ laneOwnerProviderId: 'opencode',
+ } as const;
+ })()
+ : buildPlannedMemberLaneIdentity({
+ leadProviderId,
+ member: {
+ name: member.name,
+ providerId: normalizeOptionalTeamProviderId(member.providerId),
+ },
+ });
if (
laneIdentity.laneKind !== 'secondary' ||
@@ -30040,8 +30650,6 @@ export class TeamProvisioningService {
}
let laneEntry = laneIndex.lanes[laneIdentity.laneId];
- const persistedMember =
- persistedSnapshot?.members?.[member.name] ?? bootstrapSnapshot?.members?.[member.name];
if (
!laneEntry &&
persistedMember &&
@@ -31264,9 +31872,21 @@ export class TeamProvisioningService {
const heartbeatReason = heartbeatMessage
? extractBootstrapFailureReason(heartbeatMessage.text)
: null;
+ const bootstrapFailureReason =
+ bootstrapMember?.hardFailure === true &&
+ !bootstrapMember.bootstrapConfirmed &&
+ isBootstrapMemberEvidenceCurrentForMember(
+ currentBootstrapEvidenceBoundary,
+ bootstrapMember,
+ 'confirmation'
+ )
+ ? (bootstrapMember.hardFailureReason ?? bootstrapMember.runtimeDiagnostic)
+ : null;
const acceptedAtMs =
current.firstSpawnAcceptedAt != null ? Date.parse(current.firstSpawnAcceptedAt) : NaN;
const initialFailureReason = current.hardFailureReason ?? current.runtimeDiagnostic;
+ const hasBootstrapCheckInTimeoutFailure =
+ isBootstrapCheckInTimeoutFailureReason(initialFailureReason);
const hadAutoClearableFailure = isAutoClearableLaunchFailureReason(initialFailureReason);
const requiresConfirmedBootstrapToClearFailure =
isCliProvisionedButNotAliveFailureReason(initialFailureReason);
@@ -31338,6 +31958,8 @@ export class TeamProvisioningService {
const currentProvesSpawnAcceptance =
current.agentToolAccepted === true || typeof current.firstSpawnAcceptedAt === 'string';
if (
+ !bootstrapFailureReason &&
+ !hasBootstrapCheckInTimeoutFailure &&
hadAutoClearableFailure &&
!requiresConfirmedBootstrapToClearFailure &&
(bootstrapProvesSpawnAcceptance || currentProvesSpawnAcceptance)
@@ -31366,6 +31988,18 @@ export class TeamProvisioningService {
if (heartbeatReason) {
current.hardFailure = true;
current.hardFailureReason = heartbeatReason;
+ current.runtimeDiagnostic = heartbeatReason;
+ current.runtimeDiagnosticSeverity = 'error';
+ current.diagnostics = mergeRuntimeDiagnostics(current.diagnostics, [heartbeatReason]);
+ current.sources.hardFailureSignal = true;
+ } else if (bootstrapFailureReason) {
+ current.hardFailure = true;
+ current.hardFailureReason = bootstrapFailureReason;
+ current.runtimeDiagnostic = bootstrapFailureReason;
+ current.runtimeDiagnosticSeverity = 'error';
+ current.diagnostics = mergeRuntimeDiagnostics(current.diagnostics, [
+ bootstrapFailureReason,
+ ]);
current.sources.hardFailureSignal = true;
} else if (heartbeatMessage && !isOpenCodeSecondaryLaneMember) {
current.bootstrapConfirmed = true;
diff --git a/src/main/services/team/opencode/bridge/OpenCodeManagedHostProcessCleanup.ts b/src/main/services/team/opencode/bridge/OpenCodeManagedHostProcessCleanup.ts
index cdb2b565..d948dc1b 100644
--- a/src/main/services/team/opencode/bridge/OpenCodeManagedHostProcessCleanup.ts
+++ b/src/main/services/team/opencode/bridge/OpenCodeManagedHostProcessCleanup.ts
@@ -48,6 +48,11 @@ const MANAGED_ENV_IDENTITY_MARKERS = [
'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY=',
'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL=',
] as const;
+const MANAGED_INLINE_OPENCODE_CONFIG_PATTERNS = [
+ /OPENCODE_CONFIG_CONTENT=[\s\S]*"mcp"\s*:\s*\{[\s\S]*"agent-teams(?:-runtime-\d+)?"/i,
+ /OPENCODE_CONFIG_CONTENT=[\s\S]*"claude-multimodel runtime orchestration"/i,
+ /OPENCODE_CONFIG_CONTENT=[\s\S]*"(?:agent-teams|agent_teams|mcp__agent-teams|mcp__agent_teams)_\*"/i,
+] as const;
export async function cleanupManagedOpenCodeServeProcesses(
options: OpenCodeManagedHostProcessCleanupOptions
@@ -204,7 +209,8 @@ export function isAppManagedWindowsOpenCodeServeCommand(command: string): boolea
export function isManagedOpenCodeServeProcessDetails(details: string): boolean {
return (
processDetailsIncludeMarkers(details, MANAGED_ENV_MARKERS) &&
- MANAGED_ENV_IDENTITY_MARKERS.some((marker) => processDetailsIncludeMarker(details, marker))
+ (MANAGED_ENV_IDENTITY_MARKERS.some((marker) => processDetailsIncludeMarker(details, marker)) ||
+ MANAGED_INLINE_OPENCODE_CONFIG_PATTERNS.some((pattern) => pattern.test(details)))
);
}
diff --git a/src/main/services/team/provisioning/StaleProcessRuntimeMetadataCleanup.ts b/src/main/services/team/provisioning/StaleProcessRuntimeMetadataCleanup.ts
new file mode 100644
index 00000000..025ce326
--- /dev/null
+++ b/src/main/services/team/provisioning/StaleProcessRuntimeMetadataCleanup.ts
@@ -0,0 +1,210 @@
+import type {
+ TeamAgentRuntimeBackendType,
+ TeamAgentRuntimeLivenessKind,
+ TeamProviderId,
+} from '@shared/types';
+
+export const STALE_PROCESS_RUNTIME_METADATA_DIAGNOSTIC = 'persisted runtime pid is not alive';
+
+const STALE_PROCESS_RUNTIME_METADATA_FIELDS = [
+ 'runtimePid',
+ 'bootstrapExpectedAfter',
+ 'bootstrapProofToken',
+ 'bootstrapRunId',
+ 'bootstrapProofMode',
+ 'bootstrapContextHash',
+ 'bootstrapBriefingHash',
+ 'bootstrapRuntimeEventsPath',
+] as const;
+
+export interface StaleProcessRuntimeMetadataCleanupCandidate {
+ memberName: string;
+ runtimePid: number;
+ processPaneId: string;
+ agentId?: string;
+}
+
+export interface StaleProcessRuntimeMetadataCleanupInput {
+ memberName: string;
+ providerId?: TeamProviderId | string;
+ backendType?: TeamAgentRuntimeBackendType | string;
+ agentId?: string;
+ tmuxPaneId?: string;
+ runtimePid?: number;
+ runtimeSessionId?: string;
+ runtimeRunId?: string;
+ laneId?: string;
+ laneKind?: string;
+ laneOwnerProviderId?: string;
+ livenessKind?: TeamAgentRuntimeLivenessKind | string;
+ runtimeDiagnostic?: string;
+ processTableAvailable: boolean;
+ isLead: boolean;
+ isRemoved: boolean;
+}
+
+export interface StaleProcessRuntimeMetadataCleanupResult {
+ member: Record;
+ changed: boolean;
+}
+
+export interface StaleProcessRuntimeMetadataRuntimeGuard {
+ hasTrackedRun?: boolean;
+ hasRuntimeAdapterRun?: boolean;
+ hasSecondaryRuntimeRun?: boolean;
+ isStoppingSecondaryRuntimeTeam?: boolean;
+ hasLaunchStateStoreOperation?: boolean;
+ hasTeamOperationLock?: boolean;
+ hasActiveLaunchState?: boolean;
+}
+
+function normalizePositiveInteger(value: unknown): number | undefined {
+ return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined;
+}
+
+function normalizeString(value: unknown): string {
+ return typeof value === 'string' ? value.trim() : '';
+}
+
+function isOpenCodeProvider(providerId: unknown): boolean {
+ return normalizeString(providerId).toLowerCase() === 'opencode';
+}
+
+function hasRuntimeSessionId(value: unknown): boolean {
+ return normalizeString(value).length > 0;
+}
+
+function hasLaneRuntimeMetadata(value: {
+ laneId?: unknown;
+ laneKind?: unknown;
+ laneOwnerProviderId?: unknown;
+}): boolean {
+ return (
+ normalizeString(value.laneId).length > 0 ||
+ normalizeString(value.laneKind).length > 0 ||
+ normalizeString(value.laneOwnerProviderId).length > 0
+ );
+}
+
+function isDirectProcessRuntimeMetadata(params: {
+ backendType?: unknown;
+ tmuxPaneId?: unknown;
+ runtimePid: number;
+}): boolean {
+ const backendType = normalizeString(params.backendType).toLowerCase();
+ const tmuxPaneId = normalizeString(params.tmuxPaneId);
+ const processPaneId = `process:${params.runtimePid}`;
+ if (tmuxPaneId && tmuxPaneId !== processPaneId) {
+ return false;
+ }
+ return backendType === 'process' || tmuxPaneId === processPaneId;
+}
+
+export function hasDirectProcessRuntimeMetadataForStaleCleanup(params: {
+ backendType?: unknown;
+ tmuxPaneId?: unknown;
+ runtimePid?: unknown;
+}): boolean {
+ const runtimePid = normalizePositiveInteger(params.runtimePid);
+ return runtimePid != null
+ ? isDirectProcessRuntimeMetadata({
+ backendType: params.backendType,
+ tmuxPaneId: params.tmuxPaneId,
+ runtimePid,
+ })
+ : false;
+}
+
+export function shouldSkipStaleProcessRuntimeMetadataCleanupForRuntimeGuard(
+ input: StaleProcessRuntimeMetadataRuntimeGuard
+): boolean {
+ return Boolean(
+ input.hasTrackedRun ||
+ input.hasRuntimeAdapterRun ||
+ input.hasSecondaryRuntimeRun ||
+ input.isStoppingSecondaryRuntimeTeam ||
+ input.hasLaunchStateStoreOperation ||
+ input.hasTeamOperationLock ||
+ input.hasActiveLaunchState
+ );
+}
+
+export function collectStaleProcessRuntimeMetadataCleanupCandidate(
+ input: StaleProcessRuntimeMetadataCleanupInput
+): StaleProcessRuntimeMetadataCleanupCandidate | null {
+ const memberName = input.memberName.trim();
+ const runtimePid = normalizePositiveInteger(input.runtimePid);
+ if (!memberName || runtimePid == null) {
+ return null;
+ }
+ if (input.isLead || input.isRemoved) {
+ return null;
+ }
+ if (input.livenessKind !== 'stale_metadata') {
+ return null;
+ }
+ if (input.runtimeDiagnostic !== STALE_PROCESS_RUNTIME_METADATA_DIAGNOSTIC) {
+ return null;
+ }
+ if (!input.processTableAvailable) {
+ return null;
+ }
+ if (
+ isOpenCodeProvider(input.providerId) ||
+ hasRuntimeSessionId(input.runtimeSessionId) ||
+ hasLaneRuntimeMetadata(input)
+ ) {
+ return null;
+ }
+ if (
+ !isDirectProcessRuntimeMetadata({
+ backendType: input.backendType,
+ tmuxPaneId: input.tmuxPaneId,
+ runtimePid,
+ })
+ ) {
+ return null;
+ }
+
+ return {
+ memberName,
+ runtimePid,
+ processPaneId: `process:${runtimePid}`,
+ ...(input.agentId?.trim() ? { agentId: input.agentId.trim() } : {}),
+ };
+}
+
+export function clearStaleProcessRuntimeMetadataFromMember(
+ member: Record,
+ candidate: StaleProcessRuntimeMetadataCleanupCandidate
+): StaleProcessRuntimeMetadataCleanupResult {
+ const runtimePid = normalizePositiveInteger(member.runtimePid);
+ if (runtimePid == null || runtimePid !== candidate.runtimePid) {
+ return { member: { ...member }, changed: false };
+ }
+ if (isOpenCodeProvider(member.providerId ?? member.provider)) {
+ return { member: { ...member }, changed: false };
+ }
+ if (hasRuntimeSessionId(member.runtimeSessionId) || hasLaneRuntimeMetadata(member)) {
+ return { member: { ...member }, changed: false };
+ }
+ if (
+ !isDirectProcessRuntimeMetadata({
+ backendType: member.backendType,
+ tmuxPaneId: member.tmuxPaneId,
+ runtimePid,
+ })
+ ) {
+ return { member: { ...member }, changed: false };
+ }
+
+ const next = { ...member };
+ for (const field of STALE_PROCESS_RUNTIME_METADATA_FIELDS) {
+ delete next[field];
+ }
+ if (normalizeString(member.tmuxPaneId) === candidate.processPaneId) {
+ delete next.tmuxPaneId;
+ }
+
+ return { member: next, changed: true };
+}
diff --git a/src/main/services/team/provisioning/TeamProvisioningOpenCodeRuntimeEvidencePolicy.ts b/src/main/services/team/provisioning/TeamProvisioningOpenCodeRuntimeEvidencePolicy.ts
index 6e972fd7..4a392162 100644
--- a/src/main/services/team/provisioning/TeamProvisioningOpenCodeRuntimeEvidencePolicy.ts
+++ b/src/main/services/team/provisioning/TeamProvisioningOpenCodeRuntimeEvidencePolicy.ts
@@ -600,6 +600,10 @@ export function isBootstrapMemberEvidenceCurrentForMember(
typeof current.runtimeRunId === 'string' ? current.runtimeRunId.trim() : '';
const bootstrapRuntimeRunId =
typeof bootstrapMember.runtimeRunId === 'string' ? bootstrapMember.runtimeRunId.trim() : '';
+ const hasSameRuntimeRunId =
+ currentRuntimeRunId.length > 0 &&
+ bootstrapRuntimeRunId.length > 0 &&
+ currentRuntimeRunId === bootstrapRuntimeRunId;
if (
currentRuntimeRunId.length > 0 &&
bootstrapRuntimeRunId.length > 0 &&
@@ -631,10 +635,18 @@ export function isBootstrapMemberEvidenceCurrentForMember(
const hasDurableSpawnBoundary =
Number.isFinite(firstSpawnAcceptedMs) &&
(!Number.isFinite(lastEvaluatedMs) || firstSpawnAcceptedMs <= lastEvaluatedMs);
- const boundaryMs = hasDurableSpawnBoundary ? firstSpawnAcceptedMs : NaN;
- const hasCompatibleRuntimeRunIdForSkew =
- currentRuntimeRunId.length === 0 ||
- (bootstrapRuntimeRunId.length > 0 && currentRuntimeRunId === bootstrapRuntimeRunId);
+ const currentBoundaryMs = hasDurableSpawnBoundary ? firstSpawnAcceptedMs : NaN;
+ const sameRunBootstrapBoundaryMs =
+ evidenceKind === 'confirmation' && hasSameRuntimeRunId && hasDurableBootstrapSpawnAcceptedAt
+ ? bootstrapFirstSpawnAcceptedMs
+ : NaN;
+ const boundaryMs =
+ Number.isFinite(currentBoundaryMs) && Number.isFinite(sameRunBootstrapBoundaryMs)
+ ? Math.min(currentBoundaryMs, sameRunBootstrapBoundaryMs)
+ : Number.isFinite(currentBoundaryMs)
+ ? currentBoundaryMs
+ : sameRunBootstrapBoundaryMs;
+ const hasCompatibleRuntimeRunIdForSkew = currentRuntimeRunId.length === 0 || hasSameRuntimeRunId;
const withinBootstrapConfirmationClockSkew =
evidenceKind === 'confirmation' &&
Number.isFinite(boundaryMs) &&
diff --git a/src/main/services/team/provisioning/__tests__/StaleProcessRuntimeMetadataCleanup.test.ts b/src/main/services/team/provisioning/__tests__/StaleProcessRuntimeMetadataCleanup.test.ts
new file mode 100644
index 00000000..4a62c2d7
--- /dev/null
+++ b/src/main/services/team/provisioning/__tests__/StaleProcessRuntimeMetadataCleanup.test.ts
@@ -0,0 +1,341 @@
+import { describe, expect, it } from 'vitest';
+
+import { resolveTeamMemberRuntimeLiveness } from '../../TeamRuntimeLivenessResolver';
+import {
+ clearStaleProcessRuntimeMetadataFromMember,
+ collectStaleProcessRuntimeMetadataCleanupCandidate,
+ hasDirectProcessRuntimeMetadataForStaleCleanup,
+ shouldSkipStaleProcessRuntimeMetadataCleanupForRuntimeGuard,
+ STALE_PROCESS_RUNTIME_METADATA_DIAGNOSTIC,
+} from '../StaleProcessRuntimeMetadataCleanup';
+
+import type { RuntimeProcessTableRow } from '@features/tmux-installer/main';
+
+const baseCandidateInput = {
+ memberName: 'tom',
+ providerId: 'codex',
+ backendType: 'process',
+ agentId: 'tom@signal-ops-2',
+ tmuxPaneId: 'process:37749',
+ runtimePid: 37749,
+ runtimeSessionId: undefined,
+ livenessKind: 'stale_metadata',
+ runtimeDiagnostic: STALE_PROCESS_RUNTIME_METADATA_DIAGNOSTIC,
+ processTableAvailable: true,
+ isLead: false,
+ isRemoved: false,
+} as const;
+
+function createRuntimeMember(overrides: Record = {}): Record {
+ return {
+ name: 'tom',
+ agentId: 'tom@signal-ops-2',
+ provider: 'codex',
+ providerId: 'codex',
+ model: 'gpt-5.5',
+ role: 'developer',
+ prompt: 'Build things',
+ color: 'yellow',
+ cwd: '/repo',
+ subscriptions: ['team-lead'],
+ backendType: 'process',
+ tmuxPaneId: 'process:37749',
+ runtimePid: 37749,
+ bootstrapExpectedAfter: '2026-05-16T18:35:52.562Z',
+ bootstrapProofToken: 'token',
+ bootstrapRunId: 'run-1',
+ bootstrapProofMode: 'native_app_managed_context',
+ bootstrapContextHash: 'context-hash',
+ bootstrapBriefingHash: 'briefing-hash',
+ bootstrapRuntimeEventsPath: '/repo/.agent-teams/tom.runtime.jsonl',
+ ...overrides,
+ };
+}
+
+describe('stale process runtime metadata cleanup planner', () => {
+ it('clears only stale direct-process runtime fields and preserves member identity', () => {
+ const candidate = collectStaleProcessRuntimeMetadataCleanupCandidate(baseCandidateInput);
+
+ expect(candidate).toEqual({
+ memberName: 'tom',
+ runtimePid: 37749,
+ processPaneId: 'process:37749',
+ agentId: 'tom@signal-ops-2',
+ });
+
+ const result = clearStaleProcessRuntimeMetadataFromMember(createRuntimeMember(), candidate!);
+
+ expect(result.changed).toBe(true);
+ expect(result.member).toMatchObject({
+ name: 'tom',
+ agentId: 'tom@signal-ops-2',
+ provider: 'codex',
+ providerId: 'codex',
+ model: 'gpt-5.5',
+ role: 'developer',
+ prompt: 'Build things',
+ color: 'yellow',
+ cwd: '/repo',
+ subscriptions: ['team-lead'],
+ backendType: 'process',
+ });
+ expect(result.member.runtimePid).toBeUndefined();
+ expect(result.member.tmuxPaneId).toBeUndefined();
+ expect(result.member.bootstrapRunId).toBeUndefined();
+ expect(result.member.bootstrapRuntimeEventsPath).toBeUndefined();
+ });
+
+ it('skips OpenCode members', () => {
+ expect(
+ collectStaleProcessRuntimeMetadataCleanupCandidate({
+ ...baseCandidateInput,
+ providerId: 'opencode',
+ })
+ ).toBeNull();
+
+ const candidate = collectStaleProcessRuntimeMetadataCleanupCandidate(baseCandidateInput)!;
+ const result = clearStaleProcessRuntimeMetadataFromMember(
+ createRuntimeMember({ providerId: 'opencode', provider: 'opencode' }),
+ candidate
+ );
+
+ expect(result.changed).toBe(false);
+ expect(result.member.runtimePid).toBe(37749);
+ });
+
+ it('skips normal tmux pane metadata', () => {
+ expect(
+ collectStaleProcessRuntimeMetadataCleanupCandidate({
+ ...baseCandidateInput,
+ backendType: 'tmux',
+ tmuxPaneId: '%12',
+ })
+ ).toBeNull();
+ });
+
+ it('skips mismatched process pane metadata', () => {
+ expect(
+ collectStaleProcessRuntimeMetadataCleanupCandidate({
+ ...baseCandidateInput,
+ tmuxPaneId: 'process:99999',
+ })
+ ).toBeNull();
+ });
+
+ it('matches only direct-process runtime metadata shapes', () => {
+ expect(
+ hasDirectProcessRuntimeMetadataForStaleCleanup({
+ backendType: 'process',
+ tmuxPaneId: 'process:37749',
+ runtimePid: 37749,
+ })
+ ).toBe(true);
+ expect(
+ hasDirectProcessRuntimeMetadataForStaleCleanup({
+ backendType: 'process',
+ tmuxPaneId: 'process:99999',
+ runtimePid: 37749,
+ })
+ ).toBe(false);
+ expect(
+ hasDirectProcessRuntimeMetadataForStaleCleanup({
+ backendType: 'tmux',
+ tmuxPaneId: '%12',
+ runtimePid: 37749,
+ })
+ ).toBe(false);
+ });
+
+ it('skips active or uncertain cleanup guards', () => {
+ expect(
+ collectStaleProcessRuntimeMetadataCleanupCandidate({
+ ...baseCandidateInput,
+ processTableAvailable: false,
+ })
+ ).toBeNull();
+ expect(
+ collectStaleProcessRuntimeMetadataCleanupCandidate({
+ ...baseCandidateInput,
+ isLead: true,
+ })
+ ).toBeNull();
+ expect(
+ collectStaleProcessRuntimeMetadataCleanupCandidate({
+ ...baseCandidateInput,
+ isRemoved: true,
+ })
+ ).toBeNull();
+ expect(
+ collectStaleProcessRuntimeMetadataCleanupCandidate({
+ ...baseCandidateInput,
+ runtimeSessionId: 'session-1',
+ })
+ ).toBeNull();
+
+ expect(
+ shouldSkipStaleProcessRuntimeMetadataCleanupForRuntimeGuard({ hasTrackedRun: true })
+ ).toBe(true);
+ expect(
+ shouldSkipStaleProcessRuntimeMetadataCleanupForRuntimeGuard({
+ hasRuntimeAdapterRun: true,
+ })
+ ).toBe(true);
+ expect(
+ shouldSkipStaleProcessRuntimeMetadataCleanupForRuntimeGuard({
+ hasActiveLaunchState: true,
+ })
+ ).toBe(true);
+ expect(shouldSkipStaleProcessRuntimeMetadataCleanupForRuntimeGuard({})).toBe(false);
+ });
+
+ it('skips lane metadata but allows direct bootstrap run ids', () => {
+ expect(
+ collectStaleProcessRuntimeMetadataCleanupCandidate({
+ ...baseCandidateInput,
+ laneId: 'secondary:bob',
+ })
+ ).toBeNull();
+ expect(
+ collectStaleProcessRuntimeMetadataCleanupCandidate({
+ ...baseCandidateInput,
+ runtimeRunId: 'bootstrap-run-1',
+ })?.runtimePid
+ ).toBe(37749);
+
+ const candidate = collectStaleProcessRuntimeMetadataCleanupCandidate(baseCandidateInput)!;
+ const result = clearStaleProcessRuntimeMetadataFromMember(
+ createRuntimeMember({ laneId: 'secondary:bob' }),
+ candidate
+ );
+
+ expect(result.changed).toBe(false);
+ expect(result.member.runtimePid).toBe(37749);
+ });
+
+ it('does not clear if the process table shows the same pid is alive', () => {
+ const candidate = collectStaleProcessRuntimeMetadataCleanupCandidate(baseCandidateInput)!;
+ const processRows: RuntimeProcessTableRow[] = [
+ { pid: 37749, ppid: 1, command: 'node some-other-process.js' },
+ ];
+
+ const processStillExists = processRows.some((row) => row.pid === candidate.runtimePid);
+
+ expect(processStillExists).toBe(true);
+ });
+});
+
+describe('stale process runtime metadata cleanup runtime flow', () => {
+ it('plans cleanup for stale metadata when process table is available and no pid exists', () => {
+ const resolved = resolveTeamMemberRuntimeLiveness({
+ teamName: 'signal-ops-2',
+ memberName: 'tom',
+ agentId: 'tom@signal-ops-2',
+ providerId: 'codex',
+ backendType: 'process',
+ tmuxPaneId: 'process:37749',
+ persistedRuntimePid: 37749,
+ processRows: [],
+ processTableAvailable: true,
+ nowIso: '2026-05-28T00:00:00.000Z',
+ });
+
+ const candidate = collectStaleProcessRuntimeMetadataCleanupCandidate({
+ ...baseCandidateInput,
+ livenessKind: resolved.livenessKind,
+ runtimeDiagnostic: resolved.runtimeDiagnostic,
+ runtimePid: resolved.pid,
+ });
+
+ expect(resolved.livenessKind).toBe('stale_metadata');
+ expect(candidate?.runtimePid).toBe(37749);
+ });
+
+ it('does not plan cleanup for registered-only metadata', () => {
+ const resolved = resolveTeamMemberRuntimeLiveness({
+ teamName: 'signal-ops-2',
+ memberName: 'bob',
+ agentId: 'bob@signal-ops-2',
+ providerId: 'opencode',
+ backendType: 'process',
+ processRows: [],
+ processTableAvailable: true,
+ nowIso: '2026-05-28T00:00:00.000Z',
+ });
+
+ expect(resolved.livenessKind).toBe('registered_only');
+ expect(
+ collectStaleProcessRuntimeMetadataCleanupCandidate({
+ ...baseCandidateInput,
+ memberName: 'bob',
+ providerId: 'opencode',
+ runtimePid: undefined,
+ livenessKind: resolved.livenessKind,
+ runtimeDiagnostic: resolved.runtimeDiagnostic,
+ })
+ ).toBeNull();
+ });
+
+ it('does not plan cleanup for verified runtime process evidence', () => {
+ const resolved = resolveTeamMemberRuntimeLiveness({
+ teamName: 'signal-ops-2',
+ memberName: 'tom',
+ agentId: 'tom@signal-ops-2',
+ providerId: 'codex',
+ backendType: 'process',
+ persistedRuntimePid: 37749,
+ processRows: [
+ {
+ pid: 55555,
+ ppid: 1,
+ command: 'node runtime.js --team-name signal-ops-2 --agent-id tom@signal-ops-2',
+ },
+ ],
+ processTableAvailable: true,
+ nowIso: '2026-05-28T00:00:00.000Z',
+ });
+
+ expect(resolved.livenessKind).toBe('runtime_process');
+ expect(
+ collectStaleProcessRuntimeMetadataCleanupCandidate({
+ ...baseCandidateInput,
+ runtimePid: resolved.pid,
+ livenessKind: resolved.livenessKind,
+ runtimeDiagnostic: resolved.runtimeDiagnostic,
+ })
+ ).toBeNull();
+ });
+
+ it('does not plan cleanup for confirmed bootstrap evidence', () => {
+ const resolved = resolveTeamMemberRuntimeLiveness({
+ teamName: 'signal-ops-2',
+ memberName: 'alice',
+ agentId: 'alice@signal-ops-2',
+ providerId: 'anthropic',
+ backendType: 'process',
+ trackedSpawnStatus: {
+ status: 'online',
+ launchState: 'confirmed_alive',
+ agentToolAccepted: true,
+ runtimeAlive: true,
+ bootstrapConfirmed: true,
+ hardFailure: false,
+ updatedAt: '2026-05-28T00:00:00.000Z',
+ },
+ processRows: [],
+ processTableAvailable: true,
+ nowIso: '2026-05-28T00:00:00.000Z',
+ });
+
+ expect(resolved.livenessKind).toBe('confirmed_bootstrap');
+ expect(
+ collectStaleProcessRuntimeMetadataCleanupCandidate({
+ ...baseCandidateInput,
+ memberName: 'alice',
+ providerId: 'anthropic',
+ runtimePid: undefined,
+ livenessKind: resolved.livenessKind,
+ runtimeDiagnostic: resolved.runtimeDiagnostic,
+ })
+ ).toBeNull();
+ });
+});
diff --git a/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts b/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts
index 850d0ee6..65b083ee 100644
--- a/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts
+++ b/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts
@@ -1116,10 +1116,19 @@ function buildMemberBootstrapPrompt(
const teamPrompt = input.prompt?.trim();
const role = member.role?.trim() || member.workflow?.trim() || 'teammate';
const workflow = member.workflow?.trim();
+ const isTeamLead =
+ member.name.trim().toLowerCase() === 'team-lead' || role.trim().toLowerCase() === 'team lead';
+ const identityLine = isTeamLead
+ ? `You are ${member.name}, the team lead for team "${input.teamName}".`
+ : `You are ${member.name}, a ${role} on team "${input.teamName}".`;
+ const messageTargets = isTeamLead
+ ? 'the human user or a teammate'
+ : 'the human user, team lead, or another teammate';
+ const senderRole = isTeamLead ? 'team lead' : 'OpenCode teammate';
return [
'',
'AGENT_TEAMS_APP_MANAGED_BOOTSTRAP_V1',
- `You are ${member.name}, a ${role} on team "${input.teamName}".`,
+ identityLine,
teamPrompt ? `Team launch context:\n${teamPrompt}` : null,
workflow ? `Workflow:\n${workflow}` : null,
'',
@@ -1132,8 +1141,8 @@ function buildMemberBootstrapPrompt(
'Do not call task_briefing, message_send, or cross_team_send just to announce readiness, say understood, report no tasks, or ask for work.',
'If the briefing says there are no actionable tasks, stay idle silently.',
'',
- 'When you need to message the human user, team lead, or another teammate, call MCP tool agent-teams_message_send (or mcp__agent-teams__message_send) with teamName, to, from, text, and optional summary.',
- `Always set from="${member.name}" when sending a team message from this OpenCode teammate.`,
+ `When you need to message ${messageTargets}, call MCP tool agent-teams_message_send (or mcp__agent-teams__message_send) with teamName, to, from, text, and optional summary.`,
+ `Always set from="${member.name}" when sending a team message from this ${senderRole}.`,
'Do not answer team/app messages only as plain assistant text when agent-teams_message_send is available.',
' ',
]
diff --git a/src/main/services/team/taskLogs/stream/BoardTaskLogStreamService.ts b/src/main/services/team/taskLogs/stream/BoardTaskLogStreamService.ts
index ee88bfed..7e8c50a7 100644
--- a/src/main/services/team/taskLogs/stream/BoardTaskLogStreamService.ts
+++ b/src/main/services/team/taskLogs/stream/BoardTaskLogStreamService.ts
@@ -2,6 +2,10 @@ import { extractToolCalls, extractToolResults } from '@main/utils/toolExtraction
import { isLeadMember as isLeadMemberCheck } from '@shared/utils/leadDetection';
import { createLogger } from '@shared/utils/logger';
import { getTaskDisplayId } from '@shared/utils/taskIdentity';
+import {
+ inferTeamProviderIdFromModel,
+ normalizeOptionalTeamProviderId,
+} from '@shared/utils/teamProvider';
import { TeamConfigReader } from '../../TeamConfigReader';
import { TeamMembersMetaStore } from '../../TeamMembersMetaStore';
@@ -36,6 +40,8 @@ import type {
BoardTaskLogSegment,
BoardTaskLogStreamResponse,
BoardTaskLogStreamSummary,
+ TeamMember,
+ TeamProviderId,
TeamTask,
} from '@shared/types';
@@ -104,6 +110,58 @@ function normalizeMemberName(value: string): string {
return value.trim().toLowerCase();
}
+function resolveExplicitMemberProviderId(
+ member: TeamMember | undefined
+): TeamProviderId | undefined {
+ if (!member) {
+ return undefined;
+ }
+ const legacyProvider = (member as { provider?: unknown }).provider;
+ return (
+ normalizeOptionalTeamProviderId(member.providerId) ??
+ normalizeOptionalTeamProviderId(legacyProvider)
+ );
+}
+
+function inferProviderIdFromMemberModel(
+ member: TeamMember | undefined
+): TeamProviderId | undefined {
+ return inferTeamProviderIdFromModel(member?.model);
+}
+
+function inferProviderIdFromBackend(providerBackendId: unknown): TeamProviderId | undefined {
+ const normalized = typeof providerBackendId === 'string' ? providerBackendId.trim() : '';
+ if (normalized === 'codex-native') {
+ return 'codex';
+ }
+ if (normalized === 'opencode-cli') {
+ return 'opencode';
+ }
+ return undefined;
+}
+
+function resolveProviderFromMemberSources(input: {
+ configMembers: readonly TeamMember[];
+ metaMembers: readonly TeamMember[];
+ memberName: string;
+}): TeamProviderId | undefined {
+ const normalizedMemberName = normalizeMemberName(input.memberName);
+ const configMember = input.configMembers.find(
+ (candidate) => normalizeMemberName(candidate.name) === normalizedMemberName
+ );
+ const metaMember = input.metaMembers.find(
+ (candidate) => normalizeMemberName(candidate.name) === normalizedMemberName
+ );
+ return (
+ resolveExplicitMemberProviderId(metaMember) ??
+ resolveExplicitMemberProviderId(configMember) ??
+ inferProviderIdFromBackend(configMember?.providerBackendId) ??
+ inferProviderIdFromMemberModel(configMember) ??
+ inferProviderIdFromBackend(metaMember?.providerBackendId) ??
+ inferProviderIdFromMemberModel(metaMember)
+ );
+}
+
const isBoardMcpToolName = isBoardTaskLogMcpToolName;
const canonicalizeBoardToolName = canonicalizeBoardTaskLogToolName;
@@ -2260,10 +2318,13 @@ export class BoardTaskLogStreamService {
return false;
}
- const member = [...metaMembers, ...(config?.members ?? [])].find(
- (candidate) => normalizeMemberName(candidate.name) === normalizedOwner
+ return (
+ resolveProviderFromMemberSources({
+ configMembers: config?.members ?? [],
+ metaMembers,
+ memberName: normalizedOwner,
+ }) === 'opencode'
);
- return member?.providerId === 'opencode';
} catch {
return false;
}
diff --git a/src/main/services/team/taskLogs/stream/CodexNativeTaskLogStreamSource.ts b/src/main/services/team/taskLogs/stream/CodexNativeTaskLogStreamSource.ts
index 74a051df..695f4531 100644
--- a/src/main/services/team/taskLogs/stream/CodexNativeTaskLogStreamSource.ts
+++ b/src/main/services/team/taskLogs/stream/CodexNativeTaskLogStreamSource.ts
@@ -1,4 +1,8 @@
import { getTaskDisplayId } from '@shared/utils/taskIdentity';
+import {
+ inferTeamProviderIdFromModel,
+ normalizeOptionalTeamProviderId,
+} from '@shared/utils/teamProvider';
import { TeamConfigReader } from '../../TeamConfigReader';
import { TeamMembersMetaStore } from '../../TeamMembersMetaStore';
@@ -14,6 +18,7 @@ import type {
BoardTaskLogParticipant,
BoardTaskLogSegment,
BoardTaskLogStreamResponse,
+ TeamProviderId,
TeamTask,
} from '@shared/types';
@@ -44,6 +49,31 @@ function buildActor(memberName: string, sessionId: string): BoardTaskLogActor {
};
}
+function resolveExplicitProviderId(member: {
+ providerId?: unknown;
+ provider?: unknown;
+}): ReturnType {
+ return (
+ normalizeOptionalTeamProviderId(member.providerId) ??
+ normalizeOptionalTeamProviderId(member.provider)
+ );
+}
+
+function inferProviderIdFromMemberModel(member: { model?: string } | undefined) {
+ return inferTeamProviderIdFromModel(member?.model);
+}
+
+function inferProviderIdFromBackend(providerBackendId: unknown): TeamProviderId | undefined {
+ const normalized = typeof providerBackendId === 'string' ? providerBackendId.trim() : '';
+ if (normalized === 'codex-native') {
+ return 'codex';
+ }
+ if (normalized === 'opencode-cli') {
+ return 'opencode';
+ }
+ return undefined;
+}
+
export class CodexNativeTaskLogStreamSource {
constructor(
private readonly taskReader: TeamTaskReader = new TeamTaskReader(),
@@ -171,9 +201,19 @@ export class CodexNativeTaskLogStreamSource {
this.membersMetaStore.getMembers(teamName).catch(() => []),
this.readConfigForObservation(teamName).catch(() => null),
]);
- const member = [...metaMembers, ...(config?.members ?? [])].find(
+ const configMember = (config?.members ?? []).find(
(candidate) => normalizeMemberName(candidate.name) === normalizedOwner
- ) as { providerId?: string } | undefined;
- return member?.providerId === 'codex';
+ );
+ const metaMember = metaMembers.find(
+ (candidate) => normalizeMemberName(candidate.name) === normalizedOwner
+ );
+ const providerId =
+ resolveExplicitProviderId(metaMember ?? {}) ??
+ resolveExplicitProviderId(configMember ?? {}) ??
+ inferProviderIdFromBackend(configMember?.providerBackendId) ??
+ inferProviderIdFromMemberModel(configMember) ??
+ inferProviderIdFromBackend(metaMember?.providerBackendId) ??
+ inferProviderIdFromMemberModel(metaMember);
+ return providerId === 'codex';
}
}
diff --git a/src/main/utils/childProcess.ts b/src/main/utils/childProcess.ts
index e489b250..49971f30 100644
--- a/src/main/utils/childProcess.ts
+++ b/src/main/utils/childProcess.ts
@@ -1,9 +1,7 @@
import {
type ChildProcess,
- exec,
execFile,
type ExecFileOptions,
- type ExecOptions,
spawn,
type SpawnOptions,
spawnSync,
@@ -80,65 +78,14 @@ function execFileAsync(
}
/**
- * Promise wrapper for exec. Used exclusively as a Windows shell fallback
- * when execFile fails with EINVAL on non-ASCII binary paths. The command
- * string is built from a known binary path + args, NOT from user input.
+ * cmd.exe fallback implemented through execFile so Node does not invoke an
+ * additional shell around the guarded command string.
*/
function execShellAsync(
cmd: string,
- options: ExecOptions = {}
+ options: ExecFileOptions = {}
): Promise<{ stdout: string; stderr: string }> {
- return new Promise((resolve, reject) => {
- const { timeout, killSignal, ...execOptions } = options;
- const timeoutMs = typeof timeout === 'number' && timeout > 0 ? timeout : 0;
- const timeoutSignal = normalizeKillSignal(killSignal);
- let child: ChildProcess | null = null;
- let settled = false;
- let stdoutText = '';
- let stderrText = '';
- let timeoutHandle: ReturnType | null = null;
- // eslint-disable-next-line sonarjs/os-command, security/detect-child-process -- cmd from known binaryPath+args, not user input (Windows EINVAL fallback)
- child = exec(cmd, execOptions, (err, stdout, stderr) => {
- if (settled) {
- return;
- }
- settled = true;
- timeoutHandle = cleanupTimedCliProcess(child, timeoutHandle);
- if (err)
- reject(
- err instanceof Error ? err : new Error(typeof err === 'string' ? err : 'Unknown error')
- );
- else resolve({ stdout: String(stdout), stderr: String(stderr) });
- });
- if (!settled) {
- trackCliProcess(child);
- if (timeoutMs > 0) {
- child.stdout?.on('data', (chunk: Buffer | string) => {
- stdoutText += chunk.toString();
- });
- child.stderr?.on('data', (chunk: Buffer | string) => {
- stderrText += chunk.toString();
- });
- timeoutHandle = setTimeout(() => {
- if (settled) {
- return;
- }
- settled = true;
- timeoutHandle = cleanupTimedCliProcess(child, timeoutHandle);
- killProcessTree(child, timeoutSignal);
- const error = new Error(`Command timed out after ${timeoutMs}ms: ${cmd}`);
- Object.assign(error, {
- killed: true,
- signal: timeoutSignal,
- stdout: stdoutText,
- stderr: stderrText,
- });
- reject(error);
- }, timeoutMs);
- timeoutHandle.unref?.();
- }
- }
- });
+ return execFileAsync(getWindowsCmdPath(), ['/d', '/s', '/c', cmd], options);
}
function cleanupTimedCliProcess(
@@ -300,6 +247,43 @@ function quoteArg(arg: string): string {
return quoteWindowsCmdArg(arg);
}
+function containsWindowsShellUnsafeControlChar(part: string): boolean {
+ for (let index = 0; index < part.length; index += 1) {
+ const code = part.charCodeAt(index);
+ if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+function assertSafeWindowsShellFallbackPart(part: string): void {
+ if (containsWindowsShellUnsafeControlChar(part)) {
+ throw new Error('Unsafe Windows shell fallback argument: control characters are not allowed');
+ }
+}
+
+function buildWindowsShellFallbackCommand(parts: string[]): string {
+ for (const part of parts) {
+ assertSafeWindowsShellFallbackPart(part);
+ }
+ return parts.map(quoteArg).join(' ');
+}
+
+function getWindowsCmdPath(): string {
+ return path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'cmd.exe');
+}
+
+function spawnWindowsShellFallback(
+ cmd: string,
+ options: ReturnType>
+): ReturnType {
+ return spawn(getWindowsCmdPath(), ['/d', '/s', '/c', cmd], {
+ ...options,
+ shell: false,
+ });
+}
+
/** Env vars injected into every spawned Claude CLI process. */
const CLI_ENV_DEFAULTS: Record = {
CLAUDE_HOOK_JUDGE_MODE: 'true',
@@ -408,8 +392,8 @@ export async function execCli(
}
// shell fallback (Windows only; others shouldn't reach here)
- const cmd = [target, ...args].map(quoteArg).join(' ');
- const shellResult = await execShellAsync(cmd, opts as unknown as ExecOptions);
+ const cmd = buildWindowsShellFallbackCommand([target, ...args]);
+ const shellResult = await execShellAsync(cmd, opts);
return { stdout: String(shellResult.stdout), stderr: String(shellResult.stderr) };
}
@@ -435,9 +419,8 @@ export function spawnCli(
}
if (process.platform === 'win32' && needsShell(binaryPath)) {
- const cmd = [binaryPath, ...args].map(quoteArg).join(' ');
- // eslint-disable-next-line sonarjs/os-command -- cmd from known binaryPath+args, not user input (Windows EINVAL fallback)
- return trackCliProcess(spawn(cmd, { ...opts, shell: true }));
+ const cmd = buildWindowsShellFallbackCommand([binaryPath, ...args]);
+ return trackCliProcess(spawnWindowsShellFallback(cmd, opts));
}
try {
@@ -446,9 +429,8 @@ export function spawnCli(
const code =
err && typeof err === 'object' && 'code' in err ? (err as { code?: string }).code : undefined;
if (process.platform === 'win32' && code === 'EINVAL') {
- const cmd = [binaryPath, ...args].map(quoteArg).join(' ');
- // eslint-disable-next-line sonarjs/os-command -- cmd from known binaryPath+args, not user input (Windows EINVAL fallback)
- return trackCliProcess(spawn(cmd, { ...opts, shell: true }));
+ const cmd = buildWindowsShellFallbackCommand([binaryPath, ...args]);
+ return trackCliProcess(spawnWindowsShellFallback(cmd, opts));
}
throw err;
}
diff --git a/src/renderer/components/dashboard/CliStatusBanner.tsx b/src/renderer/components/dashboard/CliStatusBanner.tsx
index 4669182b..73eab585 100644
--- a/src/renderer/components/dashboard/CliStatusBanner.tsx
+++ b/src/renderer/components/dashboard/CliStatusBanner.tsx
@@ -43,7 +43,10 @@ import {
shouldShowProviderStatusSkeleton,
} from '@renderer/components/runtime/providerConnectionUi';
import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges';
-import { getProviderRuntimeBackendSummary } from '@renderer/components/runtime/ProviderRuntimeBackendSelector';
+import {
+ buildProviderRuntimeBackendSummaryText,
+ getProviderRuntimeBackendSummary,
+} from '@renderer/components/runtime/ProviderRuntimeBackendSelector';
import {
getProviderTerminalCommand,
getProviderTerminalLogoutCommand,
@@ -827,6 +830,11 @@ const InstalledBanner = ({
}: InstalledBannerProps): React.JSX.Element => {
const { t } = useAppTranslation('dashboard');
const { t: settingsT } = useAppTranslation('settings');
+ const { t: commonT } = useAppTranslation('common');
+ const runtimeBackendSummaryText = useMemo(
+ () => buildProviderRuntimeBackendSummaryText(commonT),
+ [commonT]
+ );
const openExtensionsTab = useStore((s) => s.openExtensionsTab);
const styles = VARIANT_STYLES[variant];
const visibleProviders = useMemo(
@@ -954,7 +962,7 @@ const InstalledBanner = ({
const actionDisabled = isBusy || !cliStatus.binaryPath;
const runtimeSummary = isConnectionManagedRuntimeProvider(provider)
? getProviderCurrentRuntimeSummary(provider, settingsT)
- : getProviderRuntimeBackendSummary(provider);
+ : getProviderRuntimeBackendSummary(provider, runtimeBackendSummaryText);
const connectionModeSummary = getProviderConnectionModeSummary(provider, settingsT);
const credentialSummary = getProviderCredentialSummary(provider, settingsT);
const dashboardRateLimits = getDashboardRateLimitsForProvider(provider);
diff --git a/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx b/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx
index 80940929..445cba57 100644
--- a/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx
+++ b/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx
@@ -95,7 +95,9 @@ export const ApiKeyCard = ({ apiKey, onEdit }: ApiKeyCardProps): React.JSX.Eleme
)}
-
{copied ? 'Copied!' : 'Copy env var name'}
+
+ {copied ? t('apiKeys.actions.copied') : t('apiKeys.actions.copyEnvVarName')}
+
@@ -135,7 +137,9 @@ export const ApiKeyCard = ({ apiKey, onEdit }: ApiKeyCardProps): React.JSX.Eleme
- {confirmDelete ? 'Click again to confirm' : 'Delete'}
+
+ {confirmDelete ? t('apiKeys.actions.confirmDelete') : t('apiKeys.actions.delete')}
+
diff --git a/src/renderer/components/runtime/ProviderRuntimeBackendSelector.tsx b/src/renderer/components/runtime/ProviderRuntimeBackendSelector.tsx
index 3da2b320..c6d152eb 100644
--- a/src/renderer/components/runtime/ProviderRuntimeBackendSelector.tsx
+++ b/src/renderer/components/runtime/ProviderRuntimeBackendSelector.tsx
@@ -16,6 +16,52 @@ interface Props {
onSelect: (providerId: CliProviderStatus['providerId'], backendId: string) => void;
}
+export interface ProviderRuntimeBackendSummaryText {
+ auto: string;
+ autoCurrently: (backend: string) => string;
+ audienceInternal: string;
+ states: {
+ locked: string;
+ disabled: string;
+ authRequired: string;
+ runtimeMissing: string;
+ degraded: string;
+ unavailable: string;
+ };
+}
+
+export function buildProviderRuntimeBackendSummaryText(
+ t: ReturnType['t']
+): ProviderRuntimeBackendSummaryText {
+ return {
+ auto: t('runtimeBackendSelector.auto'),
+ autoCurrently: (backend) => t('runtimeBackendSelector.autoCurrently', { backend }),
+ audienceInternal: t('runtimeBackendSelector.audience.internal'),
+ states: {
+ locked: t('runtimeBackendSelector.states.locked'),
+ disabled: t('runtimeBackendSelector.states.disabled'),
+ authRequired: t('runtimeBackendSelector.states.authRequired'),
+ runtimeMissing: t('runtimeBackendSelector.states.runtimeMissing'),
+ degraded: t('runtimeBackendSelector.states.degraded'),
+ unavailable: t('runtimeBackendSelector.states.unavailable'),
+ },
+ };
+}
+
+const DEFAULT_SUMMARY_TEXT: ProviderRuntimeBackendSummaryText = {
+ auto: 'Auto',
+ autoCurrently: (backend) => `Auto (currently: ${backend})`,
+ audienceInternal: 'Internal',
+ states: {
+ locked: 'Locked',
+ disabled: 'Disabled',
+ authRequired: 'Auth required',
+ runtimeMissing: 'Runtime missing',
+ degraded: 'Degraded',
+ unavailable: 'Unavailable',
+ },
+};
+
export function getProviderRuntimeBackendStateLabel(
option: NonNullable[number]
): string | null {
@@ -78,7 +124,47 @@ export function getOptionDisplayLabel(
return 'Auto';
}
-export function getProviderRuntimeBackendSummary(provider: CliProviderStatus): string | null {
+function getOptionSummaryDisplayLabel(
+ provider: CliProviderStatus,
+ option: NonNullable[number],
+ resolvedOption: NonNullable[number] | null,
+ text: ProviderRuntimeBackendSummaryText
+): string {
+ if (option.id !== 'auto') {
+ return getOptionDisplayLabel(provider, option, resolvedOption);
+ }
+ if (resolvedOption?.label) {
+ return text.autoCurrently(resolvedOption.label);
+ }
+ return text.auto;
+}
+
+function getProviderRuntimeBackendStateSummaryLabel(
+ option: NonNullable[number],
+ text: ProviderRuntimeBackendSummaryText
+): string | null {
+ switch (getProviderRuntimeBackendStateLabel(option)) {
+ case 'Locked':
+ return text.states.locked;
+ case 'Disabled':
+ return text.states.disabled;
+ case 'Auth required':
+ return text.states.authRequired;
+ case 'Runtime missing':
+ return text.states.runtimeMissing;
+ case 'Degraded':
+ return text.states.degraded;
+ case 'Unavailable':
+ return text.states.unavailable;
+ default:
+ return null;
+ }
+}
+
+export function getProviderRuntimeBackendSummary(
+ provider: CliProviderStatus,
+ text: ProviderRuntimeBackendSummaryText = DEFAULT_SUMMARY_TEXT
+): string | null {
const options = provider.availableBackends ?? [];
if (options.length === 0) {
return null;
@@ -87,9 +173,11 @@ export function getProviderRuntimeBackendSummary(provider: CliProviderStatus): s
const selectedBackendId = provider.selectedBackendId ?? options[0]?.id ?? '';
const selectedOption = options.find((option) => option.id === selectedBackendId) ?? options[0];
const resolvedOption = options.find((option) => option.id === provider.resolvedBackendId) ?? null;
- const parts = [getOptionDisplayLabel(provider, selectedOption, resolvedOption)];
- const audienceLabel = getProviderRuntimeBackendAudienceLabel(selectedOption);
- const stateLabel = getProviderRuntimeBackendStateLabel(selectedOption);
+ const parts = [getOptionSummaryDisplayLabel(provider, selectedOption, resolvedOption, text)];
+ const audienceLabel = getProviderRuntimeBackendAudienceLabel(selectedOption)
+ ? text.audienceInternal
+ : null;
+ const stateLabel = getProviderRuntimeBackendStateSummaryLabel(selectedOption, text);
if (audienceLabel) {
parts.push(audienceLabel.toLowerCase());
@@ -107,6 +195,7 @@ export const ProviderRuntimeBackendSelector = ({
onSelect,
}: Props): React.JSX.Element | null => {
const { t } = useAppTranslation('common');
+ const summaryText = buildProviderRuntimeBackendSummaryText(t);
const options = getVisibleProviderRuntimeBackendOptions(provider);
if (options.length === 0) {
return null;
@@ -150,9 +239,9 @@ export const ProviderRuntimeBackendSelector = ({
): string => {
if (option.id === 'auto') {
if (resolvedOption?.label) {
- return t('runtimeBackendSelector.autoCurrently', { backend: resolvedOption.label });
+ return summaryText.autoCurrently(resolvedOption.label);
}
- return t('runtimeBackendSelector.auto');
+ return summaryText.auto;
}
return getOptionDisplayLabel(provider, option, resolvedOption);
};
diff --git a/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx b/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx
index 898a08d1..f28b3317 100644
--- a/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx
+++ b/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx
@@ -61,6 +61,7 @@ import {
isConnectionManagedRuntimeProvider,
} from './providerConnectionUi';
import {
+ buildProviderRuntimeBackendSummaryText,
getProviderRuntimeBackendSummary,
getVisibleProviderRuntimeBackendOptions,
ProviderRuntimeBackendSelector,
@@ -845,6 +846,11 @@ export const ProviderRuntimeSettingsDialog = ({
onRequestLogin,
}: Props): React.JSX.Element => {
const { t } = useAppTranslation('settings');
+ const { t: commonT } = useAppTranslation('common');
+ const runtimeBackendSummaryText = useMemo(
+ () => buildProviderRuntimeBackendSummaryText(commonT),
+ [commonT]
+ );
const [selectedProviderId, setSelectedProviderId] = useState(initialProviderId);
const [activeApiKeyFormProviderId, setActiveApiKeyFormProviderId] =
useState(null);
@@ -1107,7 +1113,7 @@ export const ProviderRuntimeSettingsDialog = ({
? providerStatusLoading[selectedProvider.providerId] === true
: false;
const runtimeSummary = selectedProvider
- ? getProviderRuntimeBackendSummary(selectedProvider)
+ ? getProviderRuntimeBackendSummary(selectedProvider, runtimeBackendSummaryText)
: null;
const codexConnection =
selectedProvider?.providerId === 'codex' ? (selectedProvider.connection?.codex ?? null) : null;
diff --git a/src/renderer/components/settings/sections/CliStatusSection.tsx b/src/renderer/components/settings/sections/CliStatusSection.tsx
index feb0ba89..a8e4a95f 100644
--- a/src/renderer/components/settings/sections/CliStatusSection.tsx
+++ b/src/renderer/components/settings/sections/CliStatusSection.tsx
@@ -30,7 +30,10 @@ import {
shouldShowProviderStatusSkeleton,
} from '@renderer/components/runtime/providerConnectionUi';
import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges';
-import { getProviderRuntimeBackendSummary } from '@renderer/components/runtime/ProviderRuntimeBackendSelector';
+import {
+ buildProviderRuntimeBackendSummaryText,
+ getProviderRuntimeBackendSummary,
+} from '@renderer/components/runtime/ProviderRuntimeBackendSelector';
import { ProviderRuntimeSettingsDialog } from '@renderer/components/runtime/ProviderRuntimeSettingsDialog';
import {
getProviderTerminalCommand,
@@ -117,6 +120,11 @@ function getProviderLabel(providerId: CliProviderId): string {
export const CliStatusSection = (): React.JSX.Element | null => {
const { t } = useAppTranslation('settings');
+ const { t: commonT } = useAppTranslation('common');
+ const runtimeBackendSummaryText = useMemo(
+ () => buildProviderRuntimeBackendSummaryText(commonT),
+ [commonT]
+ );
const isElectron = useMemo(() => isElectronMode(), []);
const appConfig = useStore((s) => s.appConfig);
const selectedProjectId = useStore((s) => s.selectedProjectId);
@@ -481,7 +489,7 @@ export const CliStatusSection = (): React.JSX.Element | null => {
isCodexSnapshotPending(provider, codexSnapshotPending);
const runtimeSummary = isConnectionManagedRuntimeProvider(provider)
? getProviderCurrentRuntimeSummary(provider, t)
- : getProviderRuntimeBackendSummary(provider);
+ : getProviderRuntimeBackendSummary(provider, runtimeBackendSummaryText);
const sourceProvider =
loadingCliProviderMap.get(provider.providerId) ?? null;
const maskNegativeBootstrapState = shouldMaskCodexNegativeBootstrapState(
diff --git a/src/renderer/components/team/ClaudeLogsSection.tsx b/src/renderer/components/team/ClaudeLogsSection.tsx
index 5a153122..5a9d18be 100644
--- a/src/renderer/components/team/ClaudeLogsSection.tsx
+++ b/src/renderer/components/team/ClaudeLogsSection.tsx
@@ -141,9 +141,14 @@ const TeamLogsSourceSelector = ({
getMemberLabel={(member) =>
isLeadMember(member)
? t('claudeLogs.sourceSelect.leadLabel')
- : formatMemberLogSourceLabel(member)
+ : formatMemberLogSourceLabel(member, t('claudeLogs.sourceSelect.removedLabel'))
+ }
+ getMemberDescription={(member) =>
+ formatMemberLogSourceDescription(member, {
+ lead: t('claudeLogs.sourceSelect.leadDescription'),
+ removed: t('claudeLogs.sourceSelect.removedDescription'),
+ })
}
- getMemberDescription={formatMemberLogSourceDescription}
/>
);
diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx
index 6be81208..1facf6fd 100644
--- a/src/renderer/components/team/TeamDetailView.tsx
+++ b/src/renderer/components/team/TeamDetailView.tsx
@@ -1352,14 +1352,16 @@ const LeadLoadBridge = memo(function LeadLoadBridge({
{t('detail.context.title')}
- {leadSessionLoading ? 'Loading…' : 'No session loaded'}
+ {leadSessionLoading
+ ? t('detail.context.loading')
+ : t('detail.context.noSessionLoaded')}
setContextPanelVisible(false)}
- aria-label={`Close ${teamName} context panel`}
+ aria-label={t('detail.context.closePanel', { team: teamName })}
>
×
@@ -1367,8 +1369,8 @@ const LeadLoadBridge = memo(function LeadLoadBridge({
{leadSessionLoading
- ? 'Loading context…'
- : 'Open the team lead session to view context.'}
+ ? t('detail.context.loadingContext')
+ : t('detail.context.openLeadSession')}
@@ -1401,7 +1403,7 @@ const LeadLoadBridge = memo(function LeadLoadBridge({
leadSessionLoaded
? `Session: ${leadSessionId}`
: leadSessionLoading
- ? 'Loading context…'
+ ? t('detail.context.loadingContext')
: leadSessionId
}
>
diff --git a/src/renderer/components/team/activity/ActiveTasksBlock.tsx b/src/renderer/components/team/activity/ActiveTasksBlock.tsx
index a52a37fc..05f7ec8e 100644
--- a/src/renderer/components/team/activity/ActiveTasksBlock.tsx
+++ b/src/renderer/components/team/activity/ActiveTasksBlock.tsx
@@ -95,7 +95,11 @@ export const ActiveTasksBlock = memo(function ActiveTasksBlock({
type="button"
className="flex min-w-0 items-center gap-1.5 text-[10px] font-medium uppercase tracking-wide text-[var(--color-text-muted)] transition-colors hover:text-[var(--color-text-secondary)]"
onClick={() => setCollapsed((v) => !v)}
- aria-label={collapsed ? 'Expand in progress' : 'Collapse in progress'}
+ aria-label={
+ collapsed
+ ? t('activity.activeTasks.expandInProgress')
+ : t('activity.activeTasks.collapseInProgress')
+ }
>
{t('activity.badges.rateLimited')}
- ) : isApiError ? (
-
-
- {message.messageKind === 'agent_error' ? 'Agent Error' : 'API Error'}
-
- ) : null;
+ ) : isApiError ? (
+
+
+ {message.messageKind === 'agent_error'
+ ? t('activity.badges.agentError')
+ : t('activity.badges.apiError')}
+
+ ) : null;
const recipientBadge =
commentTaskRef && commentTaskDisplayId ? (
diff --git a/src/renderer/components/team/dialogs/CodexReconnectPrompt.tsx b/src/renderer/components/team/dialogs/CodexReconnectPrompt.tsx
index e4d7e9e7..2418f713 100644
--- a/src/renderer/components/team/dialogs/CodexReconnectPrompt.tsx
+++ b/src/renderer/components/team/dialogs/CodexReconnectPrompt.tsx
@@ -128,7 +128,11 @@ export const CodexReconnectPrompt = ({
}}
>
- {reconnectBusy ? 'Generating...' : authUrl ? 'Open login' : 'Generate link'}
+ {reconnectBusy
+ ? t('codexReconnect.generating')
+ : authUrl
+ ? t('codexReconnect.openLogin')
+ : t('codexReconnect.generateLink')}
diff --git a/src/renderer/components/team/dialogs/OptionalSettingsSection.tsx b/src/renderer/components/team/dialogs/OptionalSettingsSection.tsx
index 9e75e2a6..72bfb36c 100644
--- a/src/renderer/components/team/dialogs/OptionalSettingsSection.tsx
+++ b/src/renderer/components/team/dialogs/OptionalSettingsSection.tsx
@@ -18,8 +18,9 @@ interface OptionalSettingsSectionProps {
const SUMMARY_PREFIXES_TO_STRIP = ['Provider:', 'Model:', 'Effort:', 'Worktree:'];
const MODEL_LABEL_OVERRIDES: Array<[RegExp, string]> = [
- [/claude[-\s]?opus[-\s]?4[-\s]?6/i, 'Opus 4.6'],
+ [/claude[-\s]?opus[-\s]?4[-\s]?8/i, 'Opus 4.8'],
[/claude[-\s]?opus[-\s]?4[-\s]?7/i, 'Opus 4.7'],
+ [/claude[-\s]?opus[-\s]?4[-\s]?6/i, 'Opus 4.6'],
[/claude[-\s]?opus[-\s]?4[-\s]?5/i, 'Opus 4.5'],
[/claude[-\s]?sonnet[-\s]?4[-\s]?6/i, 'Sonnet 4.6'],
[/claude[-\s]?sonnet[-\s]?4[-\s]?5/i, 'Sonnet 4.5'],
diff --git a/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts b/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts
index b6d36814..600234c3 100644
--- a/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts
+++ b/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts
@@ -1,9 +1,24 @@
-import { OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE } from '@shared/utils/openCodeWindowsAccessDenied';
+import {
+ OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE,
+ OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE,
+} from '@shared/utils/openCodeWindowsAccessDenied';
import { describe, expect, it } from 'vitest';
import { getProvisioningFailureHint } from './ProvisioningProviderStatusList';
describe('getProvisioningFailureHint', () => {
+ it('returns the administrator hint for the exact OpenCode node_modules symlink permission failure', () => {
+ expect(
+ getProvisioningFailureHint(null, [
+ {
+ providerId: 'opencode',
+ status: 'failed',
+ details: [OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE],
+ },
+ ])
+ ).toBe('Run Agent Teams AI as Administrator, then retry launch.');
+ });
+
it('returns the OpenCode Windows permissions hint for OpenCode access-denied details', () => {
expect(
getProvisioningFailureHint(null, [
diff --git a/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx b/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx
index 5163256f..c769004c 100644
--- a/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx
+++ b/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx
@@ -5,7 +5,9 @@ import { formatProviderBackendLabel } from '@renderer/utils/providerBackendIdent
import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog';
import {
isOpenCodeWindowsAccessDeniedDiagnostic,
+ isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic,
OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE,
+ OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE,
} from '@shared/utils/openCodeWindowsAccessDenied';
import { AlertTriangle, Check, CheckCircle2, Copy, Loader2, SlidersHorizontal } from 'lucide-react';
@@ -1042,14 +1044,31 @@ export function getProvisioningFailureHint(
const hasOpenCodeAccessDeniedDetail = failedOpenCodeChecks.some((check) =>
check.details.some(isOpenCodeWindowsAccessDeniedDiagnostic)
);
+ const hasOpenCodeNodeModulesSymlinkPermissionDetail = failedOpenCodeChecks.some((check) =>
+ check.details.some(isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic)
+ );
const hasOpenCodeBridgeNoOutputDetail = failedOpenCodeChecks.some((check) =>
check.details.some(isOpenCodeBridgeNoOutputDiagnostic)
);
const normalizedMessage = message?.trim() ?? '';
+ const hasOpenCodeNodeModulesSymlinkPermissionMessage =
+ failedOpenCodeChecks.length > 0 &&
+ (normalizedMessage === OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE ||
+ (!hasFailedNonOpenCodeCheck &&
+ isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic(normalizedMessage)));
const hasOpenCodeAccessDeniedMessage =
failedOpenCodeChecks.length > 0 &&
(normalizedMessage === OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE ||
(!hasFailedNonOpenCodeCheck && isOpenCodeWindowsAccessDeniedDiagnostic(normalizedMessage)));
+ if (
+ hasOpenCodeNodeModulesSymlinkPermissionDetail ||
+ hasOpenCodeNodeModulesSymlinkPermissionMessage
+ ) {
+ return (
+ t?.('provisioning.providerStatus.failureHints.openCodeNodeModulesSymlinkPermission') ??
+ 'Run Agent Teams AI as Administrator, then retry launch.'
+ );
+ }
if (hasOpenCodeAccessDeniedDetail || hasOpenCodeAccessDeniedMessage) {
return (
t?.('provisioning.providerStatus.failureHints.openCodeAccessDenied') ??
diff --git a/src/renderer/components/team/dialogs/TaskDetailDialog.tsx b/src/renderer/components/team/dialogs/TaskDetailDialog.tsx
index f5d69c8b..7ecb6f2e 100644
--- a/src/renderer/components/team/dialogs/TaskDetailDialog.tsx
+++ b/src/renderer/components/team/dialogs/TaskDetailDialog.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useAppTranslation } from '@features/localization/renderer';
import { api } from '@renderer/api';
@@ -138,6 +138,73 @@ interface TaskDetailDialogProps {
headerExtra?: React.ReactNode;
}
+function useTaskImplementationDurationClock(task: TeamTaskWithKanban): {
+ duration: ReturnType;
+ nowMs: number;
+} {
+ const [nowMs, setNowMs] = useState(() => Date.now());
+ const duration = useMemo(() => calculateTaskImplementationDuration(task, nowMs), [task, nowMs]);
+
+ useEffect(() => {
+ if (!duration.hasRunningInterval) return;
+
+ setNowMs(Date.now());
+ const intervalId = window.setInterval(() => {
+ setNowMs(Date.now());
+ }, 1000);
+
+ return () => window.clearInterval(intervalId);
+ }, [duration.hasRunningInterval, task.id]);
+
+ return { duration, nowMs };
+}
+
+const TaskImplementationDurationBadge = memo(function TaskImplementationDurationBadge({
+ task,
+}: {
+ task: TeamTaskWithKanban;
+}): React.JSX.Element | null {
+ const { t } = useAppTranslation('team');
+ const { duration } = useTaskImplementationDurationClock(task);
+ if (!shouldShowTaskImplementationDuration(duration)) {
+ return null;
+ }
+
+ return (
+
+
+
+ {t('taskDetail.workflow.inProgressTime', {
+ duration: formatTaskImplementationDuration(duration.elapsedMs),
+ })}
+
+
+ );
+});
+
+const WorkflowTimelineWithDuration = memo(function WorkflowTimelineWithDuration({
+ task,
+ events,
+ memberColorMap,
+}: {
+ task: TeamTaskWithKanban;
+ events: NonNullable;
+ memberColorMap: Map;
+}): React.JSX.Element {
+ const { nowMs } = useTaskImplementationDurationClock(task);
+ return (
+
+ );
+});
+
export const TaskDetailDialog = ({
open,
loading = false,
@@ -633,29 +700,6 @@ export const TaskDetailDialog = ({
: undefined
: undefined;
- const [taskDurationNowMs, setTaskDurationNowMs] = useState(() => Date.now());
- const taskImplementationDuration = useMemo(
- () => calculateTaskImplementationDuration(currentTask, taskDurationNowMs),
- [currentTask, taskDurationNowMs]
- );
- const showTaskImplementationDuration = shouldShowTaskImplementationDuration(
- taskImplementationDuration
- );
- const taskImplementationDurationLabel = formatTaskImplementationDuration(
- taskImplementationDuration.elapsedMs
- );
-
- useEffect(() => {
- if (!open || !taskImplementationDuration.hasRunningInterval) return;
-
- setTaskDurationNowMs(Date.now());
- const intervalId = window.setInterval(() => {
- setTaskDurationNowMs(Date.now());
- }, 1000);
-
- return () => window.clearInterval(intervalId);
- }, [open, taskImplementationDuration.hasRunningInterval, currentTask?.id]);
-
if (loading) {
return (
!v && onClose()}>
@@ -1493,28 +1537,13 @@ export const TaskDetailDialog = ({
contentClassName="pl-2.5"
headerClassName="-mx-6 w-[calc(100%+3rem)]"
headerContentClassName="pl-6"
- headerExtra={
- showTaskImplementationDuration ? (
-
-
-
- {t('taskDetail.workflow.inProgressTime', {
- duration: taskImplementationDurationLabel,
- })}
-
-
- ) : undefined
- }
+ headerExtra={ }
defaultOpen={false}
>
-
) : null}
diff --git a/src/renderer/components/team/members/MemberLaunchDiagnosticsButton.tsx b/src/renderer/components/team/members/MemberLaunchDiagnosticsButton.tsx
index 105ba8db..e524e895 100644
--- a/src/renderer/components/team/members/MemberLaunchDiagnosticsButton.tsx
+++ b/src/renderer/components/team/members/MemberLaunchDiagnosticsButton.tsx
@@ -1,5 +1,6 @@
import { useState } from 'react';
+import { useAppTranslation } from '@features/localization/renderer';
import { Button } from '@renderer/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { cn } from '@renderer/lib/utils';
@@ -24,6 +25,7 @@ export const MemberLaunchDiagnosticsButton = ({
size = label ? 'sm' : 'icon',
attention = false,
}: MemberLaunchDiagnosticsButtonProps): React.JSX.Element => {
+ const { t } = useAppTranslation('team');
const [copied, setCopied] = useState(false);
const copyDiagnostics = async (event: React.MouseEvent): Promise => {
@@ -39,7 +41,7 @@ export const MemberLaunchDiagnosticsButton = ({
};
const icon = copied ? : ;
- const tooltip = copied ? 'Diagnostics copied' : 'Copy diagnostics';
+ const tooltip = copied ? t('provisioning.diagnosticsCopied') : t('provisioning.copyDiagnostics');
return (
diff --git a/src/renderer/components/team/members/MemberRoleEditor.tsx b/src/renderer/components/team/members/MemberRoleEditor.tsx
index 2aff2f77..db541ebb 100644
--- a/src/renderer/components/team/members/MemberRoleEditor.tsx
+++ b/src/renderer/components/team/members/MemberRoleEditor.tsx
@@ -1,5 +1,6 @@
import { useState } from 'react';
+import { useAppTranslation } from '@features/localization/renderer';
import { RoleSelect } from '@renderer/components/team/RoleSelect';
import { Button } from '@renderer/components/ui/button';
import { CUSTOM_ROLE, FORBIDDEN_ROLES, NO_ROLE, PRESET_ROLES } from '@renderer/constants/teamRoles';
@@ -18,6 +19,7 @@ export const MemberRoleEditor = ({
onCancel,
saving,
}: MemberRoleEditorProps): React.JSX.Element => {
+ const { t } = useAppTranslation('team');
const isPreset = currentRole && (PRESET_ROLES as readonly string[]).includes(currentRole);
const [selectValue, setSelectValue] = useState(
!currentRole ? NO_ROLE : isPreset ? currentRole : CUSTOM_ROLE
@@ -44,11 +46,11 @@ export const MemberRoleEditor = ({
}
const trimmed = customInput.trim();
if (!trimmed) {
- setError('Role cannot be empty');
+ setError(t('roleSelect.emptyCustomRole'));
return;
}
if (FORBIDDEN_ROLES.has(trimmed.toLowerCase())) {
- setError('This role is reserved');
+ setError(t('roleSelect.reservedRole'));
return;
}
void onSave(trimmed);
@@ -68,7 +70,7 @@ export const MemberRoleEditor = ({
inputClassName="h-7 w-28 text-xs"
customRoleError={error}
onCustomRoleValidate={(val) => {
- if (FORBIDDEN_ROLES.has(val.trim().toLowerCase())) return 'This role is reserved';
+ if (FORBIDDEN_ROLES.has(val.trim().toLowerCase())) return t('roleSelect.reservedRole');
return null;
}}
/>
diff --git a/src/renderer/components/team/teamLogSources.ts b/src/renderer/components/team/teamLogSources.ts
index b5d04efc..71932609 100644
--- a/src/renderer/components/team/teamLogSources.ts
+++ b/src/renderer/components/team/teamLogSources.ts
@@ -26,13 +26,19 @@ export function getMemberNameFromLogSourceKey(sourceKey: TeamLogSourceKey): stri
return sourceKey.slice('member:'.length);
}
-export function formatMemberLogSourceLabel(member: ResolvedTeamMember): string {
- return member.removedAt ? `${member.name} (removed)` : member.name;
+export function formatMemberLogSourceLabel(member: ResolvedTeamMember, removedLabel = 'removed'): string {
+ return member.removedAt ? `${member.name} (${removedLabel})` : member.name;
}
-export function formatMemberLogSourceDescription(member: ResolvedTeamMember): string | null {
- if (isLeadMember(member)) return 'Team Lead';
- if (member.removedAt) return 'Removed';
+export function formatMemberLogSourceDescription(
+ member: ResolvedTeamMember,
+ labels?: {
+ lead?: string;
+ removed?: string;
+ }
+): string | null {
+ if (isLeadMember(member)) return labels?.lead ?? 'Team Lead';
+ if (member.removedAt) return labels?.removed ?? 'Removed';
return formatAgentRole(member.role) ?? formatAgentRole(member.agentType) ?? null;
}
diff --git a/src/renderer/components/team/useTeamAgentRuntimeWatcher.test.tsx b/src/renderer/components/team/useTeamAgentRuntimeWatcher.test.tsx
new file mode 100644
index 00000000..d9bd186e
--- /dev/null
+++ b/src/renderer/components/team/useTeamAgentRuntimeWatcher.test.tsx
@@ -0,0 +1,180 @@
+import React, { act } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
+
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+const hoisted = vi.hoisted(() => ({
+ state: {
+ leadActivityByTeam: {} as Record,
+ teamDataByName: {} as Record,
+ provisioningActiveByTeam: {} as Record,
+ fetchTeamAgentRuntime: vi.fn(async (_teamName: string): Promise => undefined),
+ },
+}));
+
+vi.mock('@renderer/store', () => ({
+ useStore: (selector: (state: typeof hoisted.state) => T): T => selector(hoisted.state),
+}));
+
+vi.mock('@renderer/store/slices/teamSlice', () => ({
+ isTeamProvisioningActive: (state: typeof hoisted.state, teamName: string): boolean =>
+ state.provisioningActiveByTeam[teamName] === true,
+ selectTeamDataForName: (
+ state: typeof hoisted.state,
+ teamName: string
+ ): { isAlive?: boolean } | undefined => state.teamDataByName[teamName],
+}));
+
+import {
+ __resetTeamAgentRuntimeWatcherForTests,
+ useTeamAgentRuntimeWatcher,
+} from './useTeamAgentRuntimeWatcher';
+
+interface HookProbeProps {
+ teamName: string;
+ enabled: boolean;
+ isTeamProvisioning?: boolean;
+ isTeamAlive?: boolean;
+}
+
+const HookProbe = (props: HookProbeProps): null => {
+ useTeamAgentRuntimeWatcher(props);
+ return null;
+};
+
+const mountedRoots: Root[] = [];
+
+async function renderWatcher(props: HookProbeProps): Promise {
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+ mountedRoots.push(root);
+
+ await act(async () => {
+ root.render(React.createElement(HookProbe, props));
+ await Promise.resolve();
+ });
+
+ return root;
+}
+
+describe('useTeamAgentRuntimeWatcher', () => {
+ beforeEach(() => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
+ vi.useFakeTimers();
+ vi.setSystemTime(0);
+ __resetTeamAgentRuntimeWatcherForTests();
+ hoisted.state.leadActivityByTeam = {};
+ hoisted.state.teamDataByName = {};
+ hoisted.state.provisioningActiveByTeam = {};
+ hoisted.state.fetchTeamAgentRuntime.mockReset();
+ hoisted.state.fetchTeamAgentRuntime.mockResolvedValue(undefined);
+ });
+
+ afterEach(() => {
+ for (const root of mountedRoots.splice(0)) {
+ act(() => {
+ root.unmount();
+ });
+ }
+ document.body.innerHTML = '';
+ __resetTeamAgentRuntimeWatcherForTests();
+ vi.useRealTimers();
+ vi.unstubAllGlobals();
+ });
+
+ it('backs off polling for an alive idle team', async () => {
+ hoisted.state.teamDataByName['team-a'] = { isAlive: true };
+
+ await renderWatcher({ teamName: 'team-a', enabled: true });
+
+ expect(hoisted.state.fetchTeamAgentRuntime).toHaveBeenCalledTimes(1);
+ expect(hoisted.state.fetchTeamAgentRuntime).toHaveBeenCalledWith('team-a');
+
+ await act(async () => {
+ vi.advanceTimersByTime(5_000);
+ await Promise.resolve();
+ });
+
+ expect(hoisted.state.fetchTeamAgentRuntime).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ vi.advanceTimersByTime(10_000);
+ await Promise.resolve();
+ });
+
+ expect(hoisted.state.fetchTeamAgentRuntime).toHaveBeenCalledTimes(2);
+ });
+
+ it('keeps active teams on the fast polling cadence', async () => {
+ hoisted.state.leadActivityByTeam['team-a'] = 'active';
+
+ await renderWatcher({ teamName: 'team-a', enabled: true });
+
+ expect(hoisted.state.fetchTeamAgentRuntime).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ vi.advanceTimersByTime(5_000);
+ await Promise.resolve();
+ });
+
+ expect(hoisted.state.fetchTeamAgentRuntime).toHaveBeenCalledTimes(2);
+ });
+
+ it('does not overlap runtime refreshes for the same team', async () => {
+ hoisted.state.leadActivityByTeam['team-a'] = 'active';
+ let resolveFirstRefresh: () => void = () => undefined;
+ const firstRefresh = new Promise((resolve) => {
+ resolveFirstRefresh = resolve;
+ });
+ hoisted.state.fetchTeamAgentRuntime
+ .mockImplementationOnce(() => firstRefresh)
+ .mockResolvedValue(undefined);
+
+ await renderWatcher({ teamName: 'team-a', enabled: true });
+
+ expect(hoisted.state.fetchTeamAgentRuntime).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ vi.advanceTimersByTime(5_000);
+ await Promise.resolve();
+ });
+
+ expect(hoisted.state.fetchTeamAgentRuntime).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ resolveFirstRefresh();
+ await firstRefresh;
+ await Promise.resolve();
+ });
+
+ await act(async () => {
+ vi.advanceTimersByTime(5_000);
+ await Promise.resolve();
+ });
+
+ expect(hoisted.state.fetchTeamAgentRuntime).toHaveBeenCalledTimes(2);
+ });
+
+ it('coalesces multiple mounted watchers for one team', async () => {
+ hoisted.state.leadActivityByTeam['team-a'] = 'active';
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+ mountedRoots.push(root);
+
+ await act(async () => {
+ root.render(
+ React.createElement(
+ React.Fragment,
+ null,
+ React.createElement(HookProbe, { teamName: 'team-a', enabled: true }),
+ React.createElement(HookProbe, { teamName: 'team-a', enabled: true })
+ )
+ );
+ await Promise.resolve();
+ });
+
+ expect(hoisted.state.fetchTeamAgentRuntime).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/renderer/components/team/useTeamAgentRuntimeWatcher.ts b/src/renderer/components/team/useTeamAgentRuntimeWatcher.ts
index 61d0047d..34f06676 100644
--- a/src/renderer/components/team/useTeamAgentRuntimeWatcher.ts
+++ b/src/renderer/components/team/useTeamAgentRuntimeWatcher.ts
@@ -5,6 +5,15 @@ import { isTeamProvisioningActive, selectTeamDataForName } from '@renderer/store
import { useShallow } from 'zustand/react/shallow';
const TEAM_AGENT_RUNTIME_REFRESH_MS = 10_000;
+const ACTIVE_TEAM_AGENT_RUNTIME_REFRESH_MS = 5_000;
+
+interface TeamAgentRuntimeWatchEntry {
+ refCount: number;
+ timer: number;
+ inFlight: boolean;
+}
+
+const teamAgentRuntimeWatchEntries = new Map();
export function shouldWatchTeamAgentRuntime(input: {
enabled: boolean;
@@ -19,6 +28,13 @@ export function shouldWatchTeamAgentRuntime(input: {
return input.leadActivity === 'active' || input.leadActivity === 'idle';
}
+export function __resetTeamAgentRuntimeWatcherForTests(): void {
+ for (const entry of teamAgentRuntimeWatchEntries.values()) {
+ window.clearInterval(entry.timer);
+ }
+ teamAgentRuntimeWatchEntries.clear();
+}
+
interface TeamAgentRuntimeWatcherOptions {
teamName: string;
enabled: boolean;
@@ -54,12 +70,38 @@ export function useTeamAgentRuntimeWatcher({
});
if (!shouldWatch) return;
- void fetchTeamAgentRuntime(teamName);
- const timer = window.setInterval(() => {
- void fetchTeamAgentRuntime(teamName);
- }, TEAM_AGENT_RUNTIME_REFRESH_MS);
+ const existingEntry = teamAgentRuntimeWatchEntries.get(teamName);
+ if (existingEntry) {
+ existingEntry.refCount += 1;
+ return () => {
+ existingEntry.refCount -= 1;
+ if (existingEntry.refCount <= 0) {
+ window.clearInterval(existingEntry.timer);
+ teamAgentRuntimeWatchEntries.delete(teamName);
+ }
+ };
+ }
+
+ const refreshIntervalMs =
+ leadActivity === 'active'
+ ? ACTIVE_TEAM_AGENT_RUNTIME_REFRESH_MS
+ : TEAM_AGENT_RUNTIME_REFRESH_MS;
+ const entry: TeamAgentRuntimeWatchEntry = {
+ refCount: 1,
+ timer: window.setInterval(() => {
+ refreshTeamAgentRuntime(teamName, fetchTeamAgentRuntime);
+ }, refreshIntervalMs),
+ inFlight: false,
+ };
+ teamAgentRuntimeWatchEntries.set(teamName, entry);
+ refreshTeamAgentRuntime(teamName, fetchTeamAgentRuntime);
+
return () => {
- window.clearInterval(timer);
+ entry.refCount -= 1;
+ if (entry.refCount <= 0) {
+ window.clearInterval(entry.timer);
+ teamAgentRuntimeWatchEntries.delete(teamName);
+ }
};
}, [
effectiveIsTeamAlive,
@@ -70,3 +112,21 @@ export function useTeamAgentRuntimeWatcher({
teamName,
]);
}
+
+function refreshTeamAgentRuntime(
+ teamName: string,
+ fetchTeamAgentRuntime: (teamName: string) => Promise
+): void {
+ const entry = teamAgentRuntimeWatchEntries.get(teamName);
+ if (!entry || entry.inFlight) return;
+
+ entry.inFlight = true;
+ void fetchTeamAgentRuntime(teamName)
+ .catch(() => undefined)
+ .finally(() => {
+ const latestEntry = teamAgentRuntimeWatchEntries.get(teamName);
+ if (latestEntry === entry) {
+ latestEntry.inFlight = false;
+ }
+ });
+}
diff --git a/src/renderer/components/team/useTeamChangesSummaries.ts b/src/renderer/components/team/useTeamChangesSummaries.ts
index 66c934c6..4c47610c 100644
--- a/src/renderer/components/team/useTeamChangesSummaries.ts
+++ b/src/renderer/components/team/useTeamChangesSummaries.ts
@@ -13,6 +13,7 @@ import {
TEAM_CHANGES_MAX_REQUESTS,
} from './teamChangesRequestPlan';
+import type { TaskChangeRequestOptions } from '@renderer/utils/taskChangeRequest';
import type {
TaskChangePresenceState,
TaskChangeSetV2,
@@ -64,6 +65,20 @@ interface UseTeamChangesSummariesInput {
sectionOpen: boolean;
}
+type RecordTaskChangePresences = (
+ entries: {
+ teamName: string;
+ taskId: string;
+ options: TaskChangeRequestOptions;
+ presence: TaskChangePresenceState | null;
+ }[]
+) => void;
+
+type SetSelectedTeamTaskChangePresences = (
+ teamName: string,
+ presencesByTaskId: Record
+) => void;
+
interface UseTeamChangesSummariesResult {
summariesByTaskId: Record;
badgeCount: number | null;
@@ -274,7 +289,20 @@ export function useTeamChangesSummaries({
sectionOpen,
}: UseTeamChangesSummariesInput): UseTeamChangesSummariesResult {
const recordTaskChangePresence = useStore((s) => s.recordTaskChangePresence);
+ const recordTaskChangePresences = useStore(
+ (s) =>
+ (s as unknown as { recordTaskChangePresences?: RecordTaskChangePresences })
+ .recordTaskChangePresences
+ );
const setSelectedTeamTaskChangePresence = useStore((s) => s.setSelectedTeamTaskChangePresence);
+ const setSelectedTeamTaskChangePresences = useStore(
+ (s) =>
+ (
+ s as unknown as {
+ setSelectedTeamTaskChangePresences?: SetSelectedTeamTaskChangePresences;
+ }
+ ).setSelectedTeamTaskChangePresences
+ );
const [summariesByTaskId, setSummariesByTaskId] = useState<
Record
>({});
@@ -490,6 +518,14 @@ export function useTeamChangesSummaries({
});
setCounterLoaded(true);
+ const cachePresenceUpdates: {
+ teamName: string;
+ taskId: string;
+ options: TaskChangeRequestOptions;
+ presence: TaskChangePresenceState | null;
+ }[] = [];
+ const selectedPresenceUpdates: Record = {};
+
for (const item of responseItems) {
const changeSet = item.changeSet;
const options = plan.requestOptionsByTaskId.get(item.taskId);
@@ -503,12 +539,41 @@ export function useTeamChangesSummaries({
task.changePresence !== 'unknown' &&
shouldClearSelectedTaskChangePresence(task, changeSet)
) {
- setSelectedTeamTaskChangePresence(teamName, item.taskId, 'unknown');
+ selectedPresenceUpdates[item.taskId] = 'unknown';
}
continue;
}
- recordTaskChangePresence(teamName, item.taskId, options, nextPresence);
- setSelectedTeamTaskChangePresence(teamName, item.taskId, nextPresence);
+ cachePresenceUpdates.push({
+ teamName,
+ taskId: item.taskId,
+ options,
+ presence: nextPresence,
+ });
+ selectedPresenceUpdates[item.taskId] = nextPresence;
+ }
+
+ if (cachePresenceUpdates.length > 0) {
+ if (recordTaskChangePresences) {
+ recordTaskChangePresences(cachePresenceUpdates);
+ } else {
+ for (const update of cachePresenceUpdates) {
+ recordTaskChangePresence(
+ update.teamName,
+ update.taskId,
+ update.options,
+ update.presence
+ );
+ }
+ }
+ }
+ if (Object.keys(selectedPresenceUpdates).length > 0) {
+ if (setSelectedTeamTaskChangePresences) {
+ setSelectedTeamTaskChangePresences(teamName, selectedPresenceUpdates);
+ } else {
+ for (const [taskId, presence] of Object.entries(selectedPresenceUpdates)) {
+ setSelectedTeamTaskChangePresence(teamName, taskId, presence);
+ }
+ }
}
if (storeSummaries) {
@@ -594,7 +659,14 @@ export function useTeamChangesSummaries({
}
}
},
- [recordTaskChangePresence, setSelectedTeamTaskChangePresence, tasks, teamName]
+ [
+ recordTaskChangePresence,
+ recordTaskChangePresences,
+ setSelectedTeamTaskChangePresence,
+ setSelectedTeamTaskChangePresences,
+ tasks,
+ teamName,
+ ]
);
useEffect(() => {
diff --git a/src/renderer/store/slices/changeReviewSlice.ts b/src/renderer/store/slices/changeReviewSlice.ts
index 48707f6c..2c9fde50 100644
--- a/src/renderer/store/slices/changeReviewSlice.ts
+++ b/src/renderer/store/slices/changeReviewSlice.ts
@@ -53,7 +53,6 @@ import type {
FileChangeWithContent,
FileReviewDecision,
HunkDecision,
- SnippetDiff,
TaskChangePresenceState,
TaskChangeSet,
TaskChangeSetV2,
@@ -148,6 +147,38 @@ function applyTaskChangePresenceCacheUpdate(
return nextTaskChangePresenceByKey;
}
+interface TaskChangePresenceCacheUpdate {
+ cacheKey: string;
+ presence: TaskChangePresenceState | null;
+}
+
+function applyTaskChangePresenceCacheUpdates(
+ taskChangePresenceByKey: Record>,
+ updates: readonly TaskChangePresenceCacheUpdate[]
+): Record> {
+ let nextTaskChangePresenceByKey = taskChangePresenceByKey;
+ for (const { cacheKey, presence } of updates) {
+ if (presence && presence !== 'unknown') {
+ if (nextTaskChangePresenceByKey[cacheKey] === presence) {
+ continue;
+ }
+ if (nextTaskChangePresenceByKey === taskChangePresenceByKey) {
+ nextTaskChangePresenceByKey = { ...taskChangePresenceByKey };
+ }
+ nextTaskChangePresenceByKey[cacheKey] = presence;
+ continue;
+ }
+ if (!(cacheKey in nextTaskChangePresenceByKey)) {
+ continue;
+ }
+ if (nextTaskChangePresenceByKey === taskChangePresenceByKey) {
+ nextTaskChangePresenceByKey = { ...taskChangePresenceByKey };
+ }
+ delete nextTaskChangePresenceByKey[cacheKey];
+ }
+ return nextTaskChangePresenceByKey;
+}
+
function syncTaskChangeNegativeCache(
cacheKey: string,
presence: TaskChangePresenceState | null
@@ -207,6 +238,14 @@ export interface ChangeReviewSlice {
options: TaskChangeRequestOptions,
presence: TaskChangePresenceState | null
) => void;
+ recordTaskChangePresences: (
+ entries: {
+ teamName: string;
+ taskId: string;
+ options: TaskChangeRequestOptions;
+ presence: TaskChangePresenceState | null;
+ }[]
+ ) => void;
selectReviewFile: (filePath: string | null) => void;
clearChangeReview: () => void;
clearChangeReviewCache: () => void;
@@ -570,17 +609,30 @@ export const createChangeReviewSlice: StateCreator {
- const cacheKey = buildTaskChangePresenceKey(teamName, taskId, options);
+ get().recordTaskChangePresences([{ teamName, taskId, options, presence }]);
+ },
+
+ recordTaskChangePresences: (entries) => {
+ if (entries.length === 0) {
+ return;
+ }
+ const updates = entries.map(({ teamName, taskId, options, presence }) => ({
+ cacheKey: buildTaskChangePresenceKey(teamName, taskId, options),
+ presence,
+ }));
set((s) => {
- return {
- taskChangePresenceByKey: applyTaskChangePresenceCacheUpdate(
- s.taskChangePresenceByKey,
- cacheKey,
- presence
- ),
- };
+ const nextTaskChangePresenceByKey = applyTaskChangePresenceCacheUpdates(
+ s.taskChangePresenceByKey,
+ updates
+ );
+ if (nextTaskChangePresenceByKey === s.taskChangePresenceByKey) {
+ return {};
+ }
+ return { taskChangePresenceByKey: nextTaskChangePresenceByKey };
});
- syncTaskChangeNegativeCache(cacheKey, presence);
+ for (const update of updates) {
+ syncTaskChangeNegativeCache(update.cacheKey, update.presence);
+ }
},
fetchTaskChanges: async (
diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts
index 6763c385..66321142 100644
--- a/src/renderer/store/slices/teamSlice.ts
+++ b/src/renderer/store/slices/teamSlice.ts
@@ -224,6 +224,7 @@ const MEMBER_SPAWN_STATUSES_IPC_RETRY_BACKOFF_MS = 5_000;
const TEAM_REFRESH_BURST_WINDOW_MS = 4_000;
const MEMBER_SPAWN_UI_EQUAL_WARN_THROTTLE_MS = 2_000;
const POST_PAINT_TEAM_ENRICHMENT_FALLBACK_MS = 500;
+const GLOBAL_TASKS_FOLLOW_UP_REFRESH_DELAY_MS = 1_500;
const inFlightTeamDataRequests = new Map>();
const inFlightRefreshTeamDataCalls = new Map>();
const pendingFreshTeamDataRefreshes = new Set();
@@ -1046,6 +1047,10 @@ export interface TeamSlice {
taskId: string,
presence: TaskChangePresenceState
) => void;
+ setSelectedTeamTaskChangePresences: (
+ teamName: string,
+ presencesByTaskId: Record
+ ) => void;
refreshTeamChangePresence: (teamName: string) => Promise;
selectTeam: (
teamName: string,
@@ -1658,6 +1663,10 @@ export const createTeamSlice: StateCreator = (set,
const runRefresh = async (): Promise => {
do {
+ const isFollowUpRefresh = pendingFreshGlobalTasksRefresh;
+ if (isFollowUpRefresh) {
+ await sleep(GLOBAL_TASKS_FOLLOW_UP_REFRESH_DELAY_MS);
+ }
pendingFreshGlobalTasksRefresh = false;
// Show skeleton only on the very first fetch — not on subsequent refreshes
@@ -2177,14 +2186,24 @@ export const createTeamSlice: StateCreator = (set,
},
setSelectedTeamTaskChangePresence: (teamName, taskId, presence) => {
+ get().setSelectedTeamTaskChangePresences(teamName, { [taskId]: presence });
+ },
+
+ setSelectedTeamTaskChangePresences: (teamName, presencesByTaskId) => {
set((state) => {
+ const updates = Object.entries(presencesByTaskId);
+ if (updates.length === 0) {
+ return {};
+ }
+ const presenceByTaskId = new Map(updates);
const currentTeamData = selectTeamDataForName(state, teamName);
let cacheChanged = false;
const nextTeamData = currentTeamData
? {
...currentTeamData,
tasks: currentTeamData.tasks.map((task) => {
- if (task.id !== taskId || task.changePresence === presence) {
+ const presence = presenceByTaskId.get(task.id);
+ if (!presence || task.changePresence === presence) {
return task;
}
cacheChanged = true;
@@ -2195,7 +2214,11 @@ export const createTeamSlice: StateCreator = (set,
let globalChanged = false;
const nextGlobalTasks = state.globalTasks.map((task) => {
- if (task.teamName !== teamName || task.id !== taskId || task.changePresence === presence) {
+ if (task.teamName !== teamName) {
+ return task;
+ }
+ const presence = presenceByTaskId.get(task.id);
+ if (!presence || task.changePresence === presence) {
return task;
}
globalChanged = true;
diff --git a/src/renderer/utils/mentionLinkify.ts b/src/renderer/utils/mentionLinkify.ts
index 452b5660..0ea69c18 100644
--- a/src/renderer/utils/mentionLinkify.ts
+++ b/src/renderer/utils/mentionLinkify.ts
@@ -19,10 +19,11 @@ export function linkifyMentionsInMarkdown(
text: string,
memberColorMap: Map
): string {
- if (memberColorMap.size === 0) return text;
+ if (memberColorMap.size === 0 || !text.includes('@')) return text;
// Sort by name length descending for greedy matching
const names = [...memberColorMap.keys()].sort((a, b) => b.length - a.length);
+ const canonicalNameByLower = new Map(names.map((name) => [name.toLowerCase(), name]));
const escaped = names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const pattern = new RegExp(
// eslint-disable-next-line no-useless-escape -- backslash-quote and backslash-hyphen needed in template literal for RegExp
@@ -32,7 +33,7 @@ export function linkifyMentionsInMarkdown(
return text.replace(pattern, (_match: string, prefix: string, name: string) => {
// Find the canonical name (case-insensitive lookup)
- const canonical = names.find((n) => n.toLowerCase() === name.toLowerCase()) ?? name;
+ const canonical = canonicalNameByLower.get(name.toLowerCase()) ?? name;
const color = memberColorMap.get(canonical) ?? '';
return `${prefix}[@${canonical}](mention://${encodeURIComponent(color)}/${encodeURIComponent(canonical)})`;
});
@@ -51,10 +52,11 @@ export function linkifyTeamMentionsInMarkdown(
teamNames: ReadonlySet | readonly string[]
): string {
const names: readonly string[] = Array.isArray(teamNames) ? teamNames : [...teamNames];
- if (names.length === 0) return text;
+ if (names.length === 0 || !text.includes('@')) return text;
// Sort by name length descending for greedy matching
const sorted = [...names].sort((a, b) => b.length - a.length);
+ const canonicalNameByLower = new Map(sorted.map((name) => [name.toLowerCase(), name]));
const escaped = sorted.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const pattern = new RegExp(
// eslint-disable-next-line no-useless-escape -- backslash-quote and backslash-hyphen needed in template literal for RegExp
@@ -63,7 +65,7 @@ export function linkifyTeamMentionsInMarkdown(
);
return text.replace(pattern, (_match: string, prefix: string, name: string) => {
- const canonical = sorted.find((n) => n.toLowerCase() === name.toLowerCase()) ?? name;
+ const canonical = canonicalNameByLower.get(name.toLowerCase()) ?? name;
return `${prefix}[${canonical}](team://${encodeURIComponent(canonical)})`;
});
}
diff --git a/src/renderer/utils/teamModelCatalog.ts b/src/renderer/utils/teamModelCatalog.ts
index 22d86baf..69d12f2c 100644
--- a/src/renderer/utils/teamModelCatalog.ts
+++ b/src/renderer/utils/teamModelCatalog.ts
@@ -667,7 +667,17 @@ function getSupplementalVisibleModels(
return models;
}
- return [...models, ...ANTHROPIC_VISIBLE_MODEL_FALLBACKS];
+ const existingLabels = new Set(
+ models
+ .map((model) => getTeamModelBadgeLabel(providerId, model)?.trim().toLowerCase())
+ .filter((label): label is string => Boolean(label))
+ );
+ const supplementalModels = ANTHROPIC_VISIBLE_MODEL_FALLBACKS.filter((model) => {
+ const label = getTeamModelBadgeLabel(providerId, model)?.trim().toLowerCase();
+ return !label || !existingLabels.has(label);
+ });
+
+ return [...models, ...supplementalModels];
}
export function getVisibleTeamProviderModels(
diff --git a/src/shared/utils/__tests__/openCodeWindowsAccessDenied.test.ts b/src/shared/utils/__tests__/openCodeWindowsAccessDenied.test.ts
index b9f37496..c9378134 100644
--- a/src/shared/utils/__tests__/openCodeWindowsAccessDenied.test.ts
+++ b/src/shared/utils/__tests__/openCodeWindowsAccessDenied.test.ts
@@ -2,8 +2,10 @@ import { describe, expect, it } from 'vitest';
import {
isOpenCodeWindowsAccessDeniedDiagnostic,
+ isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic,
normalizeOpenCodeWindowsAccessDeniedDiagnostic,
OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE,
+ OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE,
} from '../openCodeWindowsAccessDenied';
describe('OpenCode Windows access-denied diagnostics', () => {
@@ -24,4 +26,18 @@ describe('OpenCode Windows access-denied diagnostics', () => {
expect(isOpenCodeWindowsAccessDeniedDiagnostic('OpenCode app MCP is unreachable')).toBe(false);
expect(normalizeOpenCodeWindowsAccessDeniedDiagnostic('OpenCode CLI not found')).toBeNull();
});
+
+ it('detects the managed OpenCode node_modules symlink permission failure separately', () => {
+ const message = [
+ 'Runtime provider management command failed unexpectedly:',
+ "EPERM: operation not permitted, symlink 'C:\\Users\\ben\\AppData\\Local\\claude-multimodel-nodejs\\Cache\\opencode\\shared-cache\\config-node_modules'",
+ "-> 'C:\\Users\\ben\\AppData\\Local\\claude-multimodel-nodejs\\Data\\opencode\\profiles\\abc123\\config\\opencode\\node_modules'",
+ ].join(' ');
+
+ expect(isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic(message)).toBe(true);
+ expect(isOpenCodeWindowsAccessDeniedDiagnostic(message)).toBe(true);
+ expect(normalizeOpenCodeWindowsAccessDeniedDiagnostic(message)).toBe(
+ OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE
+ );
+ });
});
diff --git a/src/shared/utils/openCodeWindowsAccessDenied.ts b/src/shared/utils/openCodeWindowsAccessDenied.ts
index e5d6a38c..608f3427 100644
--- a/src/shared/utils/openCodeWindowsAccessDenied.ts
+++ b/src/shared/utils/openCodeWindowsAccessDenied.ts
@@ -1,15 +1,48 @@
export const OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE =
'Windows blocked OpenCode from accessing project or runtime files. Fix folder permissions or move the project to a user-writable folder. Running as administrator is only a temporary workaround.';
+export const OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE =
+ 'Windows blocked OpenCode from creating the managed node_modules symlink. Run Agent Teams AI as Administrator, then retry launch.';
+
const OPENCODE_WINDOWS_ACCESS_DENIED_PATTERN =
/\b(?:EPERM|EACCES)\b|access is denied|permission denied|operation not permitted/i;
+const OPENCODE_WINDOWS_EPERM_CODE_PATTERN = /\bEPERM\b/i;
+const WINDOWS_DRIVE_PATH_PATTERN = /\b[A-Z]:\\/i;
+
+function isOpenCodeWindowsNodeModulesSymlinkPermissionText(value: string): boolean {
+ const lower = value.toLowerCase();
+ return (
+ OPENCODE_WINDOWS_EPERM_CODE_PATTERN.test(value) &&
+ lower.includes('operation not permitted') &&
+ lower.includes('symlink') &&
+ lower.includes('opencode') &&
+ lower.includes('node_modules') &&
+ (WINDOWS_DRIVE_PATH_PATTERN.test(value) ||
+ lower.includes('appdata\\local\\claude-multimodel-nodejs'))
+ );
+}
+
+export function isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic(
+ value: string | null | undefined
+): boolean {
+ const trimmed = value?.trim();
+ if (!trimmed) {
+ return false;
+ }
+ return (
+ trimmed === OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE ||
+ isOpenCodeWindowsNodeModulesSymlinkPermissionText(trimmed)
+ );
+}
+
export function isOpenCodeWindowsAccessDeniedDiagnostic(value: string | null | undefined): boolean {
const trimmed = value?.trim();
if (!trimmed) {
return false;
}
return (
+ isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic(trimmed) ||
trimmed === OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE ||
OPENCODE_WINDOWS_ACCESS_DENIED_PATTERN.test(trimmed)
);
@@ -18,6 +51,9 @@ export function isOpenCodeWindowsAccessDeniedDiagnostic(value: string | null | u
export function normalizeOpenCodeWindowsAccessDeniedDiagnostic(
value: string | null | undefined
): string | null {
+ if (isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic(value)) {
+ return OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE;
+ }
return isOpenCodeWindowsAccessDeniedDiagnostic(value)
? OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE
: null;
diff --git a/test/features/anthropic-runtime-profile/resolveAnthropicRuntimeProfile.test.ts b/test/features/anthropic-runtime-profile/resolveAnthropicRuntimeProfile.test.ts
index 997feb6d..7cbb486b 100644
--- a/test/features/anthropic-runtime-profile/resolveAnthropicRuntimeProfile.test.ts
+++ b/test/features/anthropic-runtime-profile/resolveAnthropicRuntimeProfile.test.ts
@@ -183,7 +183,7 @@ describe('resolveAnthropicRuntimeProfile', () => {
effortResetReason: null,
nextFastMode: 'inherit',
fastModeResetReason:
- 'Fast mode is available only for Opus 4.6. Selected model resolves to Opus 4.7 (1M).',
+ 'Fast mode is available only for Opus 4.8. Selected model resolves to Opus 4.7 (1M).',
});
});
diff --git a/test/features/codex-account/main/CodexLoginSessionManager.test.ts b/test/features/codex-account/main/CodexLoginSessionManager.test.ts
index 6494f32b..2e204822 100644
--- a/test/features/codex-account/main/CodexLoginSessionManager.test.ts
+++ b/test/features/codex-account/main/CodexLoginSessionManager.test.ts
@@ -37,21 +37,21 @@ function createSession(overrides?: {
});
const close = overrides?.close ?? vi.fn().mockResolvedValue(undefined);
- const session = {
+ const session: CodexAppServerSession = {
initializeResponse: {
userAgent: 'codex-test',
codexHome: '/Users/tester/.codex',
platformFamily: 'darwin',
platformOs: 'macos',
},
- request,
- notify: vi.fn().mockResolvedValue(undefined),
+ request: request as CodexAppServerSession['request'],
+ notify: vi.fn().mockResolvedValue(undefined) as CodexAppServerSession['notify'],
onNotification: vi.fn((listener: (method: string, params: unknown) => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
- }),
- close,
- } satisfies CodexAppServerSession;
+ }) as CodexAppServerSession['onNotification'],
+ close: close as CodexAppServerSession['close'],
+ };
return {
session,
diff --git a/test/features/member-work-sync/core/MemberWorkSyncUseCases.test.ts b/test/features/member-work-sync/core/MemberWorkSyncUseCases.test.ts
index af3d67cf..71217843 100644
--- a/test/features/member-work-sync/core/MemberWorkSyncUseCases.test.ts
+++ b/test/features/member-work-sync/core/MemberWorkSyncUseCases.test.ts
@@ -311,8 +311,13 @@ class InMemoryOutboxStore implements MemberWorkSyncOutboxStorePort {
class InMemoryInboxNudge implements MemberWorkSyncInboxNudgePort {
readonly inserted: Array[0]> = [];
+ readonly repaired: Array<
+ Parameters>[0]
+ > = [];
fail = false;
conflict = false;
+ repairFail = false;
+ repairConflict = false;
async insertIfAbsent(input: Parameters[0]) {
if (this.fail) {
@@ -324,6 +329,19 @@ class InMemoryInboxNudge implements MemberWorkSyncInboxNudgePort {
this.inserted.push(input);
return { inserted: true, messageId: input.messageId };
}
+
+ async repairIfPresent(
+ input: Parameters>[0]
+ ) {
+ if (this.repairFail) {
+ throw new Error('inbox repair unavailable');
+ }
+ if (this.repairConflict) {
+ return { found: true, repaired: false, conflict: true };
+ }
+ this.repaired.push(input);
+ return { found: true, repaired: true };
+ }
}
function createDeps(options?: {
@@ -1885,6 +1903,19 @@ describe('MemberWorkSync use cases', () => {
item.payload.workSyncIntentKey?.startsWith('agenda-sync-still-stuck:')
)
).toHaveLength(1);
+ expect(inbox.inserted).toHaveLength(2);
+ expect(inbox.repaired).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ messageId: baseId,
+ payloadHash: outbox.items.get(baseId)?.payloadHash,
+ }),
+ expect.objectContaining({
+ messageId: recovery?.id,
+ payloadHash: recovery?.payloadHash,
+ }),
+ ])
+ );
clock.set('2026-04-29T01:02:00.000Z');
store.metricsGeneratedAt = '2026-04-29T01:02:00.000Z';
@@ -2007,6 +2038,33 @@ describe('MemberWorkSync use cases', () => {
expect(summary).toMatchObject({ claimed: 1, delivered: 1, retryable: 0 });
expect(inbox.inserted).toHaveLength(2);
expect(inbox.inserted[1]?.messageId).toContain('agenda-sync-still-stuck');
+
+ clock.set('2026-04-29T01:02:00.000Z');
+ store.phase2ReadinessState = 'shadow_ready';
+ store.phase2ReadinessReasons = [];
+ store.metricsGeneratedAt = '2026-04-29T01:02:00.000Z';
+ await reconciler.execute(
+ {
+ teamName: 'team-a',
+ memberName: 'bob',
+ },
+ { reconciledBy: 'queue', triggerReasons: ['config_changed', 'task_changed'] }
+ );
+
+ const recoveryItems = [...outbox.items.values()].filter((item) =>
+ item.payload.workSyncIntentKey?.startsWith('agenda-sync-still-stuck:')
+ );
+ expect(recoveryItems).toHaveLength(2);
+ expect(new Set(recoveryItems.map((item) => item.id)).size).toBe(2);
+
+ const secondSummary = await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
+ teamNames: ['team-a'],
+ claimedBy: 'test-dispatcher',
+ });
+
+ expect(secondSummary).toMatchObject({ claimed: 1, delivered: 1, retryable: 0 });
+ expect(inbox.inserted).toHaveLength(3);
+ expect(inbox.inserted[2]?.messageId).toContain('agenda-sync-still-stuck');
});
it('creates a delivered-still-stuck recovery for mixed review pickup and native work under noisy metrics', async () => {
@@ -2130,6 +2188,15 @@ describe('MemberWorkSync use cases', () => {
item.payload.workSyncIntentKey?.startsWith('agenda-sync-still-stuck:')
)
).toHaveLength(0);
+ expect(inbox.inserted).toHaveLength(1);
+ expect(inbox.repaired).toEqual([
+ expect.objectContaining({
+ teamName: 'team-a',
+ memberName: 'bob',
+ messageId: baseId,
+ payloadHash: outbox.items.get(baseId)?.payloadHash,
+ }),
+ ]);
expect(auditEvents).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -2945,6 +3012,85 @@ describe('MemberWorkSync use cases', () => {
expect(store.writes.at(-1)?.state).toBe('still_working');
});
+ it('refreshes expired fallback pending report tokens during replay', async () => {
+ const { deps, store } = createDeps();
+ const reader = new MemberWorkSyncReconciler(deps);
+ const current = await reader.execute({ teamName: 'team-a', memberName: 'bob' });
+ const baseReportToken = deps.reportToken!;
+ deps.reportToken = {
+ create: baseReportToken.create,
+ verify: async (input) =>
+ input.token === 'expired-token'
+ ? { ok: false, reason: 'expired' }
+ : baseReportToken.verify(input),
+ };
+ store.pendingIntents.set('intent-1', {
+ id: 'intent-1',
+ teamName: 'team-a',
+ memberName: 'bob',
+ status: 'pending',
+ reason: 'control_api_unavailable',
+ recordedAt: '2026-04-29T00:16:00.000Z',
+ request: {
+ teamName: 'team-a',
+ memberName: 'bob',
+ state: 'still_working',
+ agendaFingerprint: current.agenda.fingerprint,
+ reportToken: 'expired-token',
+ leaseTtlMs: 120_000,
+ source: 'mcp',
+ },
+ });
+
+ const summary = await new MemberWorkSyncPendingReportIntentReplayer(deps).replayTeam('team-a');
+
+ expect(summary).toEqual({ processed: 1, accepted: 1, rejected: 0, superseded: 0 });
+ expect(store.pendingIntents.get('intent-1')).toMatchObject({
+ status: 'accepted',
+ resultCode: 'accepted',
+ });
+ expect(store.writes.at(-1)?.report).toMatchObject({
+ accepted: true,
+ source: 'mcp',
+ state: 'still_working',
+ });
+ });
+
+ it('rejects invalid fallback pending report tokens without refreshing identity', async () => {
+ const { deps, store } = createDeps();
+ const reader = new MemberWorkSyncReconciler(deps);
+ const current = await reader.execute({ teamName: 'team-a', memberName: 'bob' });
+ store.pendingIntents.set('intent-1', {
+ id: 'intent-1',
+ teamName: 'team-a',
+ memberName: 'bob',
+ status: 'pending',
+ reason: 'control_api_unavailable',
+ recordedAt: '2026-04-29T00:00:01.000Z',
+ request: {
+ teamName: 'team-a',
+ memberName: 'bob',
+ state: 'still_working',
+ agendaFingerprint: current.agenda.fingerprint,
+ reportToken: 'invalid-token',
+ leaseTtlMs: 120_000,
+ source: 'mcp',
+ },
+ });
+
+ const summary = await new MemberWorkSyncPendingReportIntentReplayer(deps).replayTeam('team-a');
+
+ expect(summary).toEqual({ processed: 1, accepted: 0, rejected: 1, superseded: 0 });
+ expect(store.pendingIntents.get('intent-1')).toMatchObject({
+ status: 'rejected',
+ resultCode: 'invalid_report_token',
+ });
+ expect(store.writes.at(-1)?.report).toMatchObject({
+ accepted: false,
+ rejectionCode: 'invalid_report_token',
+ });
+ });
+
it('supersedes pending controller intents when the member runtime is inactive', async () => {
const { deps, store } = createDeps();
const reader = new MemberWorkSyncReconciler(deps);
diff --git a/test/features/member-work-sync/main/TeamInboxMemberWorkSyncNudgeSink.test.ts b/test/features/member-work-sync/main/TeamInboxMemberWorkSyncNudgeSink.test.ts
index 021036c9..3ab07ef7 100644
--- a/test/features/member-work-sync/main/TeamInboxMemberWorkSyncNudgeSink.test.ts
+++ b/test/features/member-work-sync/main/TeamInboxMemberWorkSyncNudgeSink.test.ts
@@ -1,6 +1,5 @@
-import { describe, expect, it, vi } from 'vitest';
-
import { TeamInboxMemberWorkSyncNudgeSink } from '@features/member-work-sync/main/adapters/output/TeamInboxMemberWorkSyncNudgeSink';
+import { describe, expect, it, vi } from 'vitest';
import type { MemberWorkSyncInboxNudgePort } from '@features/member-work-sync/core/application';
@@ -32,7 +31,11 @@ describe('TeamInboxMemberWorkSyncNudgeSink', () => {
const input = makeInput();
const inboxReader = {
getMessagesFor: vi.fn(async () => [
- { messageId: input.messageId, workSyncPayloadHash: input.payloadHash },
+ {
+ messageId: input.messageId,
+ messageKind: 'member_work_sync_nudge',
+ workSyncPayloadHash: input.payloadHash,
+ },
]),
};
const inboxWriter = {
@@ -49,6 +52,309 @@ describe('TeamInboxMemberWorkSyncNudgeSink', () => {
expect(inboxWriter.sendMessage).not.toHaveBeenCalled();
});
+ it('repairs an existing idempotent nudge row that is missing the current controlUrl', async () => {
+ const input = makeInput();
+ const inboxReader = {
+ getMessagesFor: vi.fn(async () => [
+ {
+ messageId: input.messageId,
+ text: input.payload.text,
+ messageKind: 'member_work_sync_nudge',
+ workSyncPayloadHash: input.payloadHash,
+ },
+ ]),
+ };
+ const inboxWriter = {
+ sendMessage: vi.fn(),
+ updateMessageText: vi.fn(async () => ({ found: true, updated: true })),
+ };
+ const sink = new TeamInboxMemberWorkSyncNudgeSink(
+ inboxReader as never,
+ inboxWriter as never,
+ () => 'http://127.0.0.1:43123'
+ );
+
+ await expect(sink.insertIfAbsent(input)).resolves.toEqual({
+ inserted: false,
+ messageId: input.messageId,
+ });
+
+ expect(inboxWriter.sendMessage).not.toHaveBeenCalled();
+ expect(inboxWriter.updateMessageText).toHaveBeenCalledWith('team-a', {
+ member: 'bob',
+ messageId: input.messageId,
+ text: `${input.payload.text}\nRequired control API: pass controlUrl "http://127.0.0.1:43123" in both member_work_sync_status and member_work_sync_report.`,
+ expectedMessageKind: 'member_work_sync_nudge',
+ expectedWorkSyncPayloadHash: input.payloadHash,
+ });
+ });
+
+ it('refreshes a stale controlUrl on an existing idempotent nudge row', async () => {
+ const input = makeInput();
+ const existingText = `${input.payload.text}\nRequired control API: pass controlUrl "http://127.0.0.1:11111" in both member_work_sync_status and member_work_sync_report.`;
+ const inboxReader = {
+ getMessagesFor: vi.fn(async () => [
+ {
+ messageId: input.messageId,
+ text: existingText,
+ messageKind: 'member_work_sync_nudge',
+ workSyncPayloadHash: input.payloadHash,
+ },
+ ]),
+ };
+ const inboxWriter = {
+ sendMessage: vi.fn(),
+ updateMessageText: vi.fn(async () => ({ found: true, updated: true })),
+ };
+ const sink = new TeamInboxMemberWorkSyncNudgeSink(
+ inboxReader as never,
+ inboxWriter as never,
+ () => 'http://127.0.0.1:43123'
+ );
+
+ await expect(sink.insertIfAbsent(input)).resolves.toEqual({
+ inserted: false,
+ messageId: input.messageId,
+ });
+
+ expect(inboxWriter.updateMessageText).toHaveBeenCalledWith(
+ 'team-a',
+ expect.objectContaining({
+ text: `${input.payload.text}\nRequired control API: pass controlUrl "http://127.0.0.1:43123" in both member_work_sync_status and member_work_sync_report.`,
+ })
+ );
+ });
+
+ it('fails closed when an existing idempotent nudge needs controlUrl repair but resolver is unavailable', async () => {
+ const input = makeInput();
+ const inboxReader = {
+ getMessagesFor: vi.fn(async () => [
+ {
+ messageId: input.messageId,
+ text: input.payload.text,
+ messageKind: 'member_work_sync_nudge',
+ workSyncPayloadHash: input.payloadHash,
+ },
+ ]),
+ };
+ const inboxWriter = {
+ sendMessage: vi.fn(),
+ updateMessageText: vi.fn(),
+ };
+ const sink = new TeamInboxMemberWorkSyncNudgeSink(
+ inboxReader as never,
+ inboxWriter as never,
+ () => null
+ );
+
+ await expect(sink.insertIfAbsent(input)).rejects.toThrow(
+ 'member work sync control URL unavailable'
+ );
+
+ expect(inboxWriter.sendMessage).not.toHaveBeenCalled();
+ expect(inboxWriter.updateMessageText).not.toHaveBeenCalled();
+ });
+
+ it('fails closed when an existing idempotent nudge needs controlUrl repair but writer cannot update text', async () => {
+ const input = makeInput();
+ const inboxReader = {
+ getMessagesFor: vi.fn(async () => [
+ {
+ messageId: input.messageId,
+ text: input.payload.text,
+ messageKind: 'member_work_sync_nudge',
+ workSyncPayloadHash: input.payloadHash,
+ },
+ ]),
+ };
+ const inboxWriter = {
+ sendMessage: vi.fn(),
+ };
+ const sink = new TeamInboxMemberWorkSyncNudgeSink(
+ inboxReader as never,
+ inboxWriter as never,
+ () => 'http://127.0.0.1:43123'
+ );
+
+ await expect(sink.insertIfAbsent(input)).rejects.toThrow(
+ 'member work sync inbox text update unavailable'
+ );
+
+ expect(inboxWriter.sendMessage).not.toHaveBeenCalled();
+ });
+
+ it('repairs a delivered nudge row by stable messageId without inserting a duplicate', async () => {
+ const input = makeInput();
+ const inboxReader = {
+ getMessagesFor: vi.fn(async () => [
+ {
+ messageId: input.messageId,
+ text: input.payload.text,
+ messageKind: 'member_work_sync_nudge',
+ workSyncPayloadHash: input.payloadHash,
+ },
+ ]),
+ };
+ const inboxWriter = {
+ sendMessage: vi.fn(),
+ updateMessageText: vi.fn(async () => ({ found: true, updated: true })),
+ };
+ const sink = new TeamInboxMemberWorkSyncNudgeSink(
+ inboxReader as never,
+ inboxWriter as never,
+ () => 'http://127.0.0.1:43123'
+ );
+
+ await expect(
+ sink.repairIfPresent({
+ teamName: input.teamName,
+ memberName: input.memberName,
+ messageId: input.messageId,
+ payloadHash: input.payloadHash,
+ payload: input.payload,
+ })
+ ).resolves.toEqual({ found: true, repaired: true });
+
+ expect(inboxWriter.sendMessage).not.toHaveBeenCalled();
+ expect(inboxWriter.updateMessageText).toHaveBeenCalledWith(
+ 'team-a',
+ expect.objectContaining({
+ messageId: input.messageId,
+ expectedWorkSyncPayloadHash: input.payloadHash,
+ })
+ );
+ });
+
+ it('reports direct repair as unrepaired when the guarded writer refuses the update', async () => {
+ const input = makeInput();
+ const inboxReader = {
+ getMessagesFor: vi.fn(async () => [
+ {
+ messageId: input.messageId,
+ text: input.payload.text,
+ messageKind: 'member_work_sync_nudge',
+ workSyncPayloadHash: input.payloadHash,
+ },
+ ]),
+ };
+ const inboxWriter = {
+ sendMessage: vi.fn(),
+ updateMessageText: vi.fn(async () => ({ found: true, updated: false })),
+ };
+ const sink = new TeamInboxMemberWorkSyncNudgeSink(
+ inboxReader as never,
+ inboxWriter as never,
+ () => 'http://127.0.0.1:43123'
+ );
+
+ await expect(
+ sink.repairIfPresent({
+ teamName: input.teamName,
+ memberName: input.memberName,
+ messageId: input.messageId,
+ payloadHash: input.payloadHash,
+ payload: input.payload,
+ })
+ ).resolves.toEqual({ found: true, repaired: false });
+
+ expect(inboxWriter.sendMessage).not.toHaveBeenCalled();
+ expect(inboxWriter.updateMessageText).toHaveBeenCalledWith(
+ 'team-a',
+ expect.objectContaining({
+ messageId: input.messageId,
+ expectedWorkSyncPayloadHash: input.payloadHash,
+ })
+ );
+ });
+
+ it('reports missing delivered rows during direct repair without inserting', async () => {
+ const input = makeInput();
+ const inboxReader = {
+ getMessagesFor: vi.fn(async () => []),
+ };
+ const inboxWriter = {
+ sendMessage: vi.fn(),
+ updateMessageText: vi.fn(),
+ };
+ const sink = new TeamInboxMemberWorkSyncNudgeSink(inboxReader as never, inboxWriter as never);
+
+ await expect(
+ sink.repairIfPresent({
+ teamName: input.teamName,
+ memberName: input.memberName,
+ messageId: input.messageId,
+ payloadHash: input.payloadHash,
+ payload: input.payload,
+ })
+ ).resolves.toEqual({ found: false, repaired: false });
+
+ expect(inboxWriter.sendMessage).not.toHaveBeenCalled();
+ expect(inboxWriter.updateMessageText).not.toHaveBeenCalled();
+ });
+
+ it('fails closed when direct repair finds a different payload hash', async () => {
+ const input = makeInput();
+ const inboxReader = {
+ getMessagesFor: vi.fn(async () => [
+ {
+ messageId: input.messageId,
+ text: input.payload.text,
+ messageKind: 'member_work_sync_nudge',
+ workSyncPayloadHash: 'different-payload-hash',
+ },
+ ]),
+ };
+ const inboxWriter = {
+ sendMessage: vi.fn(),
+ updateMessageText: vi.fn(),
+ };
+ const sink = new TeamInboxMemberWorkSyncNudgeSink(inboxReader as never, inboxWriter as never);
+
+ await expect(
+ sink.repairIfPresent({
+ teamName: input.teamName,
+ memberName: input.memberName,
+ messageId: input.messageId,
+ payloadHash: input.payloadHash,
+ payload: input.payload,
+ })
+ ).resolves.toEqual({ found: true, repaired: false, conflict: true });
+
+ expect(inboxWriter.sendMessage).not.toHaveBeenCalled();
+ expect(inboxWriter.updateMessageText).not.toHaveBeenCalled();
+ });
+
+ it('does not rewrite an existing idempotent nudge row with the current controlUrl', async () => {
+ const input = makeInput();
+ const existingText = `${input.payload.text}\nRequired control API: pass controlUrl "http://127.0.0.1:43123" in both member_work_sync_status and member_work_sync_report.`;
+ const inboxReader = {
+ getMessagesFor: vi.fn(async () => [
+ {
+ messageId: input.messageId,
+ text: existingText,
+ messageKind: 'member_work_sync_nudge',
+ workSyncPayloadHash: input.payloadHash,
+ },
+ ]),
+ };
+ const inboxWriter = {
+ sendMessage: vi.fn(),
+ updateMessageText: vi.fn(async () => ({ found: true, updated: true })),
+ };
+ const sink = new TeamInboxMemberWorkSyncNudgeSink(
+ inboxReader as never,
+ inboxWriter as never,
+ () => 'http://127.0.0.1:43123'
+ );
+
+ await expect(sink.insertIfAbsent(input)).resolves.toEqual({
+ inserted: false,
+ messageId: input.messageId,
+ });
+
+ expect(inboxWriter.updateMessageText).not.toHaveBeenCalled();
+ });
+
it('fails closed when the existing stable messageId has a different payload hash', async () => {
const input = makeInput();
const inboxReader = {
@@ -70,6 +376,48 @@ describe('TeamInboxMemberWorkSyncNudgeSink', () => {
expect(inboxWriter.sendMessage).not.toHaveBeenCalled();
});
+ it('fails closed when the existing stable messageId is not a work-sync nudge row', async () => {
+ const input = makeInput();
+ const inboxReader = {
+ getMessagesFor: vi.fn(async () => [
+ {
+ messageId: input.messageId,
+ messageKind: 'task_comment_notification',
+ workSyncPayloadHash: input.payloadHash,
+ text: input.payload.text,
+ },
+ ]),
+ };
+ const inboxWriter = {
+ sendMessage: vi.fn(),
+ updateMessageText: vi.fn(),
+ };
+ const sink = new TeamInboxMemberWorkSyncNudgeSink(
+ inboxReader as never,
+ inboxWriter as never,
+ () => 'http://127.0.0.1:43123'
+ );
+
+ await expect(sink.insertIfAbsent(input)).resolves.toEqual({
+ inserted: false,
+ messageId: input.messageId,
+ conflict: true,
+ });
+
+ await expect(
+ sink.repairIfPresent({
+ teamName: input.teamName,
+ memberName: input.memberName,
+ messageId: input.messageId,
+ payloadHash: input.payloadHash,
+ payload: input.payload,
+ })
+ ).resolves.toEqual({ found: true, repaired: false, conflict: true });
+
+ expect(inboxWriter.sendMessage).not.toHaveBeenCalled();
+ expect(inboxWriter.updateMessageText).not.toHaveBeenCalled();
+ });
+
it('treats legacy work-sync rows without payload hash as conflicts', async () => {
const input = makeInput();
const inboxReader = {
@@ -129,6 +477,50 @@ describe('TeamInboxMemberWorkSyncNudgeSink', () => {
});
});
+ it('does not insert a new nudge when a configured controlUrl resolver returns null', async () => {
+ const input = makeInput();
+ const inboxReader = {
+ getMessagesFor: vi.fn(async () => []),
+ };
+ const inboxWriter = {
+ sendMessage: vi.fn(),
+ };
+ const sink = new TeamInboxMemberWorkSyncNudgeSink(
+ inboxReader as never,
+ inboxWriter as never,
+ () => null
+ );
+
+ await expect(sink.insertIfAbsent(input)).rejects.toThrow(
+ 'member work sync control URL unavailable'
+ );
+
+ expect(inboxWriter.sendMessage).not.toHaveBeenCalled();
+ });
+
+ it('does not insert a new nudge when a configured controlUrl resolver fails', async () => {
+ const input = makeInput();
+ const inboxReader = {
+ getMessagesFor: vi.fn(async () => []),
+ };
+ const inboxWriter = {
+ sendMessage: vi.fn(),
+ };
+ const sink = new TeamInboxMemberWorkSyncNudgeSink(
+ inboxReader as never,
+ inboxWriter as never,
+ () => {
+ throw new Error('sidecar failed');
+ }
+ );
+
+ await expect(sink.insertIfAbsent(input)).rejects.toThrow(
+ 'member work sync control URL unavailable'
+ );
+
+ expect(inboxWriter.sendMessage).not.toHaveBeenCalled();
+ });
+
it('propagates reader failures so dispatch can classify the attempt', async () => {
const input = makeInput();
const inboxReader = {
diff --git a/test/features/member-work-sync/main/TeamRuntimeTurnSettledTargetResolver.test.ts b/test/features/member-work-sync/main/TeamRuntimeTurnSettledTargetResolver.test.ts
index 4aedea90..6bce6519 100644
--- a/test/features/member-work-sync/main/TeamRuntimeTurnSettledTargetResolver.test.ts
+++ b/test/features/member-work-sync/main/TeamRuntimeTurnSettledTargetResolver.test.ts
@@ -1,6 +1,6 @@
+import { TeamRuntimeTurnSettledTargetResolver } from '@features/member-work-sync/main/adapters/output/TeamRuntimeTurnSettledTargetResolver';
import { describe, expect, it, vi } from 'vitest';
-import { TeamRuntimeTurnSettledTargetResolver } from '@features/member-work-sync/main/adapters/output/TeamRuntimeTurnSettledTargetResolver';
import type { TeamConfig } from '@shared/types';
describe('TeamRuntimeTurnSettledTargetResolver', () => {
@@ -134,6 +134,48 @@ describe('TeamRuntimeTurnSettledTargetResolver', () => {
).resolves.toEqual({ ok: false, reason: 'provider_mismatch' });
});
+ it('preserves config provider metadata when member meta lacks provider fields', async () => {
+ const resolver = new TeamRuntimeTurnSettledTargetResolver({
+ teamSource: {
+ listTeams: vi.fn(async () => []),
+ getConfig: vi.fn(async () => ({
+ name: 'team-a',
+ members: [
+ {
+ name: 'Jack',
+ providerBackendId: 'codex-native',
+ model: 'opencode/openai/gpt-oss',
+ },
+ ],
+ }) satisfies TeamConfig),
+ },
+ membersMetaStore: {
+ getMembers: vi.fn(async () => [
+ {
+ name: 'Jack',
+ role: 'developer',
+ agentType: 'general-purpose',
+ color: 'blue',
+ },
+ ]),
+ } as never,
+ });
+
+ await expect(
+ resolver.resolve({
+ schemaVersion: 1,
+ provider: 'opencode',
+ hookEventName: 'Stop',
+ sourceId: 'source-1',
+ payloadHash: 'hash',
+ recordedAt: '2026-04-29T12:00:00.000Z',
+ sessionId: 'ses-1',
+ teamName: 'team-a',
+ memberName: 'jack',
+ })
+ ).resolves.toEqual({ ok: false, reason: 'provider_mismatch' });
+ });
+
it('resolves OpenCode turn-settled payloads from durable team/member identity', async () => {
const resolver = new TeamRuntimeTurnSettledTargetResolver({
teamSource: {
diff --git a/test/features/member-work-sync/main/adapters/output/TeamTaskAgendaSource.test.ts b/test/features/member-work-sync/main/adapters/output/TeamTaskAgendaSource.test.ts
index 6ce94a7d..22ac54b7 100644
--- a/test/features/member-work-sync/main/adapters/output/TeamTaskAgendaSource.test.ts
+++ b/test/features/member-work-sync/main/adapters/output/TeamTaskAgendaSource.test.ts
@@ -1,6 +1,7 @@
+import { TeamTaskAgendaSource } from '@features/member-work-sync/main/adapters/output/TeamTaskAgendaSource';
import { describe, expect, it, vi } from 'vitest';
-import { TeamTaskAgendaSource } from '@features/member-work-sync/main/adapters/output/TeamTaskAgendaSource';
+import type { TeamConfig } from '@shared/types';
describe('TeamTaskAgendaSource', () => {
it('applies kanban approved overlay before building member work agenda', async () => {
@@ -52,4 +53,64 @@ describe('TeamTaskAgendaSource', () => {
expect(result.agenda.items).toEqual([]);
});
+
+ it('preserves config provider metadata when member meta only has runtime fields', async () => {
+ const source = new TeamTaskAgendaSource({
+ configReader: {
+ getConfig: vi.fn(async () => ({
+ name: 'forge-labs',
+ members: [
+ {
+ name: 'Jack',
+ providerBackendId: 'codex-native',
+ model: 'opencode/openai/gpt-oss',
+ },
+ ],
+ }) satisfies TeamConfig),
+ },
+ taskReader: {
+ getTasks: vi.fn(async () => [
+ {
+ id: 'task-stale',
+ displayId: '#task-stale',
+ subject: 'Continue stale task',
+ status: 'in_progress',
+ owner: 'jack',
+ reviewState: 'none',
+ },
+ ]),
+ },
+ kanbanManager: {
+ getState: vi.fn(async () => ({
+ teamName: 'forge-labs',
+ reviewers: [],
+ tasks: {},
+ })),
+ },
+ membersMetaStore: {
+ getMembers: vi.fn(async () => [
+ {
+ name: 'Jack',
+ role: 'developer',
+ agentType: 'general-purpose',
+ color: 'blue',
+ },
+ ]),
+ },
+ hash: {
+ sha256Hex: vi.fn((value: string) => `h${value.length}`),
+ },
+ clock: {
+ now: () => new Date('2026-05-06T19:06:07.257Z'),
+ },
+ } as never);
+
+ const result = await source.loadAgenda({
+ teamName: 'forge-labs',
+ memberName: 'jack',
+ });
+
+ expect(result.providerId).toBe('codex');
+ expect(result.agenda.items).toHaveLength(1);
+ });
});
diff --git a/test/features/member-work-sync/main/adapters/output/mergeTeamMembers.test.ts b/test/features/member-work-sync/main/adapters/output/mergeTeamMembers.test.ts
new file mode 100644
index 00000000..be1cd61a
--- /dev/null
+++ b/test/features/member-work-sync/main/adapters/output/mergeTeamMembers.test.ts
@@ -0,0 +1,245 @@
+import { mergeTeamMembers } from '@features/member-work-sync/main/adapters/output/mergeTeamMembers';
+import { describe, expect, it } from 'vitest';
+
+describe('mergeTeamMembers', () => {
+ it('preserves config provider fields when member meta only carries runtime fields', () => {
+ expect(
+ mergeTeamMembers(
+ [
+ {
+ name: 'NickName',
+ providerId: 'codex',
+ providerBackendId: 'codex-native',
+ model: 'gpt-5.5',
+ effort: 'medium',
+ },
+ ],
+ [
+ {
+ name: 'NickName',
+ role: 'developer',
+ agentType: 'general-purpose',
+ color: 'blue',
+ },
+ ]
+ )
+ ).toEqual([
+ {
+ name: 'NickName',
+ providerId: 'codex',
+ providerBackendId: 'codex-native',
+ model: 'gpt-5.5',
+ effort: 'medium',
+ role: 'developer',
+ agentType: 'general-purpose',
+ color: 'blue',
+ },
+ ]);
+ });
+
+ it('allows explicit member meta values to override config values', () => {
+ expect(
+ mergeTeamMembers(
+ [{ name: 'Alice', providerId: 'codex', model: 'gpt-5.5', removedAt: undefined }],
+ [
+ {
+ name: 'Alice',
+ providerId: 'opencode',
+ model: 'minimax-m2.5-free',
+ removedAt: 1780567089118,
+ },
+ ]
+ )
+ ).toEqual([
+ {
+ name: 'Alice',
+ providerId: 'opencode',
+ model: 'minimax-m2.5-free',
+ removedAt: 1780567089118,
+ },
+ ]);
+ });
+
+ it('clears stale config provider fields when explicit member meta changes provider', () => {
+ const [member] = mergeTeamMembers(
+ [
+ {
+ name: 'Alice',
+ providerId: 'codex',
+ providerBackendId: 'codex-native',
+ model: 'gpt-5.5',
+ effort: 'medium',
+ fastMode: 'off',
+ },
+ ],
+ [{ name: 'Alice', providerId: 'opencode', role: 'developer' }]
+ );
+
+ expect(member).toEqual({
+ name: 'Alice',
+ providerId: 'opencode',
+ role: 'developer',
+ });
+ });
+
+ it('clears stale inferred config provider fields when explicit member meta changes provider', () => {
+ const [member] = mergeTeamMembers(
+ [
+ {
+ name: 'Alice',
+ model: 'gpt-5.5',
+ effort: 'medium',
+ fastMode: 'off',
+ },
+ ],
+ [{ name: 'Alice', providerId: 'opencode', role: 'developer' }]
+ );
+
+ expect(member).toEqual({
+ name: 'Alice',
+ providerId: 'opencode',
+ role: 'developer',
+ });
+ });
+
+ it('clears stale backend-inferred config provider fields when explicit member meta changes provider', () => {
+ const [member] = mergeTeamMembers(
+ [
+ {
+ name: 'Alice',
+ providerBackendId: 'codex-native',
+ fastMode: 'off',
+ },
+ ],
+ [{ name: 'Alice', providerId: 'opencode', role: 'developer' }]
+ );
+
+ expect(member).toEqual({
+ name: 'Alice',
+ providerId: 'opencode',
+ role: 'developer',
+ });
+ });
+
+ it('does not let providerless runtime meta model override config provider metadata', () => {
+ expect(
+ mergeTeamMembers(
+ [{ name: 'Alice', providerId: 'codex', model: 'gpt-5.5', effort: 'medium' }],
+ [
+ {
+ name: 'Alice',
+ role: 'developer',
+ model: 'opencode/openai/gpt-oss',
+ effort: 'high',
+ },
+ ]
+ )
+ ).toEqual([
+ {
+ name: 'Alice',
+ providerId: 'codex',
+ model: 'gpt-5.5',
+ effort: 'medium',
+ role: 'developer',
+ },
+ ]);
+ });
+
+ it('preserves config fastMode off without dropping runtime provider identity fields', () => {
+ expect(
+ mergeTeamMembers(
+ [{ name: 'Alice', fastMode: 'off' }],
+ [{ name: 'Alice', model: 'opencode/openai/gpt-oss', fastMode: 'on' }]
+ )
+ ).toEqual([{ name: 'Alice', fastMode: 'off', model: 'opencode/openai/gpt-oss' }]);
+ });
+
+ it('preserves backend-only config provider metadata over providerless runtime meta model', () => {
+ expect(
+ mergeTeamMembers(
+ [{ name: 'Alice', providerBackendId: 'codex-native' }],
+ [{ name: 'Alice', model: 'opencode/openai/gpt-oss', role: 'developer' }]
+ )
+ ).toEqual([
+ {
+ name: 'Alice',
+ providerBackendId: 'codex-native',
+ role: 'developer',
+ },
+ ]);
+ });
+
+ it('treats provider backend as stronger provider identity than a stale model', () => {
+ const [member] = mergeTeamMembers(
+ [
+ {
+ name: 'Alice',
+ providerBackendId: 'opencode-cli',
+ model: 'gpt-5.5',
+ fastMode: 'off',
+ },
+ ],
+ [{ name: 'Alice', providerId: 'codex', role: 'developer' }]
+ );
+
+ expect(member).toEqual({
+ name: 'Alice',
+ providerId: 'codex',
+ role: 'developer',
+ });
+ });
+
+ it('does not treat empty or null config provider metadata as authoritative', () => {
+ const [member] = mergeTeamMembers(
+ [{ name: 'Alice', model: '', providerBackendId: null as never }],
+ [{ name: 'Alice', model: 'gpt-5.5', role: 'developer' }]
+ );
+
+ expect(member).toMatchObject({
+ name: 'Alice',
+ model: 'gpt-5.5',
+ role: 'developer',
+ });
+ });
+
+ it('does not treat an uninferable config model as authoritative provider identity', () => {
+ const [member] = mergeTeamMembers(
+ [{ name: 'Alice', model: 'custom-local-model', fastMode: 'off' }],
+ [{ name: 'Alice', model: 'gpt-5.5', role: 'developer' }]
+ );
+
+ expect(member).toEqual({
+ name: 'Alice',
+ model: 'gpt-5.5',
+ fastMode: 'off',
+ role: 'developer',
+ });
+ });
+
+ it('allows runtime member meta to clear stale config removal state without clearing provider fields', () => {
+ const [member] = mergeTeamMembers(
+ [{ name: 'Alice', providerId: 'codex', model: 'gpt-5.5', removedAt: 1780567089118 }],
+ [
+ {
+ name: 'Alice',
+ role: 'developer',
+ removedAt: undefined,
+ },
+ ]
+ );
+
+ expect(member).toMatchObject({
+ name: 'Alice',
+ providerId: 'codex',
+ model: 'gpt-5.5',
+ role: 'developer',
+ });
+ expect(member).not.toHaveProperty('removedAt');
+ });
+
+ it('keeps meta-only members', () => {
+ expect(
+ mergeTeamMembers([], [{ name: 'Bob', role: 'reviewer', color: 'green' }])
+ ).toEqual([{ name: 'Bob', role: 'reviewer', color: 'green' }]);
+ });
+});
diff --git a/test/features/member-work-sync/main/createMemberWorkSyncFeature.test.ts b/test/features/member-work-sync/main/createMemberWorkSyncFeature.test.ts
index 45a4d82c..692cc2cd 100644
--- a/test/features/member-work-sync/main/createMemberWorkSyncFeature.test.ts
+++ b/test/features/member-work-sync/main/createMemberWorkSyncFeature.test.ts
@@ -3,6 +3,7 @@ import {
buildMemberWorkSyncRuntimeTurnSettledEnvironment,
createMemberWorkSyncFeature,
} from '@features/member-work-sync/main';
+import { HmacMemberWorkSyncReportTokenAdapter } from '@features/member-work-sync/main/infrastructure/HmacMemberWorkSyncReportTokenAdapter';
import { JsonMemberWorkSyncStore } from '@features/member-work-sync/main/infrastructure/JsonMemberWorkSyncStore';
import { MemberWorkSyncStorePaths } from '@features/member-work-sync/main/infrastructure/MemberWorkSyncStorePaths';
import { NodeHashAdapter } from '@features/member-work-sync/main/infrastructure/NodeHashAdapter';
@@ -1555,6 +1556,130 @@ describe('createMemberWorkSyncFeature composition', () => {
}
});
+ it('keeps config provider when runtime member meta omits it before native stale recovery', async () => {
+ const claudeRoot = makeTempRoot();
+ setClaudeBasePathOverride(claudeRoot);
+ const teamsBasePath = getTeamsBasePath();
+ const teamName = 'team-native-stale-meta-provider';
+ const memberName = 'nickname';
+ const nudgeDeliveryWake = {
+ schedule: vi.fn(async () => undefined),
+ };
+ const feature = createMemberWorkSyncFeature({
+ teamsBasePath,
+ configReader: {
+ getConfig: vi.fn(async () => ({
+ name: teamName,
+ members: [{ name: 'NickName', providerId: 'codex', model: 'gpt-5.5' }],
+ })),
+ } as never,
+ taskReader: {
+ getTasks: vi.fn(async () => [
+ {
+ id: 'task-1',
+ displayId: '11111111',
+ subject: 'Review landing',
+ status: 'in_progress',
+ owner: 'NickName',
+ },
+ ]),
+ } as never,
+ kanbanManager: {
+ getState: vi.fn(async () => ({
+ teamName,
+ reviewers: [],
+ tasks: {},
+ })),
+ } as never,
+ membersMetaStore: {
+ getMembers: vi.fn(async () => [
+ {
+ name: 'NickName',
+ role: 'developer',
+ agentType: 'general-purpose',
+ color: 'blue',
+ },
+ ]),
+ } as never,
+ isTeamActive: vi.fn(async () => true),
+ nudgeDeliveryWake,
+ queueQuietWindowMs: 1,
+ });
+
+ try {
+ feature.noteTeamChange({ type: 'task', teamName, taskId: 'task-1' } as never);
+
+ let agendaFingerprint = '';
+ await waitForAssertion(async () => {
+ const status = await feature.getStatus({ teamName, memberName });
+ expect(status).toMatchObject({
+ state: 'needs_sync',
+ providerId: 'codex',
+ diagnostics: expect.arrayContaining(['no_current_report']),
+ agenda: {
+ items: [
+ expect.objectContaining({
+ reason: 'owned_in_progress_task',
+ evidence: expect.objectContaining({ status: 'in_progress' }),
+ }),
+ ],
+ },
+ });
+ agendaFingerprint = status.agenda.fingerprint;
+ });
+ expect(await readInboxMessages({ teamsBasePath, teamName, memberName })).toEqual([]);
+
+ await seedNativeStaleInProgressBlockingMetrics({
+ teamsBasePath,
+ teamName,
+ memberName,
+ agendaFingerprint,
+ });
+ feature.noteTeamChange({ type: 'task', teamName, taskId: 'task-1' } as never);
+
+ await waitForAssertion(async () => {
+ const nudges = (await readInboxMessages({ teamsBasePath, teamName, memberName })).filter(
+ (message) => message.messageKind === 'member_work_sync_nudge'
+ );
+ expect(nudges).toHaveLength(1);
+ expect(nudges[0]?.text).toContain('Work sync check');
+ expect(nudges[0]?.text).toContain('11111111');
+ expect(nudgeDeliveryWake.schedule).toHaveBeenCalledWith({
+ teamName,
+ memberName,
+ messageId: nudges[0]?.messageId,
+ providerId: 'codex',
+ reason: 'member_work_sync_nudge_inserted',
+ delayMs: 500,
+ });
+ expect(
+ Object.values(await readMemberOutboxItems({ teamsBasePath, teamName, memberName }))
+ ).toEqual([
+ expect.objectContaining({
+ status: 'delivered',
+ deliveredMessageId: nudges[0]?.messageId,
+ }),
+ ]);
+ });
+
+ const journal = await fs.promises.readFile(
+ path.join(
+ teamsBasePath,
+ teamName,
+ 'members',
+ memberName,
+ '.member-work-sync',
+ 'journal.jsonl'
+ ),
+ 'utf8'
+ );
+ expect(journal).toContain('"event":"nudge_delivered"');
+ expect(journal).not.toContain('"reason":"blocking_metrics"');
+ } finally {
+ await feature.dispose();
+ }
+ });
+
it('delivers native stale pending-work recovery nudges despite noisy global metrics', async () => {
const claudeRoot = makeTempRoot();
setClaudeBasePathOverride(claudeRoot);
@@ -3335,6 +3460,103 @@ describe('createMemberWorkSyncFeature composition', () => {
}
});
+ it('keeps nudges retryable while configured controlUrl is unavailable and delivers after recovery', async () => {
+ const claudeRoot = makeTempRoot();
+ setClaudeBasePathOverride(claudeRoot);
+ const teamsBasePath = getTeamsBasePath();
+ const teamName = 'team-control-url-retry';
+ const memberName = 'bob';
+ let controlUrl: string | null = null;
+ const feature = createMemberWorkSyncFeature({
+ teamsBasePath,
+ configReader: {
+ getConfig: vi.fn(async () => ({
+ name: teamName,
+ members: [{ name: memberName, providerId: 'codex' }],
+ })),
+ } as never,
+ taskReader: {
+ getTasks: vi.fn(async () => [
+ {
+ id: 'task-1',
+ displayId: '11111111',
+ subject: 'Ship sync after control URL recovery',
+ status: 'pending',
+ owner: memberName,
+ },
+ ]),
+ } as never,
+ kanbanManager: {
+ getState: vi.fn(async () => ({
+ teamName,
+ reviewers: [],
+ tasks: {},
+ })),
+ } as never,
+ membersMetaStore: {
+ getMembers: vi.fn(async () => []),
+ } as never,
+ isTeamActive: vi.fn(async () => true),
+ queueQuietWindowMs: 1,
+ resolveControlUrl: vi.fn(async () => controlUrl),
+ });
+
+ try {
+ await seedShadowReadyMetrics({ teamsBasePath, teamName, memberName });
+ feature.noteTeamChange({ type: 'task', teamName, taskId: 'task-1' } as never);
+
+ await waitForAssertion(async () => {
+ expect(await readInboxMessages({ teamsBasePath, teamName, memberName })).toHaveLength(0);
+ expect(
+ Object.values(await readMemberOutboxItems({ teamsBasePath, teamName, memberName }))
+ ).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ status: 'failed_retryable',
+ lastError: expect.stringContaining('member work sync control URL unavailable'),
+ }),
+ ])
+ );
+ });
+ await waitForQueueIdle(feature);
+
+ controlUrl = 'http://127.0.0.1:43123';
+ await forceRetryableOutboxDue({
+ teamsBasePath,
+ teamName,
+ memberName,
+ nextAttemptAt: new Date(Date.now() - 1_000).toISOString(),
+ });
+
+ await expect(feature.dispatchDueNudges([teamName])).resolves.toEqual({
+ claimed: 1,
+ delivered: 1,
+ superseded: 0,
+ retryable: 0,
+ terminal: 0,
+ });
+
+ const nudges = (await readInboxMessages({ teamsBasePath, teamName, memberName })).filter(
+ (message) => message.messageKind === 'member_work_sync_nudge'
+ );
+ expect(nudges).toHaveLength(1);
+ expect(nudges[0]?.text).toContain('11111111');
+ expect(nudges[0]?.text).toContain('controlUrl "http://127.0.0.1:43123"');
+ expect(
+ Object.values(await readMemberOutboxItems({ teamsBasePath, teamName, memberName }))
+ ).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ status: 'delivered',
+ deliveredMessageId: expect.any(String),
+ }),
+ ])
+ );
+ } finally {
+ await feature.dispose();
+ }
+ });
+
it('respects watchdog cooldown and delivers after the retry window is due', async () => {
const claudeRoot = makeTempRoot();
setClaudeBasePathOverride(claudeRoot);
@@ -3978,6 +4200,252 @@ describe('createMemberWorkSyncFeature composition', () => {
}
});
+ it('refreshes expired fallback pending report tokens through the real HMAC validator', async () => {
+ const claudeRoot = makeTempRoot();
+ setClaudeBasePathOverride(claudeRoot);
+ const teamsBasePath = getTeamsBasePath();
+ const teamName = 'team-expired-pending-report';
+ const memberName = 'bob';
+ const storePaths = new MemberWorkSyncStorePaths(teamsBasePath);
+ const feature = createMemberWorkSyncFeature({
+ teamsBasePath,
+ configReader: {
+ getConfig: vi.fn(async () => ({
+ name: teamName,
+ members: [{ name: memberName, providerId: 'codex' }],
+ })),
+ } as never,
+ taskReader: {
+ getTasks: vi.fn(async () => [
+ {
+ id: 'task-1',
+ displayId: '11111111',
+ subject: 'Ship sync after expired fallback report',
+ status: 'pending',
+ owner: memberName,
+ },
+ ]),
+ } as never,
+ kanbanManager: {
+ getState: vi.fn(async () => ({
+ teamName,
+ reviewers: [],
+ tasks: {},
+ })),
+ } as never,
+ membersMetaStore: {
+ getMembers: vi.fn(async () => []),
+ } as never,
+ isTeamActive: vi.fn(async () => true),
+ });
+
+ try {
+ const status = await feature.refreshStatus({ teamName, memberName });
+ expect(status.reportToken).toBeTruthy();
+ const expiredToken = await new HmacMemberWorkSyncReportTokenAdapter(storePaths).create({
+ teamName,
+ memberName,
+ agendaFingerprint: status.agenda.fingerprint,
+ issuedAt: new Date(Date.now() - 60 * 60_000).toISOString(),
+ });
+ const store = new JsonMemberWorkSyncStore(storePaths);
+ await store.appendPendingReport(
+ {
+ teamName,
+ memberName,
+ state: 'still_working',
+ agendaFingerprint: status.agenda.fingerprint,
+ reportToken: expiredToken.token,
+ taskIds: ['task-1'],
+ source: 'mcp',
+ },
+ 'control_api_unavailable'
+ );
+
+ await expect(feature.replayPendingReports([teamName])).resolves.toEqual({
+ processed: 1,
+ accepted: 1,
+ rejected: 0,
+ superseded: 0,
+ });
+
+ const finalStatus = await feature.getStatus({ teamName, memberName });
+ expect(finalStatus).toMatchObject({
+ state: 'still_working',
+ report: {
+ accepted: true,
+ state: 'still_working',
+ taskIds: ['task-1'],
+ source: 'mcp',
+ },
+ });
+ const memberReports = JSON.parse(
+ await fs.promises.readFile(
+ path.join(
+ teamsBasePath,
+ teamName,
+ 'members',
+ memberName,
+ '.member-work-sync',
+ 'reports.json'
+ ),
+ 'utf8'
+ )
+ ) as {
+ intents?: Record<
+ string,
+ { status?: string; resultCode?: string; request?: { reportToken?: string } }
+ >;
+ };
+ expect(Object.values(memberReports.intents ?? {})).toContainEqual(
+ expect.objectContaining({
+ status: 'accepted',
+ resultCode: 'accepted',
+ request: expect.objectContaining({ reportToken: expiredToken.token }),
+ })
+ );
+ } finally {
+ await feature.dispose();
+ }
+ });
+
+ it('returns a reportable status with a token when no stored status exists', async () => {
+ const claudeRoot = makeTempRoot();
+ setClaudeBasePathOverride(claudeRoot);
+ const teamsBasePath = getTeamsBasePath();
+ const teamName = 'team-a';
+ const memberName = 'bob';
+ const feature = createMemberWorkSyncFeature({
+ teamsBasePath,
+ configReader: {
+ getConfig: vi.fn(async () => ({
+ name: teamName,
+ members: [{ name: memberName, providerId: 'codex' }],
+ })),
+ } as never,
+ taskReader: {
+ getTasks: vi.fn(async () => [
+ {
+ id: 'task-1',
+ displayId: '11111111',
+ subject: 'Wake from first status call',
+ status: 'pending',
+ owner: memberName,
+ },
+ ]),
+ } as never,
+ kanbanManager: {
+ getState: vi.fn(async () => ({
+ teamName,
+ reviewers: [],
+ tasks: {},
+ })),
+ } as never,
+ membersMetaStore: {
+ getMembers: vi.fn(async () => []),
+ } as never,
+ isTeamActive: vi.fn(async () => true),
+ });
+
+ try {
+ const status = await feature.getStatus({ teamName, memberName });
+ expect(status).toMatchObject({
+ state: 'needs_sync',
+ shadow: { reconciledBy: 'request' },
+ });
+ expect(status.reportToken).toBeTruthy();
+
+ await expect(
+ feature.report({
+ teamName,
+ memberName,
+ state: 'still_working',
+ agendaFingerprint: status.agenda.fingerprint,
+ reportToken: status.reportToken,
+ taskIds: ['task-1'],
+ source: 'test',
+ })
+ ).resolves.toMatchObject({
+ accepted: true,
+ status: { state: 'still_working', report: { accepted: true } },
+ });
+ } finally {
+ await feature.dispose();
+ }
+ });
+
+ it('refreshes an expired stored report token before returning status to a teammate', async () => {
+ const claudeRoot = makeTempRoot();
+ setClaudeBasePathOverride(claudeRoot);
+ const teamsBasePath = getTeamsBasePath();
+ const teamName = 'team-a';
+ const memberName = 'bob';
+ const feature = createMemberWorkSyncFeature({
+ teamsBasePath,
+ configReader: {
+ getConfig: vi.fn(async () => ({
+ name: teamName,
+ members: [{ name: memberName, providerId: 'codex' }],
+ })),
+ } as never,
+ taskReader: {
+ getTasks: vi.fn(async () => [
+ {
+ id: 'task-1',
+ displayId: '11111111',
+ subject: 'Wake with expired token',
+ status: 'pending',
+ owner: memberName,
+ },
+ ]),
+ } as never,
+ kanbanManager: {
+ getState: vi.fn(async () => ({
+ teamName,
+ reviewers: [],
+ tasks: {},
+ })),
+ } as never,
+ membersMetaStore: {
+ getMembers: vi.fn(async () => []),
+ } as never,
+ isTeamActive: vi.fn(async () => true),
+ });
+
+ try {
+ const current = await feature.refreshStatus({ teamName, memberName });
+ const store = new JsonMemberWorkSyncStore(new MemberWorkSyncStorePaths(teamsBasePath));
+ const expiredToken = 'wrs:v1.expired-token-for-regression';
+ await store.write({
+ ...current,
+ reportToken: expiredToken,
+ reportTokenExpiresAt: new Date(Date.now() - 60_000).toISOString(),
+ });
+
+ const refreshed = await feature.getStatus({ teamName, memberName });
+ expect(refreshed.reportToken).toBeTruthy();
+ expect(refreshed.reportToken).not.toBe(expiredToken);
+ expect(Date.parse(refreshed.reportTokenExpiresAt ?? '')).toBeGreaterThan(Date.now());
+
+ await expect(
+ feature.report({
+ teamName,
+ memberName,
+ state: 'still_working',
+ agendaFingerprint: refreshed.agenda.fingerprint,
+ reportToken: refreshed.reportToken,
+ taskIds: ['task-1'],
+ source: 'test',
+ })
+ ).resolves.toMatchObject({
+ accepted: true,
+ status: { state: 'still_working', report: { accepted: true } },
+ });
+ } finally {
+ await feature.dispose();
+ }
+ });
+
it('refreshes stale needs_sync into inactive after the whole team stops', async () => {
const claudeRoot = makeTempRoot();
setClaudeBasePathOverride(claudeRoot);
@@ -4030,15 +4498,9 @@ describe('createMemberWorkSyncFeature composition', () => {
teamActive = false;
await expect(feature.getStatus({ teamName, memberName })).resolves.toMatchObject({
- state: 'needs_sync',
- diagnostics: expect.arrayContaining(['status_stale_refresh_enqueued']),
- });
- await waitForQueueIdle(feature);
-
- await expect(store.read({ teamName, memberName })).resolves.toMatchObject({
state: 'inactive',
diagnostics: expect.arrayContaining(['team_runtime_inactive']),
- shadow: { reconciledBy: 'queue', triggerReasons: ['manual_refresh'] },
+ shadow: { reconciledBy: 'request', triggerReasons: ['manual_refresh'] },
});
} finally {
await feature.dispose();
diff --git a/test/features/recent-projects/core/application/ListDashboardRecentProjectsUseCase.test.ts b/test/features/recent-projects/core/application/ListDashboardRecentProjectsUseCase.test.ts
index 9a5d96aa..9744e886 100644
--- a/test/features/recent-projects/core/application/ListDashboardRecentProjectsUseCase.test.ts
+++ b/test/features/recent-projects/core/application/ListDashboardRecentProjectsUseCase.test.ts
@@ -1,4 +1,5 @@
import { describe, expect, it, vi } from 'vitest';
+import type { Mock } from 'vitest';
import { ListDashboardRecentProjectsUseCase } from '@features/recent-projects/core/application/use-cases/ListDashboardRecentProjectsUseCase';
@@ -33,15 +34,17 @@ function makeCandidate(overrides: Partial = {}): RecentP
};
}
-function createLogger(): LoggerPort & {
- info: ReturnType;
- warn: ReturnType;
- error: ReturnType;
-} {
+type LoggerMock = LoggerPort & {
+ info: Mock;
+ warn: Mock;
+ error: Mock;
+};
+
+function createLogger(): LoggerMock {
return {
- info: vi.fn(),
- warn: vi.fn(),
- error: vi.fn(),
+ info: vi.fn(),
+ warn: vi.fn(),
+ error: vi.fn(),
};
}
diff --git a/test/features/recent-projects/main/adapters/output/CodexRecentProjectsSourceAdapter.test.ts b/test/features/recent-projects/main/adapters/output/CodexRecentProjectsSourceAdapter.test.ts
index 20350c5e..784f0e20 100644
--- a/test/features/recent-projects/main/adapters/output/CodexRecentProjectsSourceAdapter.test.ts
+++ b/test/features/recent-projects/main/adapters/output/CodexRecentProjectsSourceAdapter.test.ts
@@ -1,4 +1,5 @@
import { describe, expect, it, vi } from 'vitest';
+import type { Mock } from 'vitest';
import { CodexRecentProjectsSourceAdapter } from '@features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter';
@@ -6,15 +7,17 @@ import type { LoggerPort } from '@features/recent-projects/core/application/port
import type { CodexAppServerClient } from '@features/recent-projects/main/infrastructure/codex/CodexAppServerClient';
import type { RecentProjectIdentityResolver } from '@features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver';
-function createLogger(): LoggerPort & {
- info: ReturnType;
- warn: ReturnType;
- error: ReturnType;
-} {
+type LoggerMock = LoggerPort & {
+ info: Mock;
+ warn: Mock;
+ error: Mock;
+};
+
+function createLogger(): LoggerMock {
return {
- info: vi.fn(),
- warn: vi.fn(),
- error: vi.fn(),
+ info: vi.fn(),
+ warn: vi.fn(),
+ error: vi.fn(),
};
}
diff --git a/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts b/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts
index ee4efee0..e2b2badf 100644
--- a/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts
+++ b/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts
@@ -4,19 +4,22 @@ import path from 'node:path';
import { CodexSessionFileRecentProjectsSourceAdapter } from '@features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { Mock } from 'vitest';
import type { LoggerPort } from '@features/recent-projects/core/application/ports/LoggerPort';
import type { RecentProjectIdentityResolver } from '@features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver';
-function createLogger(): LoggerPort & {
- info: ReturnType;
- warn: ReturnType;
- error: ReturnType;
-} {
+type LoggerMock = LoggerPort & {
+ info: Mock;
+ warn: Mock;
+ error: Mock;
+};
+
+function createLogger(): LoggerMock {
return {
- info: vi.fn(),
- warn: vi.fn(),
- error: vi.fn(),
+ info: vi.fn(),
+ warn: vi.fn(),
+ error: vi.fn(),
};
}
diff --git a/test/main/ipc/globalSearch.test.ts b/test/main/ipc/globalSearch.test.ts
index a25da8e9..1d46dd2b 100644
--- a/test/main/ipc/globalSearch.test.ts
+++ b/test/main/ipc/globalSearch.test.ts
@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
+import type { Mock } from 'vitest';
import { ProjectScanner } from '../../../src/main/services/discovery/ProjectScanner';
@@ -9,19 +10,24 @@ import type { Project, SearchSessionsResult } from '../../../src/main/types';
*/
describe('Global Search - ProjectScanner.searchAllProjects', () => {
let projectScanner: ProjectScanner;
- let mockScan: ReturnType;
- let mockSearchSessions: ReturnType;
+ let mockScan: Mock<() => Promise>;
+ let mockSearchSessions: Mock<
+ (projectId: string, query: string, maxResults?: number) => Promise
+ >;
beforeEach(() => {
// Create a real ProjectScanner instance
projectScanner = new ProjectScanner();
// Mock the scan() method
- mockScan = vi.fn();
+ mockScan = vi.fn<() => Promise>();
projectScanner.scan = mockScan;
// Mock the sessionSearcher.searchSessions() method
- mockSearchSessions = vi.fn();
+ mockSearchSessions =
+ vi.fn<
+ (projectId: string, query: string, maxResults?: number) => Promise
+ >();
// @ts-expect-error - Accessing private property for testing
projectScanner.sessionSearcher = {
searchSessions: mockSearchSessions,
diff --git a/test/main/ipc/teams.test.ts b/test/main/ipc/teams.test.ts
index c810dcb3..6f39ff22 100644
--- a/test/main/ipc/teams.test.ts
+++ b/test/main/ipc/teams.test.ts
@@ -12,6 +12,7 @@ import type {
BoardTaskLogStreamResponse,
InboxMessage,
MessagesPage,
+ OpenCodeRuntimeDeliveryStatus,
SendMessageResult,
TeamCreateRequest,
TeamLaunchRequest,
@@ -322,6 +323,7 @@ describe('ipc teams handlers', () => {
}
| undefined,
})),
+ getOpenCodeRuntimeDeliveryStatus: vi.fn(async () => null as OpenCodeRuntimeDeliveryStatus | null),
buildOpenCodeRuntimeDeliveryUserVisibleImpact: vi.fn(() => ({ state: 'none' })),
getLiveLeadProcessMessages: vi.fn(() => [] as InboxMessage[]),
getCurrentLeadSessionId: vi.fn(() => null as string | null),
@@ -880,6 +882,7 @@ describe('ipc teams handlers', () => {
attempted: true,
delivered: true,
});
+ expect(provisioningService.getOpenCodeRuntimeDeliveryStatus).not.toHaveBeenCalled();
});
it('returns runtimeDelivery failure without hiding the persisted OpenCode message', async () => {
@@ -962,6 +965,7 @@ describe('ipc teams handlers', () => {
provisioningService.relayOpenCodeMemberInboxMessages.mockReturnValueOnce(
new Promise(() => undefined)
);
+ provisioningService.getOpenCodeRuntimeDeliveryStatus.mockResolvedValueOnce(null);
const sendHandler = handlers.get(TEAM_SEND_MESSAGE);
expect(sendHandler).toBeDefined();
@@ -970,7 +974,7 @@ describe('ipc teams handlers', () => {
text: 'Ping bob',
}) as Promise<{ success: boolean; data?: SendMessageResult }>;
- await vi.advanceTimersByTimeAsync(12_000);
+ await vi.advanceTimersByTimeAsync(6_000);
const result = await resultPromise;
expect(result.success).toBe(true);
@@ -983,6 +987,281 @@ describe('ipc teams handlers', () => {
responseState: 'not_observed',
reason: 'opencode_runtime_delivery_ui_timeout_pending',
});
+ expect(provisioningService.getOpenCodeRuntimeDeliveryStatus).toHaveBeenCalledWith(
+ 'my-team',
+ result.data?.messageId
+ );
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+
+ it('uses durable OpenCode delivery status when UI relay timeout fires after prompt acceptance', async () => {
+ vi.useFakeTimers();
+ try {
+ provisioningService.resolveRuntimeRecipientProviderId.mockResolvedValueOnce('opencode');
+ provisioningService.relayOpenCodeMemberInboxMessages.mockReturnValueOnce(
+ new Promise(() => undefined)
+ );
+ provisioningService.getOpenCodeRuntimeDeliveryStatus.mockResolvedValueOnce({
+ messageId: 'msg-123',
+ providerId: 'opencode',
+ attempted: true,
+ delivered: true,
+ responsePending: true,
+ responseState: 'not_observed',
+ ledgerStatus: 'pending',
+ reason: 'opencode_delivery_response_pending',
+ diagnostics: ['prompt accepted'],
+ userVisibleImpact: { state: 'none' },
+ });
+ provisioningService.buildOpenCodeRuntimeDeliveryUserVisibleImpact.mockReturnValueOnce({
+ state: 'checking',
+ });
+ const sendHandler = handlers.get(TEAM_SEND_MESSAGE);
+ expect(sendHandler).toBeDefined();
+
+ const resultPromise = sendHandler!({} as never, 'my-team', {
+ member: 'bob',
+ text: 'Ping bob',
+ }) as Promise<{ success: boolean; data?: SendMessageResult }>;
+
+ await vi.advanceTimersByTimeAsync(6_000);
+ const result = await resultPromise;
+
+ expect(result.success).toBe(true);
+ expect(result.data?.runtimeDelivery).toMatchObject({
+ providerId: 'opencode',
+ attempted: true,
+ delivered: true,
+ responsePending: true,
+ responseState: 'not_observed',
+ ledgerStatus: 'pending',
+ reason: 'opencode_delivery_response_pending',
+ diagnostics: ['prompt accepted'],
+ userVisibleImpact: { state: 'checking' },
+ });
+ expect(result.data?.runtimeDelivery?.acceptanceUnknown).toBeUndefined();
+ const impactCalls = provisioningService.buildOpenCodeRuntimeDeliveryUserVisibleImpact.mock
+ .calls as unknown as Array<[Partial>]>;
+ const impactInput = impactCalls.at(-1)?.[0];
+ expect(impactInput).toMatchObject({
+ delivered: true,
+ responsePending: true,
+ });
+ expect(impactInput).not.toHaveProperty('userVisibleImpact');
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+
+ it('preserves terminal durable OpenCode delivery status after UI relay timeout', async () => {
+ vi.useFakeTimers();
+ try {
+ provisioningService.resolveRuntimeRecipientProviderId.mockResolvedValueOnce('opencode');
+ provisioningService.relayOpenCodeMemberInboxMessages.mockReturnValueOnce(
+ new Promise(() => undefined)
+ );
+ provisioningService.getOpenCodeRuntimeDeliveryStatus.mockResolvedValueOnce({
+ messageId: 'msg-responded',
+ providerId: 'opencode',
+ attempted: true,
+ delivered: true,
+ responsePending: false,
+ responseState: 'responded_visible_message',
+ ledgerStatus: 'responded',
+ visibleReplyMessageId: 'reply-1',
+ visibleReplyCorrelation: 'relayOfMessageId',
+ acceptanceUnknown: false,
+ diagnostics: [],
+ userVisibleImpact: { state: 'none' },
+ });
+ const sendHandler = handlers.get(TEAM_SEND_MESSAGE);
+ expect(sendHandler).toBeDefined();
+
+ const resultPromise = sendHandler!({} as never, 'my-team', {
+ member: 'bob',
+ text: 'Ping bob',
+ }) as Promise<{ success: boolean; data?: SendMessageResult }>;
+
+ await vi.advanceTimersByTimeAsync(6_000);
+ const result = await resultPromise;
+
+ expect(result.success).toBe(true);
+ expect(result.data?.runtimeDelivery).toMatchObject({
+ providerId: 'opencode',
+ attempted: true,
+ delivered: true,
+ responsePending: false,
+ responseState: 'responded_visible_message',
+ ledgerStatus: 'responded',
+ visibleReplyMessageId: 'reply-1',
+ visibleReplyCorrelation: 'relayOfMessageId',
+ acceptanceUnknown: false,
+ userVisibleImpact: { state: 'none' },
+ });
+ expect(provisioningService.buildOpenCodeRuntimeDeliveryUserVisibleImpact).not.toHaveBeenCalled();
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+
+ it('does not let a slow OpenCode delivery status lookup extend the UI timeout indefinitely', async () => {
+ vi.useFakeTimers();
+ try {
+ provisioningService.resolveRuntimeRecipientProviderId.mockResolvedValueOnce('opencode');
+ provisioningService.relayOpenCodeMemberInboxMessages.mockReturnValueOnce(
+ new Promise(() => undefined)
+ );
+ provisioningService.getOpenCodeRuntimeDeliveryStatus.mockReturnValueOnce(
+ new Promise(() => undefined)
+ );
+ const sendHandler = handlers.get(TEAM_SEND_MESSAGE);
+ expect(sendHandler).toBeDefined();
+
+ const resultPromise = sendHandler!({} as never, 'my-team', {
+ member: 'bob',
+ text: 'Ping bob',
+ }) as Promise<{ success: boolean; data?: SendMessageResult }>;
+
+ await vi.advanceTimersByTimeAsync(6_000);
+ await flushMicrotasks();
+ let settled = false;
+ void resultPromise.then(() => {
+ settled = true;
+ });
+ await flushMicrotasks();
+ expect(settled).toBe(false);
+
+ await vi.advanceTimersByTimeAsync(1_000);
+ const result = await resultPromise;
+
+ expect(result.success).toBe(true);
+ expect(result.data?.runtimeDelivery).toMatchObject({
+ providerId: 'opencode',
+ attempted: true,
+ delivered: true,
+ responsePending: true,
+ acceptanceUnknown: true,
+ reason: 'opencode_runtime_delivery_ui_timeout_pending',
+ });
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+
+ it('falls back to acceptance-unknown when OpenCode delivery status lookup rejects after UI timeout', async () => {
+ vi.useFakeTimers();
+ try {
+ provisioningService.resolveRuntimeRecipientProviderId.mockResolvedValueOnce('opencode');
+ provisioningService.relayOpenCodeMemberInboxMessages.mockReturnValueOnce(
+ new Promise(() => undefined)
+ );
+ provisioningService.getOpenCodeRuntimeDeliveryStatus.mockRejectedValueOnce(
+ new Error('status read failed')
+ );
+ const sendHandler = handlers.get(TEAM_SEND_MESSAGE);
+ expect(sendHandler).toBeDefined();
+
+ const resultPromise = sendHandler!({} as never, 'my-team', {
+ member: 'bob',
+ text: 'Ping bob',
+ }) as Promise<{ success: boolean; data?: SendMessageResult }>;
+
+ await vi.advanceTimersByTimeAsync(6_000);
+ const result = await resultPromise;
+
+ expect(result.success).toBe(true);
+ expect(result.data?.runtimeDelivery).toMatchObject({
+ providerId: 'opencode',
+ attempted: true,
+ delivered: true,
+ responsePending: true,
+ acceptanceUnknown: true,
+ reason: 'opencode_runtime_delivery_ui_timeout_pending',
+ diagnostics: [
+ 'opencode_runtime_delivery_ui_timeout_pending',
+ 'opencode_runtime_delivery_ui_timeout_pending: status lookup failed: status read failed',
+ ],
+ });
+ expect(vi.mocked(console.warn).mock.calls.some((call) =>
+ call.join(' ').includes('status after UI timeout failed')
+ )).toBe(true);
+ vi.mocked(console.warn).mockClear();
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+
+ it('logs OpenCode relay rejection that happens after the UI timeout fallback', async () => {
+ vi.useFakeTimers();
+ try {
+ const deferredRelay = createDeferred
+ >>();
+ provisioningService.resolveRuntimeRecipientProviderId.mockResolvedValueOnce('opencode');
+ provisioningService.relayOpenCodeMemberInboxMessages.mockReturnValueOnce(deferredRelay.promise);
+ provisioningService.getOpenCodeRuntimeDeliveryStatus.mockResolvedValueOnce(null);
+ const sendHandler = handlers.get(TEAM_SEND_MESSAGE);
+ expect(sendHandler).toBeDefined();
+
+ const resultPromise = sendHandler!({} as never, 'my-team', {
+ member: 'bob',
+ text: 'Ping bob',
+ }) as Promise<{ success: boolean; data?: SendMessageResult }>;
+
+ await vi.advanceTimersByTimeAsync(6_000);
+ await expect(resultPromise).resolves.toMatchObject({ success: true });
+
+ deferredRelay.reject(new Error('late bridge failure'));
+ await flushMicrotasks();
+
+ expect(vi.mocked(console.warn).mock.calls.some((call) =>
+ call.join(' ').includes('rejected after UI timeout')
+ )).toBe(true);
+ vi.mocked(console.warn).mockClear();
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+
+ it('logs OpenCode relay failure result that resolves after the UI timeout fallback', async () => {
+ vi.useFakeTimers();
+ try {
+ const deferredRelay = createDeferred
+ >>();
+ provisioningService.resolveRuntimeRecipientProviderId.mockResolvedValueOnce('opencode');
+ provisioningService.relayOpenCodeMemberInboxMessages.mockReturnValueOnce(deferredRelay.promise);
+ provisioningService.getOpenCodeRuntimeDeliveryStatus.mockResolvedValueOnce(null);
+ const sendHandler = handlers.get(TEAM_SEND_MESSAGE);
+ expect(sendHandler).toBeDefined();
+
+ const resultPromise = sendHandler!({} as never, 'my-team', {
+ member: 'bob',
+ text: 'Ping bob',
+ }) as Promise<{ success: boolean; data?: SendMessageResult }>;
+
+ await vi.advanceTimersByTimeAsync(6_000);
+ await expect(resultPromise).resolves.toMatchObject({ success: true });
+
+ deferredRelay.resolve({
+ relayed: 0,
+ attempted: 1,
+ delivered: 0,
+ failed: 1,
+ lastDelivery: {
+ delivered: false,
+ reason: 'late_runtime_failure',
+ diagnostics: ['late_runtime_failure'],
+ },
+ });
+ await flushMicrotasks();
+
+ expect(vi.mocked(console.warn).mock.calls.some((call) =>
+ call.join(' ').includes('completed after UI timeout')
+ )).toBe(true);
+ vi.mocked(console.warn).mockClear();
} finally {
vi.useRealTimers();
}
diff --git a/test/main/services/infrastructure/CliInstallerService.test.ts b/test/main/services/infrastructure/CliInstallerService.test.ts
index 7b1cf99f..c827d0d6 100644
--- a/test/main/services/infrastructure/CliInstallerService.test.ts
+++ b/test/main/services/infrastructure/CliInstallerService.test.ts
@@ -80,6 +80,7 @@ vi.mock('@main/services/runtime/providerAwareCliEnv', () => ({
env: { HOME: '/Users/tester' },
connectionIssues: {},
})),
+ getProviderStatusStoredCredentialAllowlist: vi.fn(() => undefined),
}));
vi.mock('@main/utils/cliAuthDiagLog', () => ({
@@ -542,7 +543,12 @@ describe('CliInstallerService', () => {
it('falls back to the installed launcher path when --version reports unknown', async () => {
allowConsoleLogs();
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/Users/tester/.local/bin/claude');
- vi.spyOn(service as never, 'inferInstalledCliVersionFromPath').mockResolvedValue('2.1.101');
+ vi.spyOn(
+ service as unknown as {
+ inferInstalledCliVersionFromPath: (binaryPath: string) => Promise;
+ },
+ 'inferInstalledCliVersionFromPath'
+ ).mockResolvedValue('2.1.101');
vi.mocked(execCli)
.mockResolvedValueOnce({ stdout: 'unknown', stderr: '' })
.mockResolvedValueOnce({
diff --git a/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts b/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts
index c1316bfb..41a28b89 100644
--- a/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts
+++ b/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts
@@ -57,6 +57,16 @@ vi.mock('@main/services/runtime/ProviderConnectionService', () => ({
vi.mock('@main/services/runtime/providerAwareCliEnv', () => ({
buildProviderAwareCliEnv: (...args: Parameters) =>
buildProviderAwareCliEnvMock(...args),
+ getAggregateProviderStatusStoredCredentialAllowlist: () => [
+ 'ANTHROPIC_AUTH_TOKEN',
+ 'OPENAI_API_KEY',
+ ],
+ getProviderStatusStoredCredentialAllowlist: (providerId?: string) =>
+ providerId === 'anthropic'
+ ? ['ANTHROPIC_AUTH_TOKEN']
+ : providerId === 'codex'
+ ? ['OPENAI_API_KEY']
+ : undefined,
}));
describe('ClaudeMultimodelBridgeService', () => {
@@ -249,6 +259,14 @@ describe('ClaudeMultimodelBridgeService', () => {
projectId: 'demo-project',
},
});
+
+ const aggregateEnvBuild = buildProviderAwareCliEnvMock.mock.calls.find(
+ ([options]) => options.providerId === undefined
+ );
+ expect(aggregateEnvBuild?.[0]).toMatchObject({
+ allowStoredApiKeyDecryption: false,
+ allowedStoredApiKeyEnvVarNames: ['ANTHROPIC_AUTH_TOKEN', 'OPENAI_API_KEY'],
+ });
});
it('falls back to provider-scoped full runtime status without probing Gemini', async () => {
@@ -956,6 +974,18 @@ describe('ClaudeMultimodelBridgeService', () => {
modelCatalogRefreshState: 'ready',
});
expect(hydratedCodex?.modelCatalog?.models.map((model) => model.id)).toEqual(['gpt-5.4']);
+
+ const codexEnvBuilds = buildProviderAwareCliEnvMock.mock.calls.filter(
+ ([options]) => options.providerId === 'codex'
+ );
+ expect(codexEnvBuilds.length).toBeGreaterThanOrEqual(2);
+ for (const [options] of codexEnvBuilds) {
+ expect(options).toMatchObject({
+ providerId: 'codex',
+ allowStoredApiKeyDecryption: false,
+ allowedStoredApiKeyEnvVarNames: ['OPENAI_API_KEY'],
+ });
+ }
});
it('promotes OpenCode auth when full catalog hydration proves built-in free access', async () => {
@@ -1845,6 +1875,44 @@ describe('ClaudeMultimodelBridgeService', () => {
);
});
+ it('allows the stored Codex API key for Codex status checks', async () => {
+ execCliMock.mockResolvedValue({
+ stdout: JSON.stringify({
+ providers: {
+ codex: {
+ supported: true,
+ authenticated: true,
+ authMethod: 'api_key',
+ verificationState: 'verified',
+ canLoginFromUi: true,
+ capabilities: { teamLaunch: true, oneShot: true },
+ },
+ },
+ }),
+ stderr: '',
+ exitCode: 0,
+ });
+
+ const { ClaudeMultimodelBridgeService } =
+ await import('@main/services/runtime/ClaudeMultimodelBridgeService');
+ const service = new ClaudeMultimodelBridgeService();
+
+ const provider = await service.getProviderStatus('/mock/agent_teams_orchestrator', 'codex');
+
+ expect(provider).toMatchObject({
+ providerId: 'codex',
+ authenticated: true,
+ authMethod: 'api_key',
+ });
+ expect(buildProviderAwareCliEnvMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ providerId: 'codex',
+ allowStoredApiKeyDecryption: false,
+ allowedStoredApiKeyEnvVarNames: ['OPENAI_API_KEY'],
+ })
+ );
+ });
+
it('falls back conservatively when the runtime omits extension capability metadata', async () => {
execCliMock.mockResolvedValue({
stdout: JSON.stringify({
diff --git a/test/main/services/runtime/CliProviderModelAvailabilityService.test.ts b/test/main/services/runtime/CliProviderModelAvailabilityService.test.ts
index 541cebb7..74b37ebb 100644
--- a/test/main/services/runtime/CliProviderModelAvailabilityService.test.ts
+++ b/test/main/services/runtime/CliProviderModelAvailabilityService.test.ts
@@ -11,6 +11,12 @@ vi.mock('@main/utils/childProcess', () => ({
vi.mock('@main/services/runtime/providerAwareCliEnv', () => ({
buildProviderAwareCliEnv: (...args: Parameters) =>
buildProviderAwareCliEnvMock(...args),
+ getProviderStatusStoredCredentialAllowlist: (providerId?: string) =>
+ providerId === 'anthropic'
+ ? ['ANTHROPIC_AUTH_TOKEN']
+ : providerId === 'codex'
+ ? ['OPENAI_API_KEY']
+ : undefined,
}));
import {
@@ -186,4 +192,25 @@ describe('CliProviderModelAvailabilityService', () => {
);
});
});
+
+ it('allows stored Codex API-key access for model probes', async () => {
+ buildProviderAwareCliEnvMock.mockResolvedValue({
+ env: { HOME: '/Users/tester' },
+ connectionIssues: {},
+ });
+ execCliMock.mockResolvedValue({ stdout: 'PONG', stderr: '' });
+
+ const service = new CliProviderModelAvailabilityService();
+ service.getSnapshot(createContext(['gpt-5.4']));
+
+ await vi.waitFor(() => {
+ expect(buildProviderAwareCliEnvMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ providerId: 'codex',
+ allowStoredApiKeyDecryption: false,
+ allowedStoredApiKeyEnvVarNames: ['OPENAI_API_KEY'],
+ })
+ );
+ });
+ });
});
diff --git a/test/main/services/runtime/ProviderConnectionService.test.ts b/test/main/services/runtime/ProviderConnectionService.test.ts
index 6cfa124e..a6171dc7 100644
--- a/test/main/services/runtime/ProviderConnectionService.test.ts
+++ b/test/main/services/runtime/ProviderConnectionService.test.ts
@@ -1209,6 +1209,115 @@ describe('ProviderConnectionService', () => {
expect(result.CODEX_API_KEY).toBe('openai-stored-key');
});
+ it('mirrors a stored Codex OpenAI key when metadata-only status checks allow it', async () => {
+ const lookupPreferred = vi.fn().mockResolvedValue({
+ envVarName: 'OPENAI_API_KEY',
+ value: 'openai-stored-key',
+ });
+ const hasPreferred = vi.fn().mockResolvedValue(true);
+ const { ProviderConnectionService } =
+ await import('@main/services/runtime/ProviderConnectionService');
+
+ const service = new ProviderConnectionService(
+ {
+ hasPreferred,
+ lookupPreferred,
+ } as never,
+ {
+ getConfig: () => createConfig('auto'),
+ } as never
+ );
+
+ const result = await service.applyConfiguredConnectionEnv({}, 'codex', undefined, {
+ allowStoredApiKeyDecryption: false,
+ allowedStoredApiKeyEnvVarNames: ['OPENAI_API_KEY'],
+ });
+
+ expect(hasPreferred).toHaveBeenCalledWith('OPENAI_API_KEY');
+ expect(lookupPreferred).toHaveBeenCalledWith('OPENAI_API_KEY');
+ expect(result.OPENAI_API_KEY).toBe('openai-stored-key');
+ expect(result.CODEX_API_KEY).toBe('openai-stored-key');
+ });
+
+ it('does not mirror a stored Codex OpenAI key during metadata-only checks without allowlist', async () => {
+ const lookupPreferred = vi.fn().mockResolvedValue({
+ envVarName: 'OPENAI_API_KEY',
+ value: 'openai-stored-key',
+ });
+ const hasPreferred = vi.fn().mockResolvedValue(true);
+ const { ProviderConnectionService } =
+ await import('@main/services/runtime/ProviderConnectionService');
+
+ const service = new ProviderConnectionService(
+ {
+ hasPreferred,
+ lookupPreferred,
+ } as never,
+ {
+ getConfig: () => createConfig('auto'),
+ } as never
+ );
+
+ const result = await service.applyConfiguredConnectionEnv({}, 'codex', undefined, {
+ allowStoredApiKeyDecryption: false,
+ });
+
+ expect(hasPreferred).toHaveBeenCalledWith('OPENAI_API_KEY');
+ expect(lookupPreferred).not.toHaveBeenCalled();
+ expect(result.OPENAI_API_KEY).toBeUndefined();
+ expect(result.CODEX_API_KEY).toBeUndefined();
+ });
+
+ it('applies aggregate metadata-only allowlist without decrypting unrelated provider keys', async () => {
+ const lookupPreferred = vi.fn(async (envVarName: string) => {
+ if (envVarName === 'ANTHROPIC_AUTH_TOKEN') {
+ return { envVarName, value: 'anthropic-compatible-token' };
+ }
+ if (envVarName === 'OPENAI_API_KEY') {
+ return { envVarName, value: 'openai-stored-key' };
+ }
+ if (envVarName === 'GEMINI_API_KEY') {
+ return { envVarName, value: 'gemini-stored-key' };
+ }
+ return null;
+ });
+ const hasPreferred = vi.fn(async (envVarName: string) => envVarName === 'OPENAI_API_KEY');
+ const { ProviderConnectionService } =
+ await import('@main/services/runtime/ProviderConnectionService');
+
+ const service = new ProviderConnectionService(
+ {
+ hasPreferred,
+ lookupPreferred,
+ } as never,
+ {
+ getConfig: () =>
+ createConfig('auto', {
+ enabled: true,
+ baseUrl: 'http://localhost:1234',
+ }),
+ } as never
+ );
+
+ const result = await service.applyAllConfiguredConnectionEnv(
+ {},
+ {
+ allowStoredApiKeyDecryption: false,
+ allowedStoredApiKeyEnvVarNames: ['ANTHROPIC_AUTH_TOKEN', 'OPENAI_API_KEY'],
+ }
+ );
+
+ expect(result.ANTHROPIC_BASE_URL).toBe('http://localhost:1234');
+ expect(result.ANTHROPIC_AUTH_TOKEN).toBe('anthropic-compatible-token');
+ expect(result.OPENAI_API_KEY).toBe('openai-stored-key');
+ expect(result.CODEX_API_KEY).toBe('openai-stored-key');
+ expect(result.GEMINI_API_KEY).toBeUndefined();
+ expect(lookupPreferred).toHaveBeenCalledWith('ANTHROPIC_AUTH_TOKEN');
+ expect(lookupPreferred).toHaveBeenCalledWith('OPENAI_API_KEY');
+ expect(lookupPreferred).not.toHaveBeenCalledWith('GEMINI_API_KEY');
+ expect(lookupPreferred).not.toHaveBeenCalledWith('ANTHROPIC_API_KEY');
+ });
+
it('keeps ambient OpenAI credentials for native Codex launches', async () => {
const { ProviderConnectionService } =
await import('@main/services/runtime/ProviderConnectionService');
diff --git a/test/main/services/runtime/providerAwareCliEnv.test.ts b/test/main/services/runtime/providerAwareCliEnv.test.ts
index fdc4d17c..3740fc78 100644
--- a/test/main/services/runtime/providerAwareCliEnv.test.ts
+++ b/test/main/services/runtime/providerAwareCliEnv.test.ts
@@ -107,6 +107,25 @@ describe('buildProviderAwareCliEnv', () => {
});
});
+ it('returns narrow provider status stored credential allowlists', async () => {
+ const {
+ getAggregateProviderStatusStoredCredentialAllowlist,
+ getProviderStatusStoredCredentialAllowlist,
+ } = await import('../../../../src/main/services/runtime/providerAwareCliEnv');
+
+ expect(getProviderStatusStoredCredentialAllowlist('anthropic')).toEqual([
+ 'ANTHROPIC_AUTH_TOKEN',
+ ]);
+ expect(getProviderStatusStoredCredentialAllowlist('codex')).toEqual(['OPENAI_API_KEY']);
+ expect(getProviderStatusStoredCredentialAllowlist('gemini')).toBeUndefined();
+ expect(getProviderStatusStoredCredentialAllowlist('opencode')).toBeUndefined();
+ expect(getProviderStatusStoredCredentialAllowlist(undefined)).toBeUndefined();
+ expect(getAggregateProviderStatusStoredCredentialAllowlist()).toEqual([
+ 'ANTHROPIC_AUTH_TOKEN',
+ 'OPENAI_API_KEY',
+ ]);
+ });
+
it('builds provider-pinned CLI env and returns provider-specific issues', async () => {
getConfiguredConnectionIssuesMock.mockResolvedValue({
anthropic: 'missing key',
@@ -234,6 +253,21 @@ describe('buildProviderAwareCliEnv', () => {
expect(applyAllConfiguredConnectionEnvMock).not.toHaveBeenCalled();
});
+ it('passes a stored API key decrypt allowlist through shared env building', async () => {
+ const { buildProviderAwareCliEnv } =
+ await import('../../../../src/main/services/runtime/providerAwareCliEnv');
+ await buildProviderAwareCliEnv({
+ allowStoredApiKeyDecryption: false,
+ allowedStoredApiKeyEnvVarNames: ['ANTHROPIC_AUTH_TOKEN', 'OPENAI_API_KEY'],
+ });
+
+ expect(applyAllConfiguredConnectionEnvMock).toHaveBeenCalledWith(expect.any(Object), {
+ allowStoredApiKeyDecryption: false,
+ allowedStoredApiKeyEnvVarNames: ['ANTHROPIC_AUTH_TOKEN', 'OPENAI_API_KEY'],
+ });
+ expect(applyConfiguredConnectionEnvMock).not.toHaveBeenCalled();
+ });
+
it('builds shared env for generic CLI launches when no provider is specified', async () => {
const { buildProviderAwareCliEnv } =
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
diff --git a/test/main/services/schedule/ScheduledTaskExecutor.test.ts b/test/main/services/schedule/ScheduledTaskExecutor.test.ts
index d2ebed47..15ccddeb 100644
--- a/test/main/services/schedule/ScheduledTaskExecutor.test.ts
+++ b/test/main/services/schedule/ScheduledTaskExecutor.test.ts
@@ -529,26 +529,32 @@ describe('ScheduledTaskExecutor', () => {
proc.emit('close', 0);
});
- it('rejects explicit Codex schedule Fast before spawn when saved eligibility is false', async () => {
+ it('runs a standard Codex schedule when saved Fast eligibility is false', async () => {
+ const proc = createMockProcess();
+ mockSpawnCli.mockReturnValue(proc);
+
const executor = new ScheduledTaskExecutor();
- await expect(
- executor.execute(
- makeRequest({
- config: {
- cwd: '/tmp/project',
- prompt: 'do it',
- providerId: 'codex',
- providerBackendId: 'codex-native',
- model: 'gpt-5.4-mini',
- fastMode: 'on',
- resolvedFastMode: false,
- },
- })
- )
- ).rejects.toThrow('Codex Fast mode was requested');
+ void executor.execute(
+ makeRequest({
+ config: {
+ cwd: '/tmp/project',
+ prompt: 'do it',
+ providerId: 'codex',
+ providerBackendId: 'codex-native',
+ model: 'gpt-5.4-mini',
+ fastMode: 'on',
+ resolvedFastMode: false,
+ },
+ })
+ );
+ await flushAsync();
- expect(mockSpawnCli).not.toHaveBeenCalled();
+ const args = mockSpawnCli.mock.calls[0][1] as string[];
+ expect(args).not.toContain('service_tier="fast"');
+ expect(args).not.toContain('features.fast_mode=true');
+
+ proc.emit('close', 0);
});
it('does not hard-code Codex Fast schedules to GPT-5.4 when saved eligibility is true', async () => {
diff --git a/test/main/services/team/BoardTaskLogStreamService.test.ts b/test/main/services/team/BoardTaskLogStreamService.test.ts
index 8ea61127..bbb0bfc2 100644
--- a/test/main/services/team/BoardTaskLogStreamService.test.ts
+++ b/test/main/services/team/BoardTaskLogStreamService.test.ts
@@ -272,7 +272,7 @@ describe('BoardTaskLogStreamService', () => {
expect(runtimeFallbackSource.getTaskLogStream).toHaveBeenCalledTimes(1);
});
- it('merges OpenCode runtime stream when board transcript slices mask member execution', async () => {
+ it('merges OpenCode runtime stream using config provider when runtime meta has stale model only', async () => {
const lead = {
role: 'lead' as const,
sessionId: 'session-lead',
@@ -344,10 +344,13 @@ describe('BoardTaskLogStreamService', () => {
getDeletedTasks: vi.fn(async () => []),
};
const membersMetaStore = {
- getMembers: vi.fn(async () => [{ name: 'jack', providerId: 'opencode' }]),
+ getMembers: vi.fn(async () => [{ name: 'jack', role: 'developer', model: 'gpt-5.5' }]),
};
const configReader = {
- getConfig: vi.fn(async () => null),
+ getConfig: vi.fn(async () => ({
+ name: 'demo',
+ members: [{ name: 'jack', providerBackendId: 'opencode-cli', model: 'gpt-5.5' }],
+ })),
};
const buildBundleChunks = vi.fn((messages: ParsedMessage[]) => [{ id: messages[0]?.uuid }]);
diff --git a/test/main/services/team/CodexNativeTaskLogStreamSource.test.ts b/test/main/services/team/CodexNativeTaskLogStreamSource.test.ts
index 4b176309..ba5a2a7f 100644
--- a/test/main/services/team/CodexNativeTaskLogStreamSource.test.ts
+++ b/test/main/services/team/CodexNativeTaskLogStreamSource.test.ts
@@ -2,8 +2,8 @@ import { describe, expect, it, vi } from 'vitest';
import { CodexNativeTaskLogStreamSource } from '../../../../src/main/services/team/taskLogs/stream/CodexNativeTaskLogStreamSource';
-import type { ParsedMessage } from '../../../../src/main/types';
import type { CodexNativeTraceRun } from '../../../../src/main/services/team/taskLogs/stream/CodexNativeTraceReader';
+import type { ParsedMessage } from '../../../../src/main/types';
import type { TeamTask } from '../../../../src/shared/types';
function task(overrides: Partial = {}): TeamTask {
@@ -37,16 +37,27 @@ function message(uuid: string, timestamp: string, toolName: string): ParsedMessa
}
describe('CodexNativeTaskLogStreamSource', () => {
- it('resolves short task refs, verifies Codex owner, and reads full/display/short trace candidates', async () => {
+ it('resolves short task refs and keeps config Codex owner when runtime meta has stale model only', async () => {
const taskReader = {
getTasks: vi.fn(async () => [task()]),
getDeletedTasks: vi.fn(async () => []),
};
const membersMetaStore = {
- getMembers: vi.fn(async () => [{ name: 'atlas', providerId: 'codex' }]),
+ getMembers: vi.fn(async () => [
+ { name: 'atlas', role: 'developer', model: 'opencode/openai/gpt-oss' },
+ ]),
};
const configReader = {
- getConfig: vi.fn(async () => null),
+ getConfig: vi.fn(async () => ({
+ name: 'vector-room-131313',
+ members: [
+ {
+ name: 'atlas',
+ providerBackendId: 'codex-native',
+ model: 'opencode/openai/gpt-oss',
+ },
+ ],
+ })),
};
const traceRuns: CodexNativeTraceRun[] = [
{
@@ -122,7 +133,10 @@ describe('CodexNativeTaskLogStreamSource', () => {
getMembers: vi.fn(async () => [{ name: 'alice', providerId: 'anthropic' }]),
} as never,
{
- getConfig: vi.fn(async () => null),
+ getConfig: vi.fn(async () => ({
+ name: 'vector-room-131313',
+ members: [{ name: 'alice', providerId: 'codex' }],
+ })),
} as never,
traceReader as never
);
diff --git a/test/main/services/team/MemberWorkSyncCodex.live.test.ts b/test/main/services/team/MemberWorkSyncCodex.live.test.ts
index 3c507599..ef82cd3c 100644
--- a/test/main/services/team/MemberWorkSyncCodex.live.test.ts
+++ b/test/main/services/team/MemberWorkSyncCodex.live.test.ts
@@ -8,6 +8,14 @@ import {
createMemberWorkSyncFeature,
type MemberWorkSyncFeatureFacade,
} from '../../../../src/features/member-work-sync/main';
+import {
+ buildCodexTrustedProjectConfigOverrides,
+ buildCodexWorkspaceTrustSettingsArgs,
+ type WorkspaceTrustArgsOnlyPlanRequest,
+ type WorkspaceTrustCoordinator,
+ type WorkspaceTrustLaunchArgPatch,
+ type WorkspaceTrustLaunchArgTargetSurface,
+} from '../../../../src/features/workspace-trust/main';
import {
getTeamsBasePath,
setClaudeBasePathOverride,
@@ -49,6 +57,13 @@ const liveDescribe =
const DEFAULT_ORCHESTRATOR_CLI = '/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source';
const DEFAULT_MODEL = 'gpt-5.4-mini';
const DEFAULT_EFFORT = 'low' as const;
+const LIVE_CODEX_WORKSPACE_TRUST_TARGET_SURFACES: WorkspaceTrustLaunchArgTargetSurface[] = [
+ 'primary_provider_args',
+ 'cross_provider_member_args',
+ 'provider_facts_probe',
+ 'default_model_probe',
+];
+const VITEST_HOME_PREFIX = 'agent-teams-vitest-home-';
liveDescribe('Member work sync Codex live e2e', () => {
let tempDir: string;
@@ -593,6 +608,355 @@ liveDescribe('Member work sync Codex live e2e', () => {
420_000
);
+ it(
+ 'wakes a real Codex teammate when runtime member meta omits provider metadata under noisy metrics',
+ async () => {
+ const orchestratorCli = process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim();
+ expect(orchestratorCli).toBeTruthy();
+ await assertExecutable(orchestratorCli!);
+
+ const model = process.env.MEMBER_WORK_SYNC_CODEX_MODEL?.trim() || DEFAULT_MODEL;
+ const effort = (process.env.MEMBER_WORK_SYNC_CODEX_EFFORT?.trim() ||
+ DEFAULT_EFFORT) as 'low' | 'medium' | 'high' | 'xhigh';
+ const requestedMemberName = 'NickName';
+ const marker = `member-work-sync-codex-runtime-meta-${Date.now()}`;
+ teamName = `member-work-sync-codex-runtime-meta-${Date.now()}`;
+ const projectPath = path.join(tempDir, 'project');
+ await fs.mkdir(projectPath, { recursive: true });
+ await fs.writeFile(
+ path.join(projectPath, 'README.md'),
+ '# Member work sync Codex runtime meta live e2e\n\nKeep this project intentionally tiny.\n',
+ 'utf8'
+ );
+ await trustProjectInTempClaudeGlobalConfig({ claudeRoot: tempClaudeRoot, projectPath });
+ process.env.CLAUDE_CODE_CODEX_NATIVE_IGNORE_USER_CONFIG = 'false';
+ if (ownsCodexHomeDir) {
+ await trustProjectInOwnedCodexHome({ codexHomeDir, projectPath });
+ }
+
+ const [
+ { TeamProvisioningService },
+ { TeamConfigReader },
+ { TeamTaskReader },
+ { TeamTaskWriter },
+ { TeamKanbanManager },
+ { TeamMembersMetaStore },
+ { createCodexAccountFeature },
+ { ProviderConnectionService },
+ ] = await Promise.all([
+ import('../../../../src/main/services/team/TeamProvisioningService'),
+ import('../../../../src/main/services/team/TeamConfigReader'),
+ import('../../../../src/main/services/team/TeamTaskReader'),
+ import('../../../../src/main/services/team/TeamTaskWriter'),
+ import('../../../../src/main/services/team/TeamKanbanManager'),
+ import('../../../../src/main/services/team/TeamMembersMetaStore'),
+ import('../../../../src/features/codex-account/main/composition/createCodexAccountFeature'),
+ import('../../../../src/main/services/runtime/ProviderConnectionService'),
+ ]);
+
+ codexAccountFeature = createCodexAccountFeature({
+ logger: {
+ info: () => undefined,
+ warn: () => undefined,
+ error: () => undefined,
+ },
+ configManager: {
+ getConfig: () => ({
+ providerConnections: {
+ codex: {
+ preferredAuthMode: hasLiveCodexApiKey() ? 'auto' : ('chatgpt' as const),
+ },
+ },
+ }),
+ },
+ });
+ providerConnectionService = ProviderConnectionService.getInstance();
+ providerConnectionService.setCodexAccountFeature(codexAccountFeature);
+
+ const provisioningService = new TeamProvisioningService();
+ provisioningService.setWorkspaceTrustCoordinator(createCodexOnlyWorkspaceTrustCoordinator());
+ svc = provisioningService;
+ const activeService = provisioningService;
+ const taskReader = new TeamTaskReader();
+ const membersMetaStore = new TeamMembersMetaStore();
+ feature = createMemberWorkSyncFeature({
+ teamsBasePath: getTeamsBasePath(),
+ configReader: new TeamConfigReader(),
+ taskReader,
+ kanbanManager: new TeamKanbanManager(),
+ membersMetaStore,
+ isTeamActive: (name) =>
+ activeService.isTeamAlive(name) || activeService.hasProvisioningRun(name),
+ listLifecycleActiveTeamNames: async () => [teamName!],
+ queueQuietWindowMs: 1,
+ resolveControlUrl: async () => controlServer?.baseUrl ?? null,
+ nudgeDeliveryWake: createLiveNudgeDeliveryWake(activeService),
+ });
+ activeService.setTeamChangeEmitter((event: TeamChangeEvent) =>
+ feature!.noteTeamChange(event)
+ );
+ activeService.setRuntimeTurnSettledEnvironmentProvider((input) =>
+ feature!.buildRuntimeTurnSettledEnvironment(input)
+ );
+ controlServer = await startMemberWorkSyncControlServer(feature);
+ process.env.CLAUDE_TEAM_CONTROL_URL = controlServer.baseUrl;
+ activeService.setControlApiBaseUrlResolver(async () => controlServer?.baseUrl ?? null);
+ await fs.writeFile(
+ path.join(tempClaudeRoot, 'team-control-api.json'),
+ JSON.stringify({ baseUrl: controlServer.baseUrl }, null, 2),
+ 'utf8'
+ );
+
+ const progressEvents: TeamProvisioningProgress[] = [];
+ await activeService.createTeam(
+ {
+ teamName,
+ cwd: projectPath,
+ providerId: 'codex',
+ providerBackendId: 'codex-native',
+ model,
+ effort,
+ fastMode: 'off',
+ skipPermissions: true,
+ prompt: [
+ 'Keep launch work minimal.',
+ 'If you receive a member_work_sync_nudge, do not complete the task.',
+ 'For a member_work_sync_nudge, call member_work_sync_status first.',
+ 'Then call member_work_sync_report with state "still_working", the returned agendaFingerprint/reportToken, and taskIds for the current agenda.',
+ `After member_work_sync_report is accepted, add one task comment containing exactly: ${marker}:still-working.`,
+ 'After that stop without a user-visible message.',
+ ].join(' '),
+ members: [
+ {
+ name: requestedMemberName,
+ role: 'developer',
+ providerId: 'codex',
+ providerBackendId: 'codex-native',
+ model,
+ effort,
+ },
+ ],
+ },
+ (progress) => {
+ progressEvents.push(progress);
+ }
+ );
+
+ await waitUntil(async () => {
+ const last = progressEvents.at(-1);
+ if (last?.state === 'failed') {
+ throw new Error(formatProgressDump(progressEvents));
+ }
+ return last?.state === 'ready';
+ }, 240_000);
+
+ const config = await new TeamConfigReader().getConfig(teamName);
+ const memberName = config?.members
+ ?.find((member) => sameMemberName(member.name, requestedMemberName))
+ ?.name?.trim();
+ expect(memberName).toBeTruthy();
+ expect(
+ config?.members?.find((member) => sameMemberName(member.name, memberName!))
+ ).toMatchObject({
+ providerId: 'codex',
+ });
+
+ await stripMemberProviderMetadataFromMembersMeta({
+ teamName,
+ memberName: memberName!,
+ fallbackRole: 'developer',
+ });
+ expect(
+ (await membersMetaStore.getMembers(teamName)).find((member) =>
+ sameMemberName(member.name, memberName!)
+ )
+ ).toMatchObject({
+ name: memberName,
+ providerId: undefined,
+ providerBackendId: undefined,
+ model: undefined,
+ effort: undefined,
+ });
+ await waitUntil(async () => {
+ await feature!.drainRuntimeTurnSettledEvents();
+ const diagnostics = feature!.getQueueDiagnostics();
+ return diagnostics.queued === 0 && diagnostics.running === 0;
+ }, 60_000, 1_000, async () =>
+ formatMemberWorkSyncDiagnostics({
+ feature: feature!,
+ teamName: teamName!,
+ memberName: memberName!,
+ })
+ );
+
+ const createdAt = new Date().toISOString();
+ const taskId = `runtime-meta-${Date.now()}`;
+ const displayId = String(Date.now()).slice(-8);
+ await new TeamTaskWriter().createTask(teamName, {
+ id: taskId,
+ displayId,
+ subject: `Member work sync live runtime meta ${marker}`,
+ description: 'Verify native stale recovery when runtime member meta lacks provider fields.',
+ owner: memberName!,
+ createdBy: 'user',
+ status: 'in_progress',
+ projectPath,
+ createdAt,
+ updatedAt: createdAt,
+ });
+ feature.noteTeamChange({ type: 'task', teamName, taskId });
+
+ let agendaFingerprint = '';
+ await waitUntil(async () => {
+ const status = await feature!.refreshStatus({ teamName: teamName!, memberName: memberName! });
+ if (!status.agenda.items.some((item) => item.taskId === taskId)) {
+ return false;
+ }
+ expect(status).toMatchObject({
+ state: 'needs_sync',
+ providerId: 'codex',
+ diagnostics: expect.arrayContaining(['no_current_report']),
+ });
+ expect(status.agenda.items).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ taskId,
+ reason: 'owned_in_progress_task',
+ evidence: expect.objectContaining({ status: 'in_progress' }),
+ }),
+ ])
+ );
+ agendaFingerprint = status.agenda.fingerprint;
+ return true;
+ }, 60_000, 500, async () =>
+ formatMemberWorkSyncDiagnostics({
+ feature: feature!,
+ teamName: teamName!,
+ memberName: memberName!,
+ taskId,
+ })
+ );
+ await waitUntil(async () => {
+ const diagnostics = feature!.getQueueDiagnostics();
+ return diagnostics.queued === 0 && diagnostics.running === 0;
+ }, 30_000, 500, async () =>
+ formatMemberWorkSyncDiagnostics({
+ feature: feature!,
+ teamName: teamName!,
+ memberName: memberName!,
+ taskId,
+ })
+ );
+ const stableStatus = await feature.refreshStatus({
+ teamName,
+ memberName: memberName!,
+ });
+ expect(stableStatus.providerId).toBe('codex');
+ expect(stableStatus.agenda.fingerprint).toBe(agendaFingerprint);
+ expect(
+ (await readInboxMessages(teamName, memberName!)).filter(
+ (message) => message.messageKind === 'member_work_sync_nudge'
+ )
+ ).toHaveLength(0);
+
+ await seedNativeStaleBlockingMetrics({
+ teamName,
+ memberName: memberName!,
+ agendaFingerprint,
+ });
+ feature.noteTeamChange({ type: 'task', teamName, taskId });
+
+ await waitUntil(async () => {
+ const diagnostics = feature!.getQueueDiagnostics();
+ return diagnostics.queued === 0 && diagnostics.running === 0;
+ }, 30_000, 500, async () =>
+ formatMemberWorkSyncDiagnostics({
+ feature: feature!,
+ teamName: teamName!,
+ memberName: memberName!,
+ taskId,
+ })
+ );
+ expect((await feature.getStatus({ teamName, memberName: memberName! })).providerId).toBe(
+ 'codex'
+ );
+
+ await waitUntil(async () => {
+ const nudges = (await readInboxMessages(teamName!, memberName!)).filter(
+ (message) => message.messageKind === 'member_work_sync_nudge'
+ );
+ return nudges.length === 1;
+ }, 60_000, 1_000, async () =>
+ formatMemberWorkSyncDiagnostics({
+ feature: feature!,
+ teamName: teamName!,
+ memberName: memberName!,
+ taskId,
+ })
+ );
+
+ const metrics = await feature.getMetrics({ teamName });
+ expect(metrics.phase2Readiness.reasons).toContain('would_nudge_rate_high');
+ const journalPath = path.join(
+ getTeamsBasePath(),
+ teamName,
+ 'members',
+ memberName!,
+ '.member-work-sync',
+ 'journal.jsonl'
+ );
+ const journal = await fs.readFile(journalPath, 'utf8');
+ const nudgeOutcomes = journal
+ .trim()
+ .split('\n')
+ .map((line) => JSON.parse(line) as { event?: string; reason?: string })
+ .filter((event) => event.event === 'nudge_skipped' || event.event === 'nudge_delivered');
+ expect(nudgeOutcomes).toContainEqual(expect.objectContaining({ event: 'nudge_delivered' }));
+ expect(nudgeOutcomes.at(-1)).toMatchObject({ event: 'nudge_delivered' });
+
+ await relayInboxIfNotAlreadyConsumed(activeService, memberName!);
+
+ await waitUntil(async () => {
+ const fatalRuntimeMessage = await readFatalRuntimeMessage(teamName!);
+ if (fatalRuntimeMessage) {
+ throw new FatalWaitError(fatalRuntimeMessage);
+ }
+ await feature!.replayPendingReports([teamName!]);
+ const status = await feature!.getStatus({ teamName: teamName!, memberName: memberName! });
+ return status.report?.accepted === true && status.report.state === 'still_working';
+ }, 240_000, 2_000, async () =>
+ formatMemberWorkSyncDiagnostics({
+ feature: feature!,
+ teamName: teamName!,
+ memberName: memberName!,
+ taskId,
+ })
+ );
+
+ const finalStatus = await feature.getStatus({ teamName, memberName: memberName! });
+ expect(finalStatus.state).toBe('still_working');
+ expect(finalStatus.report).toMatchObject({
+ accepted: true,
+ state: 'still_working',
+ });
+ await waitUntil(async () => {
+ await feature!.drainRuntimeTurnSettledEvents();
+ const metas = await readRuntimeTurnSettledProcessedMetas(getTeamsBasePath());
+ return metas.some(
+ ({ meta }) =>
+ (meta.event as { provider?: unknown; teamName?: unknown } | undefined)?.provider ===
+ 'codex' &&
+ (meta.event as { provider?: unknown; teamName?: unknown } | undefined)?.teamName ===
+ teamName
+ );
+ }, 60_000);
+ await expect(feature.dispatchDueNudges([teamName])).resolves.toMatchObject({
+ delivered: 0,
+ });
+ },
+ 480_000
+ );
+
it(
'lets a real Codex teammate complete the task and report caught-up after the board clears',
async () => {
@@ -852,6 +1216,173 @@ function resolveConnectedCodexHome(previousCodexHome: string | undefined): strin
return path.join(os.userInfo().homedir, '.codex');
}
+async function trustProjectInOwnedCodexHome(input: {
+ codexHomeDir: string;
+ projectPath: string;
+}): Promise {
+ const [override] = buildCodexTrustedProjectConfigOverrides([input.projectPath], {
+ maxOverrides: 1,
+ });
+ if (!override) {
+ return;
+ }
+ await fs.mkdir(input.codexHomeDir, { recursive: true });
+ await fs.appendFile(path.join(input.codexHomeDir, 'config.toml'), `\n${override}\n`, 'utf8');
+}
+
+async function trustProjectInTempClaudeGlobalConfig(input: {
+ claudeRoot: string;
+ projectPath: string;
+}): Promise {
+ const projectRealPath = await fs.realpath(input.projectPath).catch(() => input.projectPath);
+ const projects = Object.fromEntries(
+ [...new Set([input.projectPath, projectRealPath])].map((projectPath) => [
+ projectPath,
+ {
+ allowedTools: [],
+ mcpContextUris: [],
+ mcpServers: {},
+ enabledMcpjsonServers: [],
+ disabledMcpjsonServers: [],
+ projectOnboardingSeenCount: 0,
+ hasClaudeMdExternalIncludesApproved: false,
+ hasClaudeMdExternalIncludesWarningShown: false,
+ hasTrustDialogAccepted: true,
+ },
+ ])
+ );
+ const configPaths = [path.join(input.claudeRoot, '.claude.json')];
+ const homeDir = process.env.HOME?.trim();
+ if (homeDir && path.basename(homeDir).startsWith(VITEST_HOME_PREFIX)) {
+ configPaths.push(path.join(homeDir, '.claude.json'));
+ }
+
+ for (const configPath of configPaths) {
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
+ await fs.writeFile(configPath, `${JSON.stringify({ projects }, null, 2)}\n`, 'utf8');
+ }
+}
+
+function createCodexOnlyWorkspaceTrustCoordinator(): WorkspaceTrustCoordinator {
+ return {
+ async planArgsOnly(request) {
+ return { launchArgPatches: buildLiveCodexWorkspaceTrustPatches(request) };
+ },
+ async planFull(request) {
+ return {
+ workspaces: request.workspaces,
+ launchArgPatches: buildLiveCodexWorkspaceTrustPatches(request),
+ };
+ },
+ async execute(plan) {
+ return {
+ id: 'member-work-sync-codex-live-workspace-trust',
+ provider: 'claude',
+ status: 'skipped',
+ workspaceIds: plan.workspaces.map((workspace) => workspace.id),
+ evidence: ['live test injects Codex native trusted-project settings'],
+ };
+ },
+ };
+}
+
+function buildLiveCodexWorkspaceTrustPatches(
+ request: WorkspaceTrustArgsOnlyPlanRequest
+): WorkspaceTrustLaunchArgPatch[] {
+ if (
+ !request.featureFlags.enabled ||
+ !request.featureFlags.codexArgs ||
+ !request.providers.includes('codex')
+ ) {
+ return [];
+ }
+
+ const configKeys = request.workspaces.flatMap((workspace) => [
+ workspace.configKeyCwd,
+ workspace.realCwd,
+ ...(workspace.gitRootConfigKey ? [workspace.gitRootConfigKey] : []),
+ ]);
+ const overrides = buildCodexTrustedProjectConfigOverrides(configKeys);
+ const args = buildCodexWorkspaceTrustSettingsArgs(overrides);
+ if (args.length === 0) {
+ return [];
+ }
+
+ const workspaceIds = request.workspaces.map((workspace) => workspace.id);
+ return (request.targetSurfaces ?? LIVE_CODEX_WORKSPACE_TRUST_TARGET_SURFACES).map((surface) => ({
+ id: `member-work-sync-codex-live-workspace-trust:${surface}`,
+ owner: 'workspace-trust',
+ targetProvider: 'codex',
+ targetSurface: surface,
+ dialect: 'claude-codex-runtime-settings',
+ args,
+ dedupeKey: `member-work-sync-codex-live-workspace-trust:${surface}:${overrides.join('|')}`,
+ sourceWorkspaceIds: workspaceIds,
+ reason: 'Trust the live e2e project for Codex native headless teammate startup.',
+ }));
+}
+
+function sameMemberName(left: string | undefined, right: string | undefined): boolean {
+ return left?.trim().toLowerCase() === right?.trim().toLowerCase();
+}
+
+async function stripMemberProviderMetadataFromMembersMeta(input: {
+ teamName: string;
+ memberName: string;
+ fallbackRole: string;
+}): Promise {
+ const metaPath = path.join(getTeamsBasePath(), input.teamName, 'members.meta.json');
+ const raw = await fs.readFile(metaPath, 'utf8').catch(() => '{"version":1,"members":[]}');
+ const parsed = JSON.parse(raw) as { providerBackendId?: unknown; members?: unknown };
+ const sourceMembers = Array.isArray(parsed.members) ? parsed.members : [];
+ let found = false;
+ const members = sourceMembers.flatMap((member): Record[] => {
+ if (!member || typeof member !== 'object') {
+ return [];
+ }
+ const source = member as Record;
+ const name = typeof source.name === 'string' ? source.name.trim() : '';
+ if (!name) {
+ return [];
+ }
+ if (!sameMemberName(name, input.memberName)) {
+ return [source];
+ }
+
+ found = true;
+ const stripped: Record = { name };
+ for (const key of ['role', 'workflow', 'isolation', 'agentType', 'color', 'agentId', 'cwd']) {
+ if (typeof source[key] === 'string' && source[key].trim()) {
+ stripped[key] = source[key];
+ }
+ }
+ for (const key of ['joinedAt', 'removedAt']) {
+ if (typeof source[key] === 'number') {
+ stripped[key] = source[key];
+ }
+ }
+ return [stripped];
+ });
+
+ if (!found) {
+ members.push({
+ name: input.memberName,
+ role: input.fallbackRole,
+ agentType: 'general-purpose',
+ joinedAt: Date.now(),
+ });
+ }
+
+ const payload = {
+ version: 1,
+ ...(typeof parsed.providerBackendId === 'string'
+ ? { providerBackendId: parsed.providerBackendId }
+ : {}),
+ members,
+ };
+ await fs.writeFile(metaPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
+}
+
async function seedShadowReadyMetrics(input: {
teamName: string;
memberName: string;
@@ -899,6 +1430,68 @@ async function seedShadowReadyMetrics(input: {
);
}
+async function seedNativeStaleBlockingMetrics(input: {
+ teamName: string;
+ memberName: string;
+ agendaFingerprint: string;
+}): Promise {
+ const metricsPath = path.join(
+ getTeamsBasePath(),
+ input.teamName,
+ '.member-work-sync',
+ 'indexes',
+ 'metrics.json'
+ );
+ const nowMs = Date.now();
+ const staleObservedAt = new Date(nowMs - 6 * 60_000 - 1_000).toISOString();
+ await fs.mkdir(path.dirname(metricsPath), { recursive: true });
+ await fs.writeFile(
+ metricsPath,
+ `${JSON.stringify(
+ {
+ schemaVersion: 2,
+ members: {
+ [input.memberName]: {
+ memberName: input.memberName,
+ state: 'needs_sync',
+ agendaFingerprint: input.agendaFingerprint,
+ actionableCount: 1,
+ evaluatedAt: staleObservedAt,
+ providerId: 'codex',
+ },
+ },
+ recentEvents: [
+ {
+ id: 'native-stale-status',
+ teamName: input.teamName,
+ memberName: input.memberName,
+ kind: 'status_evaluated',
+ state: 'needs_sync',
+ agendaFingerprint: input.agendaFingerprint,
+ recordedAt: staleObservedAt,
+ actionableCount: 1,
+ providerId: 'codex',
+ },
+ ...Array.from({ length: 12 }, (_, index) => ({
+ id: `native-stale-would-nudge-${index}`,
+ teamName: input.teamName,
+ memberName: input.memberName,
+ kind: 'would_nudge',
+ state: 'needs_sync',
+ agendaFingerprint: input.agendaFingerprint,
+ recordedAt: new Date(nowMs - 5 * 60_000 + index * 5_000).toISOString(),
+ actionableCount: 1,
+ providerId: 'codex',
+ })),
+ ],
+ },
+ null,
+ 2
+ )}\n`,
+ 'utf8'
+ );
+}
+
async function readInboxMessages(teamName: string, memberName: string): Promise<
Array<{
messageId?: string;
diff --git a/test/main/services/team/OpenCodeAgendaSyncRecovery.safe-e2e.test.ts b/test/main/services/team/OpenCodeAgendaSyncRecovery.safe-e2e.test.ts
index 608da7bf..70b1c3c0 100644
--- a/test/main/services/team/OpenCodeAgendaSyncRecovery.safe-e2e.test.ts
+++ b/test/main/services/team/OpenCodeAgendaSyncRecovery.safe-e2e.test.ts
@@ -13,7 +13,9 @@ import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, describe, expect, it, vi } from 'vitest';
+import type { Mock } from 'vitest';
+import type { MemberWorkSyncNudgeDeliveryWakePort } from '@features/member-work-sync/core/application/ports';
import type { InboxMessage, TaskRef } from '@shared/types/team';
const tempRoots: string[] = [];
@@ -291,12 +293,22 @@ function buildProofMissingRecord(input: {
};
}
+type TestNudgeDeliveryWake = MemberWorkSyncNudgeDeliveryWakePort & {
+ schedule: Mock;
+};
+
+function createNudgeDeliveryWake(): TestNudgeDeliveryWake {
+ return {
+ schedule: vi.fn(async () => undefined),
+ };
+}
+
function createFeature(input: {
teamsBasePath: string;
teamName: string;
memberName: string;
service: TeamProvisioningService;
- nudgeDeliveryWake: { schedule: ReturnType };
+ nudgeDeliveryWake: TestNudgeDeliveryWake;
providerId?: 'opencode' | 'codex';
}) {
const providerId = input.providerId ?? 'opencode';
@@ -351,7 +363,7 @@ describe('OpenCode agenda-sync proof-missing recovery safe e2e', () => {
const teamName = 'team-codex-agenda-sync-nudge';
const memberName = 'bob';
const service = new TeamProvisioningService();
- const nudgeDeliveryWake = { schedule: vi.fn(async () => undefined) };
+ const nudgeDeliveryWake = createNudgeDeliveryWake();
const feature = createFeature({
teamsBasePath,
teamName,
@@ -415,7 +427,7 @@ describe('OpenCode agenda-sync proof-missing recovery safe e2e', () => {
const taskRef: TaskRef = { teamName, taskId: 'task-1', displayId: '11111111' };
const foregroundMessageId = 'proof-missing-message-1';
const service = new TeamProvisioningService();
- const nudgeDeliveryWake = { schedule: vi.fn(async () => undefined) };
+ const nudgeDeliveryWake = createNudgeDeliveryWake();
const feature = createFeature({
teamsBasePath,
teamName,
@@ -499,7 +511,7 @@ describe('OpenCode agenda-sync proof-missing recovery safe e2e', () => {
const laneId = 'secondary:opencode:jack';
const taskRef: TaskRef = { teamName, taskId: 'task-1', displayId: '11111111' };
const service = new TeamProvisioningService();
- const nudgeDeliveryWake = { schedule: vi.fn(async () => undefined) };
+ const nudgeDeliveryWake = createNudgeDeliveryWake();
const feature = createFeature({
teamsBasePath,
teamName,
diff --git a/test/main/services/team/OpenCodeManagedHostProcessCleanup.test.ts b/test/main/services/team/OpenCodeManagedHostProcessCleanup.test.ts
index 803d33cf..61e860c0 100644
--- a/test/main/services/team/OpenCodeManagedHostProcessCleanup.test.ts
+++ b/test/main/services/team/OpenCodeManagedHostProcessCleanup.test.ts
@@ -1,5 +1,3 @@
-import { describe, expect, it, vi } from 'vitest';
-
import {
cleanupManagedOpenCodeServeProcesses,
getOpenCodeServeLoopbackBaseUrl,
@@ -7,6 +5,7 @@ import {
isManagedOpenCodeServeProcessDetails,
isOpenCodeServeCommand,
} from '@main/services/team/opencode/bridge/OpenCodeManagedHostProcessCleanup';
+import { describe, expect, it, vi } from 'vitest';
const MANAGED_DETAILS = [
'/opt/homebrew/bin/opencode serve --hostname 127.0.0.1 --port 54171',
@@ -27,6 +26,16 @@ const MANAGED_DETAILS_WITH_WORKSPACE_MCP = [
'OPENCODE_CONFIG_CONTENT={}',
'AGENT_TEAMS_MCP_CLAUDE_DIR=/tmp/claude',
].join(' ');
+const MANAGED_DETAILS_WITH_INLINE_OPENCODE_CONFIG_MCP = [
+ '/opt/homebrew/bin/opencode serve --hostname 127.0.0.1 --port 54171',
+ 'CLAUDE_MULTIMODEL_DATA_HOME=/tmp/agent-teams-runtime',
+ 'OPENCODE_CONFIG_CONTENT={"mcp":{"agent-teams":{"type":"local","command":["node","mcp-server/dist/index.js"],"environment":{"AGENT_TEAMS_MCP_CLAUDE_DIR":"/tmp/claude"},"enabled":true}}}',
+].join(' ');
+const MANAGED_DETAILS_WITH_INLINE_OPENCODE_AGENT_PERMISSIONS = [
+ '/opt/homebrew/bin/opencode serve --hostname 127.0.0.1 --port 54171',
+ 'CLAUDE_MULTIMODEL_DATA_HOME=/tmp/agent-teams-runtime',
+ 'OPENCODE_CONFIG_CONTENT={"agent":{"teammate":{"description":"Managed teammate agent for claude-multimodel runtime orchestration.","permission":{"agent-teams_*":"allow","mcp__agent-teams__*":"allow"}}}}',
+].join(' ');
function resolved(value: T): Promise {
return Promise.resolve(value);
@@ -63,6 +72,12 @@ describe('OpenCodeManagedHostProcessCleanup', () => {
expect(isManagedOpenCodeServeProcessDetails(MANAGED_DETAILS)).toBe(true);
expect(isManagedOpenCodeServeProcessDetails(MANAGED_DETAILS_WITH_REMOTE_MCP)).toBe(true);
expect(isManagedOpenCodeServeProcessDetails(MANAGED_DETAILS_WITH_WORKSPACE_MCP)).toBe(true);
+ expect(isManagedOpenCodeServeProcessDetails(MANAGED_DETAILS_WITH_INLINE_OPENCODE_CONFIG_MCP)).toBe(
+ true
+ );
+ expect(
+ isManagedOpenCodeServeProcessDetails(MANAGED_DETAILS_WITH_INLINE_OPENCODE_AGENT_PERMISSIONS)
+ ).toBe(true);
expect(
isManagedOpenCodeServeProcessDetails(
'opencode serve CLAUDE_MULTIMODEL_DATA_HOME=/tmp OPENCODE_CONFIG_CONTENT={}'
@@ -78,6 +93,16 @@ describe('OpenCodeManagedHostProcessCleanup', () => {
'opencode serve NOT_CLAUDE_MULTIMODEL_DATA_HOME=/tmp OPENCODE_CONFIG_CONTENT={} AGENT_TEAMS_MCP_CLAUDE_DIR=/tmp/claude'
)
).toBe(false);
+ expect(
+ isManagedOpenCodeServeProcessDetails(
+ 'opencode serve OPENCODE_CONFIG_CONTENT={"mcp":{"agent-teams":{"enabled":true}}}'
+ )
+ ).toBe(false);
+ expect(
+ isManagedOpenCodeServeProcessDetails(
+ 'opencode serve OPENCODE_CONFIG_CONTENT={"agent":{"teammate":{"permission":{"agent-teams_*":"allow"}}}}'
+ )
+ ).toBe(false);
});
it('extracts only loopback OpenCode serve base URLs for disposal', () => {
diff --git a/test/main/services/team/OpenCodeProductionPromptArtifacts.safe-e2e.test.ts b/test/main/services/team/OpenCodeProductionPromptArtifacts.safe-e2e.test.ts
index cc7c7b4b..0bef6e24 100644
--- a/test/main/services/team/OpenCodeProductionPromptArtifacts.safe-e2e.test.ts
+++ b/test/main/services/team/OpenCodeProductionPromptArtifacts.safe-e2e.test.ts
@@ -63,7 +63,7 @@ describe('OpenCode production prompt artifacts safe e2e', () => {
const launchInput = captureAdapter.launchInputs[0];
expect(launchInput).toBeDefined();
expect(launchInput?.prompt ?? '').toContain('production desktop app');
- expect(launchInput?.expectedMembers.map((member) => member.name)).toEqual(['bob', 'jack']);
+ expect(launchInput?.expectedMembers.map((member) => member.name)).toEqual(['team-lead', 'bob', 'jack']);
expect(launchInput?.prompt?.length ?? 0).toBeGreaterThan(1_500);
const bridgeCapture = createCapturingOpenCodeBridge(selectedModel);
@@ -78,7 +78,7 @@ describe('OpenCode production prompt artifacts safe e2e', () => {
expect(launchCommand?.leadPrompt).toContain('OpenCode members bootstrap silently');
expect(launchCommand?.leadPrompt.length ?? 0).toBeGreaterThan(1_500);
expect(launchCommand?.leadPrompt.length ?? 0).toBeLessThan(80_000);
- expect(launchCommand?.members.map((member) => member.name)).toEqual(['bob', 'jack']);
+ expect(launchCommand?.members.map((member) => member.name)).toEqual(['team-lead', 'bob', 'jack']);
for (const member of launchCommand?.members ?? []) {
expect(member.prompt).toContain(`You are ${member.name}`);
diff --git a/test/main/services/team/OpenCodeSemanticMessaging.live.test.ts b/test/main/services/team/OpenCodeSemanticMessaging.live.test.ts
index a1e09ef5..740cfd4f 100644
--- a/test/main/services/team/OpenCodeSemanticMessaging.live.test.ts
+++ b/test/main/services/team/OpenCodeSemanticMessaging.live.test.ts
@@ -1,11 +1,12 @@
import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { readOpenCodeRuntimeLaneIndex } from '../../../../src/main/services/team/opencode/store/OpenCodeRuntimeManifestEvidenceReader';
+import { TeamInboxWriter } from '../../../../src/main/services/team/TeamInboxWriter';
import { getTeamsBasePath, setClaudeBasePathOverride } from '../../../../src/main/utils/pathDecoder';
+
import {
createOpenCodeLiveHarness,
getRuntimeTranscript,
@@ -162,6 +163,139 @@ liveDescribe('OpenCode semantic messaging live e2e', () => {
300_000
);
+ it(
+ 'relays a desktop inbox message to the OpenCode lead session and records the lead reply',
+ async () => {
+ const { bridgeClient, selectedModel, svc, dispose } = await createOpenCodeLiveHarness({
+ tempDir,
+ selectedModel: process.env.OPENCODE_E2E_MODEL?.trim() || DEFAULT_MODEL,
+ projectPath: PROJECT_PATH,
+ });
+
+ const teamName = `opencode-lead-message-${Date.now()}`;
+ const leadName = 'team-lead';
+ const memberName = 'bob';
+ const expectedReply = `opencode-lead-message-e2e-${Date.now()}`;
+ const progressEvents: TeamProvisioningProgress[] = [];
+
+ try {
+ const { runId } = await svc.createTeam(
+ {
+ teamName,
+ cwd: PROJECT_PATH,
+ providerId: 'opencode',
+ model: selectedModel,
+ skipPermissions: true,
+ members: [
+ {
+ name: memberName,
+ role: 'Developer',
+ providerId: 'opencode',
+ model: selectedModel,
+ },
+ ],
+ },
+ (progress) => {
+ progressEvents.push(progress);
+ }
+ );
+
+ expect(runId).toBeTruthy();
+ const progressDump = progressEvents
+ .map((progress) =>
+ [
+ progress.state,
+ progress.message,
+ progress.messageSeverity,
+ progress.error,
+ progress.cliLogsTail,
+ ]
+ .filter(Boolean)
+ .join(' | ')
+ )
+ .join('\n');
+ expect(
+ progressEvents.some((progress) =>
+ progress.message.includes('OpenCode team launch is ready')
+ ),
+ progressDump
+ ).toBe(true);
+
+ const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName);
+ expect(runtimeSnapshot.members[leadName]).toMatchObject({
+ alive: true,
+ runtimeModel: selectedModel,
+ });
+
+ const written = await new TeamInboxWriter().sendMessage(teamName, {
+ member: leadName,
+ from: 'user',
+ to: leadName,
+ source: 'user_sent',
+ text: [
+ `Reply to the app Messages UI with exactly: ${expectedReply}`,
+ `Use agent-teams_message_send with to="user" and from="${leadName}".`,
+ 'Do not answer only as plain assistant text.',
+ ].join('\n'),
+ });
+
+ let lastRelay: Awaited> | null = null;
+ const deadline = Date.now() + 90_000;
+ while (Date.now() < deadline) {
+ lastRelay = await svc.relayInboxFileToLiveRecipient(teamName, leadName, {
+ onlyMessageId: written.messageId,
+ source: 'ui-send',
+ deliveryMetadata: { replyRecipient: 'user' },
+ });
+ if (lastRelay.relayed >= 1) {
+ break;
+ }
+ if (
+ lastRelay.lastDelivery?.delivered === false &&
+ lastRelay.lastDelivery.responsePending !== true
+ ) {
+ break;
+ }
+ await new Promise((resolve) => setTimeout(resolve, 3_000));
+ }
+
+ expect(lastRelay).toMatchObject({
+ kind: 'opencode_member',
+ relayed: 1,
+ });
+
+ let reply: InboxMessage;
+ try {
+ reply = await waitForUserInboxReply(teamName, leadName, expectedReply, 90_000);
+ } catch (error) {
+ const transcript = await getRuntimeTranscript({
+ bridgeClient,
+ teamName,
+ memberName: leadName,
+ projectPath: PROJECT_PATH,
+ });
+ throw new Error(
+ `${error instanceof Error ? error.message : String(error)}\nLast relay: ${JSON.stringify(
+ lastRelay,
+ null,
+ 2
+ )}\nTranscript: ${JSON.stringify(transcript, null, 2)}`
+ );
+ }
+ expect(reply).toMatchObject({
+ from: leadName,
+ to: 'user',
+ });
+ expect(reply.text).toContain(expectedReply);
+ } finally {
+ await svc.stopTeam(teamName).catch(() => undefined);
+ await dispose();
+ await waitForOpenCodeLanesStopped(teamName);
+ }
+ },
+ 300_000
+ );
+
it(
'relays an OpenCode teammate message into another OpenCode member runtime and records the reply',
async () => {
diff --git a/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts b/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts
index c3ce08e2..cb5bf410 100644
--- a/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts
+++ b/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts
@@ -202,6 +202,75 @@ describe('OpenCodeTeamRuntimeAdapter', () => {
);
});
+ it('builds a lead-specific OpenCode bootstrap prompt for team-lead sessions', async () => {
+ const launchOpenCodeTeam = vi.fn<
+ NonNullable
+ >(async () => ({
+ runId: 'run-1',
+ teamLaunchState: 'ready',
+ members: {
+ 'team-lead': {
+ sessionId: 'oc-lead-session',
+ launchState: 'confirmed_alive',
+ runtimePid: 123,
+ model: 'openai/gpt-5.4-mini',
+ evidence: [
+ { kind: 'required_tools_proven', observedAt: '2026-04-21T00:00:00.000Z' },
+ { kind: 'delivery_ready', observedAt: '2026-04-21T00:00:00.000Z' },
+ { kind: 'member_ready', observedAt: '2026-04-21T00:00:00.000Z' },
+ { kind: 'run_ready', observedAt: '2026-04-21T00:00:00.000Z' },
+ ],
+ },
+ alice: {
+ sessionId: 'oc-alice-session',
+ launchState: 'confirmed_alive',
+ runtimePid: 124,
+ model: 'openai/gpt-5.4-mini',
+ evidence: [
+ { kind: 'required_tools_proven', observedAt: '2026-04-21T00:00:00.000Z' },
+ { kind: 'delivery_ready', observedAt: '2026-04-21T00:00:00.000Z' },
+ { kind: 'member_ready', observedAt: '2026-04-21T00:00:00.000Z' },
+ { kind: 'run_ready', observedAt: '2026-04-21T00:00:00.000Z' },
+ ],
+ },
+ },
+ warnings: [],
+ diagnostics: [],
+ }));
+ const bridge = bridgePort(readiness({ state: 'ready', launchAllowed: true }), {
+ getLastOpenCodeRuntimeSnapshot: vi.fn(() => runtimeSnapshot('cap-lead')),
+ launchOpenCodeTeam,
+ });
+ const adapter = new OpenCodeTeamRuntimeAdapter(bridge);
+
+ await adapter.launch(
+ launchInput({
+ expectedMembers: [
+ {
+ name: 'team-lead',
+ role: 'Team Lead',
+ providerId: 'opencode',
+ model: 'openai/gpt-5.4-mini',
+ cwd: '/repo',
+ },
+ {
+ name: 'alice',
+ providerId: 'opencode',
+ model: 'openai/gpt-5.4-mini',
+ cwd: '/repo',
+ },
+ ],
+ })
+ );
+
+ const command = launchOpenCodeTeam.mock.calls[0]?.[0];
+ const leadPrompt = command?.members.find((member) => member.name === 'team-lead')?.prompt;
+ expect(leadPrompt).toContain('You are team-lead, the team lead');
+ expect(leadPrompt).toContain('message the human user or a teammate');
+ expect(leadPrompt).toContain('Always set from="team-lead"');
+ expect(leadPrompt).not.toContain('human user, team lead, or another teammate');
+ });
+
it('retries transient MCP readiness transport failures before prepare succeeds', async () => {
const firstReadiness = readiness({
state: 'mcp_unavailable',
diff --git a/test/main/services/team/TaskChangeLedgerReader.test.ts b/test/main/services/team/TaskChangeLedgerReader.test.ts
index 11b494cd..46d71530 100644
--- a/test/main/services/team/TaskChangeLedgerReader.test.ts
+++ b/test/main/services/team/TaskChangeLedgerReader.test.ts
@@ -1,12 +1,10 @@
+import { TaskChangeLedgerReader } from '@main/services/team/TaskChangeLedgerReader';
import { createHash } from 'crypto';
+import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises';
import * as os from 'os';
import * as path from 'path';
import { afterEach, describe, expect, it } from 'vitest';
-import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises';
-
-import { TaskChangeLedgerReader } from '@main/services/team/TaskChangeLedgerReader';
-
const TASK_ID = 'task-1';
function safeTaskIdSegment(taskId: string): string {
@@ -363,6 +361,130 @@ describe('TaskChangeLedgerReader', () => {
expect(snippets[0]?.ledger?.modifiedFullContent).toBe(afterContent);
});
+ it('hides suppressed OpenCode journal imports without hiding legitimate same-file imports', async () => {
+ tmpDir = await fsTempDir();
+ const eventsDir = path.join(tmpDir, '.board-task-changes', 'events');
+ const blobsDir = path.join(tmpDir, '.board-task-changes', 'blobs');
+ await mkdir(eventsDir, { recursive: true });
+ await mkdir(blobsDir, { recursive: true });
+
+ const beforeContent = 'export const value = 1;\n';
+ const staleAfterContent = 'export const value = "ambient";\n';
+ const legitAfterContent = 'export const value = 2;\n';
+ await writeFile(path.join(blobsDir, 'before.txt'), beforeContent, 'utf8');
+ await writeFile(path.join(blobsDir, 'stale-after.txt'), staleAfterContent, 'utf8');
+ await writeFile(path.join(blobsDir, 'legit-after.txt'), legitAfterContent, 'utf8');
+
+ const staleSourceImportKey = 'opencode\0session-1\0part-stale\0src/file.ts';
+ const legitSourceImportKey = 'opencode\0session-1\0part-legit\0src/file.ts';
+ const baseEvent = {
+ schemaVersion: 1,
+ taskId: TASK_ID,
+ taskRef: TASK_ID,
+ taskRefKind: 'canonical',
+ phase: 'work',
+ executionSeq: 1,
+ sessionId: 'opencode-session-1',
+ memberName: 'bob',
+ source: 'opencode_toolpart_edit',
+ operation: 'modify',
+ confidence: 'high',
+ workspaceRoot: '/repo',
+ filePath: '/repo/src/file.ts',
+ relativePath: 'src/file.ts',
+ timestamp: '2026-03-01T10:00:00.000Z',
+ toolStatus: 'succeeded',
+ sourceRuntime: 'opencode',
+ sourceProvider: 'opencode',
+ evidenceProof: 'opencode-snapshot',
+ beforeState: { exists: true, sha256: sha(beforeContent), sizeBytes: beforeContent.length },
+ };
+ await writeFile(
+ path.join(eventsDir, `${encodeURIComponent(TASK_ID)}.jsonl`),
+ [
+ {
+ ...baseEvent,
+ eventId: 'event-stale',
+ toolUseId: 'part-stale',
+ sourceImportKey: staleSourceImportKey,
+ before: { sha256: sha(beforeContent), sizeBytes: beforeContent.length, blobRef: 'before.txt' },
+ after: {
+ sha256: sha(staleAfterContent),
+ sizeBytes: staleAfterContent.length,
+ blobRef: 'stale-after.txt',
+ },
+ afterState: {
+ exists: true,
+ sha256: sha(staleAfterContent),
+ sizeBytes: staleAfterContent.length,
+ },
+ linesAdded: 1,
+ linesRemoved: 1,
+ },
+ {
+ ...baseEvent,
+ eventId: 'event-stale-suppressed',
+ toolUseId: 'opencode-snapshot-only-suppression',
+ sourceImportKey: staleSourceImportKey,
+ before: null,
+ after: null,
+ afterState: {
+ exists: true,
+ sha256: sha(staleAfterContent),
+ sizeBytes: staleAfterContent.length,
+ },
+ linesAdded: 0,
+ linesRemoved: 0,
+ suppressed: true,
+ suppressionReason: 'snapshot-only evidence does not prove file authorship',
+ suppressedAt: '2026-03-01T10:01:00.000Z',
+ supersedesEventId: 'event-stale',
+ },
+ {
+ ...baseEvent,
+ eventId: 'event-legit',
+ toolUseId: 'part-legit',
+ sourceImportKey: legitSourceImportKey,
+ before: { sha256: sha(beforeContent), sizeBytes: beforeContent.length, blobRef: 'before.txt' },
+ after: {
+ sha256: sha(legitAfterContent),
+ sizeBytes: legitAfterContent.length,
+ blobRef: 'legit-after.txt',
+ },
+ afterState: {
+ exists: true,
+ sha256: sha(legitAfterContent),
+ sizeBytes: legitAfterContent.length,
+ },
+ linesAdded: 1,
+ linesRemoved: 1,
+ },
+ ]
+ .map((entry) => JSON.stringify(entry))
+ .join('\n') + '\n',
+ 'utf8'
+ );
+
+ const reader = new TaskChangeLedgerReader();
+ const result = await reader.readTaskChanges({
+ teamName: 'team',
+ taskId: TASK_ID,
+ projectDir: tmpDir,
+ projectPath: '/repo',
+ includeDetails: true,
+ });
+
+ expect(result?.files).toHaveLength(1);
+ expect(result?.files[0]?.relativePath).toBe('src/file.ts');
+ expect(result?.files[0]?.linesAdded).toBe(1);
+ expect(result?.files[0]?.linesRemoved).toBe(1);
+ const snippets = result?.files[0]?.snippets ?? [];
+ expect(snippets).toHaveLength(1);
+ expect(snippets[0]?.ledger?.eventId).toBe('event-legit');
+ expect(snippets[0]?.ledger?.originalFullContent).toBe(beforeContent);
+ expect(snippets[0]?.ledger?.modifiedFullContent).toBe(legitAfterContent);
+ });
+
it('groups rename relations in summary-only bundles without losing absolute paths', async () => {
const relation = { kind: 'rename', oldPath: 'src/old.ts', newPath: 'src/new.ts' };
tmpDir = await makeLedgerBundle({
@@ -1110,15 +1232,26 @@ async function makeLedgerBundle(params: {
taskId: TASK_ID,
generatedAt: '2026-03-01T10:00:00.000Z',
eventCount: params.events.length,
- files: params.events.map((event: any) => ({
- filePath: event.filePath,
- relativePath: event.relativePath,
- eventIds: [event.eventId],
- linesAdded: event.linesAdded ?? 0,
- linesRemoved: event.linesRemoved ?? 0,
- isNewFile: event.operation === 'create',
- latestAfterHash: event.after?.sha256 ?? null,
- })),
+ files: params.events.map((event) => {
+ const record = event as {
+ filePath?: string;
+ relativePath?: string;
+ eventId?: string;
+ linesAdded?: number;
+ linesRemoved?: number;
+ operation?: string;
+ after?: { sha256?: string } | null;
+ };
+ return {
+ filePath: record.filePath,
+ relativePath: record.relativePath,
+ eventIds: [record.eventId],
+ linesAdded: record.linesAdded ?? 0,
+ linesRemoved: record.linesRemoved ?? 0,
+ isNewFile: record.operation === 'create',
+ latestAfterHash: record.after?.sha256 ?? null,
+ };
+ }),
totalLinesAdded: 0,
totalLinesRemoved: 0,
totalFiles: params.events.length,
diff --git a/test/main/services/team/TeamAgentLaunchMatrix.safe-e2e.test.ts b/test/main/services/team/TeamAgentLaunchMatrix.safe-e2e.test.ts
index 2d79bf35..a99ea0a3 100644
--- a/test/main/services/team/TeamAgentLaunchMatrix.safe-e2e.test.ts
+++ b/test/main/services/team/TeamAgentLaunchMatrix.safe-e2e.test.ts
@@ -38,6 +38,7 @@ import {
} from '../../../../src/main/services/team/runtime/TeamRuntimeAdapter';
import { TeamConfigReader } from '../../../../src/main/services/team/TeamConfigReader';
import { createPersistedLaunchSnapshot } from '../../../../src/main/services/team/TeamLaunchStateEvaluator';
+import type { TeamMemberWorktreeManager } from '../../../../src/main/services/team/TeamMemberWorktreeManager';
import {
getMixedLaunchFallbackRecoveryError,
TeamProvisioningService,
@@ -213,6 +214,7 @@ describe('Team agent launch matrix safe e2e', () => {
expect(runId).toBe(adapter.launchInputs[0]?.runId);
expect(adapter.launchInputs).toHaveLength(1);
expect(adapter.launchInputs[0]?.expectedMembers.map((member) => member.name)).toEqual([
+ 'team-lead',
'alice',
'bob',
]);
@@ -222,6 +224,11 @@ describe('Team agent launch matrix safe e2e', () => {
});
const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot('pure-opencode-safe-e2e');
+ expect(runtimeSnapshot.members['team-lead']).toMatchObject({
+ alive: true,
+ providerId: 'opencode',
+ runtimeModel: 'opencode/big-pickle',
+ });
expect(runtimeSnapshot.members.alice).toMatchObject({
alive: true,
providerId: 'opencode',
@@ -233,11 +240,207 @@ describe('Team agent launch matrix safe e2e', () => {
runtimeModel: 'opencode/big-pickle',
});
- await expect(
- fs.readFile(path.join(getTeamsBasePath(), 'pure-opencode-safe-e2e', 'launch-state.json'), {
+ const launchState = JSON.parse(
+ await fs.readFile(path.join(getTeamsBasePath(), 'pure-opencode-safe-e2e', 'launch-state.json'), {
encoding: 'utf8',
})
- ).resolves.toContain('"teamLaunchState": "clean_success"');
+ ) as { expectedMembers: string[]; members: Record; teamLaunchState: string };
+ expect(launchState.teamLaunchState).toBe('clean_success');
+ expect(launchState.expectedMembers).toEqual(['team-lead', 'alice', 'bob']);
+ expect(Object.keys(launchState.members)).toEqual(['team-lead', 'alice', 'bob']);
+ await expect(
+ readCommittedOpenCodeBootstrapSessionEvidence({
+ teamsBasePath: getTeamsBasePath(),
+ teamName: 'pure-opencode-safe-e2e',
+ laneId: 'primary',
+ })
+ ).resolves.toMatchObject({
+ committed: true,
+ sessions: expect.arrayContaining([
+ expect.objectContaining({ memberName: 'team-lead' }),
+ expect.objectContaining({ memberName: 'alice' }),
+ expect.objectContaining({ memberName: 'bob' }),
+ ]),
+ });
+ });
+
+ it('launches pure OpenCode worktree members as aggregate worktree-root lanes', async () => {
+ const teamName = 'pure-opencode-worktree-root-lanes-safe-e2e';
+ const bobWorktree = path.join(projectPath, '.agent-teams', 'bob');
+ const tomWorktree = path.join(projectPath, '.agent-teams', 'tom');
+ const worktreeManager: Pick = {
+ ensureMemberWorktree: vi.fn(async (input) => ({
+ baseRepoPath: projectPath,
+ worktreePath: input.memberName === 'bob' ? bobWorktree : tomWorktree,
+ branchName: `agent-teams/${teamName}/${input.memberName}`,
+ })),
+ };
+ const adapter = new FakeOpenCodeRuntimeAdapter();
+ const svc = new TeamProvisioningService(
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ worktreeManager as TeamMemberWorktreeManager
+ );
+ svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter]));
+ const progressEvents: TeamProvisioningProgress[] = [];
+
+ const { runId } = await svc.createTeam(
+ {
+ teamName,
+ cwd: projectPath,
+ providerId: 'opencode',
+ model: 'opencode/big-pickle',
+ skipPermissions: true,
+ members: [
+ {
+ name: 'bob',
+ role: 'Developer',
+ providerId: 'opencode',
+ isolation: 'worktree',
+ },
+ {
+ name: 'tom',
+ role: 'Reviewer',
+ providerId: 'opencode',
+ isolation: 'worktree',
+ },
+ ],
+ },
+ (progress) => progressEvents.push(progress)
+ );
+
+ expect(runId).toMatch(/[0-9a-f-]{36}/);
+ expect(worktreeManager.ensureMemberWorktree).toHaveBeenCalledTimes(2);
+ expect(adapter.launchInputs.map((input) => input.laneId).sort()).toEqual([
+ 'secondary:opencode:bob',
+ 'secondary:opencode:tom',
+ ]);
+ expect(adapter.launchInputs).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ laneId: 'secondary:opencode:bob',
+ cwd: bobWorktree,
+ runtimeOnly: true,
+ expectedMembers: [
+ expect.objectContaining({
+ name: 'bob',
+ providerId: 'opencode',
+ isolation: 'worktree',
+ cwd: bobWorktree,
+ }),
+ ],
+ }),
+ expect.objectContaining({
+ laneId: 'secondary:opencode:tom',
+ cwd: tomWorktree,
+ runtimeOnly: true,
+ expectedMembers: [
+ expect.objectContaining({
+ name: 'tom',
+ providerId: 'opencode',
+ isolation: 'worktree',
+ cwd: tomWorktree,
+ }),
+ ],
+ }),
+ ])
+ );
+ expect(progressEvents.at(-1)).toMatchObject({
+ state: 'ready',
+ message: 'OpenCode worktree lanes are ready',
+ });
+ expect(svc.getAliveTeams()).toContain(teamName);
+
+ await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject(
+ {
+ lanes: {
+ 'secondary:opencode:bob': { state: 'active' },
+ 'secondary:opencode:tom': { state: 'active' },
+ },
+ }
+ );
+ await expect(
+ readCommittedOpenCodeBootstrapSessionEvidence({
+ teamsBasePath: getTeamsBasePath(),
+ teamName,
+ laneId: 'secondary:opencode:bob',
+ })
+ ).resolves.toMatchObject({
+ committed: true,
+ sessions: [expect.objectContaining({ memberName: 'bob' })],
+ });
+ await expect(
+ readCommittedOpenCodeBootstrapSessionEvidence({
+ teamsBasePath: getTeamsBasePath(),
+ teamName,
+ laneId: 'secondary:opencode:tom',
+ })
+ ).resolves.toMatchObject({
+ committed: true,
+ sessions: [expect.objectContaining({ memberName: 'tom' })],
+ });
+
+ const statuses = await svc.getMemberSpawnStatuses(teamName);
+ expect(statuses.statuses.bob).toMatchObject({
+ launchState: 'confirmed_alive',
+ });
+ expect(statuses.statuses.tom).toMatchObject({
+ launchState: 'confirmed_alive',
+ });
+
+ const launchCountBeforeRestart = adapter.launchInputs.length;
+ await svc.restartMember(teamName, 'bob');
+ expect(adapter.stopInputs).toEqual([
+ expect.objectContaining({
+ laneId: 'secondary:opencode:bob',
+ teamName,
+ }),
+ ]);
+ expect(adapter.launchInputs).toHaveLength(launchCountBeforeRestart + 1);
+ expect(adapter.launchInputs.at(-1)).toMatchObject({
+ laneId: 'secondary:opencode:bob',
+ cwd: bobWorktree,
+ runtimeOnly: true,
+ expectedMembers: [
+ expect.objectContaining({
+ name: 'bob',
+ providerId: 'opencode',
+ isolation: 'worktree',
+ cwd: bobWorktree,
+ }),
+ ],
+ });
+
+ const stopCountBeforeRelaunch = adapter.stopInputs.length;
+ const launchCountBeforeRelaunch = adapter.launchInputs.length;
+ await svc.launchTeam(
+ {
+ teamName,
+ cwd: projectPath,
+ providerId: 'opencode',
+ model: 'opencode/big-pickle',
+ skipPermissions: true,
+ },
+ (progress) => progressEvents.push(progress)
+ );
+ expect(
+ adapter.stopInputs
+ .slice(stopCountBeforeRelaunch)
+ .map((input) => input.laneId)
+ .sort()
+ ).toEqual(['secondary:opencode:bob', 'secondary:opencode:tom']);
+ expect(
+ adapter.launchInputs
+ .slice(launchCountBeforeRelaunch)
+ .map((input) => input.laneId)
+ .sort()
+ ).toEqual(['secondary:opencode:bob', 'secondary:opencode:tom']);
});
it('accepts pure OpenCode runtime bootstrap check-ins during adapter launch', async () => {
@@ -260,7 +463,7 @@ describe('Team agent launch matrix safe e2e', () => {
expect(runId).toBe(adapter.launchInputs[0]?.runId);
expect(adapter.bootstrapCheckins).toEqual([
{
- memberName: 'alice',
+ memberName: 'team-lead',
runId,
state: 'accepted',
},
@@ -332,6 +535,7 @@ describe('Team agent launch matrix safe e2e', () => {
expect(runId).toBe(adapter.launchInputs[0]?.runId);
expect(adapter.launchInputs[0]?.expectedMembers.map((member) => member.name)).toEqual([
+ 'team-lead',
'alice',
'bob',
]);
@@ -474,7 +678,7 @@ describe('Team agent launch matrix safe e2e', () => {
const approval = approvalEvents.find(
(event): event is ToolApprovalRequest =>
- !('dismissed' in event) && !('autoResolved' in event)
+ !('dismissed' in event) && !('autoResolved' in event) && event.source === 'alice'
);
expect(approval).toMatchObject({
runId: launch.runId,
@@ -11145,6 +11349,94 @@ describe('Team agent launch matrix safe e2e', () => {
expect(adapter.messageInputs[0]?.runId).not.toBe(first.runId);
});
+ it('delivers pure OpenCode lead inbox messages through the primary runtime lane end-to-end', async () => {
+ const teamName = 'pure-opencode-lead-inbox-delivery-safe-e2e';
+ const adapter = new VisibleReplyOpenCodeRuntimeAdapter({
+ replySource: 'runtime_delivery',
+ });
+ const svc = new TeamProvisioningService();
+ svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter]));
+ const launch = await svc.createTeam(
+ {
+ teamName,
+ cwd: projectPath,
+ providerId: 'opencode',
+ model: 'opencode/big-pickle',
+ skipPermissions: true,
+ members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }],
+ },
+ () => undefined
+ );
+ const messageId = 'msg-pure-opencode-lead-inbox';
+ const leadInboxPath = path.join(getTeamsBasePath(), teamName, 'inboxes', 'team-lead.json');
+ await fs.mkdir(path.dirname(leadInboxPath), { recursive: true });
+ await fs.writeFile(
+ leadInboxPath,
+ `${JSON.stringify(
+ [
+ {
+ from: 'user',
+ to: 'team-lead',
+ text: 'coordinate this pure opencode team',
+ timestamp: '2026-05-08T10:05:00.000Z',
+ read: false,
+ messageId,
+ },
+ ],
+ null,
+ 2
+ )}\n`,
+ 'utf8'
+ );
+
+ await expect(
+ svc.relayInboxFileToLiveRecipient(teamName, 'team-lead', {
+ onlyMessageId: messageId,
+ source: 'ui-send',
+ deliveryMetadata: {
+ replyRecipient: 'user',
+ actionMode: 'do',
+ },
+ })
+ ).resolves.toMatchObject({
+ kind: 'opencode_member',
+ relayed: 1,
+ lastDelivery: {
+ delivered: true,
+ accepted: true,
+ responsePending: false,
+ responseState: 'responded_visible_message',
+ visibleReplyMessageId: `reply-${messageId}`,
+ },
+ });
+
+ expect(adapter.messageInputs).toHaveLength(1);
+ expect(adapter.messageInputs[0]).toMatchObject({
+ runId: launch.runId,
+ teamName,
+ laneId: 'primary',
+ memberName: 'team-lead',
+ text: 'coordinate this pure opencode team',
+ messageId,
+ replyRecipient: 'user',
+ actionMode: 'do',
+ });
+
+ const leadInbox = await readInboxRows(teamName, 'team-lead');
+ expect(leadInbox[0]).toMatchObject({
+ messageId,
+ read: true,
+ });
+ const userInbox = await readInboxRows(teamName, 'user');
+ expect(userInbox[0]).toMatchObject({
+ from: 'team-lead',
+ to: 'user',
+ source: 'runtime_delivery',
+ messageId: `reply-${messageId}`,
+ relayOfMessageId: messageId,
+ });
+ });
+
it('surfaces pure OpenCode delivery permission blocks as the shared tool approval dialog', async () => {
const teamName = 'pure-opencode-delivery-permission-approval-safe-e2e';
const adapter = new PermissionBlockedOpenCodeRuntimeAdapter();
diff --git a/test/main/services/team/TeamDataService.test.ts b/test/main/services/team/TeamDataService.test.ts
index 9f1f1a34..bf3e88bd 100644
--- a/test/main/services/team/TeamDataService.test.ts
+++ b/test/main/services/team/TeamDataService.test.ts
@@ -25,6 +25,27 @@ import type {
const TASK_COMMENT_FORWARDING_ENV = 'CLAUDE_TEAM_TASK_COMMENT_FORWARDING';
const tempPaths: string[] = [];
+type TeamDataServicePrivate = {
+ extractLeadAssistantTextsFromJsonlLines(
+ rawLines: readonly string[],
+ leadName: string,
+ leadSessionId: string,
+ maxTexts: number
+ ): Promise;
+ getLeadSessionJsonlPaths(projectDir: string): Promise>;
+ extractLeadSessionTextsFromJsonl(
+ jsonlPath: string,
+ leadName: string,
+ leadSessionId: string,
+ maxTexts: number
+ ): Promise;
+ extractLeadSessionTexts(teamName: string, config: TeamConfig): Promise;
+};
+
+function teamDataServicePrivate(service: TeamDataService): TeamDataServicePrivate {
+ return service as unknown as TeamDataServicePrivate;
+}
+
function createLeadAssistantEntry(
uuid: string,
timestamp: string,
@@ -4690,7 +4711,7 @@ describe('TeamDataService', () => {
),
]);
- const assistantSpy = vi.spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never);
+ const assistantSpy = vi.spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines');
const extract = (
service as unknown as {
extractLeadSessionTextsFromJsonl: (
@@ -4698,7 +4719,7 @@ describe('TeamDataService', () => {
leadName: string,
leadSessionId: string,
maxTexts: number
- ) => Promise>;
+ ) => Promise;
}
).extractLeadSessionTextsFromJsonl.bind(service);
@@ -4730,11 +4751,11 @@ describe('TeamDataService', () => {
leadName: string,
leadSessionId: string,
maxTexts: number
- ) => Promise>;
+ ) => Promise;
}
).extractLeadAssistantTextsFromJsonlLines.bind(service);
const assistantSpy = vi
- .spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never)
+ .spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines')
.mockImplementation(async (...args: unknown[]) => {
const [rawLines, leadName, leadSessionId, maxTexts] = args as [
readonly string[],
@@ -4752,7 +4773,7 @@ describe('TeamDataService', () => {
leadName: string,
leadSessionId: string,
maxTexts: number
- ) => Promise>;
+ ) => Promise;
}
).extractLeadSessionTextsFromJsonl.bind(service);
@@ -4782,12 +4803,12 @@ describe('TeamDataService', () => {
leadName: string,
leadSessionId: string,
maxTexts: number
- ) => Promise>;
+ ) => Promise;
}
).extractLeadAssistantTextsFromJsonlLines.bind(service);
let appended = false;
const assistantSpy = vi
- .spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never)
+ .spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines')
.mockImplementation(async (...args: unknown[]) => {
const [rawLines, leadName, leadSessionId, maxTexts] = args as [
readonly string[],
@@ -4818,7 +4839,7 @@ describe('TeamDataService', () => {
leadName: string,
leadSessionId: string,
maxTexts: number
- ) => Promise>;
+ ) => Promise;
}
).extractLeadSessionTextsFromJsonl.bind(service);
@@ -4847,7 +4868,7 @@ describe('TeamDataService', () => {
leadName: string,
leadSessionId: string,
maxTexts: number
- ) => Promise>;
+ ) => Promise;
}
).extractLeadAssistantTextsFromJsonlLines.bind(service);
let releaseFirstInvocation = () => {};
@@ -4856,7 +4877,7 @@ describe('TeamDataService', () => {
firstInvocationStartedResolve = resolve;
});
const assistantSpy = vi
- .spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never)
+ .spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines')
.mockImplementation(async (...args: unknown[]) => {
const [rawLines, leadName, leadSessionId, maxTexts] = args as [
readonly string[],
@@ -4879,7 +4900,7 @@ describe('TeamDataService', () => {
leadName: string,
leadSessionId: string,
maxTexts: number
- ) => Promise>;
+ ) => Promise;
}
).extractLeadSessionTextsFromJsonl.bind(service);
@@ -4922,7 +4943,7 @@ describe('TeamDataService', () => {
),
]);
- const assistantSpy = vi.spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never);
+ const assistantSpy = vi.spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines');
const extract = (
service as unknown as {
extractLeadSessionTextsFromJsonl: (
@@ -4956,7 +4977,7 @@ describe('TeamDataService', () => {
),
]);
- const assistantSpy = vi.spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never);
+ const assistantSpy = vi.spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines');
const extract = (
service as unknown as {
extractLeadSessionTextsFromJsonl: (
@@ -4964,7 +4985,7 @@ describe('TeamDataService', () => {
leadName: string,
leadSessionId: string,
maxTexts: number
- ) => Promise>;
+ ) => Promise;
}
).extractLeadSessionTextsFromJsonl.bind(service);
@@ -4988,7 +5009,7 @@ describe('TeamDataService', () => {
]);
await fs.appendFile(jsonlPath, '{"type":"assistant"', 'utf8');
- const assistantSpy = vi.spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never);
+ const assistantSpy = vi.spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines');
const extract = (
service as unknown as {
extractLeadSessionTextsFromJsonl: (
@@ -4996,7 +5017,7 @@ describe('TeamDataService', () => {
leadName: string,
leadSessionId: string,
maxTexts: number
- ) => Promise>;
+ ) => Promise;
}
).extractLeadSessionTextsFromJsonl.bind(service);
@@ -5036,7 +5057,7 @@ describe('TeamDataService', () => {
),
]);
- const assistantSpy = vi.spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never);
+ const assistantSpy = vi.spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines');
const extract = (
service as unknown as {
extractLeadSessionTextsFromJsonl: (
@@ -5044,7 +5065,7 @@ describe('TeamDataService', () => {
leadName: string,
leadSessionId: string,
maxTexts: number
- ) => Promise>;
+ ) => Promise;
}
).extractLeadSessionTextsFromJsonl.bind(service);
@@ -5073,12 +5094,12 @@ describe('TeamDataService', () => {
leadName: string,
leadSessionId: string,
maxTexts: number
- ) => Promise>;
+ ) => Promise;
}
).extractLeadAssistantTextsFromJsonlLines.bind(service);
let shouldFail = true;
const assistantSpy = vi
- .spyOn(service as never, 'extractLeadAssistantTextsFromJsonlLines' as never)
+ .spyOn(teamDataServicePrivate(service), 'extractLeadAssistantTextsFromJsonlLines')
.mockImplementation(async (...args: unknown[]) => {
const [rawLines, leadName, leadSessionId, maxTexts] = args as [
readonly string[],
@@ -5098,7 +5119,7 @@ describe('TeamDataService', () => {
leadName: string,
leadSessionId: string,
maxTexts: number
- ) => Promise>;
+ ) => Promise;
}
).extractLeadSessionTextsFromJsonl.bind(service);
@@ -5124,10 +5145,10 @@ describe('TeamDataService', () => {
),
]);
- const firstSpy = vi.spyOn(firstService as never, 'extractLeadAssistantTextsFromJsonlLines' as never);
+ const firstSpy = vi.spyOn(teamDataServicePrivate(firstService), 'extractLeadAssistantTextsFromJsonlLines');
const secondSpy = vi.spyOn(
- secondService as never,
- 'extractLeadAssistantTextsFromJsonlLines' as never
+ teamDataServicePrivate(secondService),
+ 'extractLeadAssistantTextsFromJsonlLines'
);
const firstExtract = (
firstService as unknown as {
@@ -5136,7 +5157,7 @@ describe('TeamDataService', () => {
leadName: string,
leadSessionId: string,
maxTexts: number
- ) => Promise>;
+ ) => Promise;
}
).extractLeadSessionTextsFromJsonl.bind(firstService);
const secondExtract = (
@@ -5146,7 +5167,7 @@ describe('TeamDataService', () => {
leadName: string,
leadSessionId: string,
maxTexts: number
- ) => Promise>;
+ ) => Promise;
}
).extractLeadSessionTextsFromJsonl.bind(secondService);
@@ -5177,10 +5198,10 @@ describe('TeamDataService', () => {
};
(service as unknown as { projectResolver: typeof projectResolver }).projectResolver =
projectResolver;
- vi.spyOn(service as never, 'getLeadSessionJsonlPaths' as never).mockResolvedValue(
+ vi.spyOn(teamDataServicePrivate(service), 'getLeadSessionJsonlPaths').mockResolvedValue(
new Map([['lead-1', '/fast-project/lead-1.jsonl']])
);
- vi.spyOn(service as never, 'extractLeadSessionTextsFromJsonl' as never).mockResolvedValue([
+ vi.spyOn(teamDataServicePrivate(service), 'extractLeadSessionTextsFromJsonl').mockResolvedValue([
{
from: 'fast-lead',
text: 'Fast path recovered lead thought from the known lead session.',
@@ -5228,7 +5249,7 @@ describe('TeamDataService', () => {
};
(service as unknown as { projectResolver: typeof projectResolver }).projectResolver =
projectResolver;
- vi.spyOn(service as never, 'getLeadSessionJsonlPaths' as never).mockImplementation(
+ vi.spyOn(teamDataServicePrivate(service), 'getLeadSessionJsonlPaths').mockImplementation(
(...args: unknown[]) => {
const [projectDir] = args as [string];
if (projectDir === '/actual-project') {
@@ -5237,7 +5258,7 @@ describe('TeamDataService', () => {
return Promise.resolve(new Map());
}
);
- vi.spyOn(service as never, 'extractLeadSessionTextsFromJsonl' as never).mockResolvedValue([
+ vi.spyOn(teamDataServicePrivate(service), 'extractLeadSessionTextsFromJsonl').mockResolvedValue([
{
from: 'actual-lead',
text: 'Fallback path recovered lead thought from the repaired context.',
@@ -5292,7 +5313,7 @@ describe('TeamDataService', () => {
};
(service as unknown as { projectResolver: typeof projectResolver }).projectResolver =
projectResolver;
- vi.spyOn(service as never, 'getLeadSessionJsonlPaths' as never).mockImplementation(
+ vi.spyOn(teamDataServicePrivate(service), 'getLeadSessionJsonlPaths').mockImplementation(
(...args: unknown[]) => {
const [projectDir] = args as [string];
if (projectDir === '/current-project') {
@@ -5304,7 +5325,7 @@ describe('TeamDataService', () => {
}
);
const extractSpy = vi
- .spyOn(service as never, 'extractLeadSessionTextsFromJsonl' as never)
+ .spyOn(teamDataServicePrivate(service), 'extractLeadSessionTextsFromJsonl')
.mockResolvedValue([
{
from: 'current-lead',
@@ -5366,11 +5387,11 @@ describe('TeamDataService', () => {
(service as unknown as { projectResolver: typeof projectResolver }).projectResolver =
projectResolver;
const getPathsSpy = vi
- .spyOn(service as never, 'getLeadSessionJsonlPaths' as never)
+ .spyOn(teamDataServicePrivate(service), 'getLeadSessionJsonlPaths')
.mockResolvedValueOnce(new Map([['lead-history', '/same-project/lead-history.jsonl']]))
.mockResolvedValueOnce(new Map([['lead-current', '/same-project/lead-current.jsonl']]));
const extractSpy = vi
- .spyOn(service as never, 'extractLeadSessionTextsFromJsonl' as never)
+ .spyOn(teamDataServicePrivate(service), 'extractLeadSessionTextsFromJsonl')
.mockResolvedValue([
{
from: 'current-lead',
@@ -5974,7 +5995,7 @@ describe('TeamDataService', () => {
],
});
- vi.spyOn(harness.service as never, 'extractLeadSessionTexts' as never).mockResolvedValue([
+ vi.spyOn(teamDataServicePrivate(harness.service), 'extractLeadSessionTexts').mockResolvedValue([
{
from: 'team-lead',
text: 'Lead summary',
@@ -6028,7 +6049,7 @@ describe('TeamDataService', () => {
resolveMembers: resolveMembersSpy,
});
- vi.spyOn(harness.service as never, 'extractLeadSessionTexts' as never).mockResolvedValue([
+ vi.spyOn(teamDataServicePrivate(harness.service), 'extractLeadSessionTexts').mockResolvedValue([
{
from: 'team-lead',
text: 'Lead summary',
@@ -6110,7 +6131,7 @@ describe('TeamDataService', () => {
},
});
- vi.spyOn(harness.service as never, 'extractLeadSessionTexts' as never).mockImplementation(
+ vi.spyOn(teamDataServicePrivate(harness.service), 'extractLeadSessionTexts').mockImplementation(
() => {
order.push('leadTexts:start');
throw new Error('lead sync fail');
diff --git a/test/main/services/team/TeamInboxWriter.test.ts b/test/main/services/team/TeamInboxWriter.test.ts
index 2ef629a1..31b1fb7b 100644
--- a/test/main/services/team/TeamInboxWriter.test.ts
+++ b/test/main/services/team/TeamInboxWriter.test.ts
@@ -177,6 +177,55 @@ describe('TeamInboxWriter', () => {
});
});
+ it('updates an existing member-work-sync row text when message kind and payload hash match', async () => {
+ await writer.sendMessage('my-team', {
+ member: 'alice',
+ text: 'sync your work state',
+ source: 'system_notification',
+ messageId: 'work-sync-1',
+ messageKind: 'member_work_sync_nudge',
+ workSyncIntent: 'agenda_sync',
+ workSyncPayloadHash: 'sha256:work-sync',
+ });
+
+ const result = await writer.updateMessageText('my-team', {
+ member: 'alice',
+ messageId: 'work-sync-1',
+ text: 'sync your work state\nRequired control API: pass controlUrl "http://127.0.0.1:43123" in both member_work_sync_status and member_work_sync_report.',
+ expectedMessageKind: 'member_work_sync_nudge',
+ expectedWorkSyncPayloadHash: 'sha256:work-sync',
+ });
+
+ const persisted = JSON.parse(hoisted.files.get(inboxPath) ?? '[]') as Record[];
+ expect(result).toEqual({ found: true, updated: true });
+ expect(persisted[0]?.text).toContain('controlUrl "http://127.0.0.1:43123"');
+ expect(persisted[0]?.workSyncPayloadHash).toBe('sha256:work-sync');
+ });
+
+ it('does not update member-work-sync row text when payload hash mismatches', async () => {
+ await writer.sendMessage('my-team', {
+ member: 'alice',
+ text: 'sync your work state',
+ source: 'system_notification',
+ messageId: 'work-sync-1',
+ messageKind: 'member_work_sync_nudge',
+ workSyncIntent: 'agenda_sync',
+ workSyncPayloadHash: 'sha256:work-sync',
+ });
+
+ const result = await writer.updateMessageText('my-team', {
+ member: 'alice',
+ messageId: 'work-sync-1',
+ text: 'should not write',
+ expectedMessageKind: 'member_work_sync_nudge',
+ expectedWorkSyncPayloadHash: 'sha256:different',
+ });
+
+ const persisted = JSON.parse(hoisted.files.get(inboxPath) ?? '[]') as Record[];
+ expect(result).toEqual({ found: true, updated: false });
+ expect(persisted[0]?.text).toBe('sync your work state');
+ });
+
it('preserves provided message identity fields for dedup across live and persisted rows', async () => {
const result = await writer.sendMessage('my-team', {
member: 'alice',
diff --git a/test/main/services/team/TeamMemberRuntimeAdvisoryService.test.ts b/test/main/services/team/TeamMemberRuntimeAdvisoryService.test.ts
index e53c8d03..78ae4d78 100644
--- a/test/main/services/team/TeamMemberRuntimeAdvisoryService.test.ts
+++ b/test/main/services/team/TeamMemberRuntimeAdvisoryService.test.ts
@@ -72,7 +72,12 @@ function createStubbedServiceHarness() {
const service = new TeamMemberRuntimeAdvisoryService(logsFinder as never);
const advisoryByFilePath = new Map();
const readRecentApiRetryAdvisory = vi
- .spyOn(service as never, 'readRecentApiRetryAdvisory' as never)
+ .spyOn(
+ service as unknown as {
+ readRecentApiRetryAdvisory: (filePath: string) => Promise;
+ },
+ 'readRecentApiRetryAdvisory'
+ )
.mockImplementation(async (...args: unknown[]) => {
const filePath = String(args[0] ?? '');
if (advisoryByFilePath.has(filePath)) {
diff --git a/test/main/services/team/TeamMessageFeedService.test.ts b/test/main/services/team/TeamMessageFeedService.test.ts
index 4a3f8a6e..d8e1b611 100644
--- a/test/main/services/team/TeamMessageFeedService.test.ts
+++ b/test/main/services/team/TeamMessageFeedService.test.ts
@@ -120,6 +120,40 @@ describe('TeamMessageFeedService', () => {
expect(feed.messages[0].text).toContain('member_briefing');
});
+ it('does not stamp synthetic bootstrap prompts with Unix epoch when config has no join time', async () => {
+ const service = new TeamMessageFeedService({
+ getConfig: vi.fn(async () => ({
+ name: 'opencode-test',
+ members: [
+ { name: 'team-lead', role: 'Lead' },
+ {
+ name: 'alice',
+ role: 'Developer',
+ providerId: 'opencode' as const,
+ model: 'openrouter/big-pickle',
+ },
+ ],
+ })),
+ getInboxMessages: vi.fn(async () => []),
+ getLeadSessionMessages: vi.fn(async () => []),
+ getSentMessages: vi.fn(async () => []),
+ });
+
+ const first = await service.getFeed('opencode-test');
+
+ expect(first.messages).toHaveLength(1);
+ expect(first.messages[0].messageId).toBe('bootstrap-start:opencode-test:alice');
+ expect(first.messages[0].timestamp).toBe('2026-04-19T18:46:40.000Z');
+ expect(first.messages[0].timestamp).not.toBe('1970-01-01T00:00:00.000Z');
+
+ vi.setSystemTime(new Date('2026-04-19T18:47:00.000Z'));
+ service.invalidate('opencode-test');
+ const refreshed = await service.getFeed('opencode-test');
+
+ expect(refreshed.messages[0].timestamp).toBe(first.messages[0].timestamp);
+ expect(refreshed.feedRevision).toBe(first.feedRevision);
+ });
+
it('does not hide user-authored text just because it resembles an internal prompt', async () => {
const service = new TeamMessageFeedService({
getConfig: vi.fn(async () => config),
diff --git a/test/main/services/team/TeamProvisioningBootstrapTranscriptIndex.test.ts b/test/main/services/team/TeamProvisioningBootstrapTranscriptIndex.test.ts
new file mode 100644
index 00000000..44b284e8
--- /dev/null
+++ b/test/main/services/team/TeamProvisioningBootstrapTranscriptIndex.test.ts
@@ -0,0 +1,145 @@
+import * as fs from 'fs/promises';
+import * as os from 'os';
+import * as path from 'path';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+vi.mock('@features/tmux-installer/main', () => ({
+ killTmuxPaneForCurrentPlatformSync: vi.fn(),
+ listRuntimeProcessTableForCurrentPlatform: vi.fn(async () => []),
+ listTmuxPanePidsForCurrentPlatform: vi.fn(async () => new Map()),
+ listTmuxPaneRuntimeInfoForCurrentPlatform: vi.fn(async () => new Map()),
+ sendKeysToTmuxPaneForCurrentPlatform: vi.fn(async () => undefined),
+}));
+
+vi.mock('pidusage', () => ({
+ default: vi.fn(),
+}));
+
+import { TeamProvisioningService } from '../../../../src/main/services/team/TeamProvisioningService';
+
+interface TranscriptIndexHarness {
+ bootstrapTranscriptOutcomeCache: Map;
+ bootstrapTranscriptOutcomeInFlight: Map>;
+ parsedBootstrapTranscriptTailCache: Map;
+ getParsedBootstrapTranscriptTail: (...args: unknown[]) => Promise;
+ readRecentBootstrapTranscriptOutcome: (
+ filePath: string,
+ sinceMs: number | null,
+ memberName: string,
+ teamName: string,
+ options?: { allowAnonymousFailure?: boolean; contextMemberNames?: readonly string[] }
+ ) => Promise;
+}
+
+function createTranscriptIndexHarness(): TranscriptIndexHarness {
+ const service = Object.create(
+ TeamProvisioningService.prototype
+ ) as unknown as TranscriptIndexHarness;
+ service.bootstrapTranscriptOutcomeCache = new Map();
+ service.bootstrapTranscriptOutcomeInFlight = new Map();
+ service.parsedBootstrapTranscriptTailCache = new Map();
+ return service;
+}
+
+function transcriptLine(input: {
+ timestamp: string;
+ agentName?: string;
+ text: string;
+}): string {
+ return `${JSON.stringify({
+ type: 'assistant',
+ timestamp: input.timestamp,
+ ...(input.agentName ? { agentName: input.agentName } : {}),
+ message: {
+ role: 'assistant',
+ content: [{ type: 'text', text: input.text }],
+ },
+ })}\n`;
+}
+
+describe('TeamProvisioningService bootstrap transcript index', () => {
+ let tmpDir: string | null = null;
+
+ afterEach(async () => {
+ if (tmpDir) {
+ await fs.rm(tmpDir, { recursive: true, force: true });
+ tmpDir = null;
+ }
+ });
+
+ it('updates the transcript outcome from appended lines using the incremental file index', async () => {
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bootstrap-transcript-index-'));
+ const transcriptPath = path.join(tmpDir, 'session.jsonl');
+ await fs.writeFile(
+ transcriptPath,
+ transcriptLine({
+ timestamp: '2026-04-18T10:00:00.000Z',
+ agentName: 'alice',
+ text: 'Member briefing for alice on team "demo-team" (demo-team).',
+ }),
+ 'utf8'
+ );
+
+ const service = createTranscriptIndexHarness();
+ const originalParseTail = service.getParsedBootstrapTranscriptTail.bind(service);
+ let parseTailCalls = 0;
+ service.getParsedBootstrapTranscriptTail = async (...args: unknown[]) => {
+ parseTailCalls += 1;
+ return originalParseTail(...args);
+ };
+
+ await expect(
+ service.readRecentBootstrapTranscriptOutcome(
+ transcriptPath,
+ null,
+ 'alice',
+ 'demo-team',
+ { contextMemberNames: ['alice'] }
+ )
+ ).resolves.toEqual({
+ kind: 'success',
+ observedAt: '2026-04-18T10:00:00.000Z',
+ source: 'member_briefing',
+ });
+ expect(parseTailCalls).toBe(1);
+
+ await fs.appendFile(
+ transcriptPath,
+ transcriptLine({
+ timestamp: '2026-04-18T10:01:00.000Z',
+ text: 'Bootstrap failed: member_briefing tool is not available',
+ }),
+ 'utf8'
+ );
+
+ await expect(
+ service.readRecentBootstrapTranscriptOutcome(
+ transcriptPath,
+ null,
+ 'alice',
+ 'demo-team',
+ { contextMemberNames: ['alice'] }
+ )
+ ).resolves.toEqual({
+ kind: 'failure',
+ observedAt: '2026-04-18T10:01:00.000Z',
+ reason: 'Bootstrap failed: member_briefing tool is not available',
+ });
+ expect(parseTailCalls).toBe(2);
+
+ await expect(
+ service.readRecentBootstrapTranscriptOutcome(
+ transcriptPath,
+ null,
+ 'alice',
+ 'demo-team',
+ { contextMemberNames: ['alice'] }
+ )
+ ).resolves.toEqual({
+ kind: 'failure',
+ observedAt: '2026-04-18T10:01:00.000Z',
+ reason: 'Bootstrap failed: member_briefing tool is not available',
+ });
+ expect(parseTailCalls).toBe(2);
+ });
+});
diff --git a/test/main/services/team/TeamProvisioningOpenCodeRuntimeEvidencePolicy.test.ts b/test/main/services/team/TeamProvisioningOpenCodeRuntimeEvidencePolicy.test.ts
index 5708ed81..cf39ae37 100644
--- a/test/main/services/team/TeamProvisioningOpenCodeRuntimeEvidencePolicy.test.ts
+++ b/test/main/services/team/TeamProvisioningOpenCodeRuntimeEvidencePolicy.test.ts
@@ -395,6 +395,35 @@ describe('TeamProvisioningOpenCodeRuntimeEvidencePolicy', () => {
).toBe(false);
});
+ it('accepts same-run bootstrap confirmation before delayed app acceptance past skew', () => {
+ const current = {
+ firstSpawnAcceptedAt: '2026-05-24T09:25:52.497Z',
+ lastEvaluatedAt: '2026-05-24T09:31:05.525Z',
+ runtimeRunId: 'run-process-table-unavailable-skew',
+ };
+ const bootstrapMember = {
+ firstSpawnAcceptedAt: '2026-05-24T09:25:33.388Z',
+ lastHeartbeatAt: '2026-05-24T09:25:42.494Z',
+ lastRuntimeAliveAt: '2026-05-24T09:25:42.494Z',
+ lastEvaluatedAt: '2026-05-24T09:25:42.494Z',
+ runtimeRunId: 'run-process-table-unavailable-skew',
+ };
+
+ expect(
+ isBootstrapMemberEvidenceCurrentForMember(current, bootstrapMember, 'confirmation')
+ ).toBe(true);
+ expect(
+ isBootstrapMemberEvidenceCurrentForMember(
+ current,
+ { ...bootstrapMember, runtimeRunId: 'previous-run' },
+ 'confirmation'
+ )
+ ).toBe(false);
+ expect(isBootstrapMemberEvidenceCurrentForMember(current, bootstrapMember, 'acceptance')).toBe(
+ false
+ );
+ });
+
it('classifies recoverable persisted OpenCode runtime candidates', () => {
expect(
isRecoverablePersistedOpenCodeRuntimeCandidate(makePersisted({ runtimeSessionId: 'rt-1' }))
diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts
index 85381cd7..141a516d 100644
--- a/test/main/services/team/TeamProvisioningService.test.ts
+++ b/test/main/services/team/TeamProvisioningService.test.ts
@@ -18190,6 +18190,11 @@ describe('TeamProvisioningService', () => {
effort: 'medium',
cwd: tempClaudeRoot,
expectedMembers: [
+ expect.objectContaining({
+ name: 'team-lead',
+ providerId: 'opencode',
+ model: 'big-pickle',
+ }),
expect.objectContaining({
name: 'bob',
providerId: 'opencode',
@@ -19003,10 +19008,61 @@ describe('TeamProvisioningService', () => {
await svc.cancelProvisioning(runId);
});
- it('rejects multi-member pure OpenCode worktree isolation instead of sharing one projectPath', async () => {
+ it('launches pure OpenCode worktree members through separate runtime lanes', async () => {
allowConsoleLogs();
- const adapterLaunch = vi.fn();
- const { svc } = createSafeLaunchService();
+ const bobWorktree = path.join(tempClaudeRoot, 'worktrees', 'bob');
+ const worktreeManager = {
+ ensureMemberWorktree: vi.fn(async () => ({
+ baseRepoPath: tempClaudeRoot,
+ worktreePath: bobWorktree,
+ branchName: 'agent-teams/test/bob',
+ })),
+ };
+ const adapterLaunch = vi.fn(async (input: Record) => {
+ const expectedMembers = input.expectedMembers as Array<{ name: string }>;
+ const teamName = String(input.teamName);
+ const laneId = String(input.laneId);
+ const runId = String(input.runId);
+ await writeCommittedOpenCodeSessionStore({
+ teamName,
+ laneId,
+ runId,
+ sessions: expectedMembers.map((member) => ({
+ id: `oc-session-${laneId}-${member.name}`,
+ teamName,
+ memberName: member.name,
+ laneId,
+ runId,
+ source: 'runtime_bootstrap_checkin',
+ })),
+ });
+ return {
+ runId,
+ teamName,
+ launchPhase: 'finished',
+ teamLaunchState: 'clean_success',
+ members: Object.fromEntries(
+ expectedMembers.map((member) => [
+ member.name,
+ {
+ memberName: member.name,
+ providerId: 'opencode',
+ launchState: 'confirmed_alive',
+ agentToolAccepted: true,
+ runtimeAlive: true,
+ bootstrapConfirmed: true,
+ hardFailure: false,
+ diagnostics: [],
+ },
+ ])
+ ),
+ warnings: [],
+ diagnostics: [],
+ };
+ });
+ const { svc } = createSafeLaunchService({
+ memberWorktreeManager: worktreeManager,
+ });
svc.setRuntimeAdapterRegistry(
new TeamRuntimeAdapterRegistry([
{
@@ -19019,32 +19075,71 @@ describe('TeamProvisioningService', () => {
])
);
- await expect(
- svc.createTeam(
- {
- teamName: 'blocked-opencode-multi-worktree',
- cwd: tempClaudeRoot,
- providerId: 'opencode',
- providerBackendId: 'adapter',
- model: 'big-pickle',
- members: [
- {
- name: 'bob',
- providerId: 'opencode',
- model: 'minimax-m2.5-free',
- isolation: 'worktree',
- },
- {
- name: 'tom',
- providerId: 'opencode',
- model: 'nemotron-3-super-free',
- },
- ],
- },
- () => {}
- )
- ).rejects.toThrow('Multiple OpenCode members in one lane cannot use separate worktrees yet');
- expect(adapterLaunch).not.toHaveBeenCalled();
+ const { runId } = await svc.createTeam(
+ {
+ teamName: 'opencode-multi-worktree-lanes',
+ cwd: tempClaudeRoot,
+ providerId: 'opencode',
+ providerBackendId: 'adapter',
+ model: 'big-pickle',
+ members: [
+ {
+ name: 'bob',
+ providerId: 'opencode',
+ model: 'minimax-m2.5-free',
+ isolation: 'worktree',
+ },
+ {
+ name: 'tom',
+ providerId: 'opencode',
+ model: 'nemotron-3-super-free',
+ },
+ ],
+ },
+ () => {}
+ );
+
+ expect(worktreeManager.ensureMemberWorktree).toHaveBeenCalledWith({
+ teamName: 'opencode-multi-worktree-lanes',
+ memberName: 'bob',
+ baseCwd: tempClaudeRoot,
+ });
+ expect(adapterLaunch).toHaveBeenCalledTimes(2);
+ expect(adapterLaunch).toHaveBeenCalledWith(
+ expect.objectContaining({
+ laneId: 'primary',
+ cwd: tempClaudeRoot,
+ expectedMembers: [
+ expect.objectContaining({
+ name: 'tom',
+ providerId: 'opencode',
+ cwd: tempClaudeRoot,
+ }),
+ ],
+ })
+ );
+ expect(adapterLaunch).toHaveBeenCalledWith(
+ expect.objectContaining({
+ laneId: 'secondary:opencode:bob',
+ cwd: bobWorktree,
+ expectedMembers: [
+ expect.objectContaining({
+ name: 'bob',
+ providerId: 'opencode',
+ isolation: 'worktree',
+ cwd: bobWorktree,
+ }),
+ ],
+ })
+ );
+ const run = (svc as any).runs.get(runId);
+ expect(run?.mixedSecondaryLanes).toEqual([
+ expect.objectContaining({
+ laneId: 'secondary:opencode:bob',
+ state: 'finished',
+ member: expect.objectContaining({ name: 'bob', cwd: bobWorktree }),
+ }),
+ ]);
});
});
@@ -22269,6 +22364,298 @@ describe('TeamProvisioningService', () => {
expect(result.statuses.tom?.runtimeDiagnosticSeverity).toBeUndefined();
});
+ it('heals issue 209 Codex stale-pid hard failure when bootstrap-state confirms the member', async () => {
+ allowConsoleLogs();
+ const teamName = 'zz-unit-issue-209-codex-stale-pid-bootstrap-heals';
+ const leadSessionId = 'lead-session';
+ const bootstrapRunId = '0ebe3b51-57e5-4281-b872-8184bdea34c7';
+ const memberName = 'business-reviewer-alpha';
+ const runtimePid = 21_580;
+ const stalePidReason = 'persisted runtime pid is not alive';
+
+ writeTeamMeta(teamName, {
+ providerId: 'codex',
+ providerBackendId: 'codex-native',
+ model: 'gpt-5.5',
+ });
+ writeMembersMeta(teamName, [{ name: memberName, providerId: 'codex', model: 'gpt-5.5' }]);
+ writeLaunchConfig(teamName, '/Users/test/proj', leadSessionId, [memberName]);
+ writeMemberBootstrapRunId(teamName, memberName, bootstrapRunId);
+ writeLaunchState(
+ teamName,
+ leadSessionId,
+ {
+ [memberName]: {
+ providerId: 'codex',
+ model: 'gpt-5.5',
+ laneId: 'primary',
+ laneKind: 'primary',
+ laneOwnerProviderId: 'codex',
+ launchState: 'failed_to_start',
+ agentToolAccepted: true,
+ runtimeAlive: false,
+ runtimePid,
+ runtimeRunId: bootstrapRunId,
+ bootstrapConfirmed: false,
+ hardFailure: true,
+ hardFailureReason: stalePidReason,
+ livenessKind: 'stale_metadata',
+ runtimeDiagnostic: stalePidReason,
+ runtimeDiagnosticSeverity: 'warning',
+ firstSpawnAcceptedAt: '2026-06-06T09:49:08.513Z',
+ runtimeLastSeenAt: '2026-06-06T09:51:18.924Z',
+ lastEvaluatedAt: '2026-06-06T09:51:18.924Z',
+ },
+ },
+ { launchPhase: 'finished', updatedAt: '2026-06-06T09:59:23.165Z' }
+ );
+ writeBootstrapState(
+ teamName,
+ [
+ {
+ name: memberName,
+ status: 'bootstrap_confirmed',
+ lastAttemptAt: Date.parse('2026-06-06T09:49:06.287Z'),
+ lastObservedAt: Date.parse('2026-06-06T09:51:53.070Z'),
+ },
+ ],
+ '2026-06-06T09:59:43.154Z',
+ { runId: bootstrapRunId }
+ );
+
+ const svc = new TeamProvisioningService();
+ privateHarness(svc).getLiveTeamAgentRuntimeMetadata = vi.fn(
+ async () =>
+ new Map([
+ [
+ memberName,
+ {
+ alive: false,
+ backendType: 'process',
+ providerId: 'codex',
+ livenessKind: 'stale_metadata',
+ pidSource: 'persisted_metadata',
+ runtimeDiagnostic: stalePidReason,
+ runtimeDiagnosticSeverity: 'warning',
+ metricsPid: runtimePid,
+ model: 'gpt-5.5',
+ },
+ ],
+ ])
+ );
+
+ const result = await svc.getMemberSpawnStatuses(teamName);
+
+ expect(result.teamLaunchState).toBe('clean_success');
+ expect(result.statuses[memberName]).toMatchObject({
+ status: 'online',
+ launchState: 'confirmed_alive',
+ bootstrapConfirmed: true,
+ runtimeAlive: false,
+ livenessKind: 'confirmed_bootstrap',
+ hardFailure: false,
+ error: undefined,
+ });
+ expect(result.statuses[memberName]?.hardFailureReason).toBeUndefined();
+ expect(result.statuses[memberName]?.runtimeDiagnostic).toBeUndefined();
+ expect(result.statuses[memberName]?.runtimeDiagnosticSeverity).toBeUndefined();
+ });
+
+ it('heals issue 209 confirmed Codex member without clearing submitted-timeout failures', async () => {
+ allowConsoleLogs();
+ const teamName = 'zz-unit-issue-209-codex-mixed-partial-reconcile';
+ const leadSessionId = 'lead-session';
+ const bootstrapRunId = '0ebe3b51-57e5-4281-b872-8184bdea34c7';
+ const stalePidReason = 'persisted runtime pid is not alive';
+ const submittedTimeoutReason =
+ 'Bootstrap prompt was submitted, but teammate did not bootstrap-confirm before submitted-confirmation timeout (3m). Last transport stage: bootstrap_submitted';
+
+ writeTeamMeta(teamName, {
+ providerId: 'codex',
+ providerBackendId: 'codex-native',
+ model: 'gpt-5.5',
+ });
+ writeMembersMeta(teamName, [
+ { name: 'business-reviewer-alpha', providerId: 'codex', model: 'gpt-5.5' },
+ { name: 'business-reviewer-beta', providerId: 'codex', model: 'gpt-5.4' },
+ { name: 'ux-reviewer-beta', providerId: 'codex', model: 'gpt-5.4' },
+ ]);
+ writeLaunchConfig(teamName, '/Users/test/proj', leadSessionId, [
+ 'business-reviewer-alpha',
+ 'business-reviewer-beta',
+ 'ux-reviewer-beta',
+ ]);
+ writeMemberBootstrapRunId(teamName, 'business-reviewer-alpha', bootstrapRunId);
+ writeMemberBootstrapRunId(teamName, 'business-reviewer-beta', bootstrapRunId);
+ writeMemberBootstrapRunId(teamName, 'ux-reviewer-beta', bootstrapRunId);
+ writeLaunchState(
+ teamName,
+ leadSessionId,
+ {
+ 'business-reviewer-alpha': {
+ providerId: 'codex',
+ model: 'gpt-5.5',
+ laneId: 'primary',
+ laneKind: 'primary',
+ laneOwnerProviderId: 'codex',
+ launchState: 'failed_to_start',
+ agentToolAccepted: true,
+ runtimeAlive: false,
+ runtimePid: 21_580,
+ runtimeRunId: bootstrapRunId,
+ bootstrapConfirmed: false,
+ hardFailure: true,
+ hardFailureReason: stalePidReason,
+ livenessKind: 'stale_metadata',
+ runtimeDiagnostic: stalePidReason,
+ runtimeDiagnosticSeverity: 'warning',
+ firstSpawnAcceptedAt: '2026-06-06T09:49:08.513Z',
+ runtimeLastSeenAt: '2026-06-06T09:51:18.924Z',
+ lastEvaluatedAt: '2026-06-06T09:51:18.924Z',
+ },
+ 'business-reviewer-beta': {
+ providerId: 'codex',
+ model: 'gpt-5.4',
+ laneId: 'primary',
+ laneKind: 'primary',
+ laneOwnerProviderId: 'codex',
+ launchState: 'failed_to_start',
+ agentToolAccepted: true,
+ runtimeAlive: false,
+ runtimePid: 55_336,
+ runtimeRunId: bootstrapRunId,
+ bootstrapConfirmed: false,
+ hardFailure: true,
+ hardFailureReason: submittedTimeoutReason,
+ livenessKind: 'stale_metadata',
+ runtimeDiagnostic: stalePidReason,
+ runtimeDiagnosticSeverity: 'warning',
+ firstSpawnAcceptedAt: '2026-06-06T09:49:11.249Z',
+ runtimeLastSeenAt: '2026-06-06T09:51:18.923Z',
+ lastEvaluatedAt: '2026-06-06T09:52:27.567Z',
+ },
+ 'ux-reviewer-beta': {
+ providerId: 'codex',
+ model: 'gpt-5.4',
+ laneId: 'primary',
+ laneKind: 'primary',
+ laneOwnerProviderId: 'codex',
+ launchState: 'confirmed_alive',
+ agentToolAccepted: true,
+ runtimeAlive: true,
+ runtimePid: 2_124,
+ runtimeRunId: bootstrapRunId,
+ bootstrapConfirmed: true,
+ hardFailure: false,
+ firstSpawnAcceptedAt: '2026-06-06T09:49:34.626Z',
+ lastHeartbeatAt: '2026-06-06T09:50:52.188Z',
+ lastRuntimeAliveAt: '2026-06-06T09:59:23.165Z',
+ lastEvaluatedAt: '2026-06-06T09:59:23.165Z',
+ },
+ },
+ { launchPhase: 'finished', updatedAt: '2026-06-06T09:59:23.165Z' }
+ );
+ writeBootstrapState(
+ teamName,
+ [
+ {
+ name: 'business-reviewer-alpha',
+ status: 'bootstrap_confirmed',
+ lastAttemptAt: Date.parse('2026-06-06T09:49:06.287Z'),
+ lastObservedAt: Date.parse('2026-06-06T09:51:53.070Z'),
+ },
+ {
+ name: 'business-reviewer-beta',
+ status: 'failed',
+ lastAttemptAt: Date.parse('2026-06-06T09:49:08.512Z'),
+ lastObservedAt: Date.parse('2026-06-06T09:52:11.246Z'),
+ failureReason: submittedTimeoutReason,
+ },
+ {
+ name: 'ux-reviewer-beta',
+ status: 'bootstrap_confirmed',
+ lastAttemptAt: Date.parse('2026-06-06T09:49:30.803Z'),
+ lastObservedAt: Date.parse('2026-06-06T09:50:52.188Z'),
+ },
+ ],
+ '2026-06-06T09:59:43.154Z',
+ { runId: bootstrapRunId }
+ );
+
+ const svc = new TeamProvisioningService();
+ privateHarness(svc).getLiveTeamAgentRuntimeMetadata = vi.fn(
+ async () =>
+ new Map([
+ [
+ 'business-reviewer-alpha',
+ {
+ alive: false,
+ backendType: 'process',
+ providerId: 'codex',
+ livenessKind: 'stale_metadata',
+ pidSource: 'persisted_metadata',
+ runtimeDiagnostic: stalePidReason,
+ runtimeDiagnosticSeverity: 'warning',
+ metricsPid: 21_580,
+ model: 'gpt-5.5',
+ },
+ ],
+ [
+ 'business-reviewer-beta',
+ {
+ alive: false,
+ backendType: 'process',
+ providerId: 'codex',
+ livenessKind: 'stale_metadata',
+ pidSource: 'persisted_metadata',
+ runtimeDiagnostic: stalePidReason,
+ runtimeDiagnosticSeverity: 'warning',
+ metricsPid: 55_336,
+ model: 'gpt-5.4',
+ },
+ ],
+ [
+ 'ux-reviewer-beta',
+ {
+ alive: true,
+ backendType: 'process',
+ providerId: 'codex',
+ livenessKind: 'runtime_process',
+ pidSource: 'process_table',
+ metricsPid: 2_124,
+ model: 'gpt-5.4',
+ },
+ ],
+ ])
+ );
+
+ const result = await svc.getMemberSpawnStatuses(teamName);
+
+ expect(result.teamLaunchState).toBe('partial_failure');
+ expect(result.statuses['business-reviewer-alpha']).toMatchObject({
+ status: 'online',
+ launchState: 'confirmed_alive',
+ bootstrapConfirmed: true,
+ livenessKind: 'confirmed_bootstrap',
+ hardFailure: false,
+ });
+ expect(result.statuses['business-reviewer-alpha']?.hardFailureReason).toBeUndefined();
+ expect(result.statuses['business-reviewer-alpha']?.runtimeDiagnostic).toBeUndefined();
+ expect(result.statuses['business-reviewer-beta']).toMatchObject({
+ status: 'error',
+ launchState: 'failed_to_start',
+ bootstrapConfirmed: false,
+ hardFailure: true,
+ hardFailureReason: submittedTimeoutReason,
+ });
+ expect(result.statuses['ux-reviewer-beta']).toMatchObject({
+ status: 'online',
+ launchState: 'confirmed_alive',
+ bootstrapConfirmed: true,
+ hardFailure: false,
+ });
+ });
+
it('refreshes cached bootstrap transcript outcome when the transcript file changes', async () => {
const teamName = 'zz-unit-bootstrap-transcript-cache-refresh';
const memberName = 'tom';
diff --git a/test/main/services/team/TeamProvisioningServiceCodexPreflight.test.ts b/test/main/services/team/TeamProvisioningServiceCodexPreflight.test.ts
index 8fc59037..6e3cacc8 100644
--- a/test/main/services/team/TeamProvisioningServiceCodexPreflight.test.ts
+++ b/test/main/services/team/TeamProvisioningServiceCodexPreflight.test.ts
@@ -39,7 +39,7 @@ import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
import { TeamProvisioningService } from '@main/services/team/TeamProvisioningService';
import { resolveInteractiveShellEnvBestEffort } from '@main/utils/shellEnv';
-type CodexProbeHarness = TeamProvisioningService & {
+type CodexProbeHarness = {
probeClaudeRuntime: (
claudePath: string,
cwd: string,
@@ -89,9 +89,19 @@ describe('TeamProvisioningService Codex create-team preflight', () => {
it('uses refreshed Codex provider env for both runtime probe and deep one-shot preflight', async () => {
const service = new TeamProvisioningService();
const harness = service as unknown as CodexProbeHarness;
- const probeClaudeRuntime = vi.spyOn(harness, 'probeClaudeRuntime').mockResolvedValue({});
+ const probeClaudeRuntime = vi
+ .spyOn(
+ harness as unknown as { probeClaudeRuntime: CodexProbeHarness['probeClaudeRuntime'] },
+ 'probeClaudeRuntime'
+ )
+ .mockResolvedValue({});
const runProviderOneShotDiagnostic = vi
- .spyOn(harness, 'runProviderOneShotDiagnostic')
+ .spyOn(
+ harness as unknown as {
+ runProviderOneShotDiagnostic: CodexProbeHarness['runProviderOneShotDiagnostic'];
+ },
+ 'runProviderOneShotDiagnostic'
+ )
.mockResolvedValue({});
const result = await service.prepareForProvisioning(tempRoot, {
diff --git a/test/main/services/team/TeamProvisioningServicePrepare.test.ts b/test/main/services/team/TeamProvisioningServicePrepare.test.ts
index 531fac9e..453c2685 100644
--- a/test/main/services/team/TeamProvisioningServicePrepare.test.ts
+++ b/test/main/services/team/TeamProvisioningServicePrepare.test.ts
@@ -1,5 +1,8 @@
import { buildCodexWorkspaceTrustSettingsArgs } from '@features/workspace-trust/core/domain';
-import { OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE } from '@shared/utils/openCodeWindowsAccessDenied';
+import {
+ OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE,
+ OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE,
+} from '@shared/utils/openCodeWindowsAccessDenied';
import { DEFAULT_PROVIDER_MODEL_SELECTION } from '@shared/utils/providerModelSelection';
import { spawn } from 'child_process';
import * as fs from 'fs';
@@ -1026,6 +1029,42 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
expect(result.warnings).toEqual([OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE]);
});
+ it('keeps OpenCode managed node_modules symlink EPERM diagnostics specific during prepareForProvisioning', async () => {
+ const prepare = vi.fn(async () => ({
+ ok: false as const,
+ providerId: 'opencode' as const,
+ reason: 'unknown_error',
+ retryable: false,
+ diagnostics: [],
+ warnings: [
+ [
+ 'Runtime provider management command failed unexpectedly:',
+ "EPERM: operation not permitted, symlink 'C:\\Users\\ben\\AppData\\Local\\claude-multimodel-nodejs\\Cache\\opencode\\shared-cache\\config-node_modules'",
+ "-> 'C:\\Users\\ben\\AppData\\Local\\claude-multimodel-nodejs\\Data\\opencode\\profiles\\abc123\\config\\opencode\\node_modules'",
+ ].join(' '),
+ ],
+ }));
+ const adapter: TeamLaunchRuntimeAdapter = {
+ providerId: 'opencode',
+ prepare,
+ launch: vi.fn(),
+ reconcile: vi.fn(),
+ stop: vi.fn(),
+ };
+ const registry = new TeamRuntimeAdapterRegistry([adapter]);
+ const svc = new TeamProvisioningService();
+ svc.setRuntimeAdapterRegistry(registry);
+
+ const result = await svc.prepareForProvisioning(tempRoot, {
+ providerId: 'opencode',
+ forceFresh: true,
+ });
+
+ expect(result.ready).toBe(false);
+ expect(result.message).toBe(OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE);
+ expect(result.warnings).toEqual([OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE]);
+ });
+
it('keeps OpenCode access-denied selected-model failures provider-scoped', async () => {
const prepare = vi.fn(async () => ({
ok: false as const,
@@ -4517,7 +4556,7 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
});
});
- it('rejects explicit Codex Fast before launch when auth or model eligibility is invalid', () => {
+ it('allows explicit Codex Fast to downgrade before launch when auth or model eligibility is invalid', () => {
const svc = new TeamProvisioningService();
const facts = {
defaultModel: 'gpt-5.4-mini',
@@ -4585,7 +4624,27 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
fastMode: 'on',
facts,
})
- ).toThrow('enables Codex Fast mode');
+ ).not.toThrow();
+
+ expect(
+ (svc as any).buildProviderModelLaunchIdentity({
+ request: {
+ providerId: 'codex',
+ providerBackendId: 'codex-native',
+ model: 'gpt-5.4-mini',
+ fastMode: 'on',
+ },
+ facts,
+ })
+ ).toMatchObject({
+ providerId: 'codex',
+ providerBackendId: 'codex-native',
+ selectedModel: 'gpt-5.4-mini',
+ resolvedLaunchModel: 'gpt-5.4-mini',
+ selectedFastMode: 'on',
+ resolvedFastMode: false,
+ fastResolutionReason: expect.stringContaining('API key mode uses standard API pricing'),
+ });
});
it('rejects Anthropic max and fast when the exact resolved launch model does not support them', () => {
diff --git a/test/main/services/team/TeamProvisioningServiceRelay.test.ts b/test/main/services/team/TeamProvisioningServiceRelay.test.ts
index bd68f65f..b05eb39a 100644
--- a/test/main/services/team/TeamProvisioningServiceRelay.test.ts
+++ b/test/main/services/team/TeamProvisioningServiceRelay.test.ts
@@ -1664,6 +1664,66 @@ Messages:
expect(payload).not.toContain('MessageId: m-ordinary-11');
});
+ it('keeps native member work-sync rows unread without accepted report proof', async () => {
+ const service = new TeamProvisioningService();
+ const teamName = 'my-team';
+ seedConfig(teamName);
+ service.setMemberWorkSyncAcceptedReportChecker(async () => false);
+ seedMemberInbox(teamName, 'alice', [
+ {
+ from: 'system',
+ text: 'Call member_work_sync_status, then member_work_sync_report.',
+ timestamp: '2026-02-23T10:00:00.000Z',
+ read: false,
+ messageId: 'm-work-sync-unproved',
+ messageKind: 'member_work_sync_nudge',
+ workSyncIntent: 'agenda_sync',
+ },
+ ]);
+
+ const { writeSpy } = attachAliveRun(service, teamName);
+ const firstRelayed = await service.relayMemberInboxMessages(teamName, 'alice');
+ const rowsAfterFirst = JSON.parse(
+ hoisted.files.get(`/mock/teams/${teamName}/inboxes/alice.json`) ?? '[]'
+ ) as Array<{ read?: boolean }>;
+
+ expect(firstRelayed).toBe(1);
+ expect(rowsAfterFirst[0]?.read).toBe(false);
+
+ const secondRelayed = await service.relayMemberInboxMessages(teamName, 'alice');
+
+ expect(secondRelayed).toBe(1);
+ expect(writeSpy).toHaveBeenCalledTimes(2);
+ });
+
+ it('read-commits native member work-sync rows after accepted report proof', async () => {
+ const service = new TeamProvisioningService();
+ const teamName = 'my-team';
+ seedConfig(teamName);
+ service.setMemberWorkSyncAcceptedReportChecker(async () => true);
+ seedMemberInbox(teamName, 'alice', [
+ {
+ from: 'system',
+ text: 'Call member_work_sync_status, then member_work_sync_report.',
+ timestamp: '2026-02-23T10:00:00.000Z',
+ read: false,
+ messageId: 'm-work-sync-proved',
+ messageKind: 'member_work_sync_nudge',
+ workSyncIntent: 'agenda_sync',
+ },
+ ]);
+
+ attachAliveRun(service, teamName);
+ const relayed = await service.relayMemberInboxMessages(teamName, 'alice');
+ const rows = JSON.parse(
+ hoisted.files.get(`/mock/teams/${teamName}/inboxes/alice.json`) ?? '[]'
+ ) as Array<{ read?: boolean }>;
+
+ expect(relayed).toBe(1);
+ expect(rows[0]?.read).toBe(true);
+ await expect(service.relayMemberInboxMessages(teamName, 'alice')).resolves.toBe(0);
+ });
+
it('retries a work-sync nudge after member relay times out before stdin write completes', async () => {
vi.useFakeTimers();
const service = new TeamProvisioningService();
@@ -4170,7 +4230,7 @@ Messages:
expect(rows[0].read).toBe(true);
});
- it('leaves OpenCode lead inbox rows unread with an explicit unsupported diagnostic', async () => {
+ it('routes OpenCode lead inbox rows through OpenCode member relay', async () => {
const service = new TeamProvisioningService();
const teamName = 'my-team';
hoisted.files.set(
@@ -4198,19 +4258,50 @@ Messages:
messageId: 'opencode-lead-unread-1',
},
]);
+ const relaySpy = vi.spyOn(service, 'relayOpenCodeMemberInboxMessages').mockResolvedValue({
+ relayed: 1,
+ attempted: 1,
+ delivered: 1,
+ failed: 0,
+ diagnostics: ['fake OpenCode lead relay ready'],
+ lastDelivery: {
+ delivered: true,
+ accepted: true,
+ responsePending: false,
+ },
+ });
- const relay = await service.relayInboxFileToLiveRecipient(teamName, 'team-lead');
+ const relay = await service.relayInboxFileToLiveRecipient(teamName, 'team-lead', {
+ onlyMessageId: 'opencode-lead-unread-1',
+ source: 'ui-send',
+ deliveryMetadata: {
+ replyRecipient: 'user',
+ actionMode: 'do',
+ },
+ });
- expect(relay).toMatchObject({ kind: 'opencode_lead_unsupported', relayed: 0 });
- expect(relay.diagnostics?.join('\n')).toContain('opencode_lead_runtime_session_missing');
- expect(vi.mocked(console.warn).mock.calls[0]?.join(' ')).toContain(
- 'opencode_lead_runtime_session_missing'
+ expect(relay).toMatchObject({
+ kind: 'opencode_member',
+ relayed: 1,
+ diagnostics: ['fake OpenCode lead relay ready'],
+ lastDelivery: {
+ delivered: true,
+ accepted: true,
+ responsePending: false,
+ },
+ });
+ expect(relaySpy).toHaveBeenCalledWith(
+ teamName,
+ 'team-lead',
+ expect.objectContaining({
+ onlyMessageId: 'opencode-lead-unread-1',
+ source: 'ui-send',
+ deliveryMetadata: expect.objectContaining({
+ replyRecipient: 'user',
+ actionMode: 'do',
+ }),
+ })
);
- vi.mocked(console.warn).mockClear();
- const rows = JSON.parse(
- hoisted.files.get(`/mock/teams/${teamName}/inboxes/team-lead.json`) ?? '[]'
- );
- expect(rows[0].read).toBe(false);
});
it('keeps failed OpenCode member inbox relay rows unread for retry', async () => {
diff --git a/test/main/services/team/TeamRuntimeLivenessResolverCommandParsing.test.ts b/test/main/services/team/TeamRuntimeLivenessResolverCommandParsing.test.ts
new file mode 100644
index 00000000..f82b2746
--- /dev/null
+++ b/test/main/services/team/TeamRuntimeLivenessResolverCommandParsing.test.ts
@@ -0,0 +1,24 @@
+import {
+ commandArgEquals,
+ extractCliArgValues,
+} from '@main/services/team/TeamRuntimeLivenessResolver';
+import { describe, expect, it } from 'vitest';
+
+describe('team runtime liveness command parsing', () => {
+ it('keeps cached extracted values isolated from caller mutation', () => {
+ const command = 'node runtime --team-name demo --agent-id agent-alice';
+
+ const firstValues = extractCliArgValues(command, '--agent-id');
+ firstValues.push('mutated-agent');
+
+ expect(extractCliArgValues(command, '--agent-id')).toEqual(['agent-alice']);
+ });
+
+ it('caches command arg equality without changing quoted value matching', () => {
+ const command = 'node runtime --team-name "demo team" --agent-id agent-alice';
+
+ expect(commandArgEquals(command, '--team-name', 'demo team')).toBe(true);
+ expect(commandArgEquals(command, '--team-name', 'other team')).toBe(false);
+ expect(commandArgEquals(command, '--agent-id', 'agent-alice')).toBe(true);
+ });
+});
diff --git a/test/main/services/team/TeamTaskActivityIntervalService.test.ts b/test/main/services/team/TeamTaskActivityIntervalService.test.ts
index da36b85f..57150d1a 100644
--- a/test/main/services/team/TeamTaskActivityIntervalService.test.ts
+++ b/test/main/services/team/TeamTaskActivityIntervalService.test.ts
@@ -1165,4 +1165,350 @@ describe('TeamTaskActivityIntervalService', () => {
expect(result).toEqual({ changedTasks: 0, failed: true });
});
+
+ describe('resumeActiveIntervalsForMembers (batch)', () => {
+ it('returns zero changes for an empty members list without scanning the tasks directory', () => {
+ const service = new TeamTaskActivityIntervalService();
+ const lockSpy = vi.spyOn(
+ service as unknown as {
+ mutateTeamTasksWithLock: (
+ teamName: string,
+ run: () => { changedTasks: number; failed?: boolean }
+ ) => { changedTasks: number; failed?: boolean };
+ },
+ 'mutateTeamTasksWithLock'
+ );
+
+ const result = service.resumeActiveIntervalsForMembers(
+ 'alpha',
+ [],
+ '2026-05-08T10:20:00.000Z'
+ );
+
+ expect(result).toEqual({ changedTasks: 0 });
+ expect(lockSpy).not.toHaveBeenCalled();
+ });
+
+ it('returns zero changes when all member names are blank', () => {
+ const service = new TeamTaskActivityIntervalService();
+ const mutateSpy = vi.spyOn(
+ service as unknown as {
+ mutateTeamTasks: TeamTaskActivityIntervalService['resumeActiveIntervalsForMember'];
+ },
+ 'mutateTeamTasks'
+ );
+
+ const result = service.resumeActiveIntervalsForMembers(
+ 'alpha',
+ ['', ' ', null as unknown as string],
+ '2026-05-08T10:20:00.000Z'
+ );
+
+ expect(result).toEqual({ changedTasks: 0 });
+ expect(mutateSpy).not.toHaveBeenCalled();
+ });
+
+ it('produces the same per-task result as sequential single-member calls', async () => {
+ const baseTime = '2026-05-08T10:00:00.000Z';
+ const resumeAt = '2026-05-08T10:20:00.000Z';
+
+ async function seed(teamName: string): Promise {
+ await writeTask(teamName, {
+ id: 'work-bob',
+ subject: 'Bob work',
+ owner: 'bob',
+ status: 'in_progress',
+ workIntervals: [{ startedAt: baseTime, completedAt: '2026-05-08T10:05:00.000Z' }],
+ historyEvents: [],
+ });
+ await writeTask(teamName, {
+ id: 'work-tom',
+ subject: 'Tom work',
+ owner: 'tom',
+ status: 'in_progress',
+ workIntervals: [{ startedAt: baseTime, completedAt: '2026-05-08T10:05:00.000Z' }],
+ historyEvents: [],
+ });
+ await writeTask(teamName, {
+ id: 'review-task',
+ subject: 'Review',
+ owner: 'alice',
+ status: 'completed',
+ reviewIntervals: [
+ {
+ reviewer: 'bob',
+ startedAt: '2026-05-08T10:06:00.000Z',
+ completedAt: '2026-05-08T10:08:00.000Z',
+ },
+ ],
+ historyEvents: [
+ {
+ id: 'event-review-started',
+ type: 'review_started',
+ timestamp: '2026-05-08T10:06:00.000Z',
+ actor: 'bob',
+ },
+ ],
+ });
+ }
+
+ await seed('seq');
+ const seqService = new TeamTaskActivityIntervalService();
+ seqService.resumeActiveIntervalsForMember('seq', 'bob', resumeAt);
+ seqService.resumeActiveIntervalsForMember('seq', 'tom', resumeAt);
+
+ await seed('batch');
+ const batchService = new TeamTaskActivityIntervalService();
+ const batchResult = batchService.resumeActiveIntervalsForMembers(
+ 'batch',
+ ['bob', 'tom'],
+ resumeAt
+ );
+
+ expect(batchResult.changedTasks).toBe(3);
+ for (const id of ['work-bob', 'work-tom', 'review-task']) {
+ const seqTask = await readTask('seq', id);
+ const batchTask = await readTask('batch', id);
+ expect(batchTask.workIntervals).toEqual(seqTask.workIntervals);
+ expect(batchTask.reviewIntervals).toEqual(seqTask.reviewIntervals);
+ expect(batchTask.status).toBe(seqTask.status);
+ }
+ });
+
+ it('acquires the team lock once regardless of the number of members', async () => {
+ await writeTask('alpha', {
+ id: 'task-bob',
+ subject: 'Bob',
+ owner: 'bob',
+ status: 'in_progress',
+ workIntervals: [],
+ historyEvents: [
+ {
+ id: 'event-work-started-bob',
+ type: 'status_changed',
+ from: 'pending',
+ to: 'in_progress',
+ timestamp: '2026-05-08T10:00:00.000Z',
+ actor: 'bob',
+ },
+ ],
+ });
+ await writeTask('alpha', {
+ id: 'task-tom',
+ subject: 'Tom',
+ owner: 'tom',
+ status: 'in_progress',
+ workIntervals: [],
+ historyEvents: [
+ {
+ id: 'event-work-started-tom',
+ type: 'status_changed',
+ from: 'pending',
+ to: 'in_progress',
+ timestamp: '2026-05-08T10:01:00.000Z',
+ actor: 'tom',
+ },
+ ],
+ });
+ await writeTask('alpha', {
+ id: 'task-zoe',
+ subject: 'Zoe',
+ owner: 'zoe',
+ status: 'in_progress',
+ workIntervals: [],
+ historyEvents: [
+ {
+ id: 'event-work-started-zoe',
+ type: 'status_changed',
+ from: 'pending',
+ to: 'in_progress',
+ timestamp: '2026-05-08T10:02:00.000Z',
+ actor: 'zoe',
+ },
+ ],
+ });
+
+ const service = new TeamTaskActivityIntervalService();
+ const lockSpy = vi.spyOn(
+ service as unknown as {
+ mutateTeamTasksWithLock: (
+ teamName: string,
+ run: () => { changedTasks: number; failed?: boolean }
+ ) => { changedTasks: number; failed?: boolean };
+ },
+ 'mutateTeamTasksWithLock'
+ );
+
+ const result = service.resumeActiveIntervalsForMembers(
+ 'alpha',
+ ['bob', 'tom', 'zoe'],
+ '2026-05-08T10:20:00.000Z'
+ );
+
+ expect(lockSpy).toHaveBeenCalledTimes(1);
+ expect(result.changedTasks).toBe(3);
+ });
+
+ it('deduplicates member names that normalize to the same key', async () => {
+ await writeTask('alpha', {
+ id: 'work-task',
+ subject: 'Build',
+ owner: 'bob',
+ status: 'in_progress',
+ workIntervals: [
+ { startedAt: '2026-05-08T10:00:00.000Z', completedAt: '2026-05-08T10:05:00.000Z' },
+ ],
+ historyEvents: [],
+ });
+
+ const result = new TeamTaskActivityIntervalService().resumeActiveIntervalsForMembers(
+ 'alpha',
+ ['bob', 'BOB', ' bob '],
+ '2026-05-08T10:20:00.000Z'
+ );
+ const task = await readTask('alpha', 'work-task');
+
+ expect(result.changedTasks).toBe(1);
+ expect(task.workIntervals).toEqual([
+ { startedAt: '2026-05-08T10:00:00.000Z', completedAt: '2026-05-08T10:05:00.000Z' },
+ { startedAt: '2026-05-08T10:20:00.000Z' },
+ ]);
+ });
+
+ it('does not write task files when no listed member matches any task', async () => {
+ await writeTask('alpha', {
+ id: 'work-bob',
+ subject: 'Bob work',
+ owner: 'bob',
+ status: 'in_progress',
+ workIntervals: [{ startedAt: '2026-05-08T10:00:00.000Z' }],
+ historyEvents: [],
+ });
+
+ const taskPath = path.join(tempDir, 'tasks', 'alpha', 'work-bob.json');
+ const beforeMtime = (await fs.stat(taskPath)).mtimeMs;
+ const beforeContents = await fs.readFile(taskPath, 'utf8');
+
+ const result = new TeamTaskActivityIntervalService().resumeActiveIntervalsForMembers(
+ 'alpha',
+ ['no-such-member', 'another-ghost'],
+ '2026-05-08T10:20:00.000Z'
+ );
+
+ const afterMtime = (await fs.stat(taskPath)).mtimeMs;
+ const afterContents = await fs.readFile(taskPath, 'utf8');
+
+ expect(result.changedTasks).toBe(0);
+ expect(afterMtime).toBe(beforeMtime);
+ expect(afterContents).toBe(beforeContents);
+ });
+
+ it('opens at most one work interval per task even when the owner is listed twice', async () => {
+ await writeTask('alpha', {
+ id: 'work-task',
+ subject: 'Build',
+ owner: 'bob',
+ status: 'in_progress',
+ workIntervals: [
+ { startedAt: '2026-05-08T10:00:00.000Z', completedAt: '2026-05-08T10:05:00.000Z' },
+ ],
+ historyEvents: [],
+ });
+
+ const result = new TeamTaskActivityIntervalService().resumeActiveIntervalsForMembers(
+ 'alpha',
+ ['bob', 'bob'],
+ '2026-05-08T10:20:00.000Z'
+ );
+ const task = await readTask('alpha', 'work-task');
+
+ expect(result.changedTasks).toBe(1);
+ expect(task.workIntervals).toEqual([
+ { startedAt: '2026-05-08T10:00:00.000Z', completedAt: '2026-05-08T10:05:00.000Z' },
+ { startedAt: '2026-05-08T10:20:00.000Z' },
+ ]);
+ });
+
+ it('returns 0 changes for a non-existent team directory', async () => {
+ const result = new TeamTaskActivityIntervalService().resumeActiveIntervalsForMembers(
+ 'ghost-team',
+ ['bob'],
+ '2026-05-08T10:20:00.000Z'
+ );
+
+ expect(result).toEqual({ changedTasks: 0 });
+ });
+
+ it('reports failure when task directory cannot be scanned', async () => {
+ await fs.mkdir(path.join(tempDir, 'tasks'), { recursive: true });
+ await fs.writeFile(path.join(tempDir, 'tasks', 'alpha'), 'not a directory', 'utf8');
+
+ const result = new TeamTaskActivityIntervalService().resumeActiveIntervalsForMembers(
+ 'alpha',
+ ['bob'],
+ '2026-05-08T10:20:00.000Z'
+ );
+
+ expect(result).toEqual({ changedTasks: 0, failed: true });
+ });
+
+ it('skips malformed task JSON files and still applies updates to valid ones', async () => {
+ await writeTask('alpha', {
+ id: 'good-task',
+ subject: 'Build',
+ owner: 'bob',
+ status: 'in_progress',
+ workIntervals: [
+ { startedAt: '2026-05-08T10:00:00.000Z', completedAt: '2026-05-08T10:05:00.000Z' },
+ ],
+ historyEvents: [],
+ });
+ await fs.writeFile(
+ path.join(tempDir, 'tasks', 'alpha', 'broken-task.json'),
+ '{ this is not valid json',
+ 'utf8'
+ );
+
+ const result = new TeamTaskActivityIntervalService().resumeActiveIntervalsForMembers(
+ 'alpha',
+ ['bob'],
+ '2026-05-08T10:20:00.000Z'
+ );
+ const goodTask = await readTask('alpha', 'good-task');
+
+ expect(result.changedTasks).toBe(1);
+ expect(goodTask.workIntervals).toEqual([
+ { startedAt: '2026-05-08T10:00:00.000Z', completedAt: '2026-05-08T10:05:00.000Z' },
+ { startedAt: '2026-05-08T10:20:00.000Z' },
+ ]);
+ });
+
+ it('resumes single-member intervals through the member noop-cache path', async () => {
+ await writeTask('alpha', {
+ id: 'work-task',
+ subject: 'Build',
+ owner: 'bob',
+ status: 'in_progress',
+ workIntervals: [
+ { startedAt: '2026-05-08T10:00:00.000Z', completedAt: '2026-05-08T10:05:00.000Z' },
+ ],
+ historyEvents: [],
+ });
+
+ const service = new TeamTaskActivityIntervalService();
+
+ const result = service.resumeActiveIntervalsForMember(
+ 'alpha',
+ 'bob',
+ '2026-05-08T10:20:00.000Z'
+ );
+ const task = await readTask('alpha', 'work-task');
+
+ expect(result.changedTasks).toBe(1);
+ expect(task.workIntervals).toEqual([
+ { startedAt: '2026-05-08T10:00:00.000Z', completedAt: '2026-05-08T10:05:00.000Z' },
+ { startedAt: '2026-05-08T10:20:00.000Z' },
+ ]);
+ });
+ });
});
diff --git a/test/main/utils/childProcess.test.ts b/test/main/utils/childProcess.test.ts
index 3ed089db..579c3eb0 100644
--- a/test/main/utils/childProcess.test.ts
+++ b/test/main/utils/childProcess.test.ts
@@ -199,13 +199,18 @@ describe('cli child process helpers', () => {
env: { FOO: 'bar' },
});
expect(spawnMock).toHaveBeenCalledTimes(2);
- const secondArg0 = spawnMock.mock.calls[1][0] as string;
- expect(secondArg0).toMatch(/claude\.exe/);
- expect(spawnMock.mock.calls[1][1]).toMatchObject({ shell: true, env: { FOO: 'bar' } });
+ expect(spawnMock.mock.calls[1][0]).toMatch(/cmd\.exe$/i);
+ expect(spawnMock.mock.calls[1][1]).toEqual([
+ '/d',
+ '/s',
+ '/c',
+ expect.stringMatching(/claude\.exe/),
+ ]);
+ expect(spawnMock.mock.calls[1][2]).toMatchObject({ shell: false, env: { FOO: 'bar' } });
expect(result).toBe(fake);
});
- it('uses shell directly for Windows cmd launchers', () => {
+ it('uses cmd.exe directly for Windows cmd launcher shell fallback', () => {
setPlatform('win32');
const fake = createMockProcess();
const spawnMock = child.spawn as unknown as Mock;
@@ -213,8 +218,14 @@ describe('cli child process helpers', () => {
const result = spawnCli('C:\\runtime\\cli-dev.cmd', ['--version']);
expect(spawnMock).toHaveBeenCalledTimes(1);
- expect(spawnMock.mock.calls[0][0]).toContain('cli-dev.cmd');
- expect(spawnMock.mock.calls[0][1]).toMatchObject({ shell: true });
+ expect(spawnMock.mock.calls[0][0]).toMatch(/cmd\.exe$/i);
+ expect(spawnMock.mock.calls[0][1]).toEqual([
+ '/d',
+ '/s',
+ '/c',
+ expect.stringContaining('cli-dev.cmd'),
+ ]);
+ expect(spawnMock.mock.calls[0][2]).toMatchObject({ shell: false });
expect(result).toBe(fake);
});
@@ -282,14 +293,66 @@ describe('cli child process helpers', () => {
const result = spawnCli('C:\\Users\\Алексей\\AppData\\Roaming\\npm\\claude.cmd', ['a', 'b'], {
env: { FOO: 'bar' },
});
- // Non-ASCII detected upfront — single spawn call with shell: true
+ // Non-ASCII detected upfront, so launch through cmd.exe fallback once.
expect(spawnMock).toHaveBeenCalledTimes(1);
- const shellCmd = spawnMock.mock.calls[0][0] as string;
+ expect(spawnMock.mock.calls[0][0]).toMatch(/cmd\.exe$/i);
+ const shellCmd = spawnMock.mock.calls[0][1][3] as string;
expect(shellCmd).toMatch(/claude\.cmd/);
- expect(spawnMock.mock.calls[0][1]).toMatchObject({ shell: true, env: { FOO: 'bar' } });
+ expect(spawnMock.mock.calls[0][2]).toMatchObject({ shell: false, env: { FOO: 'bar' } });
expect(result).toBe(fake);
});
+ it('rejects control characters only when Windows shell fallback is needed', () => {
+ setPlatform('win32');
+ const spawnMock = child.spawn as unknown as Mock;
+ spawnMock.mockReturnValue(createMockProcess());
+
+ for (const unsafeArg of [
+ 'safe\0bad',
+ 'safe\rbad',
+ 'safe\nbad',
+ 'safe\u001fbad',
+ 'safe\u0085bad',
+ ]) {
+ expect(() => spawnCli('C:\\Users\\Алексей\\bin\\claude.cmd', [unsafeArg])).toThrow(
+ 'control characters are not allowed'
+ );
+ }
+ expect(spawnMock).not.toHaveBeenCalled();
+
+ spawnCli('C:\\bin\\claude.exe', ['safe\nargv']);
+ expect(spawnMock.mock.calls[0][0]).toBe('C:\\bin\\claude.exe');
+ expect(spawnMock.mock.calls[0][1]).toEqual(['safe\nargv']);
+ expect(spawnMock.mock.calls[0][2]).not.toHaveProperty('shell');
+ });
+
+ it('quotes shell metacharacters when Windows shell fallback is needed', () => {
+ setPlatform('win32');
+ const spawnMock = child.spawn as unknown as Mock;
+ spawnMock.mockReturnValue(createMockProcess());
+
+ expect(() =>
+ spawnCli('C:\\Users\\R&D\\bin\\claude.cmd', [
+ 'safe&bad',
+ 'safe|bad',
+ 'safebad',
+ 'safe^bad',
+ ])
+ ).not.toThrow();
+ expect(spawnMock).toHaveBeenCalledTimes(1);
+ const shellCmd = spawnMock.mock.calls[0][1][3] as string;
+ expect(shellCmd).toContain('"C:\\Users\\R&D\\bin\\claude.cmd"');
+ for (const shellArg of ['safe&bad', 'safe|bad', 'safebad', 'safe^bad']) {
+ expect(shellCmd).toContain(`"${shellArg}"`);
+ }
+
+ spawnCli('C:\\bin\\claude.exe', ['safe&argv']);
+ expect(spawnMock.mock.calls[1][0]).toBe('C:\\bin\\claude.exe');
+ expect(spawnMock.mock.calls[1][1]).toEqual(['safe&argv']);
+ expect(spawnMock.mock.calls[1][2]).not.toHaveProperty('shell');
+ });
+
it('does not use shell when not on windows', () => {
setPlatform('linux');
const fake = createMockProcess();
@@ -387,18 +450,25 @@ describe('cli child process helpers', () => {
expect(execFileMock.mock.calls[1][2]).toMatchObject({ windowsHide: false });
});
- it('skips straight to shell for Windows cmd launchers', async () => {
+ it('skips straight to cmd.exe fallback for Windows cmd launchers', async () => {
setPlatform('win32');
const execFileMock = child.execFile as unknown as Mock;
const execMock = child.exec as unknown as Mock;
- execMock.mockImplementation((_cmd: string, _opts: unknown, cb: ExecCallback) => {
+ execFileMock.mockImplementation(
+ (_cmd: string, _args: string[], _opts: unknown, cb: ExecCallback) => {
cb(null, '0.0.8', '');
return createMockProcess();
- });
+ }
+ );
const result = await execCli('C:\\runtime\\cli-dev.cmd', ['--version']);
- expect(execFileMock).not.toHaveBeenCalled();
- expect(execMock).toHaveBeenCalled();
+ expect(execFileMock).toHaveBeenCalledWith(
+ expect.stringMatching(/cmd\.exe$/i),
+ ['/d', '/s', '/c', expect.stringContaining('cli-dev.cmd')],
+ expect.any(Object),
+ expect.any(Function)
+ );
+ expect(execMock).not.toHaveBeenCalled();
expect(result.stdout).toBe('0.0.8');
});
@@ -429,19 +499,22 @@ describe('cli child process helpers', () => {
setPlatform('win32');
const execFileMock = child.execFile as unknown as Mock;
const execMock = child.exec as unknown as Mock;
- execMock.mockImplementation((_cmd: string, _opts: unknown, cb: ExecCallback) => {
+ execFileMock.mockImplementation(
+ (_cmd: string, _args: string[], _opts: unknown, cb: ExecCallback) => {
cb(null, 'ok', '');
return createMockProcess();
- });
+ }
+ );
const { dir, launcher } = createGeneratedBunLauncher();
try {
const result = await execCli(launcher, ['runtime', 'opencode-command'], {
preferShellForWindowsBatch: true,
});
- expect(execFileMock).not.toHaveBeenCalled();
- expect(execMock).toHaveBeenCalledTimes(1);
- expect(execMock.mock.calls[0][0]).toContain('runtime');
- expect(execMock.mock.calls[0][0]).toContain('opencode-command');
+ expect(execFileMock).toHaveBeenCalledTimes(1);
+ expect(execFileMock.mock.calls[0][0]).toMatch(/cmd\.exe$/i);
+ expect(execFileMock.mock.calls[0][1][3]).toContain('runtime');
+ expect(execFileMock.mock.calls[0][1][3]).toContain('opencode-command');
+ expect(execMock).not.toHaveBeenCalled();
expect(result.stdout).toBe('ok');
} finally {
rmSync(dir, { recursive: true, force: true });
@@ -499,30 +572,38 @@ describe('cli child process helpers', () => {
setPlatform('win32');
const execFileMock = child.execFile as unknown as Mock;
const execMock = child.exec as unknown as Mock;
- execMock.mockImplementation((_cmd: string, _opts: unknown, cb: ExecCallback) => {
+ execFileMock.mockImplementation(
+ (_cmd: string, _args: string[], _opts: unknown, cb: ExecCallback) => {
cb(null, '1.2.3', '');
return createMockProcess();
- });
+ }
+ );
const result = await execCli('C:\\Users\\Алексей\\AppData\\Roaming\\npm\\claude.cmd', [
'--version',
]);
- // non-ASCII path detected upfront — execFile should NOT be called
- expect(execFileMock).not.toHaveBeenCalled();
- expect(execMock).toHaveBeenCalled();
+ expect(execFileMock).toHaveBeenCalledWith(
+ expect.stringMatching(/cmd\.exe$/i),
+ ['/d', '/s', '/c', expect.stringContaining('claude.cmd')],
+ expect.any(Object),
+ expect.any(Function)
+ );
+ expect(execMock).not.toHaveBeenCalled();
expect(result.stdout).toBe('1.2.3');
});
it('escapes percent signs and quotes for cmd.exe in shell fallback', async () => {
setPlatform('win32');
- const execMock = child.exec as unknown as Mock;
- execMock.mockImplementation((_cmd: string, _opts: unknown, cb: ExecCallback) => {
+ const execFileMock = child.execFile as unknown as Mock;
+ execFileMock.mockImplementation(
+ (_cmd: string, _args: string[], _opts: unknown, cb: ExecCallback) => {
cb(null, 'ok', '');
return createMockProcess();
- });
+ }
+ );
await execCli('C:\\Users\\Алексей\\bin\\claude.cmd', ['--model', 'test%PATH%"arg']);
- const shellCmd = execMock.mock.calls[0][0] as string;
+ const shellCmd = execFileMock.mock.calls[0][1][3] as string;
// Keep % outside quoted chunks so cmd.exe does not expand it as an env var.
expect(shellCmd).toContain('^%"PATH"^%');
expect(shellCmd).not.toContain('%PATH%');
@@ -534,11 +615,13 @@ describe('cli child process helpers', () => {
it('keeps inline settings JSON as one argv-safe argument for Windows cmd launchers', async () => {
setPlatform('win32');
- const execMock = child.exec as unknown as Mock;
- execMock.mockImplementation((_cmd: string, _opts: unknown, cb: ExecCallback) => {
+ const execFileMock = child.execFile as unknown as Mock;
+ execFileMock.mockImplementation(
+ (_cmd: string, _args: string[], _opts: unknown, cb: ExecCallback) => {
cb(null, 'ok', '');
return createMockProcess();
- });
+ }
+ );
await execCli('C:\\runtime\\cli-dev.cmd', [
'--settings',
@@ -549,25 +632,25 @@ describe('cli child process helpers', () => {
'--provider',
'codex',
]);
- const shellCmd = execMock.mock.calls[0][0] as string;
+ const shellCmd = execFileMock.mock.calls[0][1][3] as string;
expect(shellCmd).toContain('"{\\"codex\\":{\\"forced_login_method\\":\\"chatgpt\\"}}"');
expect(shellCmd).not.toContain('{""codex"":');
});
- it('shell: true cannot be overridden by caller options', () => {
+ it('does not pass caller shell options into cmd.exe fallback', () => {
setPlatform('win32');
const spawnMock = child.spawn as unknown as Mock;
spawnMock.mockReturnValue(createMockProcess());
- spawnCli('C:\\Users\\Алексей\\bin\\claude.cmd', ['--version'], { shell: false });
- // shell: true must win over caller's shell: false
- expect(spawnMock.mock.calls[0][1]).toMatchObject({ shell: true });
+ spawnCli('C:\\Users\\Алексей\\bin\\claude.cmd', ['--version'], { shell: true });
+ expect(spawnMock.mock.calls[0][0]).toMatch(/cmd\.exe$/i);
+ expect(spawnMock.mock.calls[0][2]).toMatchObject({ shell: false });
});
it('falls back to shell when execFile throws EINVAL on windows', async () => {
setPlatform('win32');
const execFileMock = child.execFile as unknown as Mock;
- execFileMock.mockImplementation(
+ execFileMock.mockImplementationOnce(
(_cmd: string, _args: string[], _opts: unknown, cb: ExecCallback) => {
const err = new Error('spawn EINVAL') as Error & { code?: string };
err.code = 'EINVAL';
@@ -575,19 +658,67 @@ describe('cli child process helpers', () => {
return createMockProcess();
}
);
- const execMock = child.exec as unknown as Mock;
- execMock.mockImplementation((_cmd: string, _opts: unknown, cb: ExecCallback) => {
- cb(null, '2.3.4', '');
- return createMockProcess();
- });
+ execFileMock.mockImplementationOnce(
+ (_cmd: string, _args: string[], _opts: unknown, cb: ExecCallback) => {
+ cb(null, '2.3.4', '');
+ return createMockProcess();
+ }
+ );
// ASCII path — goes through execFile first, gets EINVAL, falls back to shell
const result = await execCli('C:\\bin\\claude.exe', ['--version']);
- expect(execFileMock).toHaveBeenCalled();
- expect(execMock).toHaveBeenCalled();
+ expect(execFileMock).toHaveBeenCalledTimes(2);
+ expect(execFileMock.mock.calls[1][0]).toMatch(/cmd\.exe$/i);
expect(result.stdout).toBe('2.3.4');
});
+ it('rejects control characters when execCli needs Windows shell fallback', async () => {
+ setPlatform('win32');
+ const execFileMock = child.execFile as unknown as Mock;
+ execFileMock.mockImplementationOnce(
+ (_cmd: string, _args: string[], _opts: unknown, cb: ExecCallback) => {
+ const err = new Error('spawn EINVAL') as Error & { code?: string };
+ err.code = 'EINVAL';
+ cb(err, '', '');
+ return createMockProcess();
+ }
+ );
+
+ await expect(execCli('C:\\bin\\claude.exe', ['safe\rbad'])).rejects.toThrow(
+ 'control characters are not allowed'
+ );
+ expect(execFileMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('quotes shell metacharacters when execCli needs Windows shell fallback', async () => {
+ setPlatform('win32');
+ const execFileMock = child.execFile as unknown as Mock;
+ execFileMock.mockImplementation(
+ (_cmd: string, _args: string[], _opts: unknown, cb: ExecCallback) => {
+ cb(null, 'ok', '');
+ return createMockProcess();
+ }
+ );
+
+ await expect(
+ execCli('C:\\Users\\R&D\\bin\\claude.cmd', ['safe&bad', 'safe^bad'])
+ ).resolves.toMatchObject({ stdout: 'ok' });
+ expect(execFileMock).toHaveBeenCalledWith(
+ expect.stringMatching(/cmd\.exe$/i),
+ [
+ '/d',
+ '/s',
+ '/c',
+ expect.stringContaining('"C:\\Users\\R&D\\bin\\claude.cmd"'),
+ ],
+ expect.any(Object),
+ expect.any(Function)
+ );
+ const shellCmd = execFileMock.mock.calls[0][1][3] as string;
+ expect(shellCmd).toContain('"safe&bad"');
+ expect(shellCmd).toContain('"safe^bad"');
+ });
+
it('preserves stdout and stderr on execFile failures', async () => {
setPlatform('linux');
const execFileMock = child.execFile as unknown as Mock;
diff --git a/test/renderer/components/cli/CliStatusVisibility.test.ts b/test/renderer/components/cli/CliStatusVisibility.test.ts
index faa60e6e..27439ca2 100644
--- a/test/renderer/components/cli/CliStatusVisibility.test.ts
+++ b/test/renderer/components/cli/CliStatusVisibility.test.ts
@@ -129,6 +129,7 @@ vi.mock('@renderer/components/runtime/ProviderRuntimeBackendSelector', async ()
typeof import('@renderer/components/runtime/ProviderRuntimeBackendSelector')
>('@renderer/components/runtime/ProviderRuntimeBackendSelector');
return {
+ buildProviderRuntimeBackendSummaryText: actual.buildProviderRuntimeBackendSummaryText,
getProviderRuntimeBackendSummary: actual.getProviderRuntimeBackendSummary,
};
});
diff --git a/test/renderer/components/runtime/ProviderModelBadges.test.tsx b/test/renderer/components/runtime/ProviderModelBadges.test.tsx
index 3366ffa7..c7fdbf0b 100644
--- a/test/renderer/components/runtime/ProviderModelBadges.test.tsx
+++ b/test/renderer/components/runtime/ProviderModelBadges.test.tsx
@@ -341,6 +341,16 @@ describe('ProviderModelBadges', () => {
expect(host.textContent?.match(/Opus 4\.6/g)).toHaveLength(1);
});
+ it('does not render duplicate Anthropic Opus 4.8 model badges when the runtime reports the opus alias', () => {
+ const host = render( );
+ const renderedModelLabels = Array.from(host.firstElementChild?.children ?? [])
+ .map((badge) => badge.firstElementChild?.textContent ?? '')
+ .filter(Boolean);
+
+ expect(renderedModelLabels.filter((label) => label === 'Opus 4.8')).toHaveLength(1);
+ expect(renderedModelLabels).toContain('Opus 4.8 (1M)');
+ });
+
it('collapses long model lists and expands them inline without an internal scroll area', () => {
const models = Array.from(
{ length: 18 },
diff --git a/test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts b/test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts
index 12e2d37a..9ff78742 100644
--- a/test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts
+++ b/test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts
@@ -201,6 +201,19 @@ vi.mock('@renderer/components/ui/tabs', () => ({
}));
vi.mock('@renderer/components/runtime/ProviderRuntimeBackendSelector', () => ({
+ buildProviderRuntimeBackendSummaryText: () => ({
+ auto: 'Auto',
+ autoCurrently: (backend: string) => `Auto (currently: ${backend})`,
+ audienceInternal: 'Internal',
+ states: {
+ locked: 'Locked',
+ disabled: 'Disabled',
+ authRequired: 'Auth required',
+ runtimeMissing: 'Runtime missing',
+ degraded: 'Degraded',
+ unavailable: 'Unavailable',
+ },
+ }),
ProviderRuntimeBackendSelector: ({
provider,
onSelect,
diff --git a/test/renderer/components/team/dialogs/providerPrepareDiagnostics.test.ts b/test/renderer/components/team/dialogs/providerPrepareDiagnostics.test.ts
index d65bb1d1..5d7da6ba 100644
--- a/test/renderer/components/team/dialogs/providerPrepareDiagnostics.test.ts
+++ b/test/renderer/components/team/dialogs/providerPrepareDiagnostics.test.ts
@@ -3,6 +3,7 @@ import {
mergeReusableProviderPrepareModelResults,
runProviderPrepareDiagnostics,
} from '@renderer/components/team/dialogs/providerPrepareDiagnostics';
+import { OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE } from '@shared/utils/openCodeWindowsAccessDenied';
import { DEFAULT_PROVIDER_MODEL_SELECTION } from '@shared/utils/providerModelSelection';
import { describe, expect, it, vi } from 'vitest';
@@ -488,6 +489,43 @@ describe('runProviderPrepareDiagnostics', () => {
expect(result.modelResultsById).toEqual({});
});
+ it('keeps the OpenCode node_modules symlink EPERM failure on the administrator hint path', async () => {
+ const symlinkError = [
+ 'Runtime provider management command failed unexpectedly:',
+ "EPERM: operation not permitted, symlink 'C:\\Users\\ben\\AppData\\Local\\claude-multimodel-nodejs\\Cache\\opencode\\shared-cache\\config-node_modules'",
+ "-> 'C:\\Users\\ben\\AppData\\Local\\claude-multimodel-nodejs\\Data\\opencode\\profiles\\abc123\\config\\opencode\\node_modules'",
+ ].join(' ');
+ const prepareProvisioning = vi.fn<
+ (
+ cwd?: string,
+ providerId?: TeamProviderId,
+ providerIds?: TeamProviderId[],
+ selectedModels?: string[],
+ limitContext?: boolean,
+ modelVerificationMode?: 'compatibility' | 'deep'
+ ) => Promise
+ >(() =>
+ Promise.resolve({
+ ready: false,
+ message: symlinkError,
+ details: [symlinkError],
+ warnings: [symlinkError],
+ })
+ );
+
+ const result = await runProviderPrepareDiagnostics({
+ cwd: '/tmp/project',
+ providerId: 'opencode',
+ selectedModelIds: ['opencode/big-pickle'],
+ prepareProvisioning,
+ });
+
+ expect(result.status).toBe('failed');
+ expect(result.details).toEqual([OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE]);
+ expect(result.warnings).toEqual([OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE]);
+ expect(result.modelResultsById).toEqual({});
+ });
+
it('treats OpenCode compatibility verification warnings as blocking when the batch failed', async () => {
const prepareProvisioning = vi.fn<
(
diff --git a/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts b/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts
index 3cf24e5f..2a73f1c8 100644
--- a/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts
+++ b/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts
@@ -232,6 +232,45 @@ describe('RuntimeProviderManagementPanelView', () => {
expect(details?.className).toContain('font-mono');
});
+ it('shows the Windows administrator hint only for OpenCode node_modules symlink EPERM errors', async () => {
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+ const symlinkError = [
+ 'Runtime provider management command failed unexpectedly:',
+ "EPERM: operation not permitted, symlink 'C:\\Users\\ben\\AppData\\Local\\claude-multimodel-nodejs\\Cache\\opencode\\shared-cache\\config-node_modules'",
+ "-> 'C:\\Users\\ben\\AppData\\Local\\claude-multimodel-nodejs\\Data\\opencode\\profiles\\abc123\\config\\opencode\\node_modules'",
+ ].join(' ');
+
+ await act(async () => {
+ root.render(
+ React.createElement(RuntimeProviderManagementPanelView, {
+ state: createState({ error: symlinkError }),
+ actions: createActions(),
+ disabled: false,
+ })
+ );
+ await Promise.resolve();
+ });
+
+ expect(host.textContent).toContain('Windows: run Agent Teams AI as Administrator');
+
+ await act(async () => {
+ root.render(
+ React.createElement(RuntimeProviderManagementPanelView, {
+ state: createState({
+ error: 'EPERM: operation not permitted, mkdir C:\\Program Files\\locked-project',
+ }),
+ actions: createActions(),
+ disabled: false,
+ })
+ );
+ await Promise.resolve();
+ });
+
+ expect(host.textContent).not.toContain('Windows: run Agent Teams AI as Administrator');
+ });
+
it('copies fallback error text when structured diagnostics are unavailable', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
diff --git a/test/renderer/store/changeReviewSlice.test.ts b/test/renderer/store/changeReviewSlice.test.ts
index 7b9597d0..135d80d2 100644
--- a/test/renderer/store/changeReviewSlice.test.ts
+++ b/test/renderer/store/changeReviewSlice.test.ts
@@ -213,6 +213,29 @@ describe('changeReviewSlice task changes', () => {
).toBe('no_changes');
});
+ it('records task change presence entries in one batch', () => {
+ const store = createSliceStore();
+ const keyA = buildTaskChangePresenceKey('team-a', 'task-a', OPTIONS_A);
+ const keyB = buildTaskChangePresenceKey('team-a', 'task-b', OPTIONS_B);
+
+ store.getState().recordTaskChangePresences([
+ { teamName: 'team-a', taskId: 'task-a', options: OPTIONS_A, presence: 'has_changes' },
+ { teamName: 'team-a', taskId: 'task-b', options: OPTIONS_B, presence: 'no_changes' },
+ ]);
+
+ expect(store.getState().taskChangePresenceByKey[keyA]).toBe('has_changes');
+ expect(store.getState().taskChangePresenceByKey[keyB]).toBe('no_changes');
+
+ store
+ .getState()
+ .recordTaskChangePresences([
+ { teamName: 'team-a', taskId: 'task-a', options: OPTIONS_A, presence: 'unknown' },
+ ]);
+
+ expect(store.getState().taskChangePresenceByKey[keyA]).toBeUndefined();
+ expect(store.getState().taskChangePresenceByKey[keyB]).toBe('no_changes');
+ });
+
it('updates selected team task changePresence after a positive summary check', async () => {
const store = createSliceStore();
hoisted.getTaskChanges.mockResolvedValue(makeTaskChangeSet('presence-hit'));
diff --git a/test/renderer/store/teamSlice.test.ts b/test/renderer/store/teamSlice.test.ts
index b6a3a250..f03d8603 100644
--- a/test/renderer/store/teamSlice.test.ts
+++ b/test/renderer/store/teamSlice.test.ts
@@ -382,6 +382,48 @@ describe('teamSlice actions', () => {
expect(window.localStorage.getItem('team:messagesPanelMode')).toBe('inline');
});
+ it('updates selected team task change presence in one batch', () => {
+ const store = createSliceStore();
+ const existingData = createTeamSnapshot({
+ teamName: 'my-team',
+ tasks: [
+ { id: 'task-1', subject: 'One', changePresence: 'unknown' },
+ { id: 'task-2', subject: 'Two', changePresence: 'unknown' },
+ ],
+ });
+ store.setState({
+ selectedTeamName: 'my-team',
+ selectedTeamData: existingData,
+ teamDataCacheByName: { 'my-team': existingData },
+ globalTasks: [
+ { teamName: 'my-team', id: 'task-1', changePresence: 'unknown' },
+ { teamName: 'my-team', id: 'task-2', changePresence: 'unknown' },
+ { teamName: 'other-team', id: 'task-1', changePresence: 'unknown' },
+ ],
+ });
+
+ store.getState().setSelectedTeamTaskChangePresences('my-team', {
+ 'task-1': 'no_changes',
+ 'task-2': 'has_changes',
+ });
+
+ expect(
+ store
+ .getState()
+ .selectedTeamData.tasks.map((task: { changePresence?: string }) => task.changePresence)
+ ).toEqual(['no_changes', 'has_changes']);
+ expect(
+ store
+ .getState()
+ .teamDataCacheByName[
+ 'my-team'
+ ].tasks.map((task: { changePresence?: string }) => task.changePresence)
+ ).toEqual(['no_changes', 'has_changes']);
+ expect(
+ store.getState().globalTasks.map((task: { changePresence?: string }) => task.changePresence)
+ ).toEqual(['no_changes', 'has_changes', 'unknown']);
+ });
+
it('records terminal provisioning fanout diagnostics without changing visible graph hydrate behavior', () => {
const store = createSliceStore();
const fetchTeams = vi.fn(async () => undefined);
diff --git a/test/renderer/utils/teamModelCatalog.test.ts b/test/renderer/utils/teamModelCatalog.test.ts
index e5350516..512df3f0 100644
--- a/test/renderer/utils/teamModelCatalog.test.ts
+++ b/test/renderer/utils/teamModelCatalog.test.ts
@@ -1,4 +1,5 @@
import {
+ getTeamModelBadgeLabel,
getVisibleTeamProviderModels,
isAnthropicOneMillionContextTeamModel,
isAnthropicSonnetOneMillionContextTeamModel,
@@ -44,6 +45,22 @@ describe('teamModelCatalog', () => {
]);
});
+ it('does not add duplicate Anthropic Opus 4.8 fallback badges when the runtime reports the opus alias', () => {
+ const models = getVisibleTeamProviderModels('anthropic', [
+ 'opus',
+ 'claude-opus-4-6',
+ 'sonnet',
+ 'haiku',
+ ]);
+
+ expect(models).toContain('opus');
+ expect(models).not.toContain('claude-opus-4-8');
+ expect(models).toContain('claude-opus-4-8[1m]');
+
+ const labels = models.map((model) => getTeamModelBadgeLabel('anthropic', model));
+ expect(labels.filter((label) => label === 'Opus 4.8')).toHaveLength(1);
+ });
+
it('orders OpenCode free models before paid models', () => {
expect(
getVisibleTeamProviderModels(