commit
4fdb39bd37
158 changed files with 12391 additions and 3947 deletions
10
.github/workflows/dependency-review.yml
vendored
10
.github/workflows/dependency-review.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
46
docs/articles/agent-teams-opus-4-8.en.md
Normal file
46
docs/articles/agent-teams-opus-4-8.en.md
Normal file
|
|
@ -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.
|
||||
46
docs/articles/agent-teams-opus-4-8.ru.md
Normal file
46
docs/articles/agent-teams-opus-4-8.ru.md
Normal file
|
|
@ -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-ключи на уровне приложения не нужны.
|
||||
|
||||
---
|
||||
|
||||
> Скриншоты и видео — ниже.
|
||||
|
|
@ -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/<lead>.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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
const { locale } = useI18n();
|
||||
const isRu = computed(() => locale.value === "ru");
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -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')"
|
||||
>
|
||||
<div class="cyber-video-frame__bezel" aria-hidden="true" />
|
||||
<div class="cyber-video-frame__status" aria-hidden="true">
|
||||
<span>{{ isRu ? 'Командная лента' : 'Team command feed' }}</span>
|
||||
<span>{{ isRu ? 'Живое демо' : 'Live demo' }}</span>
|
||||
<span>{{ t('hero.commandFeed') }}</span>
|
||||
<span>{{ t('hero.liveDemo') }}</span>
|
||||
</div>
|
||||
<div class="cyber-video-frame__content">
|
||||
<HeroDemoVideo />
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ const releaseDate = computed(() => {
|
|||
day: 'numeric',
|
||||
});
|
||||
});
|
||||
const linuxRobotBubble = computed(() => locale.value === 'ru' ? 'Готов начать!' : 'Ready to start!');
|
||||
const linuxRobotBubble = computed(() => t('download.readyToStart'));
|
||||
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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<SwiperContainerElement | null>(null);
|
||||
const swiperReady = ref(false);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { computed, ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { mdiRobotOutline, mdiCheckCircleOutline, mdiCodeBraces, mdiMessageTextOutline } from '@mdi/js';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// ─── State machine for demo cycle ───
|
||||
type DemoState = 'idle' | 'working' | 'reviewing' | 'done';
|
||||
const state = ref<DemoState>('idle');
|
||||
|
|
@ -9,13 +11,13 @@ const state = ref<DemoState>('idle');
|
|||
// ─── Animated task text ───
|
||||
const currentTask = ref('');
|
||||
const taskFading = ref(false);
|
||||
const TASKS = [
|
||||
'Implementing auth middleware...',
|
||||
'Writing unit tests for API...',
|
||||
'Reviewing PR #42 changes...',
|
||||
'Setting up CI/CD pipeline...',
|
||||
'Refactoring database layer...',
|
||||
];
|
||||
const taskMessages = computed(() => [
|
||||
t('hero.demo.activity.authMiddleware'),
|
||||
t('hero.demo.activity.unitTests'),
|
||||
t('hero.demo.activity.reviewPr'),
|
||||
t('hero.demo.activity.ciPipeline'),
|
||||
t('hero.demo.activity.refactorDatabase'),
|
||||
]);
|
||||
let taskIndex = 0;
|
||||
let charTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
|
|
@ -28,9 +30,9 @@ const agents = ref([
|
|||
|
||||
// ─── Kanban mini-board ───
|
||||
const kanbanTasks = ref([
|
||||
{ id: 1, text: 'Auth API', col: 'todo' as string },
|
||||
{ id: 2, text: 'Unit tests', col: 'todo' as string },
|
||||
{ id: 3, text: 'CI setup', col: 'todo' as string },
|
||||
{ id: 1, text: t('hero.demo.tasks.authApi'), col: 'todo' as string },
|
||||
{ id: 2, text: t('hero.demo.tasks.unitTests'), col: 'todo' as string },
|
||||
{ id: 3, text: t('hero.demo.tasks.ciSetup'), col: 'todo' as string },
|
||||
]);
|
||||
|
||||
function typeNextChar(text: string, index: number) {
|
||||
|
|
@ -76,9 +78,9 @@ function runCycle() {
|
|||
currentTask.value = '';
|
||||
taskFading.value = false;
|
||||
kanbanTasks.value = [
|
||||
{ id: 1, text: 'Auth API', col: 'todo' },
|
||||
{ id: 2, text: 'Unit tests', col: 'todo' },
|
||||
{ id: 3, text: 'CI setup', col: 'todo' },
|
||||
{ id: 1, text: t('hero.demo.tasks.authApi'), col: 'todo' },
|
||||
{ id: 2, text: t('hero.demo.tasks.unitTests'), col: 'todo' },
|
||||
{ id: 3, text: t('hero.demo.tasks.ciSetup'), col: 'todo' },
|
||||
];
|
||||
agents.value.forEach(a => a.status = 'idle');
|
||||
|
||||
|
|
@ -91,7 +93,8 @@ function runCycle() {
|
|||
agents.value[1].status = 'active';
|
||||
kanbanTasks.value[0].col = 'progress';
|
||||
|
||||
const task = TASKS[taskIndex % TASKS.length];
|
||||
const messages = taskMessages.value;
|
||||
const task = messages[taskIndex % messages.length];
|
||||
taskIndex++;
|
||||
typeNextChar(task, 0);
|
||||
|
||||
|
|
@ -179,6 +182,16 @@ function colColor(col: string) {
|
|||
}
|
||||
}
|
||||
|
||||
function colLabel(col: string) {
|
||||
switch (col) {
|
||||
case 'todo': return t('hero.demo.columns.todo');
|
||||
case 'progress': return t('hero.demo.columns.progress');
|
||||
case 'review': return t('hero.demo.columns.review');
|
||||
case 'done': return t('hero.demo.columns.done');
|
||||
default: return col.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
function statusDotColor(status: string) {
|
||||
switch (status) {
|
||||
case 'active': return '#00f0ff';
|
||||
|
|
@ -190,7 +203,7 @@ function statusDotColor(status: string) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" class="hero-demo" role="img" aria-label="Agent team demo">
|
||||
<div ref="containerRef" class="hero-demo" role="img" :aria-label="t('hero.demo.ariaLabel')">
|
||||
<div class="hero-demo__content">
|
||||
<!-- Header -->
|
||||
<div class="hero-demo__header">
|
||||
|
|
@ -198,7 +211,7 @@ function statusDotColor(status: string) {
|
|||
<span class="hero-demo__title">Agent Teams</span>
|
||||
<span class="hero-demo__badge-live">
|
||||
<span class="hero-demo__live-dot" />
|
||||
LIVE
|
||||
{{ t('hero.demo.live') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -225,7 +238,7 @@ function statusDotColor(status: string) {
|
|||
<div class="hero-demo__kanban">
|
||||
<div v-for="col in ['todo', 'progress', 'review', 'done']" :key="col" class="hero-demo__kanban-col">
|
||||
<div class="hero-demo__kanban-label" :style="{ color: colColor(col) }">
|
||||
{{ col === 'progress' ? 'IN PROGRESS' : col.toUpperCase() }}
|
||||
{{ colLabel(col) }}
|
||||
</div>
|
||||
<div class="hero-demo__kanban-cards">
|
||||
<TransitionGroup name="kanban-card">
|
||||
|
|
@ -249,7 +262,7 @@ function statusDotColor(status: string) {
|
|||
<span
|
||||
class="hero-demo__log-text"
|
||||
:class="{ 'hero-demo__log-text--fading': taskFading }"
|
||||
>{{ currentTask || 'Waiting for tasks...' }}</span>
|
||||
>{{ currentTask || t('hero.demo.waiting') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 "";
|
||||
|
||||
|
|
|
|||
|
|
@ -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": "التوثيق"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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": "নথিপত্র"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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": "दस्तावेज़"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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": "ドキュメント"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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": "문서"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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": "Документация"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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": "دستاویز"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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": "文档"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
290
pnpm-lock.yaml
290
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
|
||||
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@ export interface TeamGraphData extends TeamViewSnapshot {
|
|||
runtimeEntriesByMember?: Record<string, TeamAgentRuntimeEntry>;
|
||||
}
|
||||
|
||||
export interface TeamGraphAdapterText {
|
||||
hiddenBlockingLinks(count: number): string;
|
||||
}
|
||||
|
||||
function toGraphLaunchVisualState(
|
||||
visualState: ReturnType<typeof buildMemberLaunchPresentation>['launchVisualState'] | undefined
|
||||
): GraphNode['launchVisualState'] {
|
||||
|
|
@ -141,7 +145,8 @@ export class TeamGraphAdapter {
|
|||
slotAssignments?: Record<string, GraphOwnerSlotAssignment>,
|
||||
layoutMode: GraphLayoutMode = DEFAULT_TEAM_GRAPH_LAYOUT_MODE,
|
||||
gridOwnerOrder?: readonly string[],
|
||||
activeTaskLogActivity?: Record<string, true>
|
||||
activeTaskLogActivity?: Record<string, true>,
|
||||
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<string, string>,
|
||||
leadId?: string,
|
||||
leadName?: string,
|
||||
activeTaskLogActivity?: Record<string, true>
|
||||
activeTaskLogActivity?: Record<string, true>,
|
||||
text?: TeamGraphAdapterText
|
||||
): void {
|
||||
const taskStateById = new Map<
|
||||
string,
|
||||
|
|
@ -917,7 +924,8 @@ export class TeamGraphAdapter {
|
|||
label:
|
||||
edge.aggregateCount > 1 &&
|
||||
(edge.source.includes(':overflow:') || edge.target.includes(':overflow:'))
|
||||
? `${edge.aggregateCount} hidden blocking links`
|
||||
? (text?.hiddenBlockingLinks(edge.aggregateCount) ??
|
||||
`${edge.aggregateCount} hidden blocking links`)
|
||||
: undefined,
|
||||
}))
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>(TeamGraphAdapter.create());
|
||||
const inactiveGraphData = useMemo(() => emptyGraphData(teamName), [teamName]);
|
||||
const lastActiveGraphDataRef = useRef<GraphDataPort>(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(() => {
|
||||
|
|
|
|||
|
|
@ -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<BlockingEdgeLabels, 'hiddenTaskStack' | 'hiddenTasks' | 'task'>
|
||||
): 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 })}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<typeof fallbackDisplayTitle>[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<HTMLDivElement | null>(null);
|
||||
const shellRefs = useRef(new Map<string, HTMLDivElement | null>());
|
||||
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 = ({
|
|||
</button>
|
||||
);
|
||||
},
|
||||
[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)}
|
||||
</button>
|
||||
)}
|
||||
{preview && preview.overflowCount > 0 ? (
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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": "查看详情"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"fallback": "出了点问题"
|
||||
"fallback": "出了点问题。"
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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}} 权限拒绝次数"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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 ? <Check size={13} /> : <Clipboard size={13} />}
|
||||
{copied ? 'Copied' : 'Copy'}
|
||||
{copied ? tCommon('actions.copied') : t('members.runtimeLogs.copy')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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<MemberWorkSyncReportResult> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<MemberWorkSyncInboxNudgePort['repairIfPresent']>
|
||||
>[0];
|
||||
|
||||
type TeamInboxMemberWorkSyncNudgeWriter = Pick<TeamInboxWriter, 'sendMessage'> &
|
||||
Partial<Pick<TeamInboxWriter, 'updateMessageText'>>;
|
||||
|
||||
function isStoredMemberWorkSyncNudge(
|
||||
message: Awaited<ReturnType<TeamInboxReader['getMessagesFor']>>[number]
|
||||
): boolean {
|
||||
return message.messageKind === 'member_work_sync_nudge';
|
||||
}
|
||||
|
||||
export class TeamInboxMemberWorkSyncNudgeSink implements MemberWorkSyncInboxNudgePort {
|
||||
constructor(
|
||||
private readonly inboxReader: Pick<TeamInboxReader, 'getMessagesFor'> = new TeamInboxReader(),
|
||||
private readonly inboxWriter: Pick<TeamInboxWriter, 'sendMessage'> = new TeamInboxWriter(),
|
||||
private readonly inboxWriter: TeamInboxMemberWorkSyncNudgeWriter = new TeamInboxWriter(),
|
||||
private readonly controlUrlResolver?: () => Promise<string | null> | string | null
|
||||
) {}
|
||||
|
||||
async insertIfAbsent(input: Parameters<MemberWorkSyncInboxNudgePort['insertIfAbsent']>[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<string | null> {
|
||||
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<boolean> {
|
||||
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<string | null> {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TeamSummary[]>;
|
||||
|
|
@ -38,26 +40,21 @@ function memberKey(member: Pick<TeamMember, 'name'>): string {
|
|||
return normalizeMemberName(member.name);
|
||||
}
|
||||
|
||||
function mergeMembers(configMembers: TeamMember[], metaMembers: TeamMember[]): TeamMember[] {
|
||||
const byName = new Map<string, TeamMember>();
|
||||
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';
|
||||
}
|
||||
if (normalized === 'opencode-cli') {
|
||||
return 'opencode';
|
||||
}
|
||||
for (const member of metaMembers) {
|
||||
const key = memberKey(member);
|
||||
if (key) {
|
||||
byName.set(key, { ...byName.get(key), ...member });
|
||||
}
|
||||
}
|
||||
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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<TeamConfigReader, 'getConfig'>;
|
||||
|
|
@ -34,26 +36,21 @@ function memberKey(member: Pick<TeamMember, 'name'>): string {
|
|||
return normalizeMemberName(member.name);
|
||||
}
|
||||
|
||||
function mergeMembers(configMembers: TeamMember[], metaMembers: TeamMember[]): TeamMember[] {
|
||||
const byName = new Map<string, TeamMember>();
|
||||
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';
|
||||
}
|
||||
if (normalized === 'opencode-cli') {
|
||||
return 'opencode';
|
||||
}
|
||||
for (const member of metaMembers) {
|
||||
const key = memberKey(member);
|
||||
if (key) {
|
||||
byName.set(key, { ...byName.get(key), ...member });
|
||||
}
|
||||
}
|
||||
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({
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<TeamMember, 'name'>): string {
|
||||
return normalizeMemberName(member.name);
|
||||
}
|
||||
|
||||
const PROVIDER_SCOPED_MEMBER_FIELDS = new Set<keyof TeamMember>([
|
||||
'providerId',
|
||||
'providerBackendId',
|
||||
'model',
|
||||
'effort',
|
||||
'fastMode',
|
||||
]);
|
||||
|
||||
const PROVIDER_SETTING_MEMBER_FIELDS = new Set<keyof TeamMember>(['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<string, TeamMember>();
|
||||
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()];
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -26,11 +26,46 @@ const WORK_SYNC_BOOTSTRAP_ONLY_PID_SOURCES = new Set<TeamAgentRuntimePidSource>(
|
|||
'persisted_metadata',
|
||||
]);
|
||||
|
||||
const WORK_SYNC_CONFIRMED_BOOTSTRAP_ACTIVE_PID_SOURCES = new Set<TeamAgentRuntimePidSource>([
|
||||
const WORK_SYNC_MEMBER_CONFIRMED_BOOTSTRAP_ACTIVE_PID_SOURCES = new Set<TeamAgentRuntimePidSource>([
|
||||
'agent_process_table',
|
||||
'opencode_bridge',
|
||||
]);
|
||||
|
||||
const WORK_SYNC_LEAD_CONFIRMED_BOOTSTRAP_ACTIVE_PID_SOURCES = new Set<TeamAgentRuntimePidSource>([
|
||||
'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<TeamAgentRuntimeEntry, 'alive' | 'livenessKind' | 'pidSource'> | null | undefined,
|
||||
confirmedBootstrapActivePidSources: ReadonlySet<TeamAgentRuntimePidSource>
|
||||
): 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<TeamAgentRuntimeSnapshot, 'members'> | null | undefined
|
||||
): boolean {
|
||||
return Object.values(snapshot?.members ?? {}).some(
|
||||
(entry) => isRuntimeEntryActiveForWorkSync(entry) || isRuntimeLeadEntryActiveForWorkSync(entry)
|
||||
);
|
||||
}
|
||||
|
||||
export function isRuntimeMemberActiveForWorkSync(
|
||||
snapshot: Pick<TeamAgentRuntimeSnapshot, 'members'> | 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)))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export {
|
|||
export {
|
||||
hasUncertainWorkSyncRuntimeActivity,
|
||||
hasWorkSyncActiveRuntime,
|
||||
hasWorkSyncReachableRuntime,
|
||||
isRuntimeEntryActiveForWorkSync,
|
||||
isRuntimeMemberActiveForWorkSync,
|
||||
isRuntimeMemberActivityUncertainForWorkSync,
|
||||
|
|
|
|||
|
|
@ -17,36 +17,54 @@ export interface RunningTeamRowModel {
|
|||
taskCounts?: TaskStatusCounts;
|
||||
}
|
||||
|
||||
function getStatusLabel(status: RunningTeamDashboardEntry['status']): string {
|
||||
export interface RunningTeamsSectionText {
|
||||
status: Record<RunningTeamDashboardEntry['status'], string>;
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<boolean> {
|
||||
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 = ({
|
|||
<div className="flex min-w-0 flex-wrap items-start justify-between gap-2">
|
||||
<div className="min-w-0 whitespace-pre-wrap break-words font-medium leading-5">
|
||||
{headline || message}
|
||||
{showWindowsSymlinkPermissionHint ? (
|
||||
<span className="ml-2 inline-flex rounded border border-red-200/30 bg-red-500/10 px-1.5 py-0.5 text-[11px] font-semibold leading-4 text-red-50">
|
||||
{t('runtimeProvider.diagnostics.windowsSymlinkAdminHint')}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -112,6 +112,90 @@ describe('planTeamRuntimeLanes', () => {
|
|||
});
|
||||
});
|
||||
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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<RuntimeLanePlannerMemberInput, 'name'>
|
||||
): 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<TeamRuntimeLanePlan, { mode: 'pure_opencode' }> {
|
||||
return plan.mode === 'pure_opencode';
|
||||
}
|
||||
|
||||
export function isPureOpenCodeWorktreeRootLanePlan(
|
||||
plan: TeamRuntimeLanePlan
|
||||
): plan is Extract<TeamRuntimeLanePlan, { mode: 'pure_opencode_worktree_root_lanes' }> {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -9,9 +9,12 @@ export type {
|
|||
TeamRuntimeLanePlanSuccess,
|
||||
} from './core/domain/planTeamRuntimeLanes';
|
||||
export {
|
||||
buildOpenCodeSecondaryLaneId,
|
||||
buildPlannedMemberLaneIdentity,
|
||||
fromProvisioningMembers,
|
||||
isMixedOpenCodeSideLanePlan,
|
||||
isOpenCodeSideLanePlan,
|
||||
isPureOpenCodeLanePlan,
|
||||
isPureOpenCodeWorktreeRootLanePlan,
|
||||
planTeamRuntimeLanes,
|
||||
} from './core/domain/planTeamRuntimeLanes';
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ import {
|
|||
buildMemberWorkSyncRuntimeTurnSettledEnvironment,
|
||||
createMemberWorkSyncFeature,
|
||||
hasUncertainWorkSyncRuntimeActivity,
|
||||
hasWorkSyncActiveRuntime,
|
||||
hasWorkSyncReachableRuntime,
|
||||
isRuntimeMemberActivityUncertainForWorkSync,
|
||||
isRuntimeMemberActiveForWorkSync,
|
||||
type MemberWorkSyncFeatureFacade,
|
||||
|
|
@ -1919,7 +1919,7 @@ async function initializeServices(): Promise<void> {
|
|||
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<void> {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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<TeamProvisioningService['relayOpenCodeMemberInboxMessages']>
|
||||
>;
|
||||
type OpenCodeMemberInboxDelivery = NonNullable<OpenCodeMemberInboxRelayResult['lastDelivery']>;
|
||||
|
||||
type VisibleDirectReplyProtocol = 'send_message' | 'agent_teams_message_send';
|
||||
|
||||
|
|
@ -318,6 +328,158 @@ async function withTimeoutValue<T>(
|
|||
}
|
||||
}
|
||||
|
||||
async function waitForOpenCodeRuntimeRelayForUi(input: {
|
||||
provisioning: TeamProvisioningService;
|
||||
teamName: string;
|
||||
memberName: string;
|
||||
messageId: string;
|
||||
relayPromise: Promise<OpenCodeMemberInboxRelayResult>;
|
||||
timeoutMs?: number;
|
||||
}): Promise<OpenCodeMemberInboxRelayResult> {
|
||||
let timer: ReturnType<typeof setTimeout> | 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',
|
||||
|
|
|
|||
|
|
@ -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<Awaited<ReturnType<typeof buildProviderAwareCliEnv>>> {
|
||||
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'], {
|
||||
|
|
|
|||
|
|
@ -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 ?? [],
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<UpdateInboxMessageTextResult> {
|
||||
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<string, unknown>;
|
||||
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
|
||||
|
|
|
|||
|
|
@ -712,24 +712,23 @@ export function createPersistedLaunchSnapshot(params: {
|
|||
teamName: string;
|
||||
expectedMembers: readonly string[];
|
||||
bootstrapExpectedMembers?: readonly string[];
|
||||
includeLeadMembers?: boolean;
|
||||
leadSessionId?: string;
|
||||
launchPhase?: PersistedTeamLaunchPhase;
|
||||
members?: Record<string, PersistedTeamLaunchMemberState>;
|
||||
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 ?? {};
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
.map((member) => {
|
||||
const messageId = `bootstrap-start:${config.name}:${member.name}`;
|
||||
return {
|
||||
from: leadName,
|
||||
to: member.name,
|
||||
text: buildSyntheticBootstrapDisplayPrompt(config, member),
|
||||
timestamp: resolveSyntheticBootstrapTimestamp(config, member),
|
||||
timestamp:
|
||||
resolveSyntheticBootstrapTimestamp(config, member) ??
|
||||
fallbackTimestampForMessage(messageId),
|
||||
read: true,
|
||||
source: 'system_notification' as const,
|
||||
messageId: `bootstrap-start:${config.name}:${member.name}`,
|
||||
}));
|
||||
messageId,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function isVisibleTeamMessage(message: InboxMessage): boolean {
|
||||
|
|
@ -429,6 +440,7 @@ export class TeamMessageFeedService {
|
|||
private readonly dirtyTeams = new Set<string>();
|
||||
private readonly inFlightByTeam = new Map<string, InFlightTeamMessageFeed>();
|
||||
private readonly generationByTeam = new Map<string, number>();
|
||||
private readonly syntheticBootstrapTimestampByMessageId = new Map<string, string>();
|
||||
|
||||
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
|
||||
);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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)))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
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<string, unknown>,
|
||||
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 };
|
||||
}
|
||||
|
|
@ -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) &&
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> = {}): Record<string, unknown> {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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_briefing>',
|
||||
'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.',
|
||||
'</agent_teams_app_managed_bootstrap_briefing>',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<typeof normalizeOptionalTeamProviderId> {
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<typeof setTimeout> | 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<typeof withCliProcessDefaults<SpawnOptions>>
|
||||
): ReturnType<typeof spawn> {
|
||||
return spawn(getWindowsCmdPath(), ['/d', '/s', '/c', cmd], {
|
||||
...options,
|
||||
shell: false,
|
||||
});
|
||||
}
|
||||
|
||||
/** Env vars injected into every spawned Claude CLI process. */
|
||||
const CLI_ENV_DEFAULTS: Record<string, string> = {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -95,7 +95,9 @@ export const ApiKeyCard = ({ apiKey, onEdit }: ApiKeyCardProps): React.JSX.Eleme
|
|||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{copied ? 'Copied!' : 'Copy env var name'}</TooltipContent>
|
||||
<TooltipContent>
|
||||
{copied ? t('apiKeys.actions.copied') : t('apiKeys.actions.copyEnvVarName')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
|
@ -135,7 +137,9 @@ export const ApiKeyCard = ({ apiKey, onEdit }: ApiKeyCardProps): React.JSX.Eleme
|
|||
<Trash2 className="size-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{confirmDelete ? 'Click again to confirm' : 'Delete'}</TooltipContent>
|
||||
<TooltipContent>
|
||||
{confirmDelete ? t('apiKeys.actions.confirmDelete') : t('apiKeys.actions.delete')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<typeof useAppTranslation>['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<CliProviderStatus['availableBackends']>[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<CliProviderStatus['availableBackends']>[number],
|
||||
resolvedOption: NonNullable<CliProviderStatus['availableBackends']>[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<CliProviderStatus['availableBackends']>[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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<CliProviderId>(initialProviderId);
|
||||
const [activeApiKeyFormProviderId, setActiveApiKeyFormProviderId] =
|
||||
useState<ApiKeyProviderId | null>(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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1352,14 +1352,16 @@ const LeadLoadBridge = memo(function LeadLoadBridge({
|
|||
{t('detail.context.title')}
|
||||
</p>
|
||||
<p className="text-[10px] text-[var(--color-text-muted)]">
|
||||
{leadSessionLoading ? 'Loading…' : 'No session loaded'}
|
||||
{leadSessionLoading
|
||||
? t('detail.context.loading')
|
||||
: t('detail.context.noSessionLoaded')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded p-1 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text)]"
|
||||
onClick={() => setContextPanelVisible(false)}
|
||||
aria-label={`Close ${teamName} context panel`}
|
||||
aria-label={t('detail.context.closePanel', { team: teamName })}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
|
@ -1367,8 +1369,8 @@ const LeadLoadBridge = memo(function LeadLoadBridge({
|
|||
<div className="flex flex-1 items-center justify-center p-4">
|
||||
<p className="text-xs text-[var(--color-text-muted)]">
|
||||
{leadSessionLoading
|
||||
? 'Loading context…'
|
||||
: 'Open the team lead session to view context.'}
|
||||
? t('detail.context.loadingContext')
|
||||
: t('detail.context.openLeadSession')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1401,7 +1403,7 @@ const LeadLoadBridge = memo(function LeadLoadBridge({
|
|||
leadSessionLoaded
|
||||
? `Session: ${leadSessionId}`
|
||||
: leadSessionLoading
|
||||
? 'Loading context…'
|
||||
? t('detail.context.loadingContext')
|
||||
: leadSessionId
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
>
|
||||
<ChevronRight
|
||||
size={10}
|
||||
|
|
@ -118,7 +122,10 @@ export const ActiveTasksBlock = memo(function ActiveTasksBlock({
|
|||
);
|
||||
const dotPing = kind === 'reviewing' ? 'bg-amber-400' : 'bg-emerald-400';
|
||||
const dotSolid = kind === 'reviewing' ? 'bg-amber-500' : 'bg-emerald-500';
|
||||
const activityLabel = kind === 'reviewing' ? 'reviewing' : 'working on';
|
||||
const activityLabel =
|
||||
kind === 'reviewing'
|
||||
? t('activity.activeTasks.reviewing')
|
||||
: t('activity.activeTasks.workingOn');
|
||||
|
||||
return (
|
||||
<article
|
||||
|
|
|
|||
|
|
@ -1380,7 +1380,9 @@ export const ActivityItem = memo(
|
|||
) : isApiError ? (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-red-500/20 px-1.5 py-0.5 text-[10px] font-medium text-red-400">
|
||||
<AlertTriangle size={10} />
|
||||
{message.messageKind === 'agent_error' ? 'Agent Error' : 'API Error'}
|
||||
{message.messageKind === 'agent_error'
|
||||
? t('activity.badges.agentError')
|
||||
: t('activity.badges.apiError')}
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
|
|
|
|||
|
|
@ -128,7 +128,11 @@ export const CodexReconnectPrompt = ({
|
|||
}}
|
||||
>
|
||||
<LogIn className="size-3" />
|
||||
{reconnectBusy ? 'Generating...' : authUrl ? 'Open login' : 'Generate link'}
|
||||
{reconnectBusy
|
||||
? t('codexReconnect.generating')
|
||||
: authUrl
|
||||
? t('codexReconnect.openLogin')
|
||||
: t('codexReconnect.generateLink')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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, [
|
||||
|
|
|
|||
|
|
@ -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') ??
|
||||
|
|
|
|||
|
|
@ -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<typeof calculateTaskImplementationDuration>;
|
||||
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 (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 rounded-md bg-[var(--color-bg-secondary)] px-1.5 py-0.5 text-[10px] font-normal leading-none text-[var(--color-text-muted)]"
|
||||
title={t('taskDetail.workflow.implementationTimeTitle')}
|
||||
>
|
||||
<Clock size={10} />
|
||||
<span>
|
||||
{t('taskDetail.workflow.inProgressTime', {
|
||||
duration: formatTaskImplementationDuration(duration.elapsedMs),
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
const WorkflowTimelineWithDuration = memo(function WorkflowTimelineWithDuration({
|
||||
task,
|
||||
events,
|
||||
memberColorMap,
|
||||
}: {
|
||||
task: TeamTaskWithKanban;
|
||||
events: NonNullable<TeamTaskWithKanban['historyEvents']>;
|
||||
memberColorMap: Map<string, string>;
|
||||
}): React.JSX.Element {
|
||||
const { nowMs } = useTaskImplementationDurationClock(task);
|
||||
return (
|
||||
<WorkflowTimeline
|
||||
events={events}
|
||||
memberColorMap={memberColorMap}
|
||||
implementationDurationTask={task}
|
||||
nowMs={nowMs}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={(v) => !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 ? (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 rounded-md bg-[var(--color-bg-secondary)] px-1.5 py-0.5 text-[10px] font-normal leading-none text-[var(--color-text-muted)]"
|
||||
title={t('taskDetail.workflow.implementationTimeTitle')}
|
||||
>
|
||||
<Clock size={10} />
|
||||
<span>
|
||||
{t('taskDetail.workflow.inProgressTime', {
|
||||
duration: taskImplementationDurationLabel,
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
) : undefined
|
||||
}
|
||||
headerExtra={<TaskImplementationDurationBadge task={currentTask} />}
|
||||
defaultOpen={false}
|
||||
>
|
||||
<WorkflowTimeline
|
||||
<WorkflowTimelineWithDuration
|
||||
task={currentTask}
|
||||
events={currentTask.historyEvents}
|
||||
memberColorMap={colorMap}
|
||||
implementationDurationTask={currentTask}
|
||||
nowMs={taskDurationNowMs}
|
||||
/>
|
||||
</CollapsibleTeamSection>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -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<HTMLButtonElement>): Promise<void> => {
|
||||
|
|
@ -39,7 +41,7 @@ export const MemberLaunchDiagnosticsButton = ({
|
|||
};
|
||||
|
||||
const icon = copied ? <Check size={13} /> : <ClipboardList size={13} />;
|
||||
const tooltip = copied ? 'Diagnostics copied' : 'Copy diagnostics';
|
||||
const tooltip = copied ? t('provisioning.diagnosticsCopied') : t('provisioning.copyDiagnostics');
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
|
|
|
|||
|
|
@ -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<string>(
|
||||
!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;
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
180
src/renderer/components/team/useTeamAgentRuntimeWatcher.test.tsx
Normal file
180
src/renderer/components/team/useTeamAgentRuntimeWatcher.test.tsx
Normal file
|
|
@ -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<string, 'active' | 'idle' | undefined>,
|
||||
teamDataByName: {} as Record<string, { isAlive?: boolean } | undefined>,
|
||||
provisioningActiveByTeam: {} as Record<string, boolean | undefined>,
|
||||
fetchTeamAgentRuntime: vi.fn(async (_teamName: string): Promise<void> => undefined),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
useStore: <T,>(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<Root> {
|
||||
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<void>((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);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue