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:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- "**/package.json"
|
- '**/package.json'
|
||||||
- "**/package-lock.json"
|
- '**/package-lock.json'
|
||||||
- "**/pnpm-lock.yaml"
|
- '**/pnpm-lock.yaml'
|
||||||
- "pnpm-workspace.yaml"
|
- 'pnpm-workspace.yaml'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
@ -24,5 +24,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
fail-on-severity: high
|
fail-on-severity: high
|
||||||
fail-on-scopes: runtime, development, unknown
|
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
|
license-check: false
|
||||||
show-patched-versions: true
|
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.
|
- Mixed team with native Codex/Claude/Gemini lead: keep the existing `relayLeadInboxMessages()` path.
|
||||||
- OpenCode teammate or secondary lane: use `relayOpenCodeMemberInboxMessages()`.
|
- 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.
|
- 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:
|
FileWatcher change:
|
||||||
|
|
||||||
|
|
@ -2967,13 +2967,12 @@ it('routes native lead inbox relay through the legacy stdin path', async () => {
|
||||||
```
|
```
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
it('does not silently consume pure OpenCode lead inbox when no lead session exists', async () => {
|
it('relays pure OpenCode lead inbox through the stored lead session', async () => {
|
||||||
// Configure a pure OpenCode runtime-adapter team where isTeamAlive() is true via runtimeAdapterRunByTeam.
|
// Configure a pure OpenCode runtime-adapter team with a stored team-lead session.
|
||||||
// Ensure there is no stored OpenCode session record for the canonical lead name.
|
|
||||||
// Seed inboxes/<lead>.json with one unread message.
|
// Seed inboxes/<lead>.json with one unread message.
|
||||||
// Call relayInboxFileToLiveRecipient(teamName, leadName).
|
// Call relayInboxFileToLiveRecipient(teamName, leadName).
|
||||||
// Assert diagnostics include opencode_lead_runtime_session_missing.
|
// Assert the relay kind is opencode_member and the prompt targets team-lead.
|
||||||
// Assert the inbox row remains unread and no teammate session received the prompt.
|
// 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- `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.
|
- 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;
|
reducedMotion?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const localizedHeroFeatureRail = computed(() => getLocalizedHeroFeatureRail(locale.value));
|
const localizedHeroFeatureRail = computed(() => getLocalizedHeroFeatureRail(locale.value));
|
||||||
const localizedHeroReviewerFeatureCard = computed(() => getLocalizedHeroReviewerFeatureCard(locale.value));
|
const localizedHeroReviewerFeatureCard = computed(() => getLocalizedHeroReviewerFeatureCard(locale.value));
|
||||||
const statusLabel = computed(() => locale.value === "ru" ? "Статус:" : "Status:");
|
const statusLabel = computed(() => t("common.statusLabel"));
|
||||||
|
|
||||||
const icons = [
|
const icons = [
|
||||||
mdiRobotOutline,
|
mdiRobotOutline,
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@ const props = defineProps<{
|
||||||
activeReceiver?: HeroAgentRole | "video" | null;
|
activeReceiver?: HeroAgentRole | "video" | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { locale } = useI18n();
|
const { t } = useI18n();
|
||||||
const isSender = computed(() => props.activeSender === props.agent.id);
|
const isSender = computed(() => props.activeSender === props.agent.id);
|
||||||
const isReceiver = computed(() => props.activeReceiver === props.agent.id);
|
const isReceiver = computed(() => props.activeReceiver === props.agent.id);
|
||||||
const imageLoading = computed(() => (props.agent.priority ? "eager" : "lazy"));
|
const imageLoading = computed(() => (props.agent.priority ? "eager" : "lazy"));
|
||||||
const imageFetchPriority = computed(() => (props.agent.priority ? "high" : "auto"));
|
const imageFetchPriority = computed(() => (props.agent.priority ? "high" : "auto"));
|
||||||
const statusLabel = computed(() => locale.value === "ru" ? "Статус:" : "Status:");
|
const statusLabel = computed(() => t("common.statusLabel"));
|
||||||
|
|
||||||
const rootStyle = computed(() => ({
|
const rootStyle = computed(() => ({
|
||||||
"--agent-x": String(props.agent.desktop.x),
|
"--agent-x": String(props.agent.desktop.x),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { locale } = useI18n();
|
const { t } = useI18n();
|
||||||
const isRu = computed(() => locale.value === "ru");
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -8,12 +7,12 @@ const isRu = computed(() => locale.value === "ru");
|
||||||
id="hero-demo"
|
id="hero-demo"
|
||||||
class="cyber-video-frame"
|
class="cyber-video-frame"
|
||||||
role="region"
|
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__bezel" aria-hidden="true" />
|
||||||
<div class="cyber-video-frame__status" aria-hidden="true">
|
<div class="cyber-video-frame__status" aria-hidden="true">
|
||||||
<span>{{ isRu ? 'Командная лента' : 'Team command feed' }}</span>
|
<span>{{ t('hero.commandFeed') }}</span>
|
||||||
<span>{{ isRu ? 'Живое демо' : 'Live demo' }}</span>
|
<span>{{ t('hero.liveDemo') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cyber-video-frame__content">
|
<div class="cyber-video-frame__content">
|
||||||
<HeroDemoVideo />
|
<HeroDemoVideo />
|
||||||
|
|
|
||||||
|
|
@ -251,7 +251,7 @@ const releaseDate = computed(() => {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const linuxRobotBubble = computed(() => locale.value === 'ru' ? 'Готов начать!' : 'Ready to start!');
|
const linuxRobotBubble = computed(() => t('download.readyToStart'));
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,14 +55,10 @@ const supportedProviders = [
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
const supportedProvidersLabel = computed(() => (
|
const supportedProvidersLabel = computed(() => (
|
||||||
locale.value === "ru"
|
t("hero.supportedProviders")
|
||||||
? "Поддерживаем AI-провайдеры"
|
|
||||||
: "Supported AI providers"
|
|
||||||
));
|
));
|
||||||
const heroSlogan = computed(() => (
|
const heroSlogan = computed(() => (
|
||||||
locale.value === "ru"
|
t("hero.slogan")
|
||||||
? "Делайте много, почти ничего не делая"
|
|
||||||
: "Get a lot done by doing very little"
|
|
||||||
));
|
));
|
||||||
|
|
||||||
const heroDownloadUrl = computed(() => {
|
const heroDownloadUrl = computed(() => {
|
||||||
|
|
@ -79,17 +75,13 @@ const docsHref = computed(() => buildDocsHref({
|
||||||
}));
|
}));
|
||||||
const downloadActionSubtitle = computed(() => {
|
const downloadActionSubtitle = computed(() => {
|
||||||
if (!selectedDownloadAsset.value) {
|
if (!selectedDownloadAsset.value) {
|
||||||
return locale.value === "ru"
|
return t("hero.platformDefault");
|
||||||
? "Для вашей платформы"
|
|
||||||
: "For your platform";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return selectedDownloadAsset.value.actionSubtitle;
|
return selectedDownloadAsset.value.actionSubtitle;
|
||||||
});
|
});
|
||||||
const docsActionSubtitle = computed(() => (
|
const docsActionSubtitle = computed(() => (
|
||||||
locale.value === "ru"
|
t("hero.guidesSetup")
|
||||||
? "Гайды и настройка"
|
|
||||||
: "Guides and setup"
|
|
||||||
));
|
));
|
||||||
|
|
||||||
function clearHeroMessageTimers() {
|
function clearHeroMessageTimers() {
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ const screenshots = computed(() => screenshotData.map((s) => ({
|
||||||
width: s.width,
|
width: s.width,
|
||||||
height: s.height,
|
height: s.height,
|
||||||
})));
|
})));
|
||||||
const prevLabel = computed(() => locale.value === 'ru' ? 'Предыдущий' : 'Previous');
|
const prevLabel = computed(() => t('common.previous'));
|
||||||
const nextLabel = computed(() => locale.value === 'ru' ? 'Следующий' : 'Next');
|
const nextLabel = computed(() => t('common.next'));
|
||||||
|
|
||||||
const swiperRef = ref<SwiperContainerElement | null>(null);
|
const swiperRef = ref<SwiperContainerElement | null>(null);
|
||||||
const swiperReady = ref(false);
|
const swiperReady = ref(false);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
<script setup lang="ts">
|
<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';
|
import { mdiRobotOutline, mdiCheckCircleOutline, mdiCodeBraces, mdiMessageTextOutline } from '@mdi/js';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
// ─── State machine for demo cycle ───
|
// ─── State machine for demo cycle ───
|
||||||
type DemoState = 'idle' | 'working' | 'reviewing' | 'done';
|
type DemoState = 'idle' | 'working' | 'reviewing' | 'done';
|
||||||
const state = ref<DemoState>('idle');
|
const state = ref<DemoState>('idle');
|
||||||
|
|
@ -9,13 +11,13 @@ const state = ref<DemoState>('idle');
|
||||||
// ─── Animated task text ───
|
// ─── Animated task text ───
|
||||||
const currentTask = ref('');
|
const currentTask = ref('');
|
||||||
const taskFading = ref(false);
|
const taskFading = ref(false);
|
||||||
const TASKS = [
|
const taskMessages = computed(() => [
|
||||||
'Implementing auth middleware...',
|
t('hero.demo.activity.authMiddleware'),
|
||||||
'Writing unit tests for API...',
|
t('hero.demo.activity.unitTests'),
|
||||||
'Reviewing PR #42 changes...',
|
t('hero.demo.activity.reviewPr'),
|
||||||
'Setting up CI/CD pipeline...',
|
t('hero.demo.activity.ciPipeline'),
|
||||||
'Refactoring database layer...',
|
t('hero.demo.activity.refactorDatabase'),
|
||||||
];
|
]);
|
||||||
let taskIndex = 0;
|
let taskIndex = 0;
|
||||||
let charTimer: ReturnType<typeof setTimeout> | null = null;
|
let charTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
|
@ -28,9 +30,9 @@ const agents = ref([
|
||||||
|
|
||||||
// ─── Kanban mini-board ───
|
// ─── Kanban mini-board ───
|
||||||
const kanbanTasks = ref([
|
const kanbanTasks = ref([
|
||||||
{ id: 1, text: 'Auth API', col: 'todo' as string },
|
{ id: 1, text: t('hero.demo.tasks.authApi'), col: 'todo' as string },
|
||||||
{ id: 2, text: 'Unit tests', col: 'todo' as string },
|
{ id: 2, text: t('hero.demo.tasks.unitTests'), col: 'todo' as string },
|
||||||
{ id: 3, text: 'CI setup', col: 'todo' as string },
|
{ id: 3, text: t('hero.demo.tasks.ciSetup'), col: 'todo' as string },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function typeNextChar(text: string, index: number) {
|
function typeNextChar(text: string, index: number) {
|
||||||
|
|
@ -76,9 +78,9 @@ function runCycle() {
|
||||||
currentTask.value = '';
|
currentTask.value = '';
|
||||||
taskFading.value = false;
|
taskFading.value = false;
|
||||||
kanbanTasks.value = [
|
kanbanTasks.value = [
|
||||||
{ id: 1, text: 'Auth API', col: 'todo' },
|
{ id: 1, text: t('hero.demo.tasks.authApi'), col: 'todo' },
|
||||||
{ id: 2, text: 'Unit tests', col: 'todo' },
|
{ id: 2, text: t('hero.demo.tasks.unitTests'), col: 'todo' },
|
||||||
{ id: 3, text: 'CI setup', col: 'todo' },
|
{ id: 3, text: t('hero.demo.tasks.ciSetup'), col: 'todo' },
|
||||||
];
|
];
|
||||||
agents.value.forEach(a => a.status = 'idle');
|
agents.value.forEach(a => a.status = 'idle');
|
||||||
|
|
||||||
|
|
@ -91,7 +93,8 @@ function runCycle() {
|
||||||
agents.value[1].status = 'active';
|
agents.value[1].status = 'active';
|
||||||
kanbanTasks.value[0].col = 'progress';
|
kanbanTasks.value[0].col = 'progress';
|
||||||
|
|
||||||
const task = TASKS[taskIndex % TASKS.length];
|
const messages = taskMessages.value;
|
||||||
|
const task = messages[taskIndex % messages.length];
|
||||||
taskIndex++;
|
taskIndex++;
|
||||||
typeNextChar(task, 0);
|
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) {
|
function statusDotColor(status: string) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'active': return '#00f0ff';
|
case 'active': return '#00f0ff';
|
||||||
|
|
@ -190,7 +203,7 @@ function statusDotColor(status: string) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<div class="hero-demo__content">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="hero-demo__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__title">Agent Teams</span>
|
||||||
<span class="hero-demo__badge-live">
|
<span class="hero-demo__badge-live">
|
||||||
<span class="hero-demo__live-dot" />
|
<span class="hero-demo__live-dot" />
|
||||||
LIVE
|
{{ t('hero.demo.live') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -225,7 +238,7 @@ function statusDotColor(status: string) {
|
||||||
<div class="hero-demo__kanban">
|
<div class="hero-demo__kanban">
|
||||||
<div v-for="col in ['todo', 'progress', 'review', 'done']" :key="col" class="hero-demo__kanban-col">
|
<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) }">
|
<div class="hero-demo__kanban-label" :style="{ color: colColor(col) }">
|
||||||
{{ col === 'progress' ? 'IN PROGRESS' : col.toUpperCase() }}
|
{{ colLabel(col) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-demo__kanban-cards">
|
<div class="hero-demo__kanban-cards">
|
||||||
<TransitionGroup name="kanban-card">
|
<TransitionGroup name="kanban-card">
|
||||||
|
|
@ -249,7 +262,7 @@ function statusDotColor(status: string) {
|
||||||
<span
|
<span
|
||||||
class="hero-demo__log-text"
|
class="hero-demo__log-text"
|
||||||
:class="{ 'hero-demo__log-text--fading': taskFading }"
|
:class="{ 'hero-demo__log-text--fading': taskFading }"
|
||||||
>{{ currentTask || 'Waiting for tasks...' }}</span>
|
>{{ currentTask || t('hero.demo.waiting') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,15 @@
|
||||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||||
import { mdiPlay } from "@mdi/js";
|
import { mdiPlay } from "@mdi/js";
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
const { t } = useI18n();
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const muxAccentColor = "#00f0ff";
|
const muxAccentColor = "#00f0ff";
|
||||||
const muxPrimaryColor = "#e6fbff";
|
const muxPrimaryColor = "#e6fbff";
|
||||||
const muxSecondaryColor = "#020617";
|
const muxSecondaryColor = "#020617";
|
||||||
|
|
||||||
const muxPlaybackId = computed(() => String(config.public.muxPlaybackId || "").trim());
|
const muxPlaybackId = computed(() => String(config.public.muxPlaybackId || "").trim());
|
||||||
const videoTitle = computed(() => (
|
const videoTitle = computed(() => t("hero.demoVideoTitle"));
|
||||||
locale.value === "ru" ? "Демо-видео Agent Teams" : "Agent Teams demo video"
|
const muxVideoTitle = computed(() => t("hero.demoTitle"));
|
||||||
));
|
|
||||||
const muxVideoTitle = computed(() => (
|
|
||||||
locale.value === "ru" ? "Демо Agent Teams" : "Agent Teams demo"
|
|
||||||
));
|
|
||||||
const muxPlayerUrl = computed(() => {
|
const muxPlayerUrl = computed(() => {
|
||||||
if (!muxPlaybackId.value) return "";
|
if (!muxPlaybackId.value) return "";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,16 @@
|
||||||
"download": "تحميل",
|
"download": "تحميل",
|
||||||
"pricing": "مجاني",
|
"pricing": "مجاني",
|
||||||
"faq": "الأسئلة الشائعة",
|
"faq": "الأسئلة الشائعة",
|
||||||
"viewOnGithub": "View on GitHub"
|
"viewOnGithub": "View on GitHub",
|
||||||
|
"openMenu": "فتح القائمة",
|
||||||
|
"closeMenu": "إغلاق القائمة",
|
||||||
|
"short": {
|
||||||
|
"screenshots": "صور",
|
||||||
|
"docs": "Docs",
|
||||||
|
"download": "تحميل",
|
||||||
|
"comparison": "مقارنة",
|
||||||
|
"pricing": "مجاني"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "Agent Teams",
|
"badge": "Agent Teams",
|
||||||
|
|
@ -22,13 +31,46 @@
|
||||||
"openSource": "مفتوح المصدر"
|
"openSource": "مفتوح المصدر"
|
||||||
},
|
},
|
||||||
"watchDemo": "شاهد العرض",
|
"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": {
|
"download": {
|
||||||
"title": "تحميل",
|
"title": "تحميل",
|
||||||
"detected": "تم الكشف",
|
"detected": "تم الكشف",
|
||||||
"systemRequirements": "متطلبات النظام",
|
"systemRequirements": "متطلبات النظام",
|
||||||
"version": "الإصدار {version}"
|
"version": "الإصدار {version}",
|
||||||
|
"readyToStart": "جاهز للبدء!"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"dark": "داكن",
|
"dark": "داكن",
|
||||||
|
|
@ -100,7 +142,10 @@
|
||||||
"sectionSubtitle": "لقطات شاشة حقيقية من التطبيق — لوحة كانبان، مراجعة الكود، فرق الوكلاء، والمزيد."
|
"sectionSubtitle": "لقطات شاشة حقيقية من التطبيق — لوحة كانبان، مراجعة الكود، فرق الوكلاء، والمزيد."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"learnMore": "اعرف المزيد"
|
"learnMore": "اعرف المزيد",
|
||||||
|
"statusLabel": "الحالة:",
|
||||||
|
"previous": "السابق",
|
||||||
|
"next": "التالي"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© {year} Agent Teams",
|
"copyright": "© {year} Agent Teams",
|
||||||
|
|
@ -108,6 +153,7 @@
|
||||||
"robotBubble": "أنا أنتظر",
|
"robotBubble": "أنا أنتظر",
|
||||||
"links": {
|
"links": {
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
|
"author": "المؤلف",
|
||||||
"docs": "التوثيق"
|
"docs": "التوثيق"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,16 @@
|
||||||
"download": "ডাউনলোড করা হয়েছে",
|
"download": "ডাউনলোড করা হয়েছে",
|
||||||
"pricing": "মুক্ত",
|
"pricing": "মুক্ত",
|
||||||
"faq": "FAQ",
|
"faq": "FAQ",
|
||||||
"viewOnGithub": "প্রদর্শন GitHub"
|
"viewOnGithub": "প্রদর্শন GitHub",
|
||||||
|
"openMenu": "মেনু খুলুন",
|
||||||
|
"closeMenu": "মেনু বন্ধ করুন",
|
||||||
|
"short": {
|
||||||
|
"screenshots": "স্ক্রিন",
|
||||||
|
"docs": "Docs",
|
||||||
|
"download": "ডাউনলোড",
|
||||||
|
"comparison": "তুলনা",
|
||||||
|
"pricing": "ফ্রি"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "Agent Teams",
|
"badge": "Agent Teams",
|
||||||
|
|
@ -22,13 +31,46 @@
|
||||||
"openSource": "উৎস খুলুন"
|
"openSource": "উৎস খুলুন"
|
||||||
},
|
},
|
||||||
"watchDemo": "নমুনা",
|
"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": {
|
"download": {
|
||||||
"title": "ডাউনলোড করা হয়েছে",
|
"title": "ডাউনলোড করা হয়েছে",
|
||||||
"detected": "সনাক্ত",
|
"detected": "সনাক্ত",
|
||||||
"systemRequirements": "সিস্টেম প্রয়োজন",
|
"systemRequirements": "সিস্টেম প্রয়োজন",
|
||||||
"version": "সংস্করণ {version}"
|
"version": "সংস্করণ {version}",
|
||||||
|
"readyToStart": "শুরু করার জন্য প্রস্তুত!"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"dark": "কালো",
|
"dark": "কালো",
|
||||||
|
|
@ -100,7 +142,10 @@
|
||||||
"sectionSubtitle": "অ্যাপটির বাস্তব স্ক্রিনশট —কানবান বোর্ড, কোড পর্যালোচনা, এজেন্ট দল এবং আরও অনেক কিছু ।"
|
"sectionSubtitle": "অ্যাপটির বাস্তব স্ক্রিনশট —কানবান বোর্ড, কোড পর্যালোচনা, এজেন্ট দল এবং আরও অনেক কিছু ।"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"learnMore": "অারো জানুন"
|
"learnMore": "অারো জানুন",
|
||||||
|
"statusLabel": "স্ট্যাটাস:",
|
||||||
|
"previous": "আগের",
|
||||||
|
"next": "পরের"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© {year} Agent Teams",
|
"copyright": "© {year} Agent Teams",
|
||||||
|
|
@ -108,6 +153,7 @@
|
||||||
"robotBubble": "আমি অপেক্ষা করছি",
|
"robotBubble": "আমি অপেক্ষা করছি",
|
||||||
"links": {
|
"links": {
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
|
"author": "লেখক",
|
||||||
"docs": "নথিপত্র"
|
"docs": "নথিপত্র"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,16 @@
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"pricing": "Kostenlos",
|
"pricing": "Kostenlos",
|
||||||
"faq": "FAQ",
|
"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": {
|
"hero": {
|
||||||
"badge": "Agent Teams",
|
"badge": "Agent Teams",
|
||||||
|
|
@ -22,13 +31,46 @@
|
||||||
"openSource": "Open Source"
|
"openSource": "Open Source"
|
||||||
},
|
},
|
||||||
"watchDemo": "Demo ansehen",
|
"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": {
|
"download": {
|
||||||
"title": "Herunterladen",
|
"title": "Herunterladen",
|
||||||
"detected": "Erkannt",
|
"detected": "Erkannt",
|
||||||
"systemRequirements": "Systemanforderungen",
|
"systemRequirements": "Systemanforderungen",
|
||||||
"version": "Version {version}"
|
"version": "Version {version}",
|
||||||
|
"readyToStart": "Bereit zum Start!"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"dark": "Dunkel",
|
"dark": "Dunkel",
|
||||||
|
|
@ -100,7 +142,10 @@
|
||||||
"sectionSubtitle": "Echte Screenshots der App — Kanban-Board, Code-Review, Agenten-Teams und mehr."
|
"sectionSubtitle": "Echte Screenshots der App — Kanban-Board, Code-Review, Agenten-Teams und mehr."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"learnMore": "Mehr erfahren"
|
"learnMore": "Mehr erfahren",
|
||||||
|
"statusLabel": "Status:",
|
||||||
|
"previous": "Zurück",
|
||||||
|
"next": "Weiter"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© {year} Agent Teams",
|
"copyright": "© {year} Agent Teams",
|
||||||
|
|
@ -108,6 +153,7 @@
|
||||||
"robotBubble": "Ich warte",
|
"robotBubble": "Ich warte",
|
||||||
"links": {
|
"links": {
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
|
"author": "Autor",
|
||||||
"docs": "Dokumentation"
|
"docs": "Dokumentation"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,16 @@
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"pricing": "Free",
|
"pricing": "Free",
|
||||||
"faq": "FAQ",
|
"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": {
|
"hero": {
|
||||||
"badge": "Agent Teams",
|
"badge": "Agent Teams",
|
||||||
|
|
@ -22,13 +31,46 @@
|
||||||
"openSource": "Open Source"
|
"openSource": "Open Source"
|
||||||
},
|
},
|
||||||
"watchDemo": "Watch Demo",
|
"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": {
|
"download": {
|
||||||
"title": "Download",
|
"title": "Download",
|
||||||
"detected": "Detected",
|
"detected": "Detected",
|
||||||
"systemRequirements": "System requirements",
|
"systemRequirements": "System requirements",
|
||||||
"version": "Version {version}"
|
"version": "Version {version}",
|
||||||
|
"readyToStart": "Ready to start!"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
|
|
@ -100,7 +142,10 @@
|
||||||
"sectionSubtitle": "Real screenshots from the app — kanban board, code review, agent teams, and more."
|
"sectionSubtitle": "Real screenshots from the app — kanban board, code review, agent teams, and more."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"learnMore": "Learn more"
|
"learnMore": "Learn more",
|
||||||
|
"statusLabel": "Status:",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© {year} Agent Teams",
|
"copyright": "© {year} Agent Teams",
|
||||||
|
|
@ -108,6 +153,7 @@
|
||||||
"robotBubble": "I'm waiting",
|
"robotBubble": "I'm waiting",
|
||||||
"links": {
|
"links": {
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
|
"author": "Author",
|
||||||
"docs": "Documentation"
|
"docs": "Documentation"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,16 @@
|
||||||
"download": "Descargar",
|
"download": "Descargar",
|
||||||
"pricing": "Gratis",
|
"pricing": "Gratis",
|
||||||
"faq": "FAQ",
|
"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": {
|
"hero": {
|
||||||
"badge": "Agent Teams",
|
"badge": "Agent Teams",
|
||||||
|
|
@ -22,13 +31,46 @@
|
||||||
"openSource": "Open Source"
|
"openSource": "Open Source"
|
||||||
},
|
},
|
||||||
"watchDemo": "Ver demo",
|
"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": {
|
"download": {
|
||||||
"title": "Descargar",
|
"title": "Descargar",
|
||||||
"detected": "Detectado",
|
"detected": "Detectado",
|
||||||
"systemRequirements": "Requisitos del sistema",
|
"systemRequirements": "Requisitos del sistema",
|
||||||
"version": "Versión {version}"
|
"version": "Versión {version}",
|
||||||
|
"readyToStart": "Listo para empezar!"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"dark": "Oscuro",
|
"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."
|
"sectionSubtitle": "Capturas reales de la aplicación — tablero kanban, revisión de código, equipos de agentes y más."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"learnMore": "Más información"
|
"learnMore": "Más información",
|
||||||
|
"statusLabel": "Estado:",
|
||||||
|
"previous": "Anterior",
|
||||||
|
"next": "Siguiente"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© {year} Agent Teams",
|
"copyright": "© {year} Agent Teams",
|
||||||
|
|
@ -108,6 +153,7 @@
|
||||||
"robotBubble": "Estoy esperando",
|
"robotBubble": "Estoy esperando",
|
||||||
"links": {
|
"links": {
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
|
"author": "Autor",
|
||||||
"docs": "Documentación"
|
"docs": "Documentación"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,16 @@
|
||||||
"download": "Télécharger",
|
"download": "Télécharger",
|
||||||
"pricing": "Gratuit",
|
"pricing": "Gratuit",
|
||||||
"faq": "FAQ",
|
"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": {
|
"hero": {
|
||||||
"badge": "Agent Teams",
|
"badge": "Agent Teams",
|
||||||
|
|
@ -22,13 +31,46 @@
|
||||||
"openSource": "Open Source"
|
"openSource": "Open Source"
|
||||||
},
|
},
|
||||||
"watchDemo": "Voir la démo",
|
"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": {
|
"download": {
|
||||||
"title": "Télécharger",
|
"title": "Télécharger",
|
||||||
"detected": "Détecté",
|
"detected": "Détecté",
|
||||||
"systemRequirements": "Configuration requise",
|
"systemRequirements": "Configuration requise",
|
||||||
"version": "Version {version}"
|
"version": "Version {version}",
|
||||||
|
"readyToStart": "Prêt à commencer!"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"dark": "Sombre",
|
"dark": "Sombre",
|
||||||
|
|
@ -100,7 +142,10 @@
|
||||||
"sectionSubtitle": "Captures d'écran réelles — tableau kanban, revue de code, équipes d'agents et plus."
|
"sectionSubtitle": "Captures d'écran réelles — tableau kanban, revue de code, équipes d'agents et plus."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"learnMore": "En savoir plus"
|
"learnMore": "En savoir plus",
|
||||||
|
"statusLabel": "Statut:",
|
||||||
|
"previous": "Précédent",
|
||||||
|
"next": "Suivant"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© {year} Agent Teams",
|
"copyright": "© {year} Agent Teams",
|
||||||
|
|
@ -108,6 +153,7 @@
|
||||||
"robotBubble": "J'attends",
|
"robotBubble": "J'attends",
|
||||||
"links": {
|
"links": {
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
|
"author": "Auteur",
|
||||||
"docs": "Documentation"
|
"docs": "Documentation"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,16 @@
|
||||||
"download": "डाउनलोड",
|
"download": "डाउनलोड",
|
||||||
"pricing": "मुफ़्त",
|
"pricing": "मुफ़्त",
|
||||||
"faq": "FAQ",
|
"faq": "FAQ",
|
||||||
"viewOnGithub": "View on GitHub"
|
"viewOnGithub": "View on GitHub",
|
||||||
|
"openMenu": "मेनू खोलें",
|
||||||
|
"closeMenu": "मेनू बंद करें",
|
||||||
|
"short": {
|
||||||
|
"screenshots": "शॉट्स",
|
||||||
|
"docs": "Docs",
|
||||||
|
"download": "डाउनलोड",
|
||||||
|
"comparison": "तुलना",
|
||||||
|
"pricing": "मुफ्त"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "Agent Teams",
|
"badge": "Agent Teams",
|
||||||
|
|
@ -22,13 +31,46 @@
|
||||||
"openSource": "ओपन सोर्स"
|
"openSource": "ओपन सोर्स"
|
||||||
},
|
},
|
||||||
"watchDemo": "डेमो देखें",
|
"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": {
|
"download": {
|
||||||
"title": "डाउनलोड",
|
"title": "डाउनलोड",
|
||||||
"detected": "पहचाना गया",
|
"detected": "पहचाना गया",
|
||||||
"systemRequirements": "सिस्टम आवश्यकताएँ",
|
"systemRequirements": "सिस्टम आवश्यकताएँ",
|
||||||
"version": "संस्करण {version}"
|
"version": "संस्करण {version}",
|
||||||
|
"readyToStart": "शुरू करने के लिए तैयार!"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"dark": "डार्क",
|
"dark": "डार्क",
|
||||||
|
|
@ -100,7 +142,10 @@
|
||||||
"sectionSubtitle": "ऐप के असली स्क्रीनशॉट — कानबन बोर्ड, कोड रिव्यू, एजेंट टीमें, और बहुत कुछ।"
|
"sectionSubtitle": "ऐप के असली स्क्रीनशॉट — कानबन बोर्ड, कोड रिव्यू, एजेंट टीमें, और बहुत कुछ।"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"learnMore": "और जानें"
|
"learnMore": "और जानें",
|
||||||
|
"statusLabel": "स्थिति:",
|
||||||
|
"previous": "पिछला",
|
||||||
|
"next": "अगला"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© {year} Agent Teams",
|
"copyright": "© {year} Agent Teams",
|
||||||
|
|
@ -108,6 +153,7 @@
|
||||||
"robotBubble": "मैं इंतज़ार कर रहा हूँ",
|
"robotBubble": "मैं इंतज़ार कर रहा हूँ",
|
||||||
"links": {
|
"links": {
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
|
"author": "लेखक",
|
||||||
"docs": "दस्तावेज़"
|
"docs": "दस्तावेज़"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,16 @@
|
||||||
"download": "Unduh",
|
"download": "Unduh",
|
||||||
"pricing": "Bebas",
|
"pricing": "Bebas",
|
||||||
"faq": "FAQ",
|
"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": {
|
"hero": {
|
||||||
"badge": "Agent Teams",
|
"badge": "Agent Teams",
|
||||||
|
|
@ -22,13 +31,46 @@
|
||||||
"openSource": "Buka Sumber"
|
"openSource": "Buka Sumber"
|
||||||
},
|
},
|
||||||
"watchDemo": "Watch Demo",
|
"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": {
|
"download": {
|
||||||
"title": "Unduh",
|
"title": "Unduh",
|
||||||
"detected": "Terdeteksi",
|
"detected": "Terdeteksi",
|
||||||
"systemRequirements": "Kebutuhan sistem",
|
"systemRequirements": "Kebutuhan sistem",
|
||||||
"version": "Versi {version}"
|
"version": "Versi {version}",
|
||||||
|
"readyToStart": "Siap mulai!"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"dark": "Gelap",
|
"dark": "Gelap",
|
||||||
|
|
@ -100,7 +142,10 @@
|
||||||
"sectionSubtitle": "Gambar layar nyata dari aplikasi - papan kanban, ulasan kode, tim agen, dan banyak lagi."
|
"sectionSubtitle": "Gambar layar nyata dari aplikasi - papan kanban, ulasan kode, tim agen, dan banyak lagi."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"learnMore": "Pelajari lagi"
|
"learnMore": "Pelajari lagi",
|
||||||
|
"statusLabel": "Status:",
|
||||||
|
"previous": "Sebelumnya",
|
||||||
|
"next": "Berikutnya"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© {year} Agent Teams",
|
"copyright": "© {year} Agent Teams",
|
||||||
|
|
@ -108,6 +153,7 @@
|
||||||
"robotBubble": "Aku menunggu",
|
"robotBubble": "Aku menunggu",
|
||||||
"links": {
|
"links": {
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
|
"author": "Penulis",
|
||||||
"docs": "Dokumentasi"
|
"docs": "Dokumentasi"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,16 @@
|
||||||
"download": "ダウンロード",
|
"download": "ダウンロード",
|
||||||
"pricing": "無料",
|
"pricing": "無料",
|
||||||
"faq": "FAQ",
|
"faq": "FAQ",
|
||||||
"viewOnGithub": "View on GitHub"
|
"viewOnGithub": "View on GitHub",
|
||||||
|
"openMenu": "メニューを開く",
|
||||||
|
"closeMenu": "メニューを閉じる",
|
||||||
|
"short": {
|
||||||
|
"screenshots": "画像",
|
||||||
|
"docs": "Docs",
|
||||||
|
"download": "入手",
|
||||||
|
"comparison": "比較",
|
||||||
|
"pricing": "無料"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "Agent Teams",
|
"badge": "Agent Teams",
|
||||||
|
|
@ -22,13 +31,46 @@
|
||||||
"openSource": "オープンソース"
|
"openSource": "オープンソース"
|
||||||
},
|
},
|
||||||
"watchDemo": "デモを見る",
|
"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": {
|
"download": {
|
||||||
"title": "ダウンロード",
|
"title": "ダウンロード",
|
||||||
"detected": "検出済み",
|
"detected": "検出済み",
|
||||||
"systemRequirements": "動作環境",
|
"systemRequirements": "動作環境",
|
||||||
"version": "バージョン {version}"
|
"version": "バージョン {version}",
|
||||||
|
"readyToStart": "開始できます!"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"dark": "ダーク",
|
"dark": "ダーク",
|
||||||
|
|
@ -100,7 +142,10 @@
|
||||||
"sectionSubtitle": "アプリの実際のスクリーンショット — カンバンボード、コードレビュー、エージェントチームなど。"
|
"sectionSubtitle": "アプリの実際のスクリーンショット — カンバンボード、コードレビュー、エージェントチームなど。"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"learnMore": "詳細"
|
"learnMore": "詳細",
|
||||||
|
"statusLabel": "ステータス:",
|
||||||
|
"previous": "前へ",
|
||||||
|
"next": "次へ"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© {year} Agent Teams",
|
"copyright": "© {year} Agent Teams",
|
||||||
|
|
@ -108,6 +153,7 @@
|
||||||
"robotBubble": "待ってるよ",
|
"robotBubble": "待ってるよ",
|
||||||
"links": {
|
"links": {
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
|
"author": "作者",
|
||||||
"docs": "ドキュメント"
|
"docs": "ドキュメント"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,16 @@
|
||||||
"download": "다운로드",
|
"download": "다운로드",
|
||||||
"pricing": "무료",
|
"pricing": "무료",
|
||||||
"faq": "FAQ",
|
"faq": "FAQ",
|
||||||
"viewOnGithub": "GitHub에서 보기"
|
"viewOnGithub": "GitHub에서 보기",
|
||||||
|
"openMenu": "메뉴 열기",
|
||||||
|
"closeMenu": "메뉴 닫기",
|
||||||
|
"short": {
|
||||||
|
"screenshots": "샷",
|
||||||
|
"docs": "문서",
|
||||||
|
"download": "받기",
|
||||||
|
"comparison": "비교",
|
||||||
|
"pricing": "무료"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "Agent Teams",
|
"badge": "Agent Teams",
|
||||||
|
|
@ -22,13 +31,46 @@
|
||||||
"openSource": "오픈 소스"
|
"openSource": "오픈 소스"
|
||||||
},
|
},
|
||||||
"watchDemo": "데모 보기",
|
"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": {
|
"download": {
|
||||||
"title": "다운로드",
|
"title": "다운로드",
|
||||||
"detected": "감지됨",
|
"detected": "감지됨",
|
||||||
"systemRequirements": "시스템 요구 사항",
|
"systemRequirements": "시스템 요구 사항",
|
||||||
"version": "버전 {version}"
|
"version": "버전 {version}",
|
||||||
|
"readyToStart": "시작할 준비 완료!"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"dark": "다크",
|
"dark": "다크",
|
||||||
|
|
@ -100,7 +142,10 @@
|
||||||
"sectionSubtitle": "칸반 보드, 코드 리뷰, 에이전트 팀 등 앱의 실제 스크린샷입니다."
|
"sectionSubtitle": "칸반 보드, 코드 리뷰, 에이전트 팀 등 앱의 실제 스크린샷입니다."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"learnMore": "자세히 알아보기"
|
"learnMore": "자세히 알아보기",
|
||||||
|
"statusLabel": "상태:",
|
||||||
|
"previous": "이전",
|
||||||
|
"next": "다음"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© {year} Agent Teams",
|
"copyright": "© {year} Agent Teams",
|
||||||
|
|
@ -108,6 +153,7 @@
|
||||||
"robotBubble": "기다리고 있어요",
|
"robotBubble": "기다리고 있어요",
|
||||||
"links": {
|
"links": {
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
|
"author": "작성자",
|
||||||
"docs": "문서"
|
"docs": "문서"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,16 @@
|
||||||
"download": "Baixar",
|
"download": "Baixar",
|
||||||
"pricing": "Grátis",
|
"pricing": "Grátis",
|
||||||
"faq": "FAQ",
|
"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": {
|
"hero": {
|
||||||
"badge": "Agent Teams",
|
"badge": "Agent Teams",
|
||||||
|
|
@ -22,13 +31,46 @@
|
||||||
"openSource": "Open Source"
|
"openSource": "Open Source"
|
||||||
},
|
},
|
||||||
"watchDemo": "Ver demo",
|
"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": {
|
"download": {
|
||||||
"title": "Baixar",
|
"title": "Baixar",
|
||||||
"detected": "Detectado",
|
"detected": "Detectado",
|
||||||
"systemRequirements": "Requisitos do sistema",
|
"systemRequirements": "Requisitos do sistema",
|
||||||
"version": "Versão {version}"
|
"version": "Versão {version}",
|
||||||
|
"readyToStart": "Pronto para começar!"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"dark": "Escuro",
|
"dark": "Escuro",
|
||||||
|
|
@ -100,7 +142,10 @@
|
||||||
"sectionSubtitle": "Capturas reais do app — quadro kanban, revisão de código, equipes de agentes e mais."
|
"sectionSubtitle": "Capturas reais do app — quadro kanban, revisão de código, equipes de agentes e mais."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"learnMore": "Saiba mais"
|
"learnMore": "Saiba mais",
|
||||||
|
"statusLabel": "Status:",
|
||||||
|
"previous": "Anterior",
|
||||||
|
"next": "Próximo"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© {year} Agent Teams",
|
"copyright": "© {year} Agent Teams",
|
||||||
|
|
@ -108,6 +153,7 @@
|
||||||
"robotBubble": "Estou esperando",
|
"robotBubble": "Estou esperando",
|
||||||
"links": {
|
"links": {
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
|
"author": "Autor",
|
||||||
"docs": "Documentação"
|
"docs": "Documentação"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,16 @@
|
||||||
"download": "Скачать",
|
"download": "Скачать",
|
||||||
"pricing": "Бесплатно",
|
"pricing": "Бесплатно",
|
||||||
"faq": "FAQ",
|
"faq": "FAQ",
|
||||||
"viewOnGithub": "GitHub"
|
"viewOnGithub": "GitHub",
|
||||||
|
"openMenu": "Открыть меню",
|
||||||
|
"closeMenu": "Закрыть меню",
|
||||||
|
"short": {
|
||||||
|
"screenshots": "Скрины",
|
||||||
|
"docs": "Док",
|
||||||
|
"download": "Скачать",
|
||||||
|
"comparison": "Сравн.",
|
||||||
|
"pricing": "Беспл."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "Agent Teams",
|
"badge": "Agent Teams",
|
||||||
|
|
@ -22,13 +31,46 @@
|
||||||
"openSource": "Open Source"
|
"openSource": "Open Source"
|
||||||
},
|
},
|
||||||
"watchDemo": "Смотреть демо",
|
"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": {
|
"download": {
|
||||||
"title": "Скачать",
|
"title": "Скачать",
|
||||||
"detected": "Определено",
|
"detected": "Определено",
|
||||||
"systemRequirements": "Системные требования",
|
"systemRequirements": "Системные требования",
|
||||||
"version": "Версия {version}"
|
"version": "Версия {version}",
|
||||||
|
"readyToStart": "Готов начать!"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"dark": "Тёмная",
|
"dark": "Тёмная",
|
||||||
|
|
@ -100,7 +142,10 @@
|
||||||
"sectionSubtitle": "Реальные скриншоты приложения — канбан-доска, код-ревью, команды агентов и многое другое."
|
"sectionSubtitle": "Реальные скриншоты приложения — канбан-доска, код-ревью, команды агентов и многое другое."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"learnMore": "Подробнее"
|
"learnMore": "Подробнее",
|
||||||
|
"statusLabel": "Статус:",
|
||||||
|
"previous": "Предыдущий",
|
||||||
|
"next": "Следующий"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© {year} Agent Teams",
|
"copyright": "© {year} Agent Teams",
|
||||||
|
|
@ -108,6 +153,7 @@
|
||||||
"robotBubble": "Я жду",
|
"robotBubble": "Я жду",
|
||||||
"links": {
|
"links": {
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
|
"author": "Автор",
|
||||||
"docs": "Документация"
|
"docs": "Документация"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,16 @@
|
||||||
"download": "گر تے ہوئے",
|
"download": "گر تے ہوئے",
|
||||||
"pricing": "مفت",
|
"pricing": "مفت",
|
||||||
"faq": "FAQ",
|
"faq": "FAQ",
|
||||||
"viewOnGithub": "دیکھیں GitHub"
|
"viewOnGithub": "دیکھیں GitHub",
|
||||||
|
"openMenu": "مینو کھولیں",
|
||||||
|
"closeMenu": "مینو بند کریں",
|
||||||
|
"short": {
|
||||||
|
"screenshots": "تصاویر",
|
||||||
|
"docs": "Docs",
|
||||||
|
"download": "ڈاؤن لوڈ",
|
||||||
|
"comparison": "موازنہ",
|
||||||
|
"pricing": "مفت"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "Agent Teams",
|
"badge": "Agent Teams",
|
||||||
|
|
@ -22,13 +31,46 @@
|
||||||
"openSource": "کھولیں"
|
"openSource": "کھولیں"
|
||||||
},
|
},
|
||||||
"watchDemo": "دیمو",
|
"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": {
|
"download": {
|
||||||
"title": "گر تے ہوئے",
|
"title": "گر تے ہوئے",
|
||||||
"detected": "غیر متصل",
|
"detected": "غیر متصل",
|
||||||
"systemRequirements": "سسٹم تقاضوں",
|
"systemRequirements": "سسٹم تقاضوں",
|
||||||
"version": "ورژن {version}"
|
"version": "ورژن {version}",
|
||||||
|
"readyToStart": "شروع کرنے کے لیے تیار!"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"dark": "اندھیرا",
|
"dark": "اندھیرا",
|
||||||
|
|
@ -100,7 +142,10 @@
|
||||||
"sectionSubtitle": "ایپ کی طرف سے حقیقی اسکرین — کابینہ بورڈ ، کوڈ جائزہ ، ایجنٹ ٹیموں اور زیادہ سے زیادہ"
|
"sectionSubtitle": "ایپ کی طرف سے حقیقی اسکرین — کابینہ بورڈ ، کوڈ جائزہ ، ایجنٹ ٹیموں اور زیادہ سے زیادہ"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"learnMore": "مزید سیکھیں"
|
"learnMore": "مزید سیکھیں",
|
||||||
|
"statusLabel": "اسٹیٹس:",
|
||||||
|
"previous": "پچھلا",
|
||||||
|
"next": "اگلا"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© {year} Agent Teams",
|
"copyright": "© {year} Agent Teams",
|
||||||
|
|
@ -108,6 +153,7 @@
|
||||||
"robotBubble": "میں انتظار کر رہا ہوں",
|
"robotBubble": "میں انتظار کر رہا ہوں",
|
||||||
"links": {
|
"links": {
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
|
"author": "مصنف",
|
||||||
"docs": "دستاویز"
|
"docs": "دستاویز"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,16 @@
|
||||||
"download": "下载",
|
"download": "下载",
|
||||||
"pricing": "免费",
|
"pricing": "免费",
|
||||||
"faq": "常见问题",
|
"faq": "常见问题",
|
||||||
"viewOnGithub": "View on GitHub"
|
"viewOnGithub": "View on GitHub",
|
||||||
|
"openMenu": "打开菜单",
|
||||||
|
"closeMenu": "关闭菜单",
|
||||||
|
"short": {
|
||||||
|
"screenshots": "截图",
|
||||||
|
"docs": "文档",
|
||||||
|
"download": "下载",
|
||||||
|
"comparison": "比较",
|
||||||
|
"pricing": "免费"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "Agent Teams",
|
"badge": "Agent Teams",
|
||||||
|
|
@ -22,13 +31,46 @@
|
||||||
"openSource": "开源"
|
"openSource": "开源"
|
||||||
},
|
},
|
||||||
"watchDemo": "观看演示",
|
"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": {
|
"download": {
|
||||||
"title": "下载",
|
"title": "下载",
|
||||||
"detected": "已检测",
|
"detected": "已检测",
|
||||||
"systemRequirements": "系统要求",
|
"systemRequirements": "系统要求",
|
||||||
"version": "版本 {version}"
|
"version": "版本 {version}",
|
||||||
|
"readyToStart": "准备开始!"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"dark": "深色",
|
"dark": "深色",
|
||||||
|
|
@ -100,7 +142,10 @@
|
||||||
"sectionSubtitle": "应用的真实截图——看板、代码审查、智能体团队等等。"
|
"sectionSubtitle": "应用的真实截图——看板、代码审查、智能体团队等等。"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"learnMore": "了解更多"
|
"learnMore": "了解更多",
|
||||||
|
"statusLabel": "状态:",
|
||||||
|
"previous": "上一个",
|
||||||
|
"next": "下一个"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© {year} Agent Teams",
|
"copyright": "© {year} Agent Teams",
|
||||||
|
|
@ -108,6 +153,7 @@
|
||||||
"robotBubble": "我在等你",
|
"robotBubble": "我在等你",
|
||||||
"links": {
|
"links": {
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
|
"author": "作者",
|
||||||
"docs": "文档"
|
"docs": "文档"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
"tsup": "^8.5.1",
|
"tsup": "^8.5.1",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"vitest": "^3.1.4"
|
"vitest": "^3.2.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.15.0 <25"
|
"node": ">=24.15.0 <25"
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,7 @@
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"@types/ssh2": "^1.15.5",
|
"@types/ssh2": "^1.15.5",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"@vitest/coverage-v8": "^3.1.4",
|
"@vitest/coverage-v8": "^3.2.5",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
"electron": "^40.10.0",
|
"electron": "^40.10.0",
|
||||||
"electron-builder": "^26.8.1",
|
"electron-builder": "^26.8.1",
|
||||||
|
|
@ -254,7 +254,7 @@
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.54.0",
|
"typescript-eslint": "^8.54.0",
|
||||||
"vite": "^6.4.2",
|
"vite": "^6.4.2",
|
||||||
"vitest": "^3.1.4"
|
"vitest": "^3.2.5"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.agent-teams.app",
|
"appId": "com.agent-teams.app",
|
||||||
|
|
@ -413,7 +413,7 @@
|
||||||
"flatted": "3.4.2",
|
"flatted": "3.4.2",
|
||||||
"follow-redirects": "1.16.0",
|
"follow-redirects": "1.16.0",
|
||||||
"handlebars": "4.7.9",
|
"handlebars": "4.7.9",
|
||||||
"hono": "4.12.18",
|
"hono": "4.12.23",
|
||||||
"ip-address": "10.1.1",
|
"ip-address": "10.1.1",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.18.1",
|
||||||
"lodash-es": "^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
|
flatted: 3.4.2
|
||||||
follow-redirects: 1.16.0
|
follow-redirects: 1.16.0
|
||||||
handlebars: 4.7.9
|
handlebars: 4.7.9
|
||||||
hono: 4.12.18
|
hono: 4.12.23
|
||||||
ip-address: 10.1.1
|
ip-address: 10.1.1
|
||||||
lodash: ^4.18.1
|
lodash: ^4.18.1
|
||||||
lodash-es: ^4.18.1
|
lodash-es: ^4.18.1
|
||||||
|
|
@ -443,8 +443,8 @@ importers:
|
||||||
specifier: ^4.3.1
|
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))
|
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':
|
'@vitest/coverage-v8':
|
||||||
specifier: ^3.1.4
|
specifier: ^3.2.5
|
||||||
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))
|
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:
|
autoprefixer:
|
||||||
specifier: ^10.4.17
|
specifier: ^10.4.17
|
||||||
version: 10.4.23(postcss@8.5.10)
|
version: 10.4.23(postcss@8.5.10)
|
||||||
|
|
@ -539,8 +539,8 @@ importers:
|
||||||
specifier: ^6.4.2
|
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)
|
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:
|
vitest:
|
||||||
specifier: ^3.1.4
|
specifier: ^3.2.5
|
||||||
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)
|
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: {}
|
agent-teams-controller: {}
|
||||||
|
|
||||||
|
|
@ -654,8 +654,8 @@ importers:
|
||||||
specifier: ^5.8.2
|
specifier: ^5.8.2
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.1.4
|
specifier: ^3.2.5
|
||||||
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)
|
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:
|
packages/agent-graph:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -1886,7 +1886,7 @@ packages:
|
||||||
resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==}
|
resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==}
|
||||||
engines: {node: '>=18.14.1'}
|
engines: {node: '>=18.14.1'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
hono: 4.12.18
|
hono: 4.12.23
|
||||||
|
|
||||||
'@humanfs/core@0.19.1':
|
'@humanfs/core@0.19.1':
|
||||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||||
|
|
@ -2149,8 +2149,8 @@ packages:
|
||||||
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@istanbuljs/schema@0.1.3':
|
'@istanbuljs/schema@0.1.6':
|
||||||
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
|
resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
|
|
@ -5061,20 +5061,20 @@ packages:
|
||||||
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
|
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
vue: ^3.2.25
|
vue: ^3.2.25
|
||||||
|
|
||||||
'@vitest/coverage-v8@3.2.4':
|
'@vitest/coverage-v8@3.2.6':
|
||||||
resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==}
|
resolution: {integrity: sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@vitest/browser': 3.2.4
|
'@vitest/browser': 3.2.6
|
||||||
vitest: 3.2.4
|
vitest: 3.2.6
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
'@vitest/browser':
|
'@vitest/browser':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@vitest/expect@3.2.4':
|
'@vitest/expect@3.2.6':
|
||||||
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
|
resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==}
|
||||||
|
|
||||||
'@vitest/mocker@3.2.4':
|
'@vitest/mocker@3.2.6':
|
||||||
resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
|
resolution: {integrity: sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
msw: ^2.4.9
|
msw: ^2.4.9
|
||||||
vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
|
vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
|
||||||
|
|
@ -5084,20 +5084,20 @@ packages:
|
||||||
vite:
|
vite:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@vitest/pretty-format@3.2.4':
|
'@vitest/pretty-format@3.2.6':
|
||||||
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
|
resolution: {integrity: sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==}
|
||||||
|
|
||||||
'@vitest/runner@3.2.4':
|
'@vitest/runner@3.2.6':
|
||||||
resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
|
resolution: {integrity: sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==}
|
||||||
|
|
||||||
'@vitest/snapshot@3.2.4':
|
'@vitest/snapshot@3.2.6':
|
||||||
resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
|
resolution: {integrity: sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==}
|
||||||
|
|
||||||
'@vitest/spy@3.2.4':
|
'@vitest/spy@3.2.6':
|
||||||
resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
|
resolution: {integrity: sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==}
|
||||||
|
|
||||||
'@vitest/utils@3.2.4':
|
'@vitest/utils@3.2.6':
|
||||||
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
|
resolution: {integrity: sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==}
|
||||||
|
|
||||||
'@volar/language-core@2.4.28':
|
'@volar/language-core@2.4.28':
|
||||||
resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
|
resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
|
||||||
|
|
@ -5506,8 +5506,8 @@ packages:
|
||||||
ast-types-flow@0.0.8:
|
ast-types-flow@0.0.8:
|
||||||
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
|
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
|
||||||
|
|
||||||
ast-v8-to-istanbul@0.3.10:
|
ast-v8-to-istanbul@0.3.12:
|
||||||
resolution: {integrity: sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==}
|
resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==}
|
||||||
|
|
||||||
ast-walker-scope@0.6.2:
|
ast-walker-scope@0.6.2:
|
||||||
resolution: {integrity: sha512-1UWOyC50xI3QZkRuDj6PqDtpm1oHWtYs+NQGwqL/2R11eN3Q81PHAHPM0SWW3BNQm53UDwS//Jv8L4CCVLM1bQ==}
|
resolution: {integrity: sha512-1UWOyC50xI3QZkRuDj6PqDtpm1oHWtYs+NQGwqL/2R11eN3Q81PHAHPM0SWW3BNQm53UDwS//Jv8L4CCVLM1bQ==}
|
||||||
|
|
@ -7548,8 +7548,8 @@ packages:
|
||||||
hls.js@1.6.16:
|
hls.js@1.6.16:
|
||||||
resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==}
|
resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==}
|
||||||
|
|
||||||
hono@4.12.18:
|
hono@4.12.23:
|
||||||
resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==}
|
resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==}
|
||||||
engines: {node: '>=16.9.0'}
|
engines: {node: '>=16.9.0'}
|
||||||
|
|
||||||
hookable@5.5.3:
|
hookable@5.5.3:
|
||||||
|
|
@ -8030,6 +8030,9 @@ packages:
|
||||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
js-tokens@10.0.0:
|
||||||
|
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
|
|
@ -10476,8 +10479,8 @@ packages:
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
test-exclude@7.0.1:
|
test-exclude@7.0.2:
|
||||||
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
|
resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
text-decoder@1.2.7:
|
text-decoder@1.2.7:
|
||||||
|
|
@ -10513,10 +10516,6 @@ packages:
|
||||||
tinyexec@0.3.2:
|
tinyexec@0.3.2:
|
||||||
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
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:
|
tinyexec@1.1.2:
|
||||||
resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==}
|
resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
@ -11221,16 +11220,16 @@ packages:
|
||||||
postcss:
|
postcss:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
vitest@3.2.4:
|
vitest@3.2.6:
|
||||||
resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
|
resolution: {integrity: sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==}
|
||||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@edge-runtime/vm': '*'
|
'@edge-runtime/vm': '*'
|
||||||
'@types/debug': ^4.1.12
|
'@types/debug': ^4.1.12
|
||||||
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
|
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
|
||||||
'@vitest/browser': 3.2.4
|
'@vitest/browser': 3.2.6
|
||||||
'@vitest/ui': 3.2.4
|
'@vitest/ui': 3.2.6
|
||||||
happy-dom: '*'
|
happy-dom: '*'
|
||||||
jsdom: '*'
|
jsdom: '*'
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
|
|
@ -11591,7 +11590,7 @@ snapshots:
|
||||||
'@antfu/install-pkg@1.1.0':
|
'@antfu/install-pkg@1.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
package-manager-detector: 1.6.0
|
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)':
|
'@apidevtools/json-schema-ref-parser@14.2.1(@types/json-schema@7.0.15)':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -11654,15 +11653,15 @@ snapshots:
|
||||||
|
|
||||||
'@babel/generator@7.28.6':
|
'@babel/generator@7.28.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.28.6
|
'@babel/parser': 7.29.3
|
||||||
'@babel/types': 7.28.6
|
'@babel/types': 7.29.0
|
||||||
'@jridgewell/gen-mapping': 0.3.13
|
'@jridgewell/gen-mapping': 0.3.13
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
jsesc: 3.1.0
|
jsesc: 3.1.0
|
||||||
|
|
||||||
'@babel/generator@7.29.1':
|
'@babel/generator@7.29.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.29.2
|
'@babel/parser': 7.29.3
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.0
|
||||||
'@jridgewell/gen-mapping': 0.3.13
|
'@jridgewell/gen-mapping': 0.3.13
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
|
|
@ -11813,7 +11812,7 @@ snapshots:
|
||||||
'@babel/template@7.28.6':
|
'@babel/template@7.28.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.0
|
'@babel/code-frame': 7.29.0
|
||||||
'@babel/parser': 7.29.2
|
'@babel/parser': 7.29.3
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.0
|
||||||
|
|
||||||
'@babel/traverse@7.28.6':
|
'@babel/traverse@7.28.6':
|
||||||
|
|
@ -11821,9 +11820,9 @@ snapshots:
|
||||||
'@babel/code-frame': 7.28.6
|
'@babel/code-frame': 7.28.6
|
||||||
'@babel/generator': 7.29.1
|
'@babel/generator': 7.29.1
|
||||||
'@babel/helper-globals': 7.28.0
|
'@babel/helper-globals': 7.28.0
|
||||||
'@babel/parser': 7.28.6
|
'@babel/parser': 7.29.3
|
||||||
'@babel/template': 7.28.6
|
'@babel/template': 7.28.6
|
||||||
'@babel/types': 7.28.6
|
'@babel/types': 7.29.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
@ -11833,7 +11832,7 @@ snapshots:
|
||||||
'@babel/code-frame': 7.29.0
|
'@babel/code-frame': 7.29.0
|
||||||
'@babel/generator': 7.29.1
|
'@babel/generator': 7.29.1
|
||||||
'@babel/helper-globals': 7.28.0
|
'@babel/helper-globals': 7.28.0
|
||||||
'@babel/parser': 7.29.2
|
'@babel/parser': 7.29.3
|
||||||
'@babel/template': 7.28.6
|
'@babel/template': 7.28.6
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
|
|
@ -12832,9 +12831,9 @@ snapshots:
|
||||||
|
|
||||||
'@floating-ui/utils@0.2.11': {}
|
'@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:
|
dependencies:
|
||||||
hono: 4.12.18
|
hono: 4.12.23
|
||||||
|
|
||||||
'@humanfs/core@0.19.1': {}
|
'@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))':
|
'@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:
|
dependencies:
|
||||||
'@babel/parser': 7.29.2
|
'@babel/parser': 7.29.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@intlify/shared': 11.3.0
|
'@intlify/shared': 11.3.0
|
||||||
'@vue/compiler-dom': 3.5.34
|
'@vue/compiler-dom': 3.5.34
|
||||||
|
|
@ -13094,7 +13093,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
minipass: 7.1.3
|
minipass: 7.1.3
|
||||||
|
|
||||||
'@istanbuljs/schema@0.1.3': {}
|
'@istanbuljs/schema@0.1.6': {}
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -13267,7 +13266,7 @@ snapshots:
|
||||||
|
|
||||||
'@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)':
|
'@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)':
|
||||||
dependencies:
|
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: 8.18.0
|
||||||
ajv-formats: 3.0.1(ajv@8.18.0)
|
ajv-formats: 3.0.1(ajv@8.18.0)
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
|
|
@ -13277,7 +13276,7 @@ snapshots:
|
||||||
eventsource-parser: 3.0.6
|
eventsource-parser: 3.0.6
|
||||||
express: 5.2.1
|
express: 5.2.1
|
||||||
express-rate-limit: 8.3.0(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
|
jose: 6.2.0
|
||||||
json-schema-typed: 8.0.2
|
json-schema-typed: 8.0.2
|
||||||
pkce-challenge: 5.0.1
|
pkce-challenge: 5.0.1
|
||||||
|
|
@ -13532,8 +13531,8 @@ snapshots:
|
||||||
pkg-types: 2.3.0
|
pkg-types: 2.3.0
|
||||||
rc9: 3.0.0
|
rc9: 3.0.0
|
||||||
scule: 1.3.0
|
scule: 1.3.0
|
||||||
semver: 7.7.4
|
semver: 7.8.0
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.16
|
||||||
ufo: 1.6.3
|
ufo: 1.6.3
|
||||||
unctx: 2.5.0
|
unctx: 2.5.0
|
||||||
untyped: 2.0.0
|
untyped: 2.0.0
|
||||||
|
|
@ -15662,16 +15661,16 @@ snapshots:
|
||||||
|
|
||||||
'@types/babel__generator@7.27.0':
|
'@types/babel__generator@7.27.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.28.6
|
'@babel/types': 7.29.0
|
||||||
|
|
||||||
'@types/babel__template@7.4.4':
|
'@types/babel__template@7.4.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.28.6
|
'@babel/parser': 7.29.3
|
||||||
'@babel/types': 7.28.6
|
'@babel/types': 7.29.0
|
||||||
|
|
||||||
'@types/babel__traverse@7.28.0':
|
'@types/babel__traverse@7.28.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.28.6
|
'@babel/types': 7.29.0
|
||||||
|
|
||||||
'@types/cacheable-request@6.0.3':
|
'@types/cacheable-request@6.0.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -16080,7 +16079,7 @@ snapshots:
|
||||||
'@typescript-eslint/visitor-keys': 8.57.1
|
'@typescript-eslint/visitor-keys': 8.57.1
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
minimatch: 10.2.3
|
minimatch: 10.2.3
|
||||||
semver: 7.7.4
|
semver: 7.8.0
|
||||||
tinyglobby: 0.2.16
|
tinyglobby: 0.2.16
|
||||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
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)
|
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)
|
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:
|
dependencies:
|
||||||
'@ampproject/remapping': 2.3.0
|
'@ampproject/remapping': 2.3.0
|
||||||
'@bcoe/v8-coverage': 1.0.2
|
'@bcoe/v8-coverage': 1.0.2
|
||||||
ast-v8-to-istanbul: 0.3.10
|
ast-v8-to-istanbul: 0.3.12
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
istanbul-lib-coverage: 3.2.2
|
istanbul-lib-coverage: 3.2.2
|
||||||
istanbul-lib-report: 3.0.1
|
istanbul-lib-report: 3.0.1
|
||||||
|
|
@ -16271,59 +16270,59 @@ snapshots:
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
magicast: 0.3.5
|
magicast: 0.3.5
|
||||||
std-env: 3.10.0
|
std-env: 3.10.0
|
||||||
test-exclude: 7.0.1
|
test-exclude: 7.0.2
|
||||||
tinyrainbow: 2.0.0
|
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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@vitest/expect@3.2.4':
|
'@vitest/expect@3.2.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/spy': 3.2.4
|
'@vitest/spy': 3.2.6
|
||||||
'@vitest/utils': 3.2.4
|
'@vitest/utils': 3.2.6
|
||||||
chai: 5.3.3
|
chai: 5.3.3
|
||||||
tinyrainbow: 2.0.0
|
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:
|
dependencies:
|
||||||
'@vitest/spy': 3.2.4
|
'@vitest/spy': 3.2.6
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
optionalDependencies:
|
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:
|
dependencies:
|
||||||
'@vitest/spy': 3.2.4
|
'@vitest/spy': 3.2.6
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
optionalDependencies:
|
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:
|
dependencies:
|
||||||
tinyrainbow: 2.0.0
|
tinyrainbow: 2.0.0
|
||||||
|
|
||||||
'@vitest/runner@3.2.4':
|
'@vitest/runner@3.2.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/utils': 3.2.4
|
'@vitest/utils': 3.2.6
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
strip-literal: 3.1.0
|
strip-literal: 3.1.0
|
||||||
|
|
||||||
'@vitest/snapshot@3.2.4':
|
'@vitest/snapshot@3.2.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/pretty-format': 3.2.4
|
'@vitest/pretty-format': 3.2.6
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
|
|
||||||
'@vitest/spy@3.2.4':
|
'@vitest/spy@3.2.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
tinyspy: 4.0.4
|
tinyspy: 4.0.4
|
||||||
|
|
||||||
'@vitest/utils@3.2.4':
|
'@vitest/utils@3.2.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/pretty-format': 3.2.4
|
'@vitest/pretty-format': 3.2.6
|
||||||
loupe: 3.2.1
|
loupe: 3.2.1
|
||||||
tinyrainbow: 2.0.0
|
tinyrainbow: 2.0.0
|
||||||
|
|
||||||
|
|
@ -16378,14 +16377,14 @@ snapshots:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/helper-module-imports': 7.28.6
|
'@babel/helper-module-imports': 7.28.6
|
||||||
'@babel/helper-plugin-utils': 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
|
'@vue/compiler-sfc': 3.5.30
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@vue/compiler-core@3.5.30':
|
'@vue/compiler-core@3.5.30':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.29.2
|
'@babel/parser': 7.29.3
|
||||||
'@vue/shared': 3.5.30
|
'@vue/shared': 3.5.30
|
||||||
entities: 7.0.1
|
entities: 7.0.1
|
||||||
estree-walker: 2.0.2
|
estree-walker: 2.0.2
|
||||||
|
|
@ -16411,7 +16410,7 @@ snapshots:
|
||||||
|
|
||||||
'@vue/compiler-sfc@3.5.30':
|
'@vue/compiler-sfc@3.5.30':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.29.2
|
'@babel/parser': 7.29.3
|
||||||
'@vue/compiler-core': 3.5.30
|
'@vue/compiler-core': 3.5.30
|
||||||
'@vue/compiler-dom': 3.5.30
|
'@vue/compiler-dom': 3.5.30
|
||||||
'@vue/compiler-ssr': 3.5.30
|
'@vue/compiler-ssr': 3.5.30
|
||||||
|
|
@ -16859,30 +16858,30 @@ snapshots:
|
||||||
|
|
||||||
ast-kit@1.4.3:
|
ast-kit@1.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.29.2
|
'@babel/parser': 7.29.3
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
|
|
||||||
ast-kit@2.2.0:
|
ast-kit@2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.29.2
|
'@babel/parser': 7.29.3
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
|
|
||||||
ast-types-flow@0.0.8: {}
|
ast-types-flow@0.0.8: {}
|
||||||
|
|
||||||
ast-v8-to-istanbul@0.3.10:
|
ast-v8-to-istanbul@0.3.12:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
js-tokens: 9.0.1
|
js-tokens: 10.0.0
|
||||||
|
|
||||||
ast-walker-scope@0.6.2:
|
ast-walker-scope@0.6.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.29.2
|
'@babel/parser': 7.29.3
|
||||||
ast-kit: 1.4.3
|
ast-kit: 1.4.3
|
||||||
|
|
||||||
ast-walker-scope@0.8.3:
|
ast-walker-scope@0.8.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.29.2
|
'@babel/parser': 7.29.3
|
||||||
ast-kit: 2.2.0
|
ast-kit: 2.2.0
|
||||||
|
|
||||||
astral-regex@2.0.0:
|
astral-regex@2.0.0:
|
||||||
|
|
@ -18400,7 +18399,7 @@ snapshots:
|
||||||
eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
|
eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
minimatch: 9.0.7
|
minimatch: 9.0.7
|
||||||
semver: 7.7.4
|
semver: 7.8.0
|
||||||
stable-hash-x: 0.2.0
|
stable-hash-x: 0.2.0
|
||||||
unrs-resolver: 1.11.1
|
unrs-resolver: 1.11.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
|
@ -18420,7 +18419,7 @@ snapshots:
|
||||||
eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
|
eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
minimatch: 9.0.7
|
minimatch: 9.0.7
|
||||||
semver: 7.7.4
|
semver: 7.8.0
|
||||||
stable-hash-x: 0.2.0
|
stable-hash-x: 0.2.0
|
||||||
unrs-resolver: 1.11.1
|
unrs-resolver: 1.11.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
|
@ -18472,7 +18471,7 @@ snapshots:
|
||||||
html-entities: 2.6.0
|
html-entities: 2.6.0
|
||||||
object-deep-merge: 2.0.0
|
object-deep-merge: 2.0.0
|
||||||
parse-imports-exports: 0.2.4
|
parse-imports-exports: 0.2.4
|
||||||
semver: 7.7.4
|
semver: 7.8.0
|
||||||
spdx-expression-parse: 4.0.0
|
spdx-expression-parse: 4.0.0
|
||||||
to-valid-identifier: 1.0.0
|
to-valid-identifier: 1.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
|
@ -18590,7 +18589,7 @@ snapshots:
|
||||||
pluralize: 8.0.0
|
pluralize: 8.0.0
|
||||||
regexp-tree: 0.1.27
|
regexp-tree: 0.1.27
|
||||||
regjsparser: 0.13.0
|
regjsparser: 0.13.0
|
||||||
semver: 7.7.4
|
semver: 7.8.0
|
||||||
strip-indent: 4.1.1
|
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))):
|
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
|
natural-compare: 1.4.0
|
||||||
nth-check: 2.1.1
|
nth-check: 2.1.1
|
||||||
postcss-selector-parser: 7.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))
|
vue-eslint-parser: 10.4.0(eslint@9.39.4(jiti@2.7.0))
|
||||||
xml-name-validator: 4.0.0
|
xml-name-validator: 4.0.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
|
@ -18956,7 +18955,7 @@ snapshots:
|
||||||
execa: 9.6.1
|
execa: 9.6.1
|
||||||
file-type: 21.3.2
|
file-type: 21.3.2
|
||||||
fuse.js: 7.3.0
|
fuse.js: 7.3.0
|
||||||
hono: 4.12.18
|
hono: 4.12.23
|
||||||
mcp-proxy: 6.4.1
|
mcp-proxy: 6.4.1
|
||||||
strict-event-emitter-types: 2.0.0
|
strict-event-emitter-types: 2.0.0
|
||||||
undici: 7.24.0
|
undici: 7.24.0
|
||||||
|
|
@ -19269,7 +19268,7 @@ snapshots:
|
||||||
es6-error: 4.1.1
|
es6-error: 4.1.1
|
||||||
matcher: 3.0.0
|
matcher: 3.0.0
|
||||||
roarr: 2.15.4
|
roarr: 2.15.4
|
||||||
semver: 7.7.4
|
semver: 7.8.0
|
||||||
serialize-error: 7.0.1
|
serialize-error: 7.0.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
@ -19513,7 +19512,7 @@ snapshots:
|
||||||
|
|
||||||
hls.js@1.6.16: {}
|
hls.js@1.6.16: {}
|
||||||
|
|
||||||
hono@4.12.18: {}
|
hono@4.12.23: {}
|
||||||
|
|
||||||
hookable@5.5.3: {}
|
hookable@5.5.3: {}
|
||||||
|
|
||||||
|
|
@ -19992,6 +19991,8 @@ snapshots:
|
||||||
|
|
||||||
joycon@3.1.1: {}
|
joycon@3.1.1: {}
|
||||||
|
|
||||||
|
js-tokens@10.0.0: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
js-tokens@9.0.1: {}
|
js-tokens@9.0.1: {}
|
||||||
|
|
@ -20042,7 +20043,7 @@ snapshots:
|
||||||
acorn: 8.16.0
|
acorn: 8.16.0
|
||||||
eslint-visitor-keys: 3.4.3
|
eslint-visitor-keys: 3.4.3
|
||||||
espree: 9.6.1
|
espree: 9.6.1
|
||||||
semver: 7.7.4
|
semver: 7.8.0
|
||||||
|
|
||||||
jsonc-parser@3.3.1: {}
|
jsonc-parser@3.3.1: {}
|
||||||
|
|
||||||
|
|
@ -20325,19 +20326,19 @@ snapshots:
|
||||||
|
|
||||||
magicast@0.3.5:
|
magicast@0.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.29.2
|
'@babel/parser': 7.29.3
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.0
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
magicast@0.5.2:
|
magicast@0.5.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.29.2
|
'@babel/parser': 7.29.3
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.0
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
make-dir@4.0.0:
|
make-dir@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.4
|
semver: 7.8.0
|
||||||
|
|
||||||
mark.js@8.11.1: {}
|
mark.js@8.11.1: {}
|
||||||
|
|
||||||
|
|
@ -21243,7 +21244,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
citty: 0.2.2
|
citty: 0.2.2
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
tinyexec: 1.0.2
|
tinyexec: 1.1.2
|
||||||
|
|
||||||
nypm@0.6.6:
|
nypm@0.6.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -23147,11 +23148,11 @@ snapshots:
|
||||||
commander: 2.20.3
|
commander: 2.20.3
|
||||||
source-map-support: 0.5.21
|
source-map-support: 0.5.21
|
||||||
|
|
||||||
test-exclude@7.0.1:
|
test-exclude@7.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@istanbuljs/schema': 0.1.3
|
'@istanbuljs/schema': 0.1.6
|
||||||
glob: 10.5.0
|
glob: 10.5.0
|
||||||
minimatch: 9.0.7
|
minimatch: 10.2.3
|
||||||
|
|
||||||
text-decoder@1.2.7:
|
text-decoder@1.2.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -23185,8 +23186,6 @@ snapshots:
|
||||||
|
|
||||||
tinyexec@0.3.2: {}
|
tinyexec@0.3.2: {}
|
||||||
|
|
||||||
tinyexec@1.0.2: {}
|
|
||||||
|
|
||||||
tinyexec@1.1.2: {}
|
tinyexec@1.1.2: {}
|
||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
|
|
@ -23743,7 +23742,7 @@ snapshots:
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
es-module-lexer: 1.7.0
|
es-module-lexer: 1.7.0
|
||||||
pathe: 2.0.3
|
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:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- jiti
|
- jiti
|
||||||
|
|
@ -23764,7 +23763,7 @@ snapshots:
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
es-module-lexer: 1.7.0
|
es-module-lexer: 1.7.0
|
||||||
pathe: 2.0.3
|
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:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- jiti
|
- jiti
|
||||||
|
|
@ -23871,26 +23870,9 @@ snapshots:
|
||||||
tsx: 4.21.0
|
tsx: 4.21.0
|
||||||
yaml: 2.9.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:
|
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
|
|
||||||
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
|
|
||||||
fdir: 6.5.0(picomatch@4.0.4)
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
postcss: 8.5.10
|
postcss: 8.5.10
|
||||||
|
|
@ -23912,7 +23894,7 @@ snapshots:
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
postcss: 8.5.10
|
postcss: 8.5.10
|
||||||
rollup: 4.59.0
|
rollup: 4.59.0
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.16
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 25.0.7
|
'@types/node': 25.0.7
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
@ -24011,16 +23993,16 @@ snapshots:
|
||||||
- universal-cookie
|
- universal-cookie
|
||||||
- yaml
|
- 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:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/expect': 3.2.4
|
'@vitest/expect': 3.2.6
|
||||||
'@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))
|
||||||
'@vitest/pretty-format': 3.2.4
|
'@vitest/pretty-format': 3.2.6
|
||||||
'@vitest/runner': 3.2.4
|
'@vitest/runner': 3.2.6
|
||||||
'@vitest/snapshot': 3.2.4
|
'@vitest/snapshot': 3.2.6
|
||||||
'@vitest/spy': 3.2.4
|
'@vitest/spy': 3.2.6
|
||||||
'@vitest/utils': 3.2.4
|
'@vitest/utils': 3.2.6
|
||||||
chai: 5.3.3
|
chai: 5.3.3
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
expect-type: 1.3.0
|
expect-type: 1.3.0
|
||||||
|
|
@ -24030,10 +24012,10 @@ snapshots:
|
||||||
std-env: 3.10.0
|
std-env: 3.10.0
|
||||||
tinybench: 2.9.0
|
tinybench: 2.9.0
|
||||||
tinyexec: 0.3.2
|
tinyexec: 0.3.2
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.16
|
||||||
tinypool: 1.1.1
|
tinypool: 1.1.1
|
||||||
tinyrainbow: 2.0.0
|
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)
|
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
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
|
@ -24054,16 +24036,16 @@ snapshots:
|
||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- 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:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/expect': 3.2.4
|
'@vitest/expect': 3.2.6
|
||||||
'@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))
|
||||||
'@vitest/pretty-format': 3.2.4
|
'@vitest/pretty-format': 3.2.6
|
||||||
'@vitest/runner': 3.2.4
|
'@vitest/runner': 3.2.6
|
||||||
'@vitest/snapshot': 3.2.4
|
'@vitest/snapshot': 3.2.6
|
||||||
'@vitest/spy': 3.2.4
|
'@vitest/spy': 3.2.6
|
||||||
'@vitest/utils': 3.2.4
|
'@vitest/utils': 3.2.6
|
||||||
chai: 5.3.3
|
chai: 5.3.3
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
expect-type: 1.3.0
|
expect-type: 1.3.0
|
||||||
|
|
@ -24073,10 +24055,10 @@ snapshots:
|
||||||
std-env: 3.10.0
|
std-env: 3.10.0
|
||||||
tinybench: 2.9.0
|
tinybench: 2.9.0
|
||||||
tinyexec: 0.3.2
|
tinyexec: 0.3.2
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.16
|
||||||
tinypool: 1.1.1
|
tinypool: 1.1.1
|
||||||
tinyrainbow: 2.0.0
|
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)
|
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
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
|
@ -24119,7 +24101,7 @@ snapshots:
|
||||||
eslint-visitor-keys: 5.0.1
|
eslint-visitor-keys: 5.0.1
|
||||||
espree: 11.2.0
|
espree: 11.2.0
|
||||||
esquery: 1.7.0
|
esquery: 1.7.0
|
||||||
semver: 7.7.4
|
semver: 7.8.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,10 @@ export interface TeamGraphData extends TeamViewSnapshot {
|
||||||
runtimeEntriesByMember?: Record<string, TeamAgentRuntimeEntry>;
|
runtimeEntriesByMember?: Record<string, TeamAgentRuntimeEntry>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TeamGraphAdapterText {
|
||||||
|
hiddenBlockingLinks(count: number): string;
|
||||||
|
}
|
||||||
|
|
||||||
function toGraphLaunchVisualState(
|
function toGraphLaunchVisualState(
|
||||||
visualState: ReturnType<typeof buildMemberLaunchPresentation>['launchVisualState'] | undefined
|
visualState: ReturnType<typeof buildMemberLaunchPresentation>['launchVisualState'] | undefined
|
||||||
): GraphNode['launchVisualState'] {
|
): GraphNode['launchVisualState'] {
|
||||||
|
|
@ -141,7 +145,8 @@ export class TeamGraphAdapter {
|
||||||
slotAssignments?: Record<string, GraphOwnerSlotAssignment>,
|
slotAssignments?: Record<string, GraphOwnerSlotAssignment>,
|
||||||
layoutMode: GraphLayoutMode = DEFAULT_TEAM_GRAPH_LAYOUT_MODE,
|
layoutMode: GraphLayoutMode = DEFAULT_TEAM_GRAPH_LAYOUT_MODE,
|
||||||
gridOwnerOrder?: readonly string[],
|
gridOwnerOrder?: readonly string[],
|
||||||
activeTaskLogActivity?: Record<string, true>
|
activeTaskLogActivity?: Record<string, true>,
|
||||||
|
text?: TeamGraphAdapterText
|
||||||
): GraphDataPort {
|
): GraphDataPort {
|
||||||
if (teamData?.teamName !== teamName) {
|
if (teamData?.teamName !== teamName) {
|
||||||
return TeamGraphAdapter.#emptyResult(teamName);
|
return TeamGraphAdapter.#emptyResult(teamName);
|
||||||
|
|
@ -227,7 +232,8 @@ export class TeamGraphAdapter {
|
||||||
memberNodeIdByAlias,
|
memberNodeIdByAlias,
|
||||||
leadId,
|
leadId,
|
||||||
leadName,
|
leadName,
|
||||||
activeTaskLogActivity
|
activeTaskLogActivity,
|
||||||
|
text
|
||||||
);
|
);
|
||||||
this.#buildProcessNodes(nodes, edges, teamData, teamName, memberNodeIdByAlias);
|
this.#buildProcessNodes(nodes, edges, teamData, teamName, memberNodeIdByAlias);
|
||||||
this.#attachActivityFeeds(nodes, teamData, teamName, leadId, leadName);
|
this.#attachActivityFeeds(nodes, teamData, teamName, leadId, leadName);
|
||||||
|
|
@ -673,7 +679,8 @@ export class TeamGraphAdapter {
|
||||||
memberNodeIdByAlias?: ReadonlyMap<string, string>,
|
memberNodeIdByAlias?: ReadonlyMap<string, string>,
|
||||||
leadId?: string,
|
leadId?: string,
|
||||||
leadName?: string,
|
leadName?: string,
|
||||||
activeTaskLogActivity?: Record<string, true>
|
activeTaskLogActivity?: Record<string, true>,
|
||||||
|
text?: TeamGraphAdapterText
|
||||||
): void {
|
): void {
|
||||||
const taskStateById = new Map<
|
const taskStateById = new Map<
|
||||||
string,
|
string,
|
||||||
|
|
@ -915,9 +922,10 @@ export class TeamGraphAdapter {
|
||||||
sourceTaskIds: Array.from(edge.sourceTaskIds),
|
sourceTaskIds: Array.from(edge.sourceTaskIds),
|
||||||
targetTaskIds: Array.from(edge.targetTaskIds),
|
targetTaskIds: Array.from(edge.targetTaskIds),
|
||||||
label:
|
label:
|
||||||
edge.aggregateCount > 1 &&
|
edge.aggregateCount > 1 &&
|
||||||
(edge.source.includes(':overflow:') || edge.target.includes(':overflow:'))
|
(edge.source.includes(':overflow:') || edge.target.includes(':overflow:'))
|
||||||
? `${edge.aggregateCount} hidden blocking links`
|
? (text?.hiddenBlockingLinks(edge.aggregateCount) ??
|
||||||
|
`${edge.aggregateCount} hidden blocking links`)
|
||||||
: undefined,
|
: undefined,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import { useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
import { useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
||||||
|
|
||||||
|
import { useAppTranslation } from '@features/localization/renderer';
|
||||||
import { useTeamAgentRuntimeWatcher } from '@renderer/components/team/useTeamAgentRuntimeWatcher';
|
import { useTeamAgentRuntimeWatcher } from '@renderer/components/team/useTeamAgentRuntimeWatcher';
|
||||||
import { getSnapshot, subscribe } from '@renderer/services/commentReadStorage';
|
import { getSnapshot, subscribe } from '@renderer/services/commentReadStorage';
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
|
|
@ -65,9 +66,17 @@ export function useTeamGraphAdapter(
|
||||||
options?: UseTeamGraphAdapterOptions
|
options?: UseTeamGraphAdapterOptions
|
||||||
): GraphDataPort {
|
): GraphDataPort {
|
||||||
const isActive = options?.active ?? true;
|
const isActive = options?.active ?? true;
|
||||||
|
const { t } = useAppTranslation('team');
|
||||||
const adapterRef = useRef<TeamGraphAdapter>(TeamGraphAdapter.create());
|
const adapterRef = useRef<TeamGraphAdapter>(TeamGraphAdapter.create());
|
||||||
const inactiveGraphData = useMemo(() => emptyGraphData(teamName), [teamName]);
|
const inactiveGraphData = useMemo(() => emptyGraphData(teamName), [teamName]);
|
||||||
const lastActiveGraphDataRef = useRef<GraphDataPort>(inactiveGraphData);
|
const lastActiveGraphDataRef = useRef<GraphDataPort>(inactiveGraphData);
|
||||||
|
const adapterText = useMemo(
|
||||||
|
() => ({
|
||||||
|
hiddenBlockingLinks: (count: number) =>
|
||||||
|
t('agentGraph.blockingEdge.hiddenBlockingLinks', { count }),
|
||||||
|
}),
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
teamSnapshot,
|
teamSnapshot,
|
||||||
|
|
@ -216,7 +225,8 @@ export function useTeamGraphAdapter(
|
||||||
effectiveSlotAssignments,
|
effectiveSlotAssignments,
|
||||||
graphLayoutMode ?? DEFAULT_TEAM_GRAPH_LAYOUT_MODE,
|
graphLayoutMode ?? DEFAULT_TEAM_GRAPH_LAYOUT_MODE,
|
||||||
gridOwnerOrder,
|
gridOwnerOrder,
|
||||||
activeTaskLogActivity
|
activeTaskLogActivity,
|
||||||
|
adapterText
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
isActive,
|
isActive,
|
||||||
|
|
@ -236,6 +246,7 @@ export function useTeamGraphAdapter(
|
||||||
graphLayoutMode,
|
graphLayoutMode,
|
||||||
gridOwnerOrder,
|
gridOwnerOrder,
|
||||||
activeTaskLogActivity,
|
activeTaskLogActivity,
|
||||||
|
adapterText,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -21,26 +21,47 @@ function isOverflowNode(
|
||||||
return Boolean(node?.kind === 'task' && node.isOverflowStack);
|
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 (!node) return fallback;
|
||||||
if (isOverflowNode(node)) {
|
if (isOverflowNode(node)) {
|
||||||
return node.overflowCount && node.overflowCount > 1
|
return node.overflowCount && node.overflowCount > 1
|
||||||
? `${node.overflowCount} hidden tasks`
|
? labels.hiddenTasks(node.overflowCount)
|
||||||
: 'Hidden task stack';
|
: labels.hiddenTaskStack;
|
||||||
}
|
}
|
||||||
if (isTaskNode(node)) {
|
if (isTaskNode(node)) {
|
||||||
return `${node.displayId ?? node.label} - ${node.sublabel ?? 'Task'}`;
|
return `${node.displayId ?? node.label} - ${node.sublabel ?? labels.task}`;
|
||||||
}
|
}
|
||||||
return node.label;
|
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 (!node) return null;
|
||||||
if (isOverflowNode(node)) {
|
if (isOverflowNode(node)) {
|
||||||
return role === 'blocker' ? 'Open blocker stack' : 'Open blocked stack';
|
return role === 'blocker' ? labels.openBlockerStack : labels.openBlockedStack;
|
||||||
}
|
}
|
||||||
if (isTaskNode(node)) {
|
if (isTaskNode(node)) {
|
||||||
return role === 'blocker' ? 'Open blocker task' : 'Open blocked task';
|
return role === 'blocker' ? labels.openBlockerTask : labels.openBlockedTask;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -71,10 +92,19 @@ export const GraphBlockingEdgePopover = ({
|
||||||
[teamData?.tasks]
|
[teamData?.tasks]
|
||||||
);
|
);
|
||||||
const relationCount = edge.aggregateCount ?? 1;
|
const relationCount = edge.aggregateCount ?? 1;
|
||||||
const sourceLabel = describeNode(sourceNode, edge.source);
|
const labels: BlockingEdgeLabels = {
|
||||||
const targetLabel = describeNode(targetNode, edge.target);
|
hiddenTaskStack: t('agentGraph.blockingEdge.hiddenTaskStack'),
|
||||||
const sourceActionLabel = getActionLabel(sourceNode, 'blocker');
|
hiddenTasks: (count) => t('agentGraph.blockingEdge.hiddenTasks', { count }),
|
||||||
const targetActionLabel = getActionLabel(targetNode, 'blocked');
|
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 sourceHiddenTasks = resolveEdgeTaskPreview(sourceNode, edge.sourceTaskIds, tasksById);
|
||||||
const targetHiddenTasks = resolveEdgeTaskPreview(targetNode, edge.targetTaskIds, tasksById);
|
const targetHiddenTasks = resolveEdgeTaskPreview(targetNode, edge.targetTaskIds, tasksById);
|
||||||
|
|
||||||
|
|
@ -111,7 +141,7 @@ export const GraphBlockingEdgePopover = ({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="border-red-500/30 px-1.5 py-0 text-[10px] text-red-300"
|
className="border-red-500/30 px-1.5 py-0 text-[10px] text-red-300"
|
||||||
>
|
>
|
||||||
{relationCount} links
|
{t('agentGraph.blockingEdge.links', { count: relationCount })}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,14 @@ function hasOpenCodeEmptyStateWarning(preview: MemberLogPreviewMember | undefine
|
||||||
function resolveEmptyText(
|
function resolveEmptyText(
|
||||||
preview: MemberLogPreviewMember | undefined,
|
preview: MemberLogPreviewMember | undefined,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
error: string | null
|
error: string | null,
|
||||||
|
labels: {
|
||||||
|
unsupportedProvider: string;
|
||||||
|
openCodeLogsDelayed: string;
|
||||||
|
logsUnavailable: string;
|
||||||
|
loadingLogs: string;
|
||||||
|
noRecentLogs: string;
|
||||||
|
}
|
||||||
): string {
|
): string {
|
||||||
const hasCodexUnsupportedWarning = preview?.warnings.some(
|
const hasCodexUnsupportedWarning = preview?.warnings.some(
|
||||||
(warning) => warning.code === 'codex_member_wide_not_supported'
|
(warning) => warning.code === 'codex_member_wide_not_supported'
|
||||||
|
|
@ -142,34 +149,47 @@ function resolveEmptyText(
|
||||||
(preview?.coverage.length ?? 0) > 0 &&
|
(preview?.coverage.length ?? 0) > 0 &&
|
||||||
preview?.coverage.every((coverage) => coverage.provider === 'codex_native_trace');
|
preview?.coverage.every((coverage) => coverage.provider === 'codex_native_trace');
|
||||||
if (hasOnlyCodexUnsupportedCoverage) {
|
if (hasOnlyCodexUnsupportedCoverage) {
|
||||||
return 'Unsupported provider';
|
return labels.unsupportedProvider;
|
||||||
}
|
}
|
||||||
if ((preview?.items.length ?? 0) === 0 && hasOpenCodeDeliveryDelayedWarning(preview)) {
|
if ((preview?.items.length ?? 0) === 0 && hasOpenCodeDeliveryDelayedWarning(preview)) {
|
||||||
return 'OpenCode logs delayed';
|
return labels.openCodeLogsDelayed;
|
||||||
}
|
}
|
||||||
if ((preview?.items.length ?? 0) === 0 && hasOpenCodeRuntimeWarning(preview)) {
|
if ((preview?.items.length ?? 0) === 0 && hasOpenCodeRuntimeWarning(preview)) {
|
||||||
return 'Logs unavailable';
|
return labels.logsUnavailable;
|
||||||
}
|
}
|
||||||
if (loading && !preview) return 'Loading logs';
|
if (loading && !preview) return labels.loadingLogs;
|
||||||
if (error && !preview) return 'Logs unavailable';
|
if (error && !preview) return labels.logsUnavailable;
|
||||||
return 'No recent logs';
|
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') {
|
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') {
|
if (item.kind === 'tool_use') {
|
||||||
return item.toolName?.trim() || 'Tool use';
|
return item.toolName?.trim() || labels.toolUse;
|
||||||
}
|
}
|
||||||
if (item.kind === 'thinking') {
|
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 {
|
function compactDisplayTitle(
|
||||||
const title = item.title.trim() || fallbackDisplayTitle(item);
|
item: MemberLogPreviewItem,
|
||||||
|
labels: Parameters<typeof fallbackDisplayTitle>[1]
|
||||||
|
): string {
|
||||||
|
const title = item.title.trim() || fallbackDisplayTitle(item, labels);
|
||||||
if (title.toLowerCase() === 'tool result') {
|
if (title.toLowerCase() === 'tool result') {
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
@ -205,7 +225,13 @@ function trimRepeatedTitlePrefix(preview: string, title: string): string {
|
||||||
function compactPreviewText(
|
function compactPreviewText(
|
||||||
item: MemberLogPreviewItem,
|
item: MemberLogPreviewItem,
|
||||||
displayTitle: string,
|
displayTitle: string,
|
||||||
rawDisplayTitle = displayTitle
|
rawDisplayTitle = displayTitle,
|
||||||
|
labels: {
|
||||||
|
noErrorOutput: string;
|
||||||
|
noOutput: string;
|
||||||
|
noInput: string;
|
||||||
|
logEvent: string;
|
||||||
|
}
|
||||||
): string {
|
): string {
|
||||||
const preview = item.preview?.trim();
|
const preview = item.preview?.trim();
|
||||||
if (preview) {
|
if (preview) {
|
||||||
|
|
@ -217,12 +243,12 @@ function compactPreviewText(
|
||||||
return compact || preview;
|
return compact || preview;
|
||||||
}
|
}
|
||||||
if (item.kind === 'tool_result') {
|
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') {
|
if (item.kind === 'tool_use') {
|
||||||
return 'No input';
|
return labels.noInput;
|
||||||
}
|
}
|
||||||
return item.sourceLabel || 'Log event';
|
return item.sourceLabel || labels.logEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
function truncateCompactRowPreview(
|
function truncateCompactRowPreview(
|
||||||
|
|
@ -281,6 +307,25 @@ export const GraphMemberLogPreviewHud = ({
|
||||||
onOpenMemberProfile,
|
onOpenMemberProfile,
|
||||||
}: GraphMemberLogPreviewHudProps): React.JSX.Element | null => {
|
}: GraphMemberLogPreviewHudProps): React.JSX.Element | null => {
|
||||||
const { t } = useAppTranslation('team');
|
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 worldLayerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const shellRefs = useRef(new Map<string, HTMLDivElement | null>());
|
const shellRefs = useRef(new Map<string, HTMLDivElement | null>());
|
||||||
const visibleKeyRef = useRef('');
|
const visibleKeyRef = useRef('');
|
||||||
|
|
@ -514,9 +559,14 @@ export const GraphMemberLogPreviewHud = ({
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
(memberName: string, item: MemberLogPreviewItem) => {
|
(memberName: string, item: MemberLogPreviewItem) => {
|
||||||
const relativeTime = formatRelativeTime(item.timestamp);
|
const relativeTime = formatRelativeTime(item.timestamp);
|
||||||
const rawDisplayTitle = compactDisplayTitle(item);
|
const rawDisplayTitle = compactDisplayTitle(item, logPreviewLabels);
|
||||||
const displayTitle = truncateCompactTitle(rawDisplayTitle);
|
const displayTitle = truncateCompactTitle(rawDisplayTitle);
|
||||||
const fullPreviewText = compactPreviewText(item, displayTitle, rawDisplayTitle);
|
const fullPreviewText = compactPreviewText(
|
||||||
|
item,
|
||||||
|
displayTitle,
|
||||||
|
rawDisplayTitle,
|
||||||
|
logPreviewLabels
|
||||||
|
);
|
||||||
const previewText = truncateCompactRowPreview(fullPreviewText, displayTitle, relativeTime);
|
const previewText = truncateCompactRowPreview(fullPreviewText, displayTitle, relativeTime);
|
||||||
const titleText = compactRowLabel([rawDisplayTitle, relativeTime, fullPreviewText]);
|
const titleText = compactRowLabel([rawDisplayTitle, relativeTime, fullPreviewText]);
|
||||||
const isHighlighted = highlightedItemIds.has(buildRenderedItemKey(memberName, item.id));
|
const isHighlighted = highlightedItemIds.has(buildRenderedItemKey(memberName, item.id));
|
||||||
|
|
@ -565,7 +615,7 @@ export const GraphMemberLogPreviewHud = ({
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[highlightedItemIds, openLogs]
|
[highlightedItemIds, logPreviewLabels, openLogs]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!enabled || ownerNodes.length === 0) {
|
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`}
|
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)}
|
onClick={() => openLogs(memberName)}
|
||||||
>
|
>
|
||||||
{resolveEmptyText(preview, loading, error)}
|
{resolveEmptyText(preview, loading, error, logPreviewLabels)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{preview && preview.overflowCount > 0 ? (
|
{preview && preview.overflowCount > 0 ? (
|
||||||
|
|
|
||||||
|
|
@ -223,8 +223,8 @@ export function resolveAnthropicFastMode(params: {
|
||||||
'Fast mode is not supported by this Anthropic runtime.';
|
'Fast mode is not supported by this Anthropic runtime.';
|
||||||
} else if (!params.selection.supportsFastMode) {
|
} else if (!params.selection.supportsFastMode) {
|
||||||
disabledReason = params.selection.displayName
|
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.8. 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.';
|
||||||
} else if (!params.selection.providerFastModeAvailable) {
|
} else if (!params.selection.providerFastModeAvailable) {
|
||||||
disabledReason =
|
disabledReason =
|
||||||
params.selection.providerFastModeReason ?? 'Fast mode is currently unavailable.';
|
params.selection.providerFastModeReason ?? 'Fast mode is currently unavailable.';
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,17 +1,17 @@
|
||||||
{
|
{
|
||||||
"cliStatus": {
|
"cliStatus": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"alreadyLoggedIn": "已经登录了吗?",
|
"alreadyLoggedIn": "已经登录?",
|
||||||
"becomeSponsor": "成为提案国",
|
"becomeSponsor": "成为赞助商",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"checkNow": "现在检查",
|
"checkNow": "立即查看",
|
||||||
"checkUpdates": "检查更新",
|
"checkUpdates": "检查更新",
|
||||||
"checking": "正在检查...",
|
"checking": "检查…",
|
||||||
"connect": "连接",
|
"connect": "连接",
|
||||||
"extensions": "扩展",
|
"extensions": "扩展",
|
||||||
"login": "登录",
|
"login": "登录",
|
||||||
"manage": "管理",
|
"manage": "管理",
|
||||||
"manageProviders": "管理供应商",
|
"manageProviders": "管理提供商",
|
||||||
"plan": "计划",
|
"plan": "计划",
|
||||||
"recheck": "重新检查",
|
"recheck": "重新检查",
|
||||||
"recheckProvider": "重新检查 {{provider}}",
|
"recheckProvider": "重新检查 {{provider}}",
|
||||||
|
|
@ -20,161 +20,161 @@
|
||||||
"useCode": "使用代码"
|
"useCode": "使用代码"
|
||||||
},
|
},
|
||||||
"atlas": {
|
"atlas": {
|
||||||
"alt": "地图集云",
|
"alt": "Atlas Cloud",
|
||||||
"description": "Atlas Cloud是一个全模式的AI推论平台,它让开发者获得一个单一的AI API来访问视频生成,图像生成,以及LLM API. 与其管理多个供应商集成,不如连接一次,并获得所有模式300+全方位模型的统一访问. 请检查access-date=中的日期值 (帮助) Atlas Cloud新编码计划推广 更方便预算 API访问.",
|
"description": "Atlas Cloud 是一个全模态 AI 推理平台,为开发者提供单一 AI API 来访问视频生成、图像生成和 LLM API。您无需管理多个提供商集成,只需连接一次即可统一访问跨所有模态的 300 多个精选模型。查看 Atlas Cloud 的新编码计划促销活动,以获取更实惠的 API 访问权限。",
|
||||||
"openCodeProvider": "打开代码提供者",
|
"openCodeProvider": "OpenCode 提供商",
|
||||||
"plan": "阿特拉斯云编码计划",
|
"plan": "Atlas Cloud 编码计划",
|
||||||
"sponsor": "发起人"
|
"sponsor": "赞助"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"checkStatusFailed": "检查 CLI 状态失败",
|
"checkStatusFailed": "无法检查 CLI 状态",
|
||||||
"installationFailed": "安装失败",
|
"installationFailed": "安装失败",
|
||||||
"refreshFailed": "检查更新失败 。 检查您的网络连接并再次尝试 。",
|
"refreshFailed": "无法检查更新。检查您的网络连接并重试。",
|
||||||
"runtimeUpdatedRefreshFailed": "运行时间已更新, 但无法刷新提供者状态 。"
|
"runtimeUpdatedRefreshFailed": "运行时已更新,但无法刷新提供商状态。"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"backgroundStatus": "{{runtime}}状态将在背景中检查.",
|
"backgroundStatus": "{{runtime}} 状态将在后台检查。",
|
||||||
"codexApiKeyFallback": "{{hint}} ZXCV 1ZXCV 如果您切换了认证模式,则可以使用密钥倒置。",
|
"codexApiKeyFallback": "如果您切换认证模式,{{hint}} API 密钥备用选项可用。",
|
||||||
"codexAutoApiKey": "{{hint}} 苏维埃社会主义共和国 自动会继续使用API密钥,直到ChatGPT连接.",
|
"codexAutoApiKey": "{{hint}} Auto 将继续使用 API 密钥,直到连接 ChatGPT。",
|
||||||
"codexFinishLogin": "在浏览器中完成 ChatGPT 登录 。 如果提示, 请输入显示的代码 。",
|
"codexFinishLogin": "在浏览器中完成 ChatGPT 登录。如有提示,请输入显示的代码。",
|
||||||
"codexNoActiveLogin": "用法限制仅在Codex CLI看到一个活动中的ChatGPT账户后出现. 现在它没有报告ChatGPT的登录。",
|
"codexNoActiveLogin": "仅当 Codex CLI 看到有效的 ChatGPT 帐户后,才会出现用量限制。目前,它报告没有有效的 ChatGPT 登录。",
|
||||||
"codexNoActiveManagedSession": "用法限制仅在Codex CLI看到一个活动中的ChatGPT账户后出现. 本地 Codex 账户数据已存在, 但目前没有选择活动管理会话 。",
|
"codexNoActiveManagedSession": "仅当 Codex CLI 看到有效的 ChatGPT 帐户后,才会出现用量限制。本地 Codex 帐户数据存在,但目前未选择有效的托管会话。",
|
||||||
"codexReconnectNeeded": "用法限制仅在 Codex 刷新当前选中的 ChatGPT 会话后才会出现 。 现在本地会议需要重新连接。",
|
"codexReconnectNeeded": "仅在 Codex 刷新当前选定的 ChatGPT 会话后才会出现用量限制。现在本地会话需要重新连接。",
|
||||||
"firstCheckSlow": "第一次检查可能要30秒",
|
"firstCheckSlow": "第一次检查最多可能需要 30 秒",
|
||||||
"loginRequiredForTeams": "浏览会话和项目在不登录的情况下工作. 只需要登录即可运行代理团队.",
|
"loginRequiredForTeams": "无需登录即可浏览会话和项目。仅需要登录才能运行 Agent Team。",
|
||||||
"troubleshootTitle": "如果你确定你登录, 尝试这些步骤:"
|
"troubleshootTitle": "如果您确定已登录,请尝试以下步骤:"
|
||||||
},
|
},
|
||||||
"installer": {
|
"installer": {
|
||||||
"checkingLatest": "正在检查最新版本...",
|
"checkingLatest": "正在检查最新版本…",
|
||||||
"downloading": "正在下载 {{runtime}}...",
|
"downloading": "正在下载 {{runtime}}…",
|
||||||
"installing": "正在安装 {{runtime}}...",
|
"installing": "正在安装 {{runtime}}…",
|
||||||
"success": "成功安装 {{runtime}} v{{version}}",
|
"success": "成功安装 {{runtime}} v{{version}}",
|
||||||
"verifying": "正在验证校验和..."
|
"verifying": "正在验证校验和…"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"apiKeyRequired": "需要的 API 密钥",
|
"apiKeyRequired": "需要 API 密钥",
|
||||||
"comingSoon": "马上就来",
|
"comingSoon": "即将推出",
|
||||||
"collapseProviderDetails": "折叠提供者细节",
|
"collapseProviderDetails": "折叠提供商详细信息",
|
||||||
"expandProviderDetails": "扩展提供者细节",
|
"expandProviderDetails": "展开提供商详细信息",
|
||||||
"generateLink": "生成链接",
|
"generateLink": "生成链接",
|
||||||
"loadingRateLimits": "速率限制加载",
|
"loadingRateLimits": "速率限制加载",
|
||||||
"loggedOut": "供应商已登录",
|
"loggedOut": "提供商已注销",
|
||||||
"loginAuthFailed": "认证失败",
|
"loginAuthFailed": "认证失败",
|
||||||
"loginAuthUpdated": "更新认证",
|
"loginAuthUpdated": "认证已更新",
|
||||||
"loginComplete": "登录完成",
|
"loginComplete": "登录完成",
|
||||||
"loginFailed": "登录失败",
|
"loginFailed": "登录失败",
|
||||||
"loginTitle": "登录",
|
"loginTitle": "登录",
|
||||||
"logoutFailed": "注销失败",
|
"logoutFailed": "注销失败",
|
||||||
"logoutTitle": "注销",
|
"logoutTitle": "退出",
|
||||||
"notLoggedIn": "未登录",
|
"notLoggedIn": "未登录",
|
||||||
"openLogin": "打开登录",
|
"openLogin": "打开登录",
|
||||||
"providerActionRequired": "需要提供者采取的行动",
|
"providerActionRequired": "需要提供商采取行动",
|
||||||
"resets": "重新发送 {{time}}",
|
"resets": "复位 {{time}}",
|
||||||
"runtimeLoginTitle": "{{runtime}} 苏维埃社会主义共和国 登录"
|
"runtimeLoginTitle": "{{runtime}} 登录"
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
"aiProviders": "正在检查 AI 提供者...",
|
"aiProviders": "检查 AI 提供商…",
|
||||||
"claudeCli": "正在检查克劳德CLI..."
|
"claudeCli": "检查 Claude CLI…"
|
||||||
},
|
},
|
||||||
"provider": {
|
"provider": {
|
||||||
"authenticated": "已认证",
|
"authenticated": "已认证",
|
||||||
"backend": "后端: {{backend}}",
|
"backend": "后端:{{backend}}",
|
||||||
"checkingAuthentication": "正在检查认证...",
|
"checkingAuthentication": "正在检查认证…",
|
||||||
"checkingProviders": "正在检查提供者...",
|
"checkingProviders": "正在检查提供商…",
|
||||||
"configuredLocalCount": "{{count}} 本地配置",
|
"configuredLocalCount": "{{count}} 配置本地",
|
||||||
"configuredLocalCount_few": "{{count}} 本地配置",
|
"configuredLocalCount_few": "{{count}} 配置本地",
|
||||||
"configuredLocalCount_many": "{{count}} 本地配置",
|
"configuredLocalCount_many": "{{count}} 配置本地",
|
||||||
"configuredLocalCount_one": "{{count}} 本地配置",
|
"configuredLocalCount_one": "{{count}} 配置本地",
|
||||||
"configuredLocalCount_other": "{{count}} 本地配置",
|
"configuredLocalCount_other": "{{count}} 配置本地",
|
||||||
"configuredLocalTitle": "从您的 OpenCode 配置导入本地 OpenCode 路由 。",
|
"configuredLocalTitle": "从 OpenCode 配置导入的本地 OpenCode 路由。",
|
||||||
"connectedCount": "供应商:{{connected}}/{{denominator}}连接",
|
"connectedCount": "提供商:{{connected}}/{{denominator}} 连接",
|
||||||
"freeModels": "免费模式",
|
"freeModels": "免费模型",
|
||||||
"freeModelsTitle": "OpenCode 包含一些免费的模型选项, 如在您的设置中可用时的 Big Pickle 。 通过OpenCode的OpenRouter也可以曝光自由模型,但并不是每个OpenCode/OpenRouter模型都是免费的. 可用性和限制可能会改变。",
|
"freeModelsTitle": "OpenCode 包含免费模型选项,例如您的设置中可用的 Big Pickle。 OpenRouter 通过 OpenCode 也可以公开免费模型,但并非每个 OpenCode/OpenRouter 模型都是免费的。可用性和限制可能会发生变化。",
|
||||||
"loadingModels": "正在装入模型...",
|
"loadingModels": "正在加载模型…",
|
||||||
"modelsUnavailable": "此运行时间构建无法使用的模型",
|
"modelsUnavailable": "模型不适用于此运行时构建",
|
||||||
"runtime": "运行时间: {{runtime}}",
|
"runtime": "运行时:{{runtime}}",
|
||||||
"verifiedCount": "{{count}} 经核查",
|
"verifiedCount": "{{count}} 已验证",
|
||||||
"verifiedCount_few": "{{count}} 经核查",
|
"verifiedCount_few": "{{count}} 已验证",
|
||||||
"verifiedCount_many": "{{count}} 经核查",
|
"verifiedCount_many": "{{count}} 已验证",
|
||||||
"verifiedCount_one": "{{count}} 经核查",
|
"verifiedCount_one": "{{count}} 已验证",
|
||||||
"verifiedCount_other": "{{count}} 经核查",
|
"verifiedCount_other": "{{count}} 已验证",
|
||||||
"verifiedTitle": "带有成功执行证明的 OpenCode 路由 。"
|
"verifiedTitle": "OpenCode 路由具有成功的执行证明。"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"configuredHealthCheckFailed": "配置的 {{runtime}} 失败启动健康检查.",
|
"configuredHealthCheckFailed": "配置的 {{runtime}} 启动运行状况检查失败。",
|
||||||
"configuredNotFound": "未找到配置的 {{runtime}} 。",
|
"configuredNotFound": "未找到配置的 {{runtime}}。",
|
||||||
"foundButFailed": "发现 {{runtime}} 失败启动",
|
"foundButFailed": "找到 {{runtime}} 但启动失败",
|
||||||
"healthCheckFailedDescription": "该应用程序发现了配置的{{runtime}},但其启动健康检查失败. 修理或重新安装,然后重试。",
|
"healthCheckFailedDescription": "应用找到配置的 {{runtime}},但其启动健康检查失败。修复或重新安装,然后重试。",
|
||||||
"install": "安装 {{runtime}}",
|
"install": "安装 {{runtime}}",
|
||||||
"installRequiredDescription": "{{runtime}}是团队提供和会话管理所需的. 安装开始 。",
|
"installRequiredDescription": "团队配置和会话管理需要 {{runtime}}。安装它即可开始。",
|
||||||
"isRequired": "需要{{runtime}}",
|
"isRequired": "{{runtime}} 为必填项",
|
||||||
"reinstall": "莱因斯托尔 {{runtime}}"
|
"reinstall": "重新安装 {{runtime}}"
|
||||||
},
|
},
|
||||||
"runtimeInstall": {
|
"runtimeInstall": {
|
||||||
"checking": "检查中",
|
"checking": "检查",
|
||||||
"codexTitle": "在应用数据中安装代码CLI",
|
"codexTitle": "将 Codex CLI 安装到应用数据中",
|
||||||
"downloading": "下载",
|
"downloading": "正在下载",
|
||||||
"downloadingPercent": "下载 {{percent}}%",
|
"downloadingPercent": "正在下载 {{percent}}%",
|
||||||
"install": "安装",
|
"install": "安装",
|
||||||
"installing": "安装",
|
"installing": "安装中",
|
||||||
"openCodeTitle": "安装 OpenCode 运行时间到应用数据",
|
"openCodeTitle": "将 OpenCode 运行时安装到应用数据中",
|
||||||
"retryInstall": "重试安装"
|
"retryInstall": "重试安装"
|
||||||
},
|
},
|
||||||
"troubleshoot": {
|
"troubleshoot": {
|
||||||
"again": "再来一次",
|
"again": "再次",
|
||||||
"authStatusCommand": "您所配置的 CLI 认证状态命令",
|
"authStatusCommand": "您配置的 CLI 认证状态命令",
|
||||||
"checkLoggedIn": "- 检查它是否显示\"Logged in\"",
|
"checkLoggedIn": "- 检查是否显示“已登录”",
|
||||||
"click": "单击",
|
"click": "点击",
|
||||||
"loginCommand": "运行时间登录命令",
|
"loginCommand": "运行时登录命令",
|
||||||
"logoutCommand": "运行时间登录命令",
|
"logoutCommand": "运行时注销命令",
|
||||||
"openTerminal": "打开终端并运行:",
|
"openTerminal": "打开终端并运行:",
|
||||||
"reloginPrefix": "如果上面写着登录但应用程序看不到的话,请试试:",
|
"reloginPrefix": "如果显示已登录,但应用看不到它,请尝试:",
|
||||||
"sameRuntime": "确保您的终端中的 CLI 与应用程序使用的运行时间相同",
|
"sameRuntime": "确保终端中的 CLI 与应用使用的运行时相同",
|
||||||
"statusCacheHint": "- 有时状态会缓存几秒钟",
|
"statusCacheHint": "- 有时状态会缓存几秒钟",
|
||||||
"then": "接下来"
|
"then": "然后"
|
||||||
},
|
},
|
||||||
"warnings": {
|
"warnings": {
|
||||||
"multipleApiKeysMissing": "一个或多个提供者被设定为API密钥模式,但没有配置API密钥. 打开管理供应商以添加密钥或切换连接模式.",
|
"multipleApiKeysMissing": "一个或多个提供商设置为 API 密钥模式,但未配置 API 密钥。打开管理提供商以添加密钥或切换连接模式。",
|
||||||
"multipleApiKeysNeedAttention": "一个或多个提供者被设定为API密钥模式,需要关注. 打开管理供应商来审查保存的密钥或切换连接模式 。",
|
"multipleApiKeysNeedAttention": "一个或多个提供商已设置为 API 密钥模式,需要引起注意。打开管理提供商以查看保存的密钥或切换连接模式。",
|
||||||
"notAuthenticated": "{{runtime}}已经安装,但您没有认证 。 团队提供和AI功能需要登录.",
|
"notAuthenticated": "{{runtime}} 已安装,但您未经过认证。团队配置和 AI 功能需要登录。",
|
||||||
"singleApiKeyMissing": "{{provider}}被设定为API密钥模式,但没有配置API密钥. 打开管理供应商以添加密钥或切换连接模式 。",
|
"singleApiKeyMissing": "{{provider}} 设置为 API 密钥模式,但未配置 API 密钥。打开管理提供商以添加密钥或切换连接模式。",
|
||||||
"singleApiKeyNeedsAttention": "{{provider}}设定为API密钥模式,但没有连接. 打开管理提供者来审查保存的密钥或切换连接模式 。"
|
"singleApiKeyNeedsAttention": "{{provider}} 设置为 API 密钥模式,但未连接。打开管理提供商以查看保存的密钥或切换连接模式。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"recentProjects": {
|
"recentProjects": {
|
||||||
"selectFolderTitle": "选择工程文件夹",
|
"selectFolderTitle": "选择项目文件夹",
|
||||||
"selectFolder": "选择文件夹",
|
"selectFolder": "选择文件夹",
|
||||||
"failedToLoad": "装入工程失败",
|
"failedToLoad": "无法加载项目",
|
||||||
"retry": "重试",
|
"retry": "重试",
|
||||||
"noProjects": "未找到工程",
|
"noProjects": "未找到项目",
|
||||||
"noMatches": "没有“ {{query}}” 的匹配",
|
"noMatches": "没有匹配“{{query}}”",
|
||||||
"noRecentProjects": "未找到最近的项目",
|
"noRecentProjects": "未找到最近的项目",
|
||||||
"emptyDescription": "最近Claude和Codex的活动会在这里出现.",
|
"emptyDescription": "最近的 Claude 和 Codex 活动将出现在这里。",
|
||||||
"loadMore": "装入更多",
|
"loadMore": "加载更多",
|
||||||
"card": {
|
"card": {
|
||||||
"deleted": "删除",
|
"deleted": "已删除",
|
||||||
"projectFolderMissing": "项目文件夹已不存在",
|
"projectFolderMissing": "项目文件夹不再存在",
|
||||||
"taskCounts": {
|
"taskCounts": {
|
||||||
"active": "{{count}}活动",
|
"active": "{{count}} 活跃",
|
||||||
"active_one": "{{count}}活动",
|
"active_one": "{{count}} 活跃",
|
||||||
"active_other": "{{count}}活动",
|
"active_other": "{{count}} 活跃",
|
||||||
"active_few": "{{count}}活动",
|
"active_few": "{{count}} 活跃",
|
||||||
"active_many": "{{count}}活动",
|
"active_many": "{{count}} 活跃",
|
||||||
"pending": "{{count}}待处理",
|
"pending": "{{count}} 待定",
|
||||||
"pending_one": "{{count}}待处理",
|
"pending_one": "{{count}} 待定",
|
||||||
"pending_other": "{{count}}待处理",
|
"pending_other": "{{count}} 待定",
|
||||||
"pending_few": "{{count}}待处理",
|
"pending_few": "{{count}} 待定",
|
||||||
"pending_many": "{{count}}待处理",
|
"pending_many": "{{count}} 待定",
|
||||||
"done": "{{count}}已执行",
|
"done": "{{count}} 完成",
|
||||||
"done_one": "{{count}}已执行",
|
"done_one": "{{count}} 完成",
|
||||||
"done_other": "{{count}}已执行",
|
"done_other": "{{count}} 完成",
|
||||||
"done_few": "{{count}}已执行",
|
"done_few": "{{count}} 完成",
|
||||||
"done_many": "{{count}}已执行"
|
"done_many": "{{count}} 完成"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "最近的项目",
|
"title": "最近的项目",
|
||||||
"searchResults": "搜索结果",
|
"searchResults": "搜索结果",
|
||||||
"searchPlaceholder": "搜索项目..."
|
"searchPlaceholder": "搜索项目…"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"selectTeam": "选择团队",
|
"selectTeam": "选择团队",
|
||||||
|
|
@ -182,16 +182,16 @@
|
||||||
"clearSearch": "清除搜索"
|
"clearSearch": "清除搜索"
|
||||||
},
|
},
|
||||||
"windowsAdmin": {
|
"windowsAdmin": {
|
||||||
"title": "建议使用 Windows 管理员模式",
|
"title": "推荐使用 Windows 管理员模式",
|
||||||
"description": "OpenCode 运行时间检查可以在代理 Teams AI 没有提升时超时. 在启动 OpenCode 团队前以管理员身份重新启动应用程序 。"
|
"description": "当 Agent Teams AI 未提升时,OpenCode 运行时检查可能会超时。在启动 OpenCode 团队之前,使用以管理员身份运行重新启动应用。"
|
||||||
},
|
},
|
||||||
"webPreview": {
|
"webPreview": {
|
||||||
"title": "打开桌面应用程序以完整功能",
|
"title": "打开桌面应用以获取完整功能",
|
||||||
"description": "浏览器版本仍在开发中. 这里的项目行动、整合和现场状态更新可能有限。 使用桌面应用程序可靠地访问所有特性 。"
|
"description": "浏览器版本仍在开发中。项目操作、集成和实时状态更新可能会受到限制。使用桌面应用可靠地访问所有功能。"
|
||||||
},
|
},
|
||||||
"updateBanner": {
|
"updateBanner": {
|
||||||
"newVersionAvailable": "新版本可用",
|
"newVersionAvailable": "新版本可用",
|
||||||
"restartNow": "重新开始",
|
"restartNow": "立即重新启动",
|
||||||
"viewDetails": "查看细节"
|
"viewDetails": "查看详情"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"fallback": "出了点问题"
|
"fallback": "出了点问题。"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,31 +1,31 @@
|
||||||
{
|
{
|
||||||
"cost": {
|
"cost": {
|
||||||
"breakdownTitle": "成本细目(每100个令牌)",
|
"breakdownTitle": "成本明细(每 100 万个 Token)",
|
||||||
"cacheRead": "缓存已读",
|
"cacheRead": "缓存读取",
|
||||||
"cacheWrite": "快取写入",
|
"cacheWrite": "缓存写入",
|
||||||
"cost": "费用",
|
"cost": "成本",
|
||||||
"input": "投入",
|
"input": "输入",
|
||||||
"noCommits": "无承诺",
|
"noCommits": "没有提交",
|
||||||
"noLinesChanged": "无行更改",
|
"noLinesChanged": "没有改变行",
|
||||||
"output": "产出",
|
"output": "输出",
|
||||||
"parent": "父母:{{cost}}",
|
"parent": "父级:{{cost}}",
|
||||||
"parentCost": "父母费用",
|
"parentCost": "父级成本",
|
||||||
"perCommit": "提交",
|
"perCommit": "每次提交",
|
||||||
"perCommitFormula": "{{count}}总费用",
|
"perCommitFormula": "总成本 ÷ {{count}} 提交",
|
||||||
"perCommitFormula_few": "{{count}}总费用",
|
"perCommitFormula_few": "总成本 ÷ {{count}} 提交",
|
||||||
"perCommitFormula_many": "{{count}}总费用",
|
"perCommitFormula_many": "总成本 ÷ {{count}} 提交",
|
||||||
"perCommitFormula_one": "{{count}}总费用",
|
"perCommitFormula_one": "总成本 ÷ {{count}} 提交",
|
||||||
"perCommitFormula_other": "{{count}}总费用",
|
"perCommitFormula_other": "总成本 ÷ {{count}} 提交",
|
||||||
"perLineChanged": "每行变化",
|
"perLineChanged": "每行更改",
|
||||||
"perLineFormula": "{{count}}项目",
|
"perLineFormula": "总成本 ÷ {{count}} 行",
|
||||||
"perLineFormula_few": "{{count}}线路",
|
"perLineFormula_few": "总成本 ÷ {{count}} 行",
|
||||||
"perLineFormula_many": "{{count}}线路",
|
"perLineFormula_many": "总成本 ÷ {{count}} 行",
|
||||||
"perLineFormula_one": "{{count}}项目",
|
"perLineFormula_one": "总成本 ÷ {{count}} 行",
|
||||||
"perLineFormula_other": "{{count}}线路",
|
"perLineFormula_other": "总成本 ÷ {{count}} 行",
|
||||||
"subagent": "副剂: {{cost}}",
|
"subagent": "子智能体:{{cost}}",
|
||||||
"subagentCost": "亚剂费用",
|
"subagentCost": "子智能体成本",
|
||||||
"title": "成本分析",
|
"title": "成本分析",
|
||||||
"total": "共计"
|
"total": "总计"
|
||||||
},
|
},
|
||||||
"insights": {
|
"insights": {
|
||||||
"agent": "代理人",
|
"agent": "代理人",
|
||||||
|
|
@ -33,185 +33,185 @@
|
||||||
"agent_many": "代理人",
|
"agent_many": "代理人",
|
||||||
"agent_one": "代理人",
|
"agent_one": "代理人",
|
||||||
"agent_other": "代理人",
|
"agent_other": "代理人",
|
||||||
"agentTree": "代理树 ({{count}}) 代理树 ({{unit}})",
|
"agentTree": "智能体树 ({{count}} {{unit}})",
|
||||||
"background": "(背景情况)",
|
"background": "(后台)",
|
||||||
"bashCommands": "Bash 命令",
|
"bashCommands": "Bash 命令",
|
||||||
"outOfScopeFindings": "范围外调查结果({{count}})",
|
"outOfScopeFindings": "超出范围的调查结果 ({{count}})",
|
||||||
"questionsAsked": "提出的问题({{count}})",
|
"questionsAsked": "提出的问题 ({{count}})",
|
||||||
"repeated": "重复",
|
"repeated": "重复",
|
||||||
"skillsInvoked": "被举报技能({{count}})",
|
"skillsInvoked": "调用的技能 ({{count}})",
|
||||||
"taskDispatches": "任务调度({{count}})",
|
"taskDispatches": "任务调度 ({{count}})",
|
||||||
"tasksCreated": "创建的任务( {{count}})",
|
"tasksCreated": "已创建任务 ({{count}})",
|
||||||
"teamMode": "团队模式",
|
"teamMode": "团队模式",
|
||||||
"teams": "团队:{{teams}}",
|
"teams": "队伍:{{teams}}",
|
||||||
"title": "会话透视",
|
"title": "会话见解",
|
||||||
"total": "共计",
|
"total": "总计",
|
||||||
"unique": "独一无二",
|
"unique": "唯一",
|
||||||
"skillsInvoked_few": "被举报技能({{count}})",
|
"skillsInvoked_few": "调用的技能 ({{count}})",
|
||||||
"skillsInvoked_many": "被举报技能({{count}})",
|
"skillsInvoked_many": "调用的技能 ({{count}})",
|
||||||
"skillsInvoked_one": "被举报技能({{count}})",
|
"skillsInvoked_one": "调用的技能 ({{count}})",
|
||||||
"skillsInvoked_other": "被举报技能({{count}})",
|
"skillsInvoked_other": "调用的技能 ({{count}})",
|
||||||
"taskDispatches_few": "任务调度({{count}})",
|
"taskDispatches_few": "任务调度 ({{count}})",
|
||||||
"taskDispatches_many": "任务调度({{count}})",
|
"taskDispatches_many": "任务调度 ({{count}})",
|
||||||
"taskDispatches_one": "任务调度({{count}})",
|
"taskDispatches_one": "任务调度 ({{count}})",
|
||||||
"taskDispatches_other": "任务调度({{count}})",
|
"taskDispatches_other": "任务调度 ({{count}})",
|
||||||
"tasksCreated_few": "创建的任务( {{count}})",
|
"tasksCreated_few": "已创建任务 ({{count}})",
|
||||||
"tasksCreated_many": "创建的任务( {{count}})",
|
"tasksCreated_many": "已创建任务 ({{count}})",
|
||||||
"tasksCreated_one": "创建的任务( {{count}})",
|
"tasksCreated_one": "已创建任务 ({{count}})",
|
||||||
"tasksCreated_other": "创建的任务( {{count}})",
|
"tasksCreated_other": "已创建任务 ({{count}})",
|
||||||
"questionsAsked_few": "提出的问题({{count}})",
|
"questionsAsked_few": "提出的问题 ({{count}})",
|
||||||
"questionsAsked_many": "提出的问题({{count}})",
|
"questionsAsked_many": "提出的问题 ({{count}})",
|
||||||
"questionsAsked_one": "提出的问题({{count}})",
|
"questionsAsked_one": "提出的问题 ({{count}})",
|
||||||
"questionsAsked_other": "提出的问题({{count}})",
|
"questionsAsked_other": "提出的问题 ({{count}})",
|
||||||
"agentTree_few": "代理树 ({{count}}) 代理树 ({{unit}})",
|
"agentTree_few": "智能体树 ({{count}} {{unit}})",
|
||||||
"agentTree_many": "代理树 ({{count}}) 代理树 ({{unit}})",
|
"agentTree_many": "智能体树 ({{count}} {{unit}})",
|
||||||
"agentTree_one": "代理树 ({{count}}) 代理树 ({{unit}})",
|
"agentTree_one": "智能体树 ({{count}} {{unit}})",
|
||||||
"agentTree_other": "代理树 ({{count}}) 代理树 ({{unit}})",
|
"agentTree_other": "智能体树 ({{count}} {{unit}})",
|
||||||
"outOfScopeFindings_few": "范围外调查结果({{count}})",
|
"outOfScopeFindings_few": "超出范围的调查结果 ({{count}})",
|
||||||
"outOfScopeFindings_many": "范围外调查结果({{count}})",
|
"outOfScopeFindings_many": "超出范围的调查结果 ({{count}})",
|
||||||
"outOfScopeFindings_one": "范围外调查结果({{count}})",
|
"outOfScopeFindings_one": "超出范围的调查结果 ({{count}})",
|
||||||
"outOfScopeFindings_other": "范围外调查结果({{count}})",
|
"outOfScopeFindings_other": "超出范围的调查结果 ({{count}})",
|
||||||
"keyTakeaways": "关键外卖"
|
"keyTakeaways": "要点"
|
||||||
},
|
},
|
||||||
"quality": {
|
"quality": {
|
||||||
"chars": "字符",
|
"chars": "字符",
|
||||||
"corrections": "惩戒",
|
"corrections": "修正",
|
||||||
"failed": "失败",
|
"failed": "失败",
|
||||||
"fileReadRedundancy": "文件读取冗余",
|
"fileReadRedundancy": "文件读取冗余",
|
||||||
"firstMessage": "第一个消息",
|
"firstMessage": "第一条消息",
|
||||||
"firstRun": "第一个运行",
|
"firstRun": "第一次运行",
|
||||||
"frictionRate": "摩擦率",
|
"frictionRate": "摩擦率",
|
||||||
"lastRun": "上次运行",
|
"lastRun": "最后一次运行",
|
||||||
"messagesBeforeWork": "工作前的信件",
|
"messagesBeforeWork": "开始工作前消息",
|
||||||
"passed": "通过",
|
"passed": "通过",
|
||||||
"promptQuality": "提示质量",
|
"promptQuality": "提示词质量",
|
||||||
"readsPerUniqueFile": "读取/ 唯一文件",
|
"readsPerUniqueFile": "读取/唯一文件",
|
||||||
"snapshot": "简介",
|
"snapshot": "快照",
|
||||||
"snapshot_few": "快照",
|
"snapshot_few": "快照",
|
||||||
"snapshot_many": "快照",
|
"snapshot_many": "快照",
|
||||||
"snapshot_one": "简介",
|
"snapshot_one": "快照",
|
||||||
"snapshot_other": "快照",
|
"snapshot_other": "快照",
|
||||||
"startupOverhead": "启动间接费用",
|
"startupOverhead": "启动开销",
|
||||||
"testProgression": "测试进度",
|
"testProgression": "测试进展",
|
||||||
"title": "质量信号",
|
"title": "质量信号",
|
||||||
"tokensBeforeWork": "工作前托肯斯语Name",
|
"tokensBeforeWork": "工作前的 Token",
|
||||||
"totalReads": "读数共计",
|
"totalReads": "总读取次数",
|
||||||
"uniqueFiles": "独一无二的文件",
|
"uniqueFiles": "唯一文件",
|
||||||
"userMessages": "用户信件",
|
"userMessages": "用户消息",
|
||||||
"percentOfTotal": "占总数的百分比"
|
"percentOfTotal": "占总数的%"
|
||||||
},
|
},
|
||||||
"tokens": {
|
"tokens": {
|
||||||
"apiCalls": "API 苏维埃社会主义共和国 电话",
|
"apiCalls": "API 调用",
|
||||||
"cacheCreate": "缓存创建",
|
"cacheCreate": "缓存创建",
|
||||||
"cacheEfficiency": "缓存效率",
|
"cacheEfficiency": "缓存效率",
|
||||||
"cacheRead": "缓存已读",
|
"cacheRead": "缓存读取",
|
||||||
"cacheReadPct": "快取读取%",
|
"cacheReadPct": "缓存读取率",
|
||||||
"coldStart": "冷启动",
|
"coldStart": "冷启动",
|
||||||
"cost": "费用",
|
"cost": "成本",
|
||||||
"input": "投入",
|
"input": "输入",
|
||||||
"model": "型号",
|
"model": "模型",
|
||||||
"no": "没有",
|
"no": "否",
|
||||||
"output": "产出",
|
"output": "输出",
|
||||||
"readWriteRatio": "R/W比率",
|
"readWriteRatio": "读/写比",
|
||||||
"title": "调用",
|
"title": "Token 使用",
|
||||||
"total": "共计",
|
"total": "总计",
|
||||||
"yes": "对"
|
"yes": "是"
|
||||||
},
|
},
|
||||||
"subagents": {
|
"subagents": {
|
||||||
"title": "副剂",
|
"title": "子智能体",
|
||||||
"metrics": {
|
"metrics": {
|
||||||
"count": "计数",
|
"count": "数量",
|
||||||
"totalTokens": "共计",
|
"totalTokens": "Token 总数",
|
||||||
"totalDuration": "期间共计",
|
"totalDuration": "总持续时间",
|
||||||
"totalCost": "费用共计"
|
"totalCost": "总成本"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"description": "说明",
|
"description": "描述",
|
||||||
"type": "类型",
|
"type": "类型",
|
||||||
"tokens": "键",
|
"tokens": "Token",
|
||||||
"duration": "会期",
|
"duration": "持续时间",
|
||||||
"cost": "费用"
|
"cost": "成本"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"overview": {
|
"overview": {
|
||||||
"title": "概览",
|
"title": "概述",
|
||||||
"yes": "对",
|
"yes": "是",
|
||||||
"no": "没有",
|
"no": "否",
|
||||||
"metrics": {
|
"metrics": {
|
||||||
"duration": "会期",
|
"duration": "持续时间",
|
||||||
"messages": "信件",
|
"messages": "消息",
|
||||||
"contextUsage": "背景使用情况",
|
"contextUsage": "上下文使用",
|
||||||
"compactions": "压缩",
|
"compactions": "压缩",
|
||||||
"branch": "处",
|
"branch": "分支",
|
||||||
"subagents": "副剂",
|
"subagents": "子智能体",
|
||||||
"project": "项目",
|
"project": "项目",
|
||||||
"sessionId": "会话编号"
|
"sessionId": "会话 ID"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"title": "时间线和活动( A)",
|
"title": "时间线与活动",
|
||||||
"idleAnalysis": "空闲分析",
|
"idleAnalysis": "空闲分析",
|
||||||
"metrics": {
|
"metrics": {
|
||||||
"idleGaps": "空闲差距",
|
"idleGaps": "空闲间隙",
|
||||||
"totalIdle": "闲置共计",
|
"totalIdle": "总空闲时间",
|
||||||
"activeTime": "活动时间",
|
"activeTime": "活跃时间",
|
||||||
"idlePercent": "闲置%"
|
"idlePercent": "空闲率"
|
||||||
},
|
},
|
||||||
"modelSwitches": "型号开关({{count}})",
|
"modelSwitches": "模型切换({{count}})",
|
||||||
"modelSwitches_one": "型号开关({{count}})",
|
"modelSwitches_one": "模型切换({{count}})",
|
||||||
"modelSwitches_other": "型号开关({{count}})",
|
"modelSwitches_other": "模型切换({{count}})",
|
||||||
"messageNumber": "# 迈克 #{{number}}",
|
"messageNumber": "消息#{{number}}",
|
||||||
"keyEvents": "关键事件",
|
"keyEvents": "重要事件",
|
||||||
"modelSwitches_few": "型号开关({{count}})",
|
"modelSwitches_few": "模型切换({{count}})",
|
||||||
"modelSwitches_many": "型号开关({{count}})"
|
"modelSwitches_many": "模型切换({{count}})"
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"title": "工具使用",
|
"title": "工具使用",
|
||||||
"summary": "{{formattedCount}} 跨越{{toolCount}}工具的总通话量",
|
"summary": "共 {{formattedCount}} 次调用,涵盖 {{toolCount}} 个工具",
|
||||||
"columns": {
|
"columns": {
|
||||||
"tool": "工具",
|
"tool": "工具",
|
||||||
"calls": "电话",
|
"calls": "调用次数",
|
||||||
"errors": "错误",
|
"errors": "错误",
|
||||||
"successPercent": "成功率(%)",
|
"successPercent": "成功率",
|
||||||
"health": "卫生"
|
"health": "健康状态"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"git": {
|
"git": {
|
||||||
"title": "Git 活动",
|
"title": "Git 活动",
|
||||||
"commits": "提交",
|
"commits": "提交",
|
||||||
"pushes": "推动",
|
"pushes": "推送",
|
||||||
"linesAdded": "添加的行数",
|
"linesAdded": "新增行数",
|
||||||
"linesRemoved": "删除的行",
|
"linesRemoved": "删除行数",
|
||||||
"branchesCreated": "创建分支"
|
"branchesCreated": "创建的分支"
|
||||||
},
|
},
|
||||||
"friction": {
|
"friction": {
|
||||||
"title": "Friction 信号",
|
"title": "摩擦信号",
|
||||||
"rate": "滑动率:{{rate}}百分比(%)",
|
"rate": "摩擦率:{{rate}}%",
|
||||||
"correctionsCount": "{{count}}更正",
|
"correctionsCount": "{{count}} 修正",
|
||||||
"correctionsCount_one": "{{count}}更正",
|
"correctionsCount_one": "{{count}} 修正",
|
||||||
"corrections": "惩戒",
|
"corrections": "修正",
|
||||||
"thrashingSignals": "闪烁信号",
|
"thrashingSignals": "反复修改信号",
|
||||||
"repeatedBashCommands": "重复的巴什命令",
|
"repeatedBashCommands": "重复的 Bash 命令",
|
||||||
"reworkedFiles": "重修的文件( 3+编辑)",
|
"reworkedFiles": "返工文件(3 次以上编辑)",
|
||||||
"correctionsCount_few": "{{count}}更正",
|
"correctionsCount_few": "{{count}} 修正",
|
||||||
"correctionsCount_many": "{{count}}更正",
|
"correctionsCount_many": "{{count}} 修正",
|
||||||
"correctionsCount_other": "{{count}}更正"
|
"correctionsCount_other": "{{count}} 修正"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"title": "错误",
|
"title": "错误",
|
||||||
"permissionDenied": "拒绝权限",
|
"permissionDenied": "权限被拒绝",
|
||||||
"messageIndex": "# 迈克 #{{index}}",
|
"messageIndex": "消息#{{index}}",
|
||||||
"input": "投入",
|
"input": "输入",
|
||||||
"error": "错误",
|
"error": "错误",
|
||||||
"count": "{{count}}错误",
|
"count": "{{count}} 错误",
|
||||||
"count_one": "{{count}} 错误",
|
"count_one": "{{count}} 错误",
|
||||||
"permissionDenialCount": "{{count}} 许可拒绝",
|
"permissionDenialCount": "{{count}} 权限拒绝次数",
|
||||||
"permissionDenialCount_one": "{{count}} 许可被拒绝",
|
"permissionDenialCount_one": "{{count}} 权限拒绝次数",
|
||||||
"count_few": "{{count}}错误",
|
"count_few": "{{count}} 错误",
|
||||||
"count_many": "{{count}}错误",
|
"count_many": "{{count}} 错误",
|
||||||
"count_other": "{{count}}错误",
|
"count_other": "{{count}} 错误",
|
||||||
"permissionDenialCount_few": "{{count}} 许可拒绝",
|
"permissionDenialCount_few": "{{count}} 权限拒绝次数",
|
||||||
"permissionDenialCount_many": "{{count}} 许可拒绝",
|
"permissionDenialCount_many": "{{count}} 权限拒绝次数",
|
||||||
"permissionDenialCount_other": "{{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`;
|
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) return null;
|
||||||
if (log.missing) return 'No process log file captured for this member yet.';
|
if (log.missing) return labels.empty;
|
||||||
if (!log.content) return 'Process log file is empty.';
|
if (!log.content) return labels.fileEmpty;
|
||||||
if (log.truncated) return `Showing last ${formatBytes(log.bytesRead)}.`;
|
if (log.truncated) return labels.showingLast(formatBytes(log.bytesRead));
|
||||||
return `Showing ${formatBytes(log.bytesRead)}.`;
|
return labels.showing(formatBytes(log.bytesRead));
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProcessLogKindTabs({
|
function ProcessLogKindTabs({
|
||||||
|
|
@ -204,7 +212,12 @@ export function MemberRuntimeProcessLogsPanel({
|
||||||
}
|
}
|
||||||
}, [log?.content]);
|
}, [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);
|
const hasContent = Boolean(log?.content);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -252,7 +265,7 @@ export function MemberRuntimeProcessLogsPanel({
|
||||||
disabled={!hasContent}
|
disabled={!hasContent}
|
||||||
>
|
>
|
||||||
{copied ? <Check size={13} /> : <Clipboard size={13} />}
|
{copied ? <Check size={13} /> : <Clipboard size={13} />}
|
||||||
{copied ? 'Copied' : 'Copy'}
|
{copied ? tCommon('actions.copied') : t('members.runtimeLogs.copy')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { decideMemberWorkSyncStatus } from '../domain';
|
import { decideMemberWorkSyncStatus } from '../domain';
|
||||||
|
|
||||||
import { finalizeMemberWorkSyncAgenda } from './MemberWorkSyncReconciler';
|
import {
|
||||||
|
attachMemberWorkSyncReportToken,
|
||||||
|
finalizeMemberWorkSyncAgenda,
|
||||||
|
} from './MemberWorkSyncReconciler';
|
||||||
import { resolveMemberWorkSyncRuntimeActivity } from './MemberWorkSyncRuntimeActivity';
|
import { resolveMemberWorkSyncRuntimeActivity } from './MemberWorkSyncRuntimeActivity';
|
||||||
|
|
||||||
import type { MemberWorkSyncStatus, MemberWorkSyncStatusRequest } from '../../contracts';
|
import type { MemberWorkSyncStatus, MemberWorkSyncStatusRequest } from '../../contracts';
|
||||||
|
|
@ -28,7 +31,7 @@ export class MemberWorkSyncDiagnosticsReader {
|
||||||
inactive: source.inactive || runtimeActivity.inactive,
|
inactive: source.inactive || runtimeActivity.inactive,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return attachMemberWorkSyncReportToken(this.deps, {
|
||||||
teamName: agenda.teamName,
|
teamName: agenda.teamName,
|
||||||
memberName: agenda.memberName,
|
memberName: agenda.memberName,
|
||||||
state: decision.state,
|
state: decision.state,
|
||||||
|
|
@ -46,6 +49,6 @@ export class MemberWorkSyncDiagnosticsReader {
|
||||||
'status_snapshot_not_persisted',
|
'status_snapshot_not_persisted',
|
||||||
],
|
],
|
||||||
...(source.providerId ? { providerId: source.providerId } : {}),
|
...(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 {
|
function isOutboxItemAwaitingDelivery(item: MemberWorkSyncOutboxItem): boolean {
|
||||||
return item.status !== 'delivered' && item.status !== 'failed_terminal';
|
return item.status !== 'delivered' && item.status !== 'failed_terminal';
|
||||||
}
|
}
|
||||||
|
|
@ -296,6 +311,7 @@ export class MemberWorkSyncNudgeOutboxPlanner {
|
||||||
await this.appendPlanAudit(status, { planned: false, code: 'payload_conflict' });
|
await this.appendPlanAudit(status, { planned: false, code: 'payload_conflict' });
|
||||||
return { planned: false, code: 'payload_conflict' };
|
return { planned: false, code: 'payload_conflict' };
|
||||||
}
|
}
|
||||||
|
await this.repairDeliveredAgendaSyncNudgeIfNeeded(status, recoveryInput, recoveryResult.item);
|
||||||
|
|
||||||
if (activationReason) {
|
if (activationReason) {
|
||||||
const deliveredStillStuckRecovery = await this.planDeliveredStillStuckRecovery(
|
const deliveredStillStuckRecovery = await this.planDeliveredStillStuckRecovery(
|
||||||
|
|
@ -371,6 +387,7 @@ export class MemberWorkSyncNudgeOutboxPlanner {
|
||||||
await this.appendPlanAudit(status, { planned: false, code: 'payload_conflict' });
|
await this.appendPlanAudit(status, { planned: false, code: 'payload_conflict' });
|
||||||
return { planned: false, code: 'payload_conflict' };
|
return { planned: false, code: 'payload_conflict' };
|
||||||
}
|
}
|
||||||
|
await this.repairDeliveredAgendaSyncNudgeIfNeeded(status, recoveryInput, recoveryResult.item);
|
||||||
|
|
||||||
const recoveryPlanned = isOutboxItemAwaitingDelivery(recoveryResult.item);
|
const recoveryPlanned = isOutboxItemAwaitingDelivery(recoveryResult.item);
|
||||||
const recoveryPlanResult = {
|
const recoveryPlanResult = {
|
||||||
|
|
@ -491,6 +508,11 @@ export class MemberWorkSyncNudgeOutboxPlanner {
|
||||||
await this.appendPlanAudit(status, { planned: false, code: 'payload_conflict' });
|
await this.appendPlanAudit(status, { planned: false, code: 'payload_conflict' });
|
||||||
return { planned: false, code: 'payload_conflict' };
|
return { planned: false, code: 'payload_conflict' };
|
||||||
}
|
}
|
||||||
|
await this.repairDeliveredAgendaSyncNudgeIfNeeded(
|
||||||
|
status,
|
||||||
|
recoveryInput,
|
||||||
|
recoveryResult.item
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
shouldPlanStatusOnlyRecovery({
|
shouldPlanStatusOnlyRecovery({
|
||||||
status,
|
status,
|
||||||
|
|
@ -544,6 +566,7 @@ export class MemberWorkSyncNudgeOutboxPlanner {
|
||||||
await this.appendPlanAudit(status, { planned: false, code });
|
await this.appendPlanAudit(status, { planned: false, code });
|
||||||
return { planned: false, code };
|
return { planned: false, code };
|
||||||
}
|
}
|
||||||
|
await this.repairDeliveredAgendaSyncNudgeIfNeeded(status, input, result.item);
|
||||||
if (
|
if (
|
||||||
shouldPlanStatusOnlyRecovery({
|
shouldPlanStatusOnlyRecovery({
|
||||||
status,
|
status,
|
||||||
|
|
@ -580,6 +603,37 @@ export class MemberWorkSyncNudgeOutboxPlanner {
|
||||||
return planResult;
|
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(
|
private async appendReviewPickupEscalationAudit(
|
||||||
status: MemberWorkSyncStatus,
|
status: MemberWorkSyncStatus,
|
||||||
reason: string
|
reason: string
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import { MemberWorkSyncReporter } from './MemberWorkSyncReporter';
|
import { MemberWorkSyncReporter } from './MemberWorkSyncReporter';
|
||||||
|
|
||||||
import type { MemberWorkSyncReportIntentStatus } from '../../contracts';
|
import type {
|
||||||
|
MemberWorkSyncReportIntent,
|
||||||
|
MemberWorkSyncReportIntentStatus,
|
||||||
|
MemberWorkSyncReportResult,
|
||||||
|
} from '../../contracts';
|
||||||
import type { MemberWorkSyncUseCaseDeps } from './ports';
|
import type { MemberWorkSyncUseCaseDeps } from './ports';
|
||||||
|
|
||||||
export interface MemberWorkSyncPendingReportReplaySummary {
|
export interface MemberWorkSyncPendingReportReplaySummary {
|
||||||
|
|
@ -52,10 +56,7 @@ export class MemberWorkSyncPendingReportIntentReplayer {
|
||||||
let status: MemberWorkSyncReportIntentStatus = 'rejected';
|
let status: MemberWorkSyncReportIntentStatus = 'rejected';
|
||||||
let resultCode = 'replay_failed';
|
let resultCode = 'replay_failed';
|
||||||
try {
|
try {
|
||||||
const result = await this.reporter.execute({
|
const result = await this.executeReplay(intent);
|
||||||
...intent.request,
|
|
||||||
source: intent.request.source ?? 'mcp',
|
|
||||||
});
|
|
||||||
status = statusForResult(result);
|
status = statusForResult(result);
|
||||||
resultCode = result.code;
|
resultCode = result.code;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -83,4 +84,56 @@ export class MemberWorkSyncPendingReportIntentReplayer {
|
||||||
|
|
||||||
return summary;
|
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'];
|
payload: MemberWorkSyncOutboxItem['payload'];
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
}): Promise<{ inserted: boolean; messageId: string; conflict?: boolean }>;
|
}): 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 {
|
export interface MemberWorkSyncWatchdogCooldownPort {
|
||||||
|
|
|
||||||
|
|
@ -3,24 +3,48 @@ import { TeamInboxWriter } from '@main/services/team/TeamInboxWriter';
|
||||||
|
|
||||||
import type { MemberWorkSyncInboxNudgePort } from '../../../core/application';
|
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 {
|
export class TeamInboxMemberWorkSyncNudgeSink implements MemberWorkSyncInboxNudgePort {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly inboxReader: Pick<TeamInboxReader, 'getMessagesFor'> = new TeamInboxReader(),
|
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
|
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 existing = await this.inboxReader.getMessagesFor(input.teamName, input.memberName);
|
||||||
const existingMessage = existing.find((message) => message.messageId === input.messageId);
|
const existingMessage = existing.find((message) => message.messageId === input.messageId);
|
||||||
if (existingMessage) {
|
if (existingMessage) {
|
||||||
if (existingMessage.workSyncPayloadHash !== input.payloadHash) {
|
if (
|
||||||
|
existingMessage.workSyncPayloadHash !== input.payloadHash ||
|
||||||
|
!isStoredMemberWorkSyncNudge(existingMessage)
|
||||||
|
) {
|
||||||
return { inserted: false, messageId: input.messageId, conflict: true };
|
return { inserted: false, messageId: input.messageId, conflict: true };
|
||||||
}
|
}
|
||||||
|
await this.repairExistingControlUrlIfNeeded(input, existingMessage.text, {
|
||||||
|
required: Boolean(this.controlUrlResolver),
|
||||||
|
});
|
||||||
return { inserted: false, messageId: input.messageId };
|
return { inserted: false, messageId: input.messageId };
|
||||||
}
|
}
|
||||||
|
|
||||||
const controlUrl = await this.resolveControlUrl();
|
const controlUrl = await this.resolveControlUrl({
|
||||||
|
required: Boolean(this.controlUrlResolver),
|
||||||
|
});
|
||||||
const text = controlUrl
|
const text = controlUrl
|
||||||
? this.withControlUrl(input.payload.text, controlUrl)
|
? this.withControlUrl(input.payload.text, controlUrl)
|
||||||
: input.payload.text;
|
: 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) {
|
if (!this.controlUrlResolver) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let value: string | null | undefined;
|
||||||
try {
|
try {
|
||||||
const value = await this.controlUrlResolver();
|
value = await this.controlUrlResolver();
|
||||||
const trimmed = value?.trim();
|
} catch (error) {
|
||||||
return trimmed ? trimmed : null;
|
if (options.required) {
|
||||||
} catch {
|
throw new Error(`member work sync control URL unavailable: ${String(error)}`);
|
||||||
|
}
|
||||||
return null;
|
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 {
|
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;
|
||||||
}
|
}
|
||||||
return [
|
return [text, controlLine].join('\n');
|
||||||
text,
|
|
||||||
`Required control API: pass controlUrl "${controlUrl}" in both member_work_sync_status and member_work_sync_report.`,
|
|
||||||
].join('\n');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import path from 'path';
|
||||||
|
|
||||||
import { isReservedMemberName, normalizeMemberName } from '../../../core/domain';
|
import { isReservedMemberName, normalizeMemberName } from '../../../core/domain';
|
||||||
|
|
||||||
|
import { mergeTeamMembers } from './mergeTeamMembers';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
RuntimeTurnSettledTargetResolution,
|
RuntimeTurnSettledTargetResolution,
|
||||||
RuntimeTurnSettledTargetResolverPort,
|
RuntimeTurnSettledTargetResolverPort,
|
||||||
|
|
@ -14,7 +16,7 @@ import type {
|
||||||
import type { RuntimeTurnSettledEvent } from '../../../core/domain';
|
import type { RuntimeTurnSettledEvent } from '../../../core/domain';
|
||||||
import type { TeamConfigReader } from '@main/services/team/TeamConfigReader';
|
import type { TeamConfigReader } from '@main/services/team/TeamConfigReader';
|
||||||
import type { TeamMembersMetaStore } from '@main/services/team/TeamMembersMetaStore';
|
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 {
|
export interface RuntimeTurnSettledTeamSource {
|
||||||
listTeams(): Promise<TeamSummary[]>;
|
listTeams(): Promise<TeamSummary[]>;
|
||||||
|
|
@ -38,26 +40,21 @@ function memberKey(member: Pick<TeamMember, 'name'>): string {
|
||||||
return normalizeMemberName(member.name);
|
return normalizeMemberName(member.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeMembers(configMembers: TeamMember[], metaMembers: TeamMember[]): TeamMember[] {
|
function providerIdFromBackend(providerBackendId: unknown): TeamProviderId | undefined {
|
||||||
const byName = new Map<string, TeamMember>();
|
const normalized = typeof providerBackendId === 'string' ? providerBackendId.trim() : '';
|
||||||
for (const member of configMembers) {
|
if (normalized === 'codex-native') {
|
||||||
const key = memberKey(member);
|
return 'codex';
|
||||||
if (key) {
|
|
||||||
byName.set(key, member);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (const member of metaMembers) {
|
if (normalized === 'opencode-cli') {
|
||||||
const key = memberKey(member);
|
return 'opencode';
|
||||||
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 (
|
return (
|
||||||
normalizeOptionalTeamProviderId(member?.providerId) ??
|
normalizeOptionalTeamProviderId(member?.providerId) ??
|
||||||
|
providerIdFromBackend(member?.providerBackendId) ??
|
||||||
inferTeamProviderIdFromModel(member?.model)
|
inferTeamProviderIdFromModel(member?.model)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +199,7 @@ export class TeamRuntimeTurnSettledTargetResolver implements RuntimeTurnSettledT
|
||||||
|
|
||||||
const normalizedTarget = normalizeMemberName(memberName);
|
const normalizedTarget = normalizeMemberName(memberName);
|
||||||
return (
|
return (
|
||||||
mergeMembers(config.members ?? [], metaMembers).find(
|
mergeTeamMembers(config.members ?? [], metaMembers).find(
|
||||||
(member) => !member.removedAt && memberKey(member) === normalizedTarget
|
(member) => !member.removedAt && memberKey(member) === normalizedTarget
|
||||||
) ?? null
|
) ?? null
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import {
|
||||||
normalizeMemberName,
|
normalizeMemberName,
|
||||||
} from '../../../core/domain';
|
} from '../../../core/domain';
|
||||||
|
|
||||||
|
import { mergeTeamMembers } from './mergeTeamMembers';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
MemberWorkSyncAgendaSourcePort,
|
MemberWorkSyncAgendaSourcePort,
|
||||||
MemberWorkSyncAgendaSourceResult,
|
MemberWorkSyncAgendaSourceResult,
|
||||||
|
|
@ -19,7 +21,7 @@ import type { TeamConfigReader } from '@main/services/team/TeamConfigReader';
|
||||||
import type { TeamKanbanManager } from '@main/services/team/TeamKanbanManager';
|
import type { TeamKanbanManager } from '@main/services/team/TeamKanbanManager';
|
||||||
import type { TeamMembersMetaStore } from '@main/services/team/TeamMembersMetaStore';
|
import type { TeamMembersMetaStore } from '@main/services/team/TeamMembersMetaStore';
|
||||||
import type { TeamTaskReader } from '@main/services/team/TeamTaskReader';
|
import type { TeamTaskReader } from '@main/services/team/TeamTaskReader';
|
||||||
import type { TeamMember } from '@shared/types';
|
import type { TeamMember, TeamProviderId } from '@shared/types';
|
||||||
|
|
||||||
export interface TeamTaskAgendaSourceDeps {
|
export interface TeamTaskAgendaSourceDeps {
|
||||||
configReader: Pick<TeamConfigReader, 'getConfig'>;
|
configReader: Pick<TeamConfigReader, 'getConfig'>;
|
||||||
|
|
@ -34,26 +36,21 @@ function memberKey(member: Pick<TeamMember, 'name'>): string {
|
||||||
return normalizeMemberName(member.name);
|
return normalizeMemberName(member.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeMembers(configMembers: TeamMember[], metaMembers: TeamMember[]): TeamMember[] {
|
function providerIdFromBackend(providerBackendId: unknown): TeamProviderId | undefined {
|
||||||
const byName = new Map<string, TeamMember>();
|
const normalized = typeof providerBackendId === 'string' ? providerBackendId.trim() : '';
|
||||||
for (const member of configMembers) {
|
if (normalized === 'codex-native') {
|
||||||
const key = memberKey(member);
|
return 'codex';
|
||||||
if (key) {
|
|
||||||
byName.set(key, member);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (const member of metaMembers) {
|
if (normalized === 'opencode-cli') {
|
||||||
const key = memberKey(member);
|
return 'opencode';
|
||||||
if (key) {
|
|
||||||
byName.set(key, { ...byName.get(key), ...member });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return [...byName.values()];
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toMemberLike(member: TeamMember): MemberWorkSyncMemberLike {
|
function toMemberLike(member: TeamMember): MemberWorkSyncMemberLike {
|
||||||
const providerId =
|
const providerId =
|
||||||
normalizeOptionalTeamProviderId(member.providerId) ??
|
normalizeOptionalTeamProviderId(member.providerId) ??
|
||||||
|
providerIdFromBackend(member.providerBackendId) ??
|
||||||
inferTeamProviderIdFromModel(member.model);
|
inferTeamProviderIdFromModel(member.model);
|
||||||
return {
|
return {
|
||||||
name: member.name,
|
name: member.name,
|
||||||
|
|
@ -74,7 +71,7 @@ export class TeamTaskAgendaSource implements MemberWorkSyncAgendaSourcePort {
|
||||||
}
|
}
|
||||||
|
|
||||||
const metaMembers = await this.deps.membersMetaStore.getMembers(teamName);
|
const metaMembers = await this.deps.membersMetaStore.getMembers(teamName);
|
||||||
return mergeMembers(config.members ?? [], metaMembers)
|
return mergeTeamMembers(config.members ?? [], metaMembers)
|
||||||
.filter((member) => !member.removedAt)
|
.filter((member) => !member.removedAt)
|
||||||
.map((member) => normalizeMemberName(member.name))
|
.map((member) => normalizeMemberName(member.name))
|
||||||
.filter((memberName) => memberName.length > 0 && !isReservedMemberName(memberName))
|
.filter((memberName) => memberName.length > 0 && !isReservedMemberName(memberName))
|
||||||
|
|
@ -107,7 +104,7 @@ export class TeamTaskAgendaSource implements MemberWorkSyncAgendaSourcePort {
|
||||||
this.deps.kanbanManager.getState(input.teamName),
|
this.deps.kanbanManager.getState(input.teamName),
|
||||||
this.deps.membersMetaStore.getMembers(input.teamName),
|
this.deps.membersMetaStore.getMembers(input.teamName),
|
||||||
]);
|
]);
|
||||||
const members = mergeMembers(config.members ?? [], metaMembers);
|
const members = mergeTeamMembers(config.members ?? [], metaMembers);
|
||||||
const activeMemberNames = members
|
const activeMemberNames = members
|
||||||
.filter((member) => !member.removedAt)
|
.filter((member) => !member.removedAt)
|
||||||
.map((member) => normalizeMemberName(member.name))
|
.map((member) => normalizeMemberName(member.name))
|
||||||
|
|
@ -116,6 +113,7 @@ export class TeamTaskAgendaSource implements MemberWorkSyncAgendaSourcePort {
|
||||||
const member = members.find((candidate) => memberKey(candidate) === normalizedMemberName);
|
const member = members.find((candidate) => memberKey(candidate) === normalizedMemberName);
|
||||||
const providerId =
|
const providerId =
|
||||||
normalizeOptionalTeamProviderId(member?.providerId) ??
|
normalizeOptionalTeamProviderId(member?.providerId) ??
|
||||||
|
providerIdFromBackend(member?.providerBackendId) ??
|
||||||
inferTeamProviderIdFromModel(member?.model);
|
inferTeamProviderIdFromModel(member?.model);
|
||||||
|
|
||||||
const agenda = buildActionableWorkAgenda({
|
const agenda = buildActionableWorkAgenda({
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,10 @@ interface StallJournalEntry {
|
||||||
alertedAt?: string;
|
alertedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type WatchdogCooldownResult = { active: boolean; retryAfterIso?: string };
|
interface WatchdogCooldownResult {
|
||||||
|
active: boolean;
|
||||||
|
retryAfterIso?: string;
|
||||||
|
}
|
||||||
|
|
||||||
function parseTime(value: string | undefined): number | null {
|
function parseTime(value: string | undefined): number | null {
|
||||||
if (!value) {
|
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 {
|
import {
|
||||||
hasUncertainWorkSyncRuntimeActivity,
|
hasUncertainWorkSyncRuntimeActivity,
|
||||||
hasWorkSyncActiveRuntime,
|
hasWorkSyncActiveRuntime,
|
||||||
|
hasWorkSyncReachableRuntime,
|
||||||
isRuntimeEntryActiveForWorkSync,
|
isRuntimeEntryActiveForWorkSync,
|
||||||
isRuntimeMemberActiveForWorkSync,
|
isRuntimeMemberActiveForWorkSync,
|
||||||
isRuntimeMemberActivityUncertainForWorkSync,
|
isRuntimeMemberActivityUncertainForWorkSync,
|
||||||
|
|
@ -87,6 +88,60 @@ describe('member work sync team activity', () => {
|
||||||
).toBe(false);
|
).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', () => {
|
it('does not treat inactive liveness diagnostics as active by themselves', () => {
|
||||||
for (const livenessKind of [
|
for (const livenessKind of [
|
||||||
'permission_blocked',
|
'permission_blocked',
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,22 @@ function getAcceptedWorkLeaseStaleness(
|
||||||
return reportExpiresAtMs <= nowMs ? 'expired' : null;
|
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 {
|
function isEmptyAgendaStaleState(status: MemberWorkSyncStatus): boolean {
|
||||||
return (
|
return (
|
||||||
status.agenda.items.length === 0 &&
|
status.agenda.items.length === 0 &&
|
||||||
|
|
@ -99,6 +115,10 @@ function isEmptyAgendaStaleState(status: MemberWorkSyncStatus): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusNeedsBackgroundRefresh(status: MemberWorkSyncStatus, nowMs: number): boolean {
|
function statusNeedsBackgroundRefresh(status: MemberWorkSyncStatus, nowMs: number): boolean {
|
||||||
|
if (getReportTokenStaleness(status, nowMs) !== null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (isEmptyAgendaStaleState(status)) {
|
if (isEmptyAgendaStaleState(status)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -125,6 +145,13 @@ function statusNeedsBackgroundRefresh(status: MemberWorkSyncStatus, nowMs: numbe
|
||||||
|
|
||||||
function getStatusStalenessDiagnostics(status: MemberWorkSyncStatus, nowMs: number): string[] {
|
function getStatusStalenessDiagnostics(status: MemberWorkSyncStatus, nowMs: number): string[] {
|
||||||
const diagnostics: 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);
|
const evaluatedAtMs = Date.parse(status.evaluatedAt);
|
||||||
if (!Number.isFinite(evaluatedAtMs)) {
|
if (!Number.isFinite(evaluatedAtMs)) {
|
||||||
diagnostics.push('status_evaluated_at_invalid');
|
diagnostics.push('status_evaluated_at_invalid');
|
||||||
|
|
@ -150,6 +177,12 @@ function getStatusStalenessDiagnostics(status: MemberWorkSyncStatus, nowMs: numb
|
||||||
return [...new Set(diagnostics)];
|
return [...new Set(diagnostics)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldRefreshStatusSynchronously(stalenessDiagnostics: string[]): boolean {
|
||||||
|
return stalenessDiagnostics.some(
|
||||||
|
(diagnostic) => diagnostic !== 'caught_up_stale_refresh_enqueued'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function buildMemberWorkSyncRuntimeTurnSettledEnvironment(input: {
|
export function buildMemberWorkSyncRuntimeTurnSettledEnvironment(input: {
|
||||||
teamsBasePath: string;
|
teamsBasePath: string;
|
||||||
provider: RuntimeTurnSettledProvider;
|
provider: RuntimeTurnSettledProvider;
|
||||||
|
|
@ -505,6 +538,21 @@ export function createMemberWorkSyncFeature(deps: {
|
||||||
if (stalenessDiagnostics.length === 0) {
|
if (stalenessDiagnostics.length === 0) {
|
||||||
return status;
|
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({
|
queue.enqueue({
|
||||||
teamName: status.teamName,
|
teamName: status.teamName,
|
||||||
memberName: status.memberName,
|
memberName: status.memberName,
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,46 @@ const WORK_SYNC_BOOTSTRAP_ONLY_PID_SOURCES = new Set<TeamAgentRuntimePidSource>(
|
||||||
'persisted_metadata',
|
'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',
|
'agent_process_table',
|
||||||
'opencode_bridge',
|
'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(
|
export function isRuntimeEntryActiveForWorkSync(
|
||||||
entry:
|
entry:
|
||||||
| Pick<
|
| Pick<
|
||||||
|
|
@ -40,7 +75,7 @@ export function isRuntimeEntryActiveForWorkSync(
|
||||||
| null
|
| null
|
||||||
| undefined
|
| undefined
|
||||||
): boolean {
|
): boolean {
|
||||||
if (entry?.alive !== true) {
|
if (!entry) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
|
@ -50,17 +85,33 @@ export function isRuntimeEntryActiveForWorkSync(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
entry.livenessKind === 'confirmed_bootstrap' &&
|
entry.pidSource &&
|
||||||
(!entry.pidSource ||
|
WORK_SYNC_LEAD_CONFIRMED_BOOTSTRAP_ACTIVE_PID_SOURCES.has(entry.pidSource)
|
||||||
WORK_SYNC_BOOTSTRAP_ONLY_PID_SOURCES.has(entry.pidSource) ||
|
|
||||||
!WORK_SYNC_CONFIRMED_BOOTSTRAP_ACTIVE_PID_SOURCES.has(entry.pidSource))
|
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!entry.livenessKind) {
|
return hasActiveWorkSyncProcessEvidence(
|
||||||
return true;
|
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(
|
function isRuntimeEntryRelevantForWorkSync(
|
||||||
|
|
@ -95,6 +146,14 @@ export function hasWorkSyncActiveRuntime(
|
||||||
return Object.values(snapshot?.members ?? {}).some(isRuntimeEntryActiveForWorkSync);
|
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(
|
export function isRuntimeMemberActiveForWorkSync(
|
||||||
snapshot: Pick<TeamAgentRuntimeSnapshot, 'members'> | null | undefined,
|
snapshot: Pick<TeamAgentRuntimeSnapshot, 'members'> | null | undefined,
|
||||||
memberName: string
|
memberName: string
|
||||||
|
|
@ -106,7 +165,9 @@ export function isRuntimeMemberActiveForWorkSync(
|
||||||
return Object.values(snapshot?.members ?? {}).some(
|
return Object.values(snapshot?.members ?? {}).some(
|
||||||
(entry) =>
|
(entry) =>
|
||||||
normalizeMemberName(entry.memberName) === normalizedMemberName &&
|
normalizeMemberName(entry.memberName) === normalizedMemberName &&
|
||||||
isRuntimeEntryActiveForWorkSync(entry)
|
(isRuntimeEntryActiveForWorkSync(entry) ||
|
||||||
|
(isWorkSyncLeadLikeMemberName(normalizedMemberName) &&
|
||||||
|
isRuntimeLeadEntryActiveForWorkSync(entry)))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export {
|
||||||
export {
|
export {
|
||||||
hasUncertainWorkSyncRuntimeActivity,
|
hasUncertainWorkSyncRuntimeActivity,
|
||||||
hasWorkSyncActiveRuntime,
|
hasWorkSyncActiveRuntime,
|
||||||
|
hasWorkSyncReachableRuntime,
|
||||||
isRuntimeEntryActiveForWorkSync,
|
isRuntimeEntryActiveForWorkSync,
|
||||||
isRuntimeMemberActiveForWorkSync,
|
isRuntimeMemberActiveForWorkSync,
|
||||||
isRuntimeMemberActivityUncertainForWorkSync,
|
isRuntimeMemberActivityUncertainForWorkSync,
|
||||||
|
|
|
||||||
|
|
@ -17,36 +17,54 @@ export interface RunningTeamRowModel {
|
||||||
taskCounts?: TaskStatusCounts;
|
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) {
|
switch (status) {
|
||||||
case 'active':
|
case 'active':
|
||||||
return 'Active';
|
return text.status.active;
|
||||||
case 'provisioning':
|
case 'provisioning':
|
||||||
return 'Launching';
|
return text.status.provisioning;
|
||||||
case 'idle':
|
case 'idle':
|
||||||
return 'Running';
|
return text.status.idle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProjectLabel(projectPath?: string): string {
|
function getProjectLabel(projectPath: string | undefined, text: RunningTeamsSectionText): string {
|
||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
return 'No project';
|
return text.noProject;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getBaseName(projectPath) || projectPath;
|
return getBaseName(projectPath) || projectPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function adaptRunningTeamsSection(
|
export function adaptRunningTeamsSection(
|
||||||
teams: RunningTeamDashboardEntry[]
|
teams: RunningTeamDashboardEntry[],
|
||||||
|
text: RunningTeamsSectionText = DEFAULT_TEXT
|
||||||
): RunningTeamRowModel[] {
|
): RunningTeamRowModel[] {
|
||||||
return teams.map((team) => ({
|
return teams.map((team) => ({
|
||||||
id: team.teamName,
|
id: team.teamName,
|
||||||
teamName: team.teamName,
|
teamName: team.teamName,
|
||||||
displayName: team.displayName,
|
displayName: team.displayName,
|
||||||
projectPath: team.projectPath,
|
projectPath: team.projectPath,
|
||||||
projectLabel: getProjectLabel(team.projectPath),
|
projectLabel: getProjectLabel(team.projectPath, text),
|
||||||
status: team.status,
|
status: team.status,
|
||||||
statusLabel: getStatusLabel(team.status),
|
statusLabel: getStatusLabel(team.status, text),
|
||||||
iconColor: team.color
|
iconColor: team.color
|
||||||
? getTeamColorSet(team.color).border
|
? getTeamColorSet(team.color).border
|
||||||
: nameColorSet(team.displayName).border,
|
: nameColorSet(team.displayName).border,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { useAppTranslation } from '@features/localization/renderer';
|
||||||
import { api } from '@renderer/api';
|
import { api } from '@renderer/api';
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
import {
|
import {
|
||||||
|
|
@ -58,6 +59,7 @@ function toCandidate(input: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRunningTeamsSection(searchQuery: string): RunningTeamsSectionState {
|
export function useRunningTeamsSection(searchQuery: string): RunningTeamsSectionState {
|
||||||
|
const { t } = useAppTranslation('team');
|
||||||
const {
|
const {
|
||||||
teams,
|
teams,
|
||||||
globalTasks,
|
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,
|
aliveTeams,
|
||||||
globalTasks,
|
globalTasks,
|
||||||
|
|
@ -182,6 +191,7 @@ export function useRunningTeamsSection(searchQuery: string): RunningTeamsSection
|
||||||
provisioningTeamNames,
|
provisioningTeamNames,
|
||||||
searchActive,
|
searchActive,
|
||||||
teams,
|
teams,
|
||||||
|
t,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const openRunningTeam = useCallback(
|
const openRunningTeam = useCallback(
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import {
|
||||||
getOpenCodeTeamModelRecommendation,
|
getOpenCodeTeamModelRecommendation,
|
||||||
isOpenCodeTeamModelRecommended,
|
isOpenCodeTeamModelRecommended,
|
||||||
} from '@renderer/utils/openCodeModelRecommendations';
|
} from '@renderer/utils/openCodeModelRecommendations';
|
||||||
|
import { isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic } from '@shared/utils/openCodeWindowsAccessDenied';
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Check,
|
Check,
|
||||||
|
|
@ -785,6 +786,20 @@ function getRuntimeProviderDiagnosticRows(
|
||||||
.map(([label, value]) => [label, String(value)]);
|
.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> {
|
async function writeRuntimeProviderDiagnosticsToClipboard(text: string): Promise<boolean> {
|
||||||
if (navigator.clipboard?.writeText) {
|
if (navigator.clipboard?.writeText) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -826,6 +841,10 @@ const RuntimeProviderErrorAlert = ({
|
||||||
const [headline = message, ...detailLines] = message.trim().split(/\r?\n/);
|
const [headline = message, ...detailLines] = message.trim().split(/\r?\n/);
|
||||||
const fallbackDetails = detailLines.join('\n').trim();
|
const fallbackDetails = detailLines.join('\n').trim();
|
||||||
const hints = diagnostics?.hints ?? [];
|
const hints = diagnostics?.hints ?? [];
|
||||||
|
const showWindowsSymlinkPermissionHint = isOpenCodeWindowsNodeModulesSymlinkPermissionError(
|
||||||
|
message,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
const copyText = useMemo(
|
const copyText = useMemo(
|
||||||
() => formatRuntimeProviderDiagnosticsCopyText(message, diagnostics),
|
() => formatRuntimeProviderDiagnosticsCopyText(message, diagnostics),
|
||||||
[diagnostics, message]
|
[diagnostics, message]
|
||||||
|
|
@ -859,6 +878,11 @@ const RuntimeProviderErrorAlert = ({
|
||||||
<div className="flex min-w-0 flex-wrap items-start justify-between gap-2">
|
<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">
|
<div className="min-w-0 whitespace-pre-wrap break-words font-medium leading-5">
|
||||||
{headline || message}
|
{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>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="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', () => {
|
it('creates a secondary OpenCode lane for an Anthropic-led mixed team', () => {
|
||||||
const result = planTeamRuntimeLanes({
|
const result = planTeamRuntimeLanes({
|
||||||
leadProviderId: 'anthropic',
|
leadProviderId: 'anthropic',
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,16 @@ export type TeamRuntimeLanePlan =
|
||||||
allMembers: PlannedRuntimeMember[];
|
allMembers: PlannedRuntimeMember[];
|
||||||
sideLanes: [];
|
sideLanes: [];
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
mode: 'pure_opencode_worktree_root_lanes';
|
||||||
|
primaryMembers: PlannedRuntimeMember[];
|
||||||
|
allMembers: PlannedRuntimeMember[];
|
||||||
|
sideLanes: {
|
||||||
|
laneId: string;
|
||||||
|
providerId: 'opencode';
|
||||||
|
member: PlannedRuntimeMember;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
mode: 'mixed_opencode_side_lanes';
|
mode: 'mixed_opencode_side_lanes';
|
||||||
primaryMembers: PlannedRuntimeMember[];
|
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: {
|
export function planTeamRuntimeLanes(params: {
|
||||||
leadProviderId?: TeamProviderId;
|
leadProviderId?: TeamProviderId;
|
||||||
members: readonly RuntimeLanePlannerMemberInput[];
|
members: readonly RuntimeLanePlannerMemberInput[];
|
||||||
|
baseCwd?: string;
|
||||||
}): TeamRuntimeLanePlanResult {
|
}): TeamRuntimeLanePlanResult {
|
||||||
const leadProviderId = normalizeLeadProviderId(params.leadProviderId);
|
const leadProviderId = normalizeLeadProviderId(params.leadProviderId);
|
||||||
const allMembers = normalizePlannedMembers(params.members, 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.',
|
'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 {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
plan: {
|
plan: {
|
||||||
|
|
@ -175,18 +213,37 @@ export function isMixedOpenCodeSideLanePlan(
|
||||||
return plan.mode === 'mixed_opencode_side_lanes';
|
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(
|
export function isPureOpenCodeLanePlan(
|
||||||
plan: TeamRuntimeLanePlan
|
plan: TeamRuntimeLanePlan
|
||||||
): plan is Extract<TeamRuntimeLanePlan, { mode: 'pure_opencode' }> {
|
): plan is Extract<TeamRuntimeLanePlan, { mode: 'pure_opencode' }> {
|
||||||
return plan.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(
|
export function fromProvisioningMembers(
|
||||||
leadProviderId: TeamProviderId | undefined,
|
leadProviderId: TeamProviderId | undefined,
|
||||||
members: readonly TeamProvisioningMemberInput[]
|
members: readonly TeamProvisioningMemberInput[],
|
||||||
|
options: { baseCwd?: string } = {}
|
||||||
): TeamRuntimeLanePlanResult {
|
): TeamRuntimeLanePlanResult {
|
||||||
return planTeamRuntimeLanes({
|
return planTeamRuntimeLanes({
|
||||||
leadProviderId,
|
leadProviderId,
|
||||||
|
baseCwd: options.baseCwd,
|
||||||
members: members.map((member) => ({
|
members: members.map((member) => ({
|
||||||
name: member.name,
|
name: member.name,
|
||||||
role: member.role,
|
role: member.role,
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,12 @@ export type {
|
||||||
TeamRuntimeLanePlanSuccess,
|
TeamRuntimeLanePlanSuccess,
|
||||||
} from './core/domain/planTeamRuntimeLanes';
|
} from './core/domain/planTeamRuntimeLanes';
|
||||||
export {
|
export {
|
||||||
|
buildOpenCodeSecondaryLaneId,
|
||||||
buildPlannedMemberLaneIdentity,
|
buildPlannedMemberLaneIdentity,
|
||||||
fromProvisioningMembers,
|
fromProvisioningMembers,
|
||||||
isMixedOpenCodeSideLanePlan,
|
isMixedOpenCodeSideLanePlan,
|
||||||
|
isOpenCodeSideLanePlan,
|
||||||
isPureOpenCodeLanePlan,
|
isPureOpenCodeLanePlan,
|
||||||
|
isPureOpenCodeWorktreeRootLanePlan,
|
||||||
planTeamRuntimeLanes,
|
planTeamRuntimeLanes,
|
||||||
} from './core/domain/planTeamRuntimeLanes';
|
} from './core/domain/planTeamRuntimeLanes';
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ describe('createTeamRuntimeLaneCoordinator', () => {
|
||||||
{ name: 'tom', providerId: 'opencode', model: 'minimax-m2.5-free' },
|
{ 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', () => {
|
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 { buildMixedPersistedLaunchSnapshot } from '@features/team-runtime-lanes/core/domain/buildMixedPersistedLaunchSnapshot';
|
||||||
import {
|
import {
|
||||||
fromProvisioningMembers,
|
fromProvisioningMembers,
|
||||||
isMixedOpenCodeSideLanePlan,
|
isOpenCodeSideLanePlan,
|
||||||
type TeamRuntimeLanePlan,
|
type TeamRuntimeLanePlan,
|
||||||
} from '@features/team-runtime-lanes/core/domain/planTeamRuntimeLanes';
|
} from '@features/team-runtime-lanes/core/domain/planTeamRuntimeLanes';
|
||||||
|
|
||||||
|
|
@ -11,6 +11,7 @@ export interface TeamRuntimeLaneCoordinator {
|
||||||
planProvisioningMembers(params: {
|
planProvisioningMembers(params: {
|
||||||
leadProviderId?: TeamProviderId;
|
leadProviderId?: TeamProviderId;
|
||||||
members: TeamCreateRequest['members'];
|
members: TeamCreateRequest['members'];
|
||||||
|
baseCwd?: string;
|
||||||
hasOpenCodeRuntimeAdapter: boolean;
|
hasOpenCodeRuntimeAdapter: boolean;
|
||||||
}): TeamRuntimeLanePlan;
|
}): TeamRuntimeLanePlan;
|
||||||
buildAggregateLaunchSnapshot(
|
buildAggregateLaunchSnapshot(
|
||||||
|
|
@ -22,13 +23,15 @@ export interface TeamRuntimeLaneCoordinator {
|
||||||
export function createTeamRuntimeLaneCoordinator(): TeamRuntimeLaneCoordinator {
|
export function createTeamRuntimeLaneCoordinator(): TeamRuntimeLaneCoordinator {
|
||||||
return {
|
return {
|
||||||
planProvisioningMembers(params) {
|
planProvisioningMembers(params) {
|
||||||
const lanePlan = fromProvisioningMembers(params.leadProviderId, params.members);
|
const lanePlan = fromProvisioningMembers(params.leadProviderId, params.members, {
|
||||||
|
baseCwd: params.baseCwd,
|
||||||
|
});
|
||||||
if (!lanePlan.ok) {
|
if (!lanePlan.ok) {
|
||||||
throw new Error(lanePlan.message);
|
throw new Error(lanePlan.message);
|
||||||
}
|
}
|
||||||
if (isMixedOpenCodeSideLanePlan(lanePlan.plan) && !params.hasOpenCodeRuntimeAdapter) {
|
if (isOpenCodeSideLanePlan(lanePlan.plan) && !params.hasOpenCodeRuntimeAdapter) {
|
||||||
throw new Error(
|
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;
|
return lanePlan.plan;
|
||||||
|
|
@ -37,7 +40,7 @@ export function createTeamRuntimeLaneCoordinator(): TeamRuntimeLaneCoordinator {
|
||||||
return buildMixedPersistedLaunchSnapshot(params);
|
return buildMixedPersistedLaunchSnapshot(params);
|
||||||
},
|
},
|
||||||
isMixedSideLanePlan(plan) {
|
isMixedSideLanePlan(plan) {
|
||||||
return isMixedOpenCodeSideLanePlan(plan);
|
return isOpenCodeSideLanePlan(plan);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ import {
|
||||||
buildMemberWorkSyncRuntimeTurnSettledEnvironment,
|
buildMemberWorkSyncRuntimeTurnSettledEnvironment,
|
||||||
createMemberWorkSyncFeature,
|
createMemberWorkSyncFeature,
|
||||||
hasUncertainWorkSyncRuntimeActivity,
|
hasUncertainWorkSyncRuntimeActivity,
|
||||||
hasWorkSyncActiveRuntime,
|
hasWorkSyncReachableRuntime,
|
||||||
isRuntimeMemberActivityUncertainForWorkSync,
|
isRuntimeMemberActivityUncertainForWorkSync,
|
||||||
isRuntimeMemberActiveForWorkSync,
|
isRuntimeMemberActiveForWorkSync,
|
||||||
type MemberWorkSyncFeatureFacade,
|
type MemberWorkSyncFeatureFacade,
|
||||||
|
|
@ -1919,7 +1919,7 @@ async function initializeServices(): Promise<void> {
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const active = hasWorkSyncActiveRuntime(snapshot);
|
const active = hasWorkSyncReachableRuntime(snapshot);
|
||||||
if (!active && hasUncertainWorkSyncRuntimeActivity(snapshot)) {
|
if (!active && hasUncertainWorkSyncRuntimeActivity(snapshot)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -2037,7 +2037,12 @@ async function initializeServices(): Promise<void> {
|
||||||
isBusy: (input) => teamProvisioningService.getOpenCodeMemberDeliveryBusyStatus(input),
|
isBusy: (input) => teamProvisioningService.getOpenCodeMemberDeliveryBusyStatus(input),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
resolveControlUrl: async () => getTeamControlApiBaseUrl(),
|
resolveControlUrl: async () => {
|
||||||
|
if (!httpServer.isRunning()) {
|
||||||
|
await startHttpServer(handleModeSwitch);
|
||||||
|
}
|
||||||
|
return getTeamControlApiBaseUrl();
|
||||||
|
},
|
||||||
proofMissingRecoveryGuard: {
|
proofMissingRecoveryGuard: {
|
||||||
shouldDispatch: async (input) => {
|
shouldDispatch: async (input) => {
|
||||||
const isOpenCodeRecipient = await teamProvisioningService
|
const isOpenCodeRecipient = await teamProvisioningService
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,17 @@ import type {
|
||||||
import type { CliArgsValidationResult } from '@shared/utils/cliArgsParser';
|
import type { CliArgsValidationResult } from '@shared/utils/cliArgsParser';
|
||||||
|
|
||||||
const logger = createLogger('IPC:teams');
|
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';
|
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 {
|
function noteHeavyTeamDataWorkerFallback(operation: string): void {
|
||||||
if (!app.isPackaged) {
|
if (!app.isPackaged) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -2866,9 +3028,10 @@ async function handleSendMessage(
|
||||||
: leadName !== null && memberName === leadName;
|
: leadName !== null && memberName === leadName;
|
||||||
const actionMode = payload.actionMode;
|
const actionMode = payload.actionMode;
|
||||||
|
|
||||||
const recipientProviderId = !isLeadRecipient
|
const recipientProviderId = await provisioning.resolveRuntimeRecipientProviderId(
|
||||||
? await provisioning.resolveRuntimeRecipientProviderId(tn, memberName)
|
tn,
|
||||||
: undefined;
|
memberName
|
||||||
|
);
|
||||||
const isOpenCodeRecipient = recipientProviderId === 'opencode';
|
const isOpenCodeRecipient = recipientProviderId === 'opencode';
|
||||||
|
|
||||||
// Attachments are routed through explicit provider transports only.
|
// Attachments are routed through explicit provider transports only.
|
||||||
|
|
@ -2889,7 +3052,7 @@ async function handleSendMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smart routing: lead + alive → stdin direct, else → inbox
|
// Smart routing: lead + alive → stdin direct, else → inbox
|
||||||
if (isLeadRecipient && isAlive) {
|
if (isLeadRecipient && isAlive && !isOpenCodeRecipient) {
|
||||||
const resolvedLeadName = leadName ?? memberName;
|
const resolvedLeadName = leadName ?? memberName;
|
||||||
const teammateRoster = await getDurableLeadTeammateRoster(tn, resolvedLeadName);
|
const teammateRoster = await getDurableLeadTeammateRoster(tn, resolvedLeadName);
|
||||||
const rosterContextBlock = buildLeadRosterContextBlock(tn, resolvedLeadName, teammateRoster);
|
const rosterContextBlock = buildLeadRosterContextBlock(tn, resolvedLeadName, teammateRoster);
|
||||||
|
|
@ -3083,8 +3246,12 @@ async function handleSendMessage(
|
||||||
// }
|
// }
|
||||||
if (isOpenCodeRecipient) {
|
if (isOpenCodeRecipient) {
|
||||||
try {
|
try {
|
||||||
const relay = await withTimeoutValue(
|
const relay = await waitForOpenCodeRuntimeRelayForUi({
|
||||||
provisioning.relayOpenCodeMemberInboxMessages(tn, memberName, {
|
provisioning,
|
||||||
|
teamName: tn,
|
||||||
|
memberName,
|
||||||
|
messageId: result.messageId,
|
||||||
|
relayPromise: provisioning.relayOpenCodeMemberInboxMessages(tn, memberName, {
|
||||||
onlyMessageId: result.messageId,
|
onlyMessageId: result.messageId,
|
||||||
source: 'ui-send',
|
source: 'ui-send',
|
||||||
deliveryMetadata: {
|
deliveryMetadata: {
|
||||||
|
|
@ -3093,23 +3260,7 @@ async function handleSendMessage(
|
||||||
taskRefs: validatedTaskRefs.value,
|
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 ?? {
|
const delivery = relay.lastDelivery ?? {
|
||||||
delivered: relay.relayed > 0,
|
delivered: relay.relayed > 0,
|
||||||
reason: relay.relayed > 0 ? undefined : 'opencode_message_delivery_not_attempted',
|
reason: relay.relayed > 0 ? undefined : 'opencode_message_delivery_not_attempted',
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,11 @@ import { tmpdir } from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { resolveGeminiRuntimeAuth } from './geminiRuntimeAuth';
|
import { resolveGeminiRuntimeAuth } from './geminiRuntimeAuth';
|
||||||
import { buildProviderAwareCliEnv } from './providerAwareCliEnv';
|
import {
|
||||||
|
buildProviderAwareCliEnv,
|
||||||
|
getAggregateProviderStatusStoredCredentialAllowlist,
|
||||||
|
getProviderStatusStoredCredentialAllowlist,
|
||||||
|
} from './providerAwareCliEnv';
|
||||||
import { providerConnectionService } from './ProviderConnectionService';
|
import { providerConnectionService } from './ProviderConnectionService';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -839,12 +843,15 @@ export class ClaudeMultimodelBridgeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildCliEnv(
|
private async buildCliEnv(
|
||||||
binaryPath: string
|
binaryPath: string,
|
||||||
|
options: { allowedStoredApiKeyEnvVarNames?: readonly string[] } = {}
|
||||||
): Promise<Awaited<ReturnType<typeof buildProviderAwareCliEnv>>> {
|
): Promise<Awaited<ReturnType<typeof buildProviderAwareCliEnv>>> {
|
||||||
return buildProviderAwareCliEnv({
|
return buildProviderAwareCliEnv({
|
||||||
binaryPath,
|
binaryPath,
|
||||||
allowStoredApiKeyDecryption: false,
|
allowStoredApiKeyDecryption: false,
|
||||||
allowedStoredApiKeyEnvVarNames: ['ANTHROPIC_AUTH_TOKEN'],
|
allowedStoredApiKeyEnvVarNames: options.allowedStoredApiKeyEnvVarNames ?? [
|
||||||
|
'ANTHROPIC_AUTH_TOKEN',
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -856,8 +863,7 @@ export class ClaudeMultimodelBridgeService {
|
||||||
binaryPath,
|
binaryPath,
|
||||||
providerId,
|
providerId,
|
||||||
allowStoredApiKeyDecryption: false,
|
allowStoredApiKeyDecryption: false,
|
||||||
allowedStoredApiKeyEnvVarNames:
|
allowedStoredApiKeyEnvVarNames: getProviderStatusStoredCredentialAllowlist(providerId),
|
||||||
providerId === 'anthropic' ? ['ANTHROPIC_AUTH_TOKEN'] : undefined,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1747,7 +1753,9 @@ export class ClaudeMultimodelBridgeService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { env, connectionIssues } = await this.buildCliEnv(binaryPath);
|
const { env, connectionIssues } = await this.buildCliEnv(binaryPath, {
|
||||||
|
allowedStoredApiKeyEnvVarNames: getAggregateProviderStatusStoredCredentialAllowlist(),
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execCli(binaryPath, ['runtime', 'status', '--json'], {
|
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 { createLogger } from '@shared/utils/logger';
|
||||||
import { filterVisibleProviderRuntimeModels } from '@shared/utils/providerModelVisibility';
|
import { filterVisibleProviderRuntimeModels } from '@shared/utils/providerModelVisibility';
|
||||||
|
|
||||||
import { buildProviderAwareCliEnv } from './providerAwareCliEnv';
|
import {
|
||||||
|
buildProviderAwareCliEnv,
|
||||||
|
getProviderStatusStoredCredentialAllowlist,
|
||||||
|
} from './providerAwareCliEnv';
|
||||||
import {
|
import {
|
||||||
buildProviderModelProbeArgs,
|
buildProviderModelProbeArgs,
|
||||||
classifyProviderModelProbeFailure,
|
classifyProviderModelProbeFailure,
|
||||||
|
|
@ -194,8 +197,9 @@ export class CliProviderModelAvailabilityService {
|
||||||
binaryPath: context.binaryPath,
|
binaryPath: context.binaryPath,
|
||||||
providerId: context.provider.providerId,
|
providerId: context.provider.providerId,
|
||||||
allowStoredApiKeyDecryption: false,
|
allowStoredApiKeyDecryption: false,
|
||||||
allowedStoredApiKeyEnvVarNames:
|
allowedStoredApiKeyEnvVarNames: getProviderStatusStoredCredentialAllowlist(
|
||||||
context.provider.providerId === 'anthropic' ? ['ANTHROPIC_AUTH_TOKEN'] : undefined,
|
context.provider.providerId
|
||||||
|
),
|
||||||
}).then((result) => ({
|
}).then((result) => ({
|
||||||
env: result.env,
|
env: result.env,
|
||||||
providerArgs: result.providerArgs ?? [],
|
providerArgs: result.providerArgs ?? [],
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,14 @@ import type { CliProviderId, TeamProviderId } from '@shared/types';
|
||||||
|
|
||||||
type ProviderEnvTargetId = CliProviderId | TeamProviderId | undefined;
|
type ProviderEnvTargetId = CliProviderId | TeamProviderId | undefined;
|
||||||
const ELECTRON_RUN_AS_NODE_ENV = 'ELECTRON_RUN_AS_NODE';
|
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 {
|
export interface ProviderAwareCliEnvOptions {
|
||||||
binaryPath?: string | null;
|
binaryPath?: string | null;
|
||||||
|
|
@ -30,6 +38,20 @@ export interface ProviderAwareCliEnvResult {
|
||||||
providerArgs: string[];
|
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 {
|
function removeGlobalElectronRunAsNodeEnv(env: NodeJS.ProcessEnv): void {
|
||||||
delete env[ELECTRON_RUN_AS_NODE_ENV];
|
delete env[ELECTRON_RUN_AS_NODE_ENV];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,15 +55,6 @@ function buildProviderFastModeArgs(config: ScheduleLaunchConfig): string[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateFastModeLaunchConfig(config: ScheduleLaunchConfig): void {
|
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) {
|
if (config.providerId !== 'codex' || config.resolvedFastMode !== true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,9 @@ interface LedgerEvent {
|
||||||
sourceImportKey?: string;
|
sourceImportKey?: string;
|
||||||
evidenceProof?: string;
|
evidenceProof?: string;
|
||||||
supersedesEventId?: string;
|
supersedesEventId?: string;
|
||||||
|
suppressed?: true;
|
||||||
|
suppressionReason?: string;
|
||||||
|
suppressedAt?: string;
|
||||||
snapshotId?: string;
|
snapshotId?: string;
|
||||||
snapshotSource?: string;
|
snapshotSource?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -1209,10 +1212,12 @@ export class TaskChangeLedgerReader {
|
||||||
events.forEach((event, index) => {
|
events.forEach((event, index) => {
|
||||||
const sourceImportKey = this.sourceImportKeyForEvent(event);
|
const sourceImportKey = this.sourceImportKeyForEvent(event);
|
||||||
if (!sourceImportKey) {
|
if (!sourceImportKey) {
|
||||||
passthrough.push({ event, index });
|
if (event.suppressed !== true) {
|
||||||
|
passthrough.push({ event, index });
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rank = this.evidenceRankForEvent(event);
|
const rank = this.projectionRankForEvent(event);
|
||||||
const existing = selectedBySourceImportKey.get(sourceImportKey);
|
const existing = selectedBySourceImportKey.get(sourceImportKey);
|
||||||
if (!existing || rank >= existing.rank) {
|
if (!existing || rank >= existing.rank) {
|
||||||
selectedBySourceImportKey.set(sourceImportKey, { event, index, rank });
|
selectedBySourceImportKey.set(sourceImportKey, { event, index, rank });
|
||||||
|
|
@ -1221,7 +1226,9 @@ export class TaskChangeLedgerReader {
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...passthrough,
|
...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)
|
.sort((left, right) => left.index - right.index)
|
||||||
.map(({ event }) => event);
|
.map(({ event }) => event);
|
||||||
|
|
@ -1241,6 +1248,10 @@ export class TaskChangeLedgerReader {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private projectionRankForEvent(event: LedgerEvent): number {
|
||||||
|
return event.suppressed === true ? Number.MAX_SAFE_INTEGER : this.evidenceRankForEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
private evidenceRankForEvent(event: LedgerEvent): number {
|
private evidenceRankForEvent(event: LedgerEvent): number {
|
||||||
const hasFullText = this.hasFullTextEvidence(event);
|
const hasFullText = this.hasFullTextEvidence(event);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,23 @@ import * as path from 'path';
|
||||||
import { atomicWriteAsync } from './atomicWrite';
|
import { atomicWriteAsync } from './atomicWrite';
|
||||||
import { withFileLock } from './fileLock';
|
import { withFileLock } from './fileLock';
|
||||||
import { withInboxLock } from './inboxLock';
|
import { withInboxLock } from './inboxLock';
|
||||||
|
import { getEffectiveInboxMessageId } from './inboxMessageIdentity';
|
||||||
|
|
||||||
import type { InboxMessage, SendMessageRequest, SendMessageResult, TaskRef } from '@shared/types';
|
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 {
|
export interface MergeRuntimeDeliveryTaskRefsRequest {
|
||||||
inboxName: string;
|
inboxName: string;
|
||||||
messageId: 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(
|
async mergeRuntimeDeliveryTaskRefs(
|
||||||
teamName: string,
|
teamName: string,
|
||||||
request: MergeRuntimeDeliveryTaskRefsRequest
|
request: MergeRuntimeDeliveryTaskRefsRequest
|
||||||
|
|
|
||||||
|
|
@ -712,24 +712,23 @@ export function createPersistedLaunchSnapshot(params: {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
expectedMembers: readonly string[];
|
expectedMembers: readonly string[];
|
||||||
bootstrapExpectedMembers?: readonly string[];
|
bootstrapExpectedMembers?: readonly string[];
|
||||||
|
includeLeadMembers?: boolean;
|
||||||
leadSessionId?: string;
|
leadSessionId?: string;
|
||||||
launchPhase?: PersistedTeamLaunchPhase;
|
launchPhase?: PersistedTeamLaunchPhase;
|
||||||
members?: Record<string, PersistedTeamLaunchMemberState>;
|
members?: Record<string, PersistedTeamLaunchMemberState>;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
}): PersistedTeamLaunchSnapshot {
|
}): PersistedTeamLaunchSnapshot {
|
||||||
const updatedAt = params.updatedAt ?? new Date().toISOString();
|
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(
|
const expectedMembers = Array.from(
|
||||||
new Set(
|
new Set(params.expectedMembers.map(normalizeMemberName).filter(shouldKeepExpectedMemberName))
|
||||||
params.expectedMembers
|
|
||||||
.map(normalizeMemberName)
|
|
||||||
.filter((name) => name.length > 0 && name !== 'user' && !isLeadMember({ name }))
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
const bootstrapExpectedMembers = Array.from(
|
const bootstrapExpectedMembers = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
(params.bootstrapExpectedMembers ?? expectedMembers)
|
(params.bootstrapExpectedMembers ?? expectedMembers)
|
||||||
.map(normalizeMemberName)
|
.map(normalizeMemberName)
|
||||||
.filter((name) => name.length > 0 && name !== 'user' && !isLeadMember({ name }))
|
.filter(shouldKeepExpectedMemberName)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const members = params.members ?? {};
|
const members = params.members ?? {};
|
||||||
|
|
|
||||||
|
|
@ -85,18 +85,21 @@ function resolveLeadName(config: TeamConfig): string {
|
||||||
return lead?.name?.trim() || 'team-lead';
|
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;
|
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();
|
return new Date(raw).toISOString();
|
||||||
}
|
}
|
||||||
if (typeof raw === 'string') {
|
if (typeof raw === 'string') {
|
||||||
const parsed = Date.parse(raw);
|
const parsed = Date.parse(raw);
|
||||||
if (Number.isFinite(parsed)) {
|
if (Number.isFinite(parsed) && parsed > 0) {
|
||||||
return new Date(parsed).toISOString();
|
return new Date(parsed).toISOString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new Date(0).toISOString();
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSyntheticBootstrapDisplayPrompt(
|
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.`;
|
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 members = Array.isArray(config.members) ? config.members : [];
|
||||||
const leadName = resolveLeadName(config);
|
const leadName = resolveLeadName(config);
|
||||||
const normalizedLeadName = leadName.trim().toLowerCase();
|
const normalizedLeadName = leadName.trim().toLowerCase();
|
||||||
|
|
@ -134,15 +140,20 @@ function buildSyntheticBootstrapMessages(config: TeamConfig): InboxMessage[] {
|
||||||
member.name.trim().toLowerCase() !== normalizedLeadName &&
|
member.name.trim().toLowerCase() !== normalizedLeadName &&
|
||||||
member.removedAt == null
|
member.removedAt == null
|
||||||
)
|
)
|
||||||
.map((member) => ({
|
.map((member) => {
|
||||||
from: leadName,
|
const messageId = `bootstrap-start:${config.name}:${member.name}`;
|
||||||
to: member.name,
|
return {
|
||||||
text: buildSyntheticBootstrapDisplayPrompt(config, member),
|
from: leadName,
|
||||||
timestamp: resolveSyntheticBootstrapTimestamp(config, member),
|
to: member.name,
|
||||||
read: true,
|
text: buildSyntheticBootstrapDisplayPrompt(config, member),
|
||||||
source: 'system_notification' as const,
|
timestamp:
|
||||||
messageId: `bootstrap-start:${config.name}:${member.name}`,
|
resolveSyntheticBootstrapTimestamp(config, member) ??
|
||||||
}));
|
fallbackTimestampForMessage(messageId),
|
||||||
|
read: true,
|
||||||
|
source: 'system_notification' as const,
|
||||||
|
messageId,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function isVisibleTeamMessage(message: InboxMessage): boolean {
|
function isVisibleTeamMessage(message: InboxMessage): boolean {
|
||||||
|
|
@ -429,6 +440,7 @@ export class TeamMessageFeedService {
|
||||||
private readonly dirtyTeams = new Set<string>();
|
private readonly dirtyTeams = new Set<string>();
|
||||||
private readonly inFlightByTeam = new Map<string, InFlightTeamMessageFeed>();
|
private readonly inFlightByTeam = new Map<string, InFlightTeamMessageFeed>();
|
||||||
private readonly generationByTeam = new Map<string, number>();
|
private readonly generationByTeam = new Map<string, number>();
|
||||||
|
private readonly syntheticBootstrapTimestampByMessageId = new Map<string, string>();
|
||||||
|
|
||||||
constructor(private readonly deps: TeamMessageFeedDeps) {}
|
constructor(private readonly deps: TeamMessageFeedDeps) {}
|
||||||
|
|
||||||
|
|
@ -487,6 +499,17 @@ export class TeamMessageFeedService {
|
||||||
return this.generationByTeam.get(teamName) ?? 0;
|
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(
|
private refreshCleanExpiredCacheInBackground(
|
||||||
teamName: string,
|
teamName: string,
|
||||||
cached: TeamMessageFeedCacheEntry,
|
cached: TeamMessageFeedCacheEntry,
|
||||||
|
|
@ -554,7 +577,9 @@ export class TeamMessageFeedService {
|
||||||
const sourceMs = Date.now() - sourceStartedAt;
|
const sourceMs = Date.now() - sourceStartedAt;
|
||||||
|
|
||||||
const normalizeStartedAt = Date.now();
|
const normalizeStartedAt = Date.now();
|
||||||
const syntheticMessages = buildSyntheticBootstrapMessages(config);
|
const syntheticMessages = buildSyntheticBootstrapMessages(config, (messageId) =>
|
||||||
|
this.getSyntheticBootstrapFallbackTimestamp(messageId)
|
||||||
|
);
|
||||||
let messages = [...inboxMessages, ...leadTexts, ...sentMessages, ...syntheticMessages].filter(
|
let messages = [...inboxMessages, ...leadTexts, ...sentMessages, ...syntheticMessages].filter(
|
||||||
isVisibleTeamMessage
|
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_ENTRY=',
|
||||||
'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL=',
|
'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL=',
|
||||||
] as const;
|
] 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(
|
export async function cleanupManagedOpenCodeServeProcesses(
|
||||||
options: OpenCodeManagedHostProcessCleanupOptions
|
options: OpenCodeManagedHostProcessCleanupOptions
|
||||||
|
|
@ -204,7 +209,8 @@ export function isAppManagedWindowsOpenCodeServeCommand(command: string): boolea
|
||||||
export function isManagedOpenCodeServeProcessDetails(details: string): boolean {
|
export function isManagedOpenCodeServeProcessDetails(details: string): boolean {
|
||||||
return (
|
return (
|
||||||
processDetailsIncludeMarkers(details, MANAGED_ENV_MARKERS) &&
|
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() : '';
|
typeof current.runtimeRunId === 'string' ? current.runtimeRunId.trim() : '';
|
||||||
const bootstrapRuntimeRunId =
|
const bootstrapRuntimeRunId =
|
||||||
typeof bootstrapMember.runtimeRunId === 'string' ? bootstrapMember.runtimeRunId.trim() : '';
|
typeof bootstrapMember.runtimeRunId === 'string' ? bootstrapMember.runtimeRunId.trim() : '';
|
||||||
|
const hasSameRuntimeRunId =
|
||||||
|
currentRuntimeRunId.length > 0 &&
|
||||||
|
bootstrapRuntimeRunId.length > 0 &&
|
||||||
|
currentRuntimeRunId === bootstrapRuntimeRunId;
|
||||||
if (
|
if (
|
||||||
currentRuntimeRunId.length > 0 &&
|
currentRuntimeRunId.length > 0 &&
|
||||||
bootstrapRuntimeRunId.length > 0 &&
|
bootstrapRuntimeRunId.length > 0 &&
|
||||||
|
|
@ -631,10 +635,18 @@ export function isBootstrapMemberEvidenceCurrentForMember(
|
||||||
const hasDurableSpawnBoundary =
|
const hasDurableSpawnBoundary =
|
||||||
Number.isFinite(firstSpawnAcceptedMs) &&
|
Number.isFinite(firstSpawnAcceptedMs) &&
|
||||||
(!Number.isFinite(lastEvaluatedMs) || firstSpawnAcceptedMs <= lastEvaluatedMs);
|
(!Number.isFinite(lastEvaluatedMs) || firstSpawnAcceptedMs <= lastEvaluatedMs);
|
||||||
const boundaryMs = hasDurableSpawnBoundary ? firstSpawnAcceptedMs : NaN;
|
const currentBoundaryMs = hasDurableSpawnBoundary ? firstSpawnAcceptedMs : NaN;
|
||||||
const hasCompatibleRuntimeRunIdForSkew =
|
const sameRunBootstrapBoundaryMs =
|
||||||
currentRuntimeRunId.length === 0 ||
|
evidenceKind === 'confirmation' && hasSameRuntimeRunId && hasDurableBootstrapSpawnAcceptedAt
|
||||||
(bootstrapRuntimeRunId.length > 0 && currentRuntimeRunId === bootstrapRuntimeRunId);
|
? 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 =
|
const withinBootstrapConfirmationClockSkew =
|
||||||
evidenceKind === 'confirmation' &&
|
evidenceKind === 'confirmation' &&
|
||||||
Number.isFinite(boundaryMs) &&
|
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 teamPrompt = input.prompt?.trim();
|
||||||
const role = member.role?.trim() || member.workflow?.trim() || 'teammate';
|
const role = member.role?.trim() || member.workflow?.trim() || 'teammate';
|
||||||
const workflow = member.workflow?.trim();
|
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 [
|
return [
|
||||||
'<agent_teams_app_managed_bootstrap_briefing>',
|
'<agent_teams_app_managed_bootstrap_briefing>',
|
||||||
'AGENT_TEAMS_APP_MANAGED_BOOTSTRAP_V1',
|
'AGENT_TEAMS_APP_MANAGED_BOOTSTRAP_V1',
|
||||||
`You are ${member.name}, a ${role} on team "${input.teamName}".`,
|
identityLine,
|
||||||
teamPrompt ? `Team launch context:\n${teamPrompt}` : null,
|
teamPrompt ? `Team launch context:\n${teamPrompt}` : null,
|
||||||
workflow ? `Workflow:\n${workflow}` : 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.',
|
'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.',
|
'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.',
|
`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 OpenCode teammate.`,
|
`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.',
|
'Do not answer team/app messages only as plain assistant text when agent-teams_message_send is available.',
|
||||||
'</agent_teams_app_managed_bootstrap_briefing>',
|
'</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 { isLeadMember as isLeadMemberCheck } from '@shared/utils/leadDetection';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
import { getTaskDisplayId } from '@shared/utils/taskIdentity';
|
import { getTaskDisplayId } from '@shared/utils/taskIdentity';
|
||||||
|
import {
|
||||||
|
inferTeamProviderIdFromModel,
|
||||||
|
normalizeOptionalTeamProviderId,
|
||||||
|
} from '@shared/utils/teamProvider';
|
||||||
|
|
||||||
import { TeamConfigReader } from '../../TeamConfigReader';
|
import { TeamConfigReader } from '../../TeamConfigReader';
|
||||||
import { TeamMembersMetaStore } from '../../TeamMembersMetaStore';
|
import { TeamMembersMetaStore } from '../../TeamMembersMetaStore';
|
||||||
|
|
@ -36,6 +40,8 @@ import type {
|
||||||
BoardTaskLogSegment,
|
BoardTaskLogSegment,
|
||||||
BoardTaskLogStreamResponse,
|
BoardTaskLogStreamResponse,
|
||||||
BoardTaskLogStreamSummary,
|
BoardTaskLogStreamSummary,
|
||||||
|
TeamMember,
|
||||||
|
TeamProviderId,
|
||||||
TeamTask,
|
TeamTask,
|
||||||
} from '@shared/types';
|
} from '@shared/types';
|
||||||
|
|
||||||
|
|
@ -104,6 +110,58 @@ function normalizeMemberName(value: string): string {
|
||||||
return value.trim().toLowerCase();
|
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 isBoardMcpToolName = isBoardTaskLogMcpToolName;
|
||||||
const canonicalizeBoardToolName = canonicalizeBoardTaskLogToolName;
|
const canonicalizeBoardToolName = canonicalizeBoardTaskLogToolName;
|
||||||
|
|
||||||
|
|
@ -2260,10 +2318,13 @@ export class BoardTaskLogStreamService {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const member = [...metaMembers, ...(config?.members ?? [])].find(
|
return (
|
||||||
(candidate) => normalizeMemberName(candidate.name) === normalizedOwner
|
resolveProviderFromMemberSources({
|
||||||
|
configMembers: config?.members ?? [],
|
||||||
|
metaMembers,
|
||||||
|
memberName: normalizedOwner,
|
||||||
|
}) === 'opencode'
|
||||||
);
|
);
|
||||||
return member?.providerId === 'opencode';
|
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
import { getTaskDisplayId } from '@shared/utils/taskIdentity';
|
import { getTaskDisplayId } from '@shared/utils/taskIdentity';
|
||||||
|
import {
|
||||||
|
inferTeamProviderIdFromModel,
|
||||||
|
normalizeOptionalTeamProviderId,
|
||||||
|
} from '@shared/utils/teamProvider';
|
||||||
|
|
||||||
import { TeamConfigReader } from '../../TeamConfigReader';
|
import { TeamConfigReader } from '../../TeamConfigReader';
|
||||||
import { TeamMembersMetaStore } from '../../TeamMembersMetaStore';
|
import { TeamMembersMetaStore } from '../../TeamMembersMetaStore';
|
||||||
|
|
@ -14,6 +18,7 @@ import type {
|
||||||
BoardTaskLogParticipant,
|
BoardTaskLogParticipant,
|
||||||
BoardTaskLogSegment,
|
BoardTaskLogSegment,
|
||||||
BoardTaskLogStreamResponse,
|
BoardTaskLogStreamResponse,
|
||||||
|
TeamProviderId,
|
||||||
TeamTask,
|
TeamTask,
|
||||||
} from '@shared/types';
|
} 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 {
|
export class CodexNativeTaskLogStreamSource {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly taskReader: TeamTaskReader = new TeamTaskReader(),
|
private readonly taskReader: TeamTaskReader = new TeamTaskReader(),
|
||||||
|
|
@ -171,9 +201,19 @@ export class CodexNativeTaskLogStreamSource {
|
||||||
this.membersMetaStore.getMembers(teamName).catch(() => []),
|
this.membersMetaStore.getMembers(teamName).catch(() => []),
|
||||||
this.readConfigForObservation(teamName).catch(() => null),
|
this.readConfigForObservation(teamName).catch(() => null),
|
||||||
]);
|
]);
|
||||||
const member = [...metaMembers, ...(config?.members ?? [])].find(
|
const configMember = (config?.members ?? []).find(
|
||||||
(candidate) => normalizeMemberName(candidate.name) === normalizedOwner
|
(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 {
|
import {
|
||||||
type ChildProcess,
|
type ChildProcess,
|
||||||
exec,
|
|
||||||
execFile,
|
execFile,
|
||||||
type ExecFileOptions,
|
type ExecFileOptions,
|
||||||
type ExecOptions,
|
|
||||||
spawn,
|
spawn,
|
||||||
type SpawnOptions,
|
type SpawnOptions,
|
||||||
spawnSync,
|
spawnSync,
|
||||||
|
|
@ -80,65 +78,14 @@ function execFileAsync(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Promise wrapper for exec. Used exclusively as a Windows shell fallback
|
* cmd.exe fallback implemented through execFile so Node does not invoke an
|
||||||
* when execFile fails with EINVAL on non-ASCII binary paths. The command
|
* additional shell around the guarded command string.
|
||||||
* string is built from a known binary path + args, NOT from user input.
|
|
||||||
*/
|
*/
|
||||||
function execShellAsync(
|
function execShellAsync(
|
||||||
cmd: string,
|
cmd: string,
|
||||||
options: ExecOptions = {}
|
options: ExecFileOptions = {}
|
||||||
): Promise<{ stdout: string; stderr: string }> {
|
): Promise<{ stdout: string; stderr: string }> {
|
||||||
return new Promise((resolve, reject) => {
|
return execFileAsync(getWindowsCmdPath(), ['/d', '/s', '/c', cmd], options);
|
||||||
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?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupTimedCliProcess(
|
function cleanupTimedCliProcess(
|
||||||
|
|
@ -300,6 +247,43 @@ function quoteArg(arg: string): string {
|
||||||
return quoteWindowsCmdArg(arg);
|
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. */
|
/** Env vars injected into every spawned Claude CLI process. */
|
||||||
const CLI_ENV_DEFAULTS: Record<string, string> = {
|
const CLI_ENV_DEFAULTS: Record<string, string> = {
|
||||||
CLAUDE_HOOK_JUDGE_MODE: 'true',
|
CLAUDE_HOOK_JUDGE_MODE: 'true',
|
||||||
|
|
@ -408,8 +392,8 @@ export async function execCli(
|
||||||
}
|
}
|
||||||
|
|
||||||
// shell fallback (Windows only; others shouldn't reach here)
|
// shell fallback (Windows only; others shouldn't reach here)
|
||||||
const cmd = [target, ...args].map(quoteArg).join(' ');
|
const cmd = buildWindowsShellFallbackCommand([target, ...args]);
|
||||||
const shellResult = await execShellAsync(cmd, opts as unknown as ExecOptions);
|
const shellResult = await execShellAsync(cmd, opts);
|
||||||
return { stdout: String(shellResult.stdout), stderr: String(shellResult.stderr) };
|
return { stdout: String(shellResult.stdout), stderr: String(shellResult.stderr) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -435,9 +419,8 @@ export function spawnCli(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === 'win32' && needsShell(binaryPath)) {
|
if (process.platform === 'win32' && needsShell(binaryPath)) {
|
||||||
const cmd = [binaryPath, ...args].map(quoteArg).join(' ');
|
const cmd = buildWindowsShellFallbackCommand([binaryPath, ...args]);
|
||||||
// eslint-disable-next-line sonarjs/os-command -- cmd from known binaryPath+args, not user input (Windows EINVAL fallback)
|
return trackCliProcess(spawnWindowsShellFallback(cmd, opts));
|
||||||
return trackCliProcess(spawn(cmd, { ...opts, shell: true }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -446,9 +429,8 @@ export function spawnCli(
|
||||||
const code =
|
const code =
|
||||||
err && typeof err === 'object' && 'code' in err ? (err as { code?: string }).code : undefined;
|
err && typeof err === 'object' && 'code' in err ? (err as { code?: string }).code : undefined;
|
||||||
if (process.platform === 'win32' && code === 'EINVAL') {
|
if (process.platform === 'win32' && code === 'EINVAL') {
|
||||||
const cmd = [binaryPath, ...args].map(quoteArg).join(' ');
|
const cmd = buildWindowsShellFallbackCommand([binaryPath, ...args]);
|
||||||
// eslint-disable-next-line sonarjs/os-command -- cmd from known binaryPath+args, not user input (Windows EINVAL fallback)
|
return trackCliProcess(spawnWindowsShellFallback(cmd, opts));
|
||||||
return trackCliProcess(spawn(cmd, { ...opts, shell: true }));
|
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,10 @@ import {
|
||||||
shouldShowProviderStatusSkeleton,
|
shouldShowProviderStatusSkeleton,
|
||||||
} from '@renderer/components/runtime/providerConnectionUi';
|
} from '@renderer/components/runtime/providerConnectionUi';
|
||||||
import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges';
|
import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges';
|
||||||
import { getProviderRuntimeBackendSummary } from '@renderer/components/runtime/ProviderRuntimeBackendSelector';
|
import {
|
||||||
|
buildProviderRuntimeBackendSummaryText,
|
||||||
|
getProviderRuntimeBackendSummary,
|
||||||
|
} from '@renderer/components/runtime/ProviderRuntimeBackendSelector';
|
||||||
import {
|
import {
|
||||||
getProviderTerminalCommand,
|
getProviderTerminalCommand,
|
||||||
getProviderTerminalLogoutCommand,
|
getProviderTerminalLogoutCommand,
|
||||||
|
|
@ -827,6 +830,11 @@ const InstalledBanner = ({
|
||||||
}: InstalledBannerProps): React.JSX.Element => {
|
}: InstalledBannerProps): React.JSX.Element => {
|
||||||
const { t } = useAppTranslation('dashboard');
|
const { t } = useAppTranslation('dashboard');
|
||||||
const { t: settingsT } = useAppTranslation('settings');
|
const { t: settingsT } = useAppTranslation('settings');
|
||||||
|
const { t: commonT } = useAppTranslation('common');
|
||||||
|
const runtimeBackendSummaryText = useMemo(
|
||||||
|
() => buildProviderRuntimeBackendSummaryText(commonT),
|
||||||
|
[commonT]
|
||||||
|
);
|
||||||
const openExtensionsTab = useStore((s) => s.openExtensionsTab);
|
const openExtensionsTab = useStore((s) => s.openExtensionsTab);
|
||||||
const styles = VARIANT_STYLES[variant];
|
const styles = VARIANT_STYLES[variant];
|
||||||
const visibleProviders = useMemo(
|
const visibleProviders = useMemo(
|
||||||
|
|
@ -954,7 +962,7 @@ const InstalledBanner = ({
|
||||||
const actionDisabled = isBusy || !cliStatus.binaryPath;
|
const actionDisabled = isBusy || !cliStatus.binaryPath;
|
||||||
const runtimeSummary = isConnectionManagedRuntimeProvider(provider)
|
const runtimeSummary = isConnectionManagedRuntimeProvider(provider)
|
||||||
? getProviderCurrentRuntimeSummary(provider, settingsT)
|
? getProviderCurrentRuntimeSummary(provider, settingsT)
|
||||||
: getProviderRuntimeBackendSummary(provider);
|
: getProviderRuntimeBackendSummary(provider, runtimeBackendSummaryText);
|
||||||
const connectionModeSummary = getProviderConnectionModeSummary(provider, settingsT);
|
const connectionModeSummary = getProviderConnectionModeSummary(provider, settingsT);
|
||||||
const credentialSummary = getProviderCredentialSummary(provider, settingsT);
|
const credentialSummary = getProviderCredentialSummary(provider, settingsT);
|
||||||
const dashboardRateLimits = getDashboardRateLimitsForProvider(provider);
|
const dashboardRateLimits = getDashboardRateLimitsForProvider(provider);
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,9 @@ export const ApiKeyCard = ({ apiKey, onEdit }: ApiKeyCardProps): React.JSX.Eleme
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{copied ? 'Copied!' : 'Copy env var name'}</TooltipContent>
|
<TooltipContent>
|
||||||
|
{copied ? t('apiKeys.actions.copied') : t('apiKeys.actions.copyEnvVarName')}
|
||||||
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -135,7 +137,9 @@ export const ApiKeyCard = ({ apiKey, onEdit }: ApiKeyCardProps): React.JSX.Eleme
|
||||||
<Trash2 className="size-3.5" />
|
<Trash2 className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{confirmDelete ? 'Click again to confirm' : 'Delete'}</TooltipContent>
|
<TooltipContent>
|
||||||
|
{confirmDelete ? t('apiKeys.actions.confirmDelete') : t('apiKeys.actions.delete')}
|
||||||
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,52 @@ interface Props {
|
||||||
onSelect: (providerId: CliProviderStatus['providerId'], backendId: string) => void;
|
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(
|
export function getProviderRuntimeBackendStateLabel(
|
||||||
option: NonNullable<CliProviderStatus['availableBackends']>[number]
|
option: NonNullable<CliProviderStatus['availableBackends']>[number]
|
||||||
): string | null {
|
): string | null {
|
||||||
|
|
@ -78,7 +124,47 @@ export function getOptionDisplayLabel(
|
||||||
return 'Auto';
|
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 ?? [];
|
const options = provider.availableBackends ?? [];
|
||||||
if (options.length === 0) {
|
if (options.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -87,9 +173,11 @@ export function getProviderRuntimeBackendSummary(provider: CliProviderStatus): s
|
||||||
const selectedBackendId = provider.selectedBackendId ?? options[0]?.id ?? '';
|
const selectedBackendId = provider.selectedBackendId ?? options[0]?.id ?? '';
|
||||||
const selectedOption = options.find((option) => option.id === selectedBackendId) ?? options[0];
|
const selectedOption = options.find((option) => option.id === selectedBackendId) ?? options[0];
|
||||||
const resolvedOption = options.find((option) => option.id === provider.resolvedBackendId) ?? null;
|
const resolvedOption = options.find((option) => option.id === provider.resolvedBackendId) ?? null;
|
||||||
const parts = [getOptionDisplayLabel(provider, selectedOption, resolvedOption)];
|
const parts = [getOptionSummaryDisplayLabel(provider, selectedOption, resolvedOption, text)];
|
||||||
const audienceLabel = getProviderRuntimeBackendAudienceLabel(selectedOption);
|
const audienceLabel = getProviderRuntimeBackendAudienceLabel(selectedOption)
|
||||||
const stateLabel = getProviderRuntimeBackendStateLabel(selectedOption);
|
? text.audienceInternal
|
||||||
|
: null;
|
||||||
|
const stateLabel = getProviderRuntimeBackendStateSummaryLabel(selectedOption, text);
|
||||||
|
|
||||||
if (audienceLabel) {
|
if (audienceLabel) {
|
||||||
parts.push(audienceLabel.toLowerCase());
|
parts.push(audienceLabel.toLowerCase());
|
||||||
|
|
@ -107,6 +195,7 @@ export const ProviderRuntimeBackendSelector = ({
|
||||||
onSelect,
|
onSelect,
|
||||||
}: Props): React.JSX.Element | null => {
|
}: Props): React.JSX.Element | null => {
|
||||||
const { t } = useAppTranslation('common');
|
const { t } = useAppTranslation('common');
|
||||||
|
const summaryText = buildProviderRuntimeBackendSummaryText(t);
|
||||||
const options = getVisibleProviderRuntimeBackendOptions(provider);
|
const options = getVisibleProviderRuntimeBackendOptions(provider);
|
||||||
if (options.length === 0) {
|
if (options.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -150,9 +239,9 @@ export const ProviderRuntimeBackendSelector = ({
|
||||||
): string => {
|
): string => {
|
||||||
if (option.id === 'auto') {
|
if (option.id === 'auto') {
|
||||||
if (resolvedOption?.label) {
|
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);
|
return getOptionDisplayLabel(provider, option, resolvedOption);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ import {
|
||||||
isConnectionManagedRuntimeProvider,
|
isConnectionManagedRuntimeProvider,
|
||||||
} from './providerConnectionUi';
|
} from './providerConnectionUi';
|
||||||
import {
|
import {
|
||||||
|
buildProviderRuntimeBackendSummaryText,
|
||||||
getProviderRuntimeBackendSummary,
|
getProviderRuntimeBackendSummary,
|
||||||
getVisibleProviderRuntimeBackendOptions,
|
getVisibleProviderRuntimeBackendOptions,
|
||||||
ProviderRuntimeBackendSelector,
|
ProviderRuntimeBackendSelector,
|
||||||
|
|
@ -845,6 +846,11 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
onRequestLogin,
|
onRequestLogin,
|
||||||
}: Props): React.JSX.Element => {
|
}: Props): React.JSX.Element => {
|
||||||
const { t } = useAppTranslation('settings');
|
const { t } = useAppTranslation('settings');
|
||||||
|
const { t: commonT } = useAppTranslation('common');
|
||||||
|
const runtimeBackendSummaryText = useMemo(
|
||||||
|
() => buildProviderRuntimeBackendSummaryText(commonT),
|
||||||
|
[commonT]
|
||||||
|
);
|
||||||
const [selectedProviderId, setSelectedProviderId] = useState<CliProviderId>(initialProviderId);
|
const [selectedProviderId, setSelectedProviderId] = useState<CliProviderId>(initialProviderId);
|
||||||
const [activeApiKeyFormProviderId, setActiveApiKeyFormProviderId] =
|
const [activeApiKeyFormProviderId, setActiveApiKeyFormProviderId] =
|
||||||
useState<ApiKeyProviderId | null>(null);
|
useState<ApiKeyProviderId | null>(null);
|
||||||
|
|
@ -1107,7 +1113,7 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
? providerStatusLoading[selectedProvider.providerId] === true
|
? providerStatusLoading[selectedProvider.providerId] === true
|
||||||
: false;
|
: false;
|
||||||
const runtimeSummary = selectedProvider
|
const runtimeSummary = selectedProvider
|
||||||
? getProviderRuntimeBackendSummary(selectedProvider)
|
? getProviderRuntimeBackendSummary(selectedProvider, runtimeBackendSummaryText)
|
||||||
: null;
|
: null;
|
||||||
const codexConnection =
|
const codexConnection =
|
||||||
selectedProvider?.providerId === 'codex' ? (selectedProvider.connection?.codex ?? null) : null;
|
selectedProvider?.providerId === 'codex' ? (selectedProvider.connection?.codex ?? null) : null;
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,10 @@ import {
|
||||||
shouldShowProviderStatusSkeleton,
|
shouldShowProviderStatusSkeleton,
|
||||||
} from '@renderer/components/runtime/providerConnectionUi';
|
} from '@renderer/components/runtime/providerConnectionUi';
|
||||||
import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges';
|
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 { ProviderRuntimeSettingsDialog } from '@renderer/components/runtime/ProviderRuntimeSettingsDialog';
|
||||||
import {
|
import {
|
||||||
getProviderTerminalCommand,
|
getProviderTerminalCommand,
|
||||||
|
|
@ -117,6 +120,11 @@ function getProviderLabel(providerId: CliProviderId): string {
|
||||||
|
|
||||||
export const CliStatusSection = (): React.JSX.Element | null => {
|
export const CliStatusSection = (): React.JSX.Element | null => {
|
||||||
const { t } = useAppTranslation('settings');
|
const { t } = useAppTranslation('settings');
|
||||||
|
const { t: commonT } = useAppTranslation('common');
|
||||||
|
const runtimeBackendSummaryText = useMemo(
|
||||||
|
() => buildProviderRuntimeBackendSummaryText(commonT),
|
||||||
|
[commonT]
|
||||||
|
);
|
||||||
const isElectron = useMemo(() => isElectronMode(), []);
|
const isElectron = useMemo(() => isElectronMode(), []);
|
||||||
const appConfig = useStore((s) => s.appConfig);
|
const appConfig = useStore((s) => s.appConfig);
|
||||||
const selectedProjectId = useStore((s) => s.selectedProjectId);
|
const selectedProjectId = useStore((s) => s.selectedProjectId);
|
||||||
|
|
@ -481,7 +489,7 @@ export const CliStatusSection = (): React.JSX.Element | null => {
|
||||||
isCodexSnapshotPending(provider, codexSnapshotPending);
|
isCodexSnapshotPending(provider, codexSnapshotPending);
|
||||||
const runtimeSummary = isConnectionManagedRuntimeProvider(provider)
|
const runtimeSummary = isConnectionManagedRuntimeProvider(provider)
|
||||||
? getProviderCurrentRuntimeSummary(provider, t)
|
? getProviderCurrentRuntimeSummary(provider, t)
|
||||||
: getProviderRuntimeBackendSummary(provider);
|
: getProviderRuntimeBackendSummary(provider, runtimeBackendSummaryText);
|
||||||
const sourceProvider =
|
const sourceProvider =
|
||||||
loadingCliProviderMap.get(provider.providerId) ?? null;
|
loadingCliProviderMap.get(provider.providerId) ?? null;
|
||||||
const maskNegativeBootstrapState = shouldMaskCodexNegativeBootstrapState(
|
const maskNegativeBootstrapState = shouldMaskCodexNegativeBootstrapState(
|
||||||
|
|
|
||||||
|
|
@ -141,9 +141,14 @@ const TeamLogsSourceSelector = ({
|
||||||
getMemberLabel={(member) =>
|
getMemberLabel={(member) =>
|
||||||
isLeadMember(member)
|
isLeadMember(member)
|
||||||
? t('claudeLogs.sourceSelect.leadLabel')
|
? 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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1352,14 +1352,16 @@ const LeadLoadBridge = memo(function LeadLoadBridge({
|
||||||
{t('detail.context.title')}
|
{t('detail.context.title')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[10px] text-[var(--color-text-muted)]">
|
<p className="text-[10px] text-[var(--color-text-muted)]">
|
||||||
{leadSessionLoading ? 'Loading…' : 'No session loaded'}
|
{leadSessionLoading
|
||||||
|
? t('detail.context.loading')
|
||||||
|
: t('detail.context.noSessionLoaded')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="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)]"
|
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)}
|
onClick={() => setContextPanelVisible(false)}
|
||||||
aria-label={`Close ${teamName} context panel`}
|
aria-label={t('detail.context.closePanel', { team: teamName })}
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1367,8 +1369,8 @@ const LeadLoadBridge = memo(function LeadLoadBridge({
|
||||||
<div className="flex flex-1 items-center justify-center p-4">
|
<div className="flex flex-1 items-center justify-center p-4">
|
||||||
<p className="text-xs text-[var(--color-text-muted)]">
|
<p className="text-xs text-[var(--color-text-muted)]">
|
||||||
{leadSessionLoading
|
{leadSessionLoading
|
||||||
? 'Loading context…'
|
? t('detail.context.loadingContext')
|
||||||
: 'Open the team lead session to view context.'}
|
: t('detail.context.openLeadSession')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1401,7 +1403,7 @@ const LeadLoadBridge = memo(function LeadLoadBridge({
|
||||||
leadSessionLoaded
|
leadSessionLoaded
|
||||||
? `Session: ${leadSessionId}`
|
? `Session: ${leadSessionId}`
|
||||||
: leadSessionLoading
|
: leadSessionLoading
|
||||||
? 'Loading context…'
|
? t('detail.context.loadingContext')
|
||||||
: leadSessionId
|
: leadSessionId
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,11 @@ export const ActiveTasksBlock = memo(function ActiveTasksBlock({
|
||||||
type="button"
|
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)]"
|
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)}
|
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
|
<ChevronRight
|
||||||
size={10}
|
size={10}
|
||||||
|
|
@ -118,7 +122,10 @@ export const ActiveTasksBlock = memo(function ActiveTasksBlock({
|
||||||
);
|
);
|
||||||
const dotPing = kind === 'reviewing' ? 'bg-amber-400' : 'bg-emerald-400';
|
const dotPing = kind === 'reviewing' ? 'bg-amber-400' : 'bg-emerald-400';
|
||||||
const dotSolid = kind === 'reviewing' ? 'bg-amber-500' : 'bg-emerald-500';
|
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 (
|
return (
|
||||||
<article
|
<article
|
||||||
|
|
|
||||||
|
|
@ -1377,12 +1377,14 @@ export const ActivityItem = memo(
|
||||||
<AlertTriangle size={10} />
|
<AlertTriangle size={10} />
|
||||||
{t('activity.badges.rateLimited')}
|
{t('activity.badges.rateLimited')}
|
||||||
</span>
|
</span>
|
||||||
) : isApiError ? (
|
) : 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">
|
<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} />
|
<AlertTriangle size={10} />
|
||||||
{message.messageKind === 'agent_error' ? 'Agent Error' : 'API Error'}
|
{message.messageKind === 'agent_error'
|
||||||
</span>
|
? t('activity.badges.agentError')
|
||||||
) : null;
|
: t('activity.badges.apiError')}
|
||||||
|
</span>
|
||||||
|
) : null;
|
||||||
|
|
||||||
const recipientBadge =
|
const recipientBadge =
|
||||||
commentTaskRef && commentTaskDisplayId ? (
|
commentTaskRef && commentTaskDisplayId ? (
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,11 @@ export const CodexReconnectPrompt = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LogIn className="size-3" />
|
<LogIn className="size-3" />
|
||||||
{reconnectBusy ? 'Generating...' : authUrl ? 'Open login' : 'Generate link'}
|
{reconnectBusy
|
||||||
|
? t('codexReconnect.generating')
|
||||||
|
: authUrl
|
||||||
|
? t('codexReconnect.openLogin')
|
||||||
|
: t('codexReconnect.generateLink')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,9 @@ interface OptionalSettingsSectionProps {
|
||||||
const SUMMARY_PREFIXES_TO_STRIP = ['Provider:', 'Model:', 'Effort:', 'Worktree:'];
|
const SUMMARY_PREFIXES_TO_STRIP = ['Provider:', 'Model:', 'Effort:', 'Worktree:'];
|
||||||
|
|
||||||
const MODEL_LABEL_OVERRIDES: Array<[RegExp, string]> = [
|
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]?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]?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]?6/i, 'Sonnet 4.6'],
|
||||||
[/claude[-\s]?sonnet[-\s]?4[-\s]?5/i, 'Sonnet 4.5'],
|
[/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 { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { getProvisioningFailureHint } from './ProvisioningProviderStatusList';
|
import { getProvisioningFailureHint } from './ProvisioningProviderStatusList';
|
||||||
|
|
||||||
describe('getProvisioningFailureHint', () => {
|
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', () => {
|
it('returns the OpenCode Windows permissions hint for OpenCode access-denied details', () => {
|
||||||
expect(
|
expect(
|
||||||
getProvisioningFailureHint(null, [
|
getProvisioningFailureHint(null, [
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ import { formatProviderBackendLabel } from '@renderer/utils/providerBackendIdent
|
||||||
import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog';
|
import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog';
|
||||||
import {
|
import {
|
||||||
isOpenCodeWindowsAccessDeniedDiagnostic,
|
isOpenCodeWindowsAccessDeniedDiagnostic,
|
||||||
|
isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic,
|
||||||
OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE,
|
OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE,
|
||||||
|
OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE,
|
||||||
} from '@shared/utils/openCodeWindowsAccessDenied';
|
} from '@shared/utils/openCodeWindowsAccessDenied';
|
||||||
import { AlertTriangle, Check, CheckCircle2, Copy, Loader2, SlidersHorizontal } from 'lucide-react';
|
import { AlertTriangle, Check, CheckCircle2, Copy, Loader2, SlidersHorizontal } from 'lucide-react';
|
||||||
|
|
||||||
|
|
@ -1042,14 +1044,31 @@ export function getProvisioningFailureHint(
|
||||||
const hasOpenCodeAccessDeniedDetail = failedOpenCodeChecks.some((check) =>
|
const hasOpenCodeAccessDeniedDetail = failedOpenCodeChecks.some((check) =>
|
||||||
check.details.some(isOpenCodeWindowsAccessDeniedDiagnostic)
|
check.details.some(isOpenCodeWindowsAccessDeniedDiagnostic)
|
||||||
);
|
);
|
||||||
|
const hasOpenCodeNodeModulesSymlinkPermissionDetail = failedOpenCodeChecks.some((check) =>
|
||||||
|
check.details.some(isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic)
|
||||||
|
);
|
||||||
const hasOpenCodeBridgeNoOutputDetail = failedOpenCodeChecks.some((check) =>
|
const hasOpenCodeBridgeNoOutputDetail = failedOpenCodeChecks.some((check) =>
|
||||||
check.details.some(isOpenCodeBridgeNoOutputDiagnostic)
|
check.details.some(isOpenCodeBridgeNoOutputDiagnostic)
|
||||||
);
|
);
|
||||||
const normalizedMessage = message?.trim() ?? '';
|
const normalizedMessage = message?.trim() ?? '';
|
||||||
|
const hasOpenCodeNodeModulesSymlinkPermissionMessage =
|
||||||
|
failedOpenCodeChecks.length > 0 &&
|
||||||
|
(normalizedMessage === OPENCODE_WINDOWS_NODE_MODULES_SYMLINK_PERMISSION_MESSAGE ||
|
||||||
|
(!hasFailedNonOpenCodeCheck &&
|
||||||
|
isOpenCodeWindowsNodeModulesSymlinkPermissionDiagnostic(normalizedMessage)));
|
||||||
const hasOpenCodeAccessDeniedMessage =
|
const hasOpenCodeAccessDeniedMessage =
|
||||||
failedOpenCodeChecks.length > 0 &&
|
failedOpenCodeChecks.length > 0 &&
|
||||||
(normalizedMessage === OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE ||
|
(normalizedMessage === OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE ||
|
||||||
(!hasFailedNonOpenCodeCheck && isOpenCodeWindowsAccessDeniedDiagnostic(normalizedMessage)));
|
(!hasFailedNonOpenCodeCheck && isOpenCodeWindowsAccessDeniedDiagnostic(normalizedMessage)));
|
||||||
|
if (
|
||||||
|
hasOpenCodeNodeModulesSymlinkPermissionDetail ||
|
||||||
|
hasOpenCodeNodeModulesSymlinkPermissionMessage
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
t?.('provisioning.providerStatus.failureHints.openCodeNodeModulesSymlinkPermission') ??
|
||||||
|
'Run Agent Teams AI as Administrator, then retry launch.'
|
||||||
|
);
|
||||||
|
}
|
||||||
if (hasOpenCodeAccessDeniedDetail || hasOpenCodeAccessDeniedMessage) {
|
if (hasOpenCodeAccessDeniedDetail || hasOpenCodeAccessDeniedMessage) {
|
||||||
return (
|
return (
|
||||||
t?.('provisioning.providerStatus.failureHints.openCodeAccessDenied') ??
|
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 { useAppTranslation } from '@features/localization/renderer';
|
||||||
import { api } from '@renderer/api';
|
import { api } from '@renderer/api';
|
||||||
|
|
@ -138,6 +138,73 @@ interface TaskDetailDialogProps {
|
||||||
headerExtra?: React.ReactNode;
|
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 = ({
|
export const TaskDetailDialog = ({
|
||||||
open,
|
open,
|
||||||
loading = false,
|
loading = false,
|
||||||
|
|
@ -633,29 +700,6 @@ export const TaskDetailDialog = ({
|
||||||
: undefined
|
: undefined
|
||||||
: 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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||||
|
|
@ -1493,28 +1537,13 @@ export const TaskDetailDialog = ({
|
||||||
contentClassName="pl-2.5"
|
contentClassName="pl-2.5"
|
||||||
headerClassName="-mx-6 w-[calc(100%+3rem)]"
|
headerClassName="-mx-6 w-[calc(100%+3rem)]"
|
||||||
headerContentClassName="pl-6"
|
headerContentClassName="pl-6"
|
||||||
headerExtra={
|
headerExtra={<TaskImplementationDurationBadge task={currentTask} />}
|
||||||
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
|
|
||||||
}
|
|
||||||
defaultOpen={false}
|
defaultOpen={false}
|
||||||
>
|
>
|
||||||
<WorkflowTimeline
|
<WorkflowTimelineWithDuration
|
||||||
|
task={currentTask}
|
||||||
events={currentTask.historyEvents}
|
events={currentTask.historyEvents}
|
||||||
memberColorMap={colorMap}
|
memberColorMap={colorMap}
|
||||||
implementationDurationTask={currentTask}
|
|
||||||
nowMs={taskDurationNowMs}
|
|
||||||
/>
|
/>
|
||||||
</CollapsibleTeamSection>
|
</CollapsibleTeamSection>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useAppTranslation } from '@features/localization/renderer';
|
||||||
import { Button } from '@renderer/components/ui/button';
|
import { Button } from '@renderer/components/ui/button';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
||||||
import { cn } from '@renderer/lib/utils';
|
import { cn } from '@renderer/lib/utils';
|
||||||
|
|
@ -24,6 +25,7 @@ export const MemberLaunchDiagnosticsButton = ({
|
||||||
size = label ? 'sm' : 'icon',
|
size = label ? 'sm' : 'icon',
|
||||||
attention = false,
|
attention = false,
|
||||||
}: MemberLaunchDiagnosticsButtonProps): React.JSX.Element => {
|
}: MemberLaunchDiagnosticsButtonProps): React.JSX.Element => {
|
||||||
|
const { t } = useAppTranslation('team');
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const copyDiagnostics = async (event: React.MouseEvent<HTMLButtonElement>): Promise<void> => {
|
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 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 (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useAppTranslation } from '@features/localization/renderer';
|
||||||
import { RoleSelect } from '@renderer/components/team/RoleSelect';
|
import { RoleSelect } from '@renderer/components/team/RoleSelect';
|
||||||
import { Button } from '@renderer/components/ui/button';
|
import { Button } from '@renderer/components/ui/button';
|
||||||
import { CUSTOM_ROLE, FORBIDDEN_ROLES, NO_ROLE, PRESET_ROLES } from '@renderer/constants/teamRoles';
|
import { CUSTOM_ROLE, FORBIDDEN_ROLES, NO_ROLE, PRESET_ROLES } from '@renderer/constants/teamRoles';
|
||||||
|
|
@ -18,6 +19,7 @@ export const MemberRoleEditor = ({
|
||||||
onCancel,
|
onCancel,
|
||||||
saving,
|
saving,
|
||||||
}: MemberRoleEditorProps): React.JSX.Element => {
|
}: MemberRoleEditorProps): React.JSX.Element => {
|
||||||
|
const { t } = useAppTranslation('team');
|
||||||
const isPreset = currentRole && (PRESET_ROLES as readonly string[]).includes(currentRole);
|
const isPreset = currentRole && (PRESET_ROLES as readonly string[]).includes(currentRole);
|
||||||
const [selectValue, setSelectValue] = useState<string>(
|
const [selectValue, setSelectValue] = useState<string>(
|
||||||
!currentRole ? NO_ROLE : isPreset ? currentRole : CUSTOM_ROLE
|
!currentRole ? NO_ROLE : isPreset ? currentRole : CUSTOM_ROLE
|
||||||
|
|
@ -44,11 +46,11 @@ export const MemberRoleEditor = ({
|
||||||
}
|
}
|
||||||
const trimmed = customInput.trim();
|
const trimmed = customInput.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
setError('Role cannot be empty');
|
setError(t('roleSelect.emptyCustomRole'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (FORBIDDEN_ROLES.has(trimmed.toLowerCase())) {
|
if (FORBIDDEN_ROLES.has(trimmed.toLowerCase())) {
|
||||||
setError('This role is reserved');
|
setError(t('roleSelect.reservedRole'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void onSave(trimmed);
|
void onSave(trimmed);
|
||||||
|
|
@ -68,7 +70,7 @@ export const MemberRoleEditor = ({
|
||||||
inputClassName="h-7 w-28 text-xs"
|
inputClassName="h-7 w-28 text-xs"
|
||||||
customRoleError={error}
|
customRoleError={error}
|
||||||
onCustomRoleValidate={(val) => {
|
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;
|
return null;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,19 @@ export function getMemberNameFromLogSourceKey(sourceKey: TeamLogSourceKey): stri
|
||||||
return sourceKey.slice('member:'.length);
|
return sourceKey.slice('member:'.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatMemberLogSourceLabel(member: ResolvedTeamMember): string {
|
export function formatMemberLogSourceLabel(member: ResolvedTeamMember, removedLabel = 'removed'): string {
|
||||||
return member.removedAt ? `${member.name} (removed)` : member.name;
|
return member.removedAt ? `${member.name} (${removedLabel})` : member.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatMemberLogSourceDescription(member: ResolvedTeamMember): string | null {
|
export function formatMemberLogSourceDescription(
|
||||||
if (isLeadMember(member)) return 'Team Lead';
|
member: ResolvedTeamMember,
|
||||||
if (member.removedAt) return 'Removed';
|
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;
|
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