diff --git a/desktop/src/components/agentTeams/AgentTeamsCanvas.test.tsx b/desktop/src/components/agentTeams/AgentTeamsCanvas.test.tsx index feb9e566..d740bcfd 100644 --- a/desktop/src/components/agentTeams/AgentTeamsCanvas.test.tsx +++ b/desktop/src/components/agentTeams/AgentTeamsCanvas.test.tsx @@ -402,6 +402,32 @@ describe('AgentTeamsCanvas', () => { expect(lead).toBeNull() }) + it('chips what each member has done, is doing and does next, with the subject on hover', () => { + render() + const chips = (agentId: string) => Array.from( + screen.getByTestId(`agent-teams-canvas-member-${agentId}`).querySelectorAll('[data-member-task]'), + ).map(chip => ({ + id: chip.getAttribute('data-member-task'), + status: chip.getAttribute('data-task-status'), + next: chip.getAttribute('data-task-next') === 'true', + title: chip.getAttribute('title'), + })) + + expect(chips('builder@canvas-team')).toEqual([ + { id: '1', status: 'completed', next: false, title: 'Task 1' }, + { id: '2', status: 'in_progress', next: false, title: 'Task 2' }, + ]) + // Nothing started yet, but the member's next task is already known. + expect(chips('reviewer@canvas-team')).toEqual([ + { id: '3', status: 'pending', next: true, title: 'Task 3' }, + ]) + // The connector line runs behind the chip row; a see-through chip lets + // it strike through the label. + const next = screen.getByTestId('agent-teams-canvas-member-reviewer@canvas-team').querySelector('[data-task-next]')! + expect(next.style.backgroundColor).not.toBe('transparent') + expect(next.style.backgroundColor).not.toBe('') + }) + it('labels stopped, retrying and failed members and keeps their failure on hover', () => { const current = snapshot('current') const recovering: TeamWorkbenchSnapshot = { @@ -436,7 +462,15 @@ describe('AgentTeamsCanvas', () => { const builder = screen.getByTestId('agent-teams-canvas-member-builder@canvas-team') expect(builder.getAttribute('data-member-state')).toBe('retrying') - expect(screen.getByText('Auto-retry 2/5').getAttribute('title')).toBe('API Error: 529 overloaded') + // It names the task it stopped on, not just that it stopped. + expect(screen.getByText('Auto-retry 2/5 · at #2').getAttribute('title')).toBe('API Error: 529 overloaded') + // The task list still says in progress; the card must not animate work + // nobody is doing. + const stalledTask = screen.getByTestId('agent-teams-canvas-task-2') + expect(stalledTask.getAttribute('data-stalled')).toBe('retrying') + expect(stalledTask.textContent).toContain('Auto-retry 2/5') + expect(stalledTask.textContent).not.toContain('In progress') + expect(stalledTask.querySelector('[data-progress="indeterminate"]')).toBeNull() const reviewer = screen.getByTestId('agent-teams-canvas-member-reviewer@canvas-team') expect(reviewer.getAttribute('data-member-state')).toBe('stopped') diff --git a/desktop/src/components/agentTeams/AgentTeamsCanvas.tsx b/desktop/src/components/agentTeams/AgentTeamsCanvas.tsx index 56409579..f8174242 100644 --- a/desktop/src/components/agentTeams/AgentTeamsCanvas.tsx +++ b/desktop/src/components/agentTeams/AgentTeamsCanvas.tsx @@ -17,9 +17,11 @@ import { parseWorkbenchMessageBody, resolveMemberModel, resolveTeamMemberIdentity, + stalledTaskOwnerState, taskOwnedByMember, type MemberWorkState, type PositionedWorkbenchTask, + type StalledTaskOwnerState, type WorkbenchTaskState, } from './agentTeamsModel' @@ -50,6 +52,8 @@ type MemberPosition = { percent: number inbox: number recentTasks: TeamWorkbenchTask[] + /** The first of the member's tasks that has not started yet. */ + nextTask?: TeamWorkbenchTask } type OwnerVisual = { @@ -138,6 +142,32 @@ function memberStateLabel(state: MemberWorkState, member: TeamMember, t: Transla return t(`agentTeams.member.${state}` as TranslationKey) } +function isStalledState(state: MemberWorkState): state is StalledTaskOwnerState { + return state === 'stopped' || state === 'error' || state === 'retrying' +} + +function stalledColors(state: StalledTaskOwnerState) { + if (state === 'error') { + return { background: 'var(--color-error-container)', foreground: 'var(--color-on-error-container)', border: 'var(--color-error)' } + } + if (state === 'retrying') { + return { background: 'var(--color-warning-container)', foreground: 'var(--color-on-warning-container)', border: 'var(--color-warning)' } + } + return { background: 'var(--color-surface-container-high)', foreground: 'var(--color-text-secondary)', border: 'var(--color-border-strong)' } +} + +function memberTaskChipStyle(task: TeamWorkbenchTask, next: boolean, accent: string) { + if (next) { + // Opaque like the other chips: the formation's connector line runs + // behind this row and would cut through the label. + return { borderStyle: 'dashed', borderColor: 'var(--color-outline)', backgroundColor: 'var(--color-background)', color: 'var(--color-text-tertiary)' } + } + if (task.status === 'completed') { + return { borderColor: 'var(--color-border)', backgroundColor: 'var(--color-success-container)', color: 'var(--color-on-success-container)' } + } + return { borderColor: accent, backgroundColor: 'var(--color-surface-container-lowest)', color: 'var(--color-text-primary)' } +} + function leadStatusLabel(snapshot: TeamWorkbenchSnapshot, t: TranslationFn): string { if (snapshot.deletedAt) return t('agentTeams.lead.archived') if (snapshot.tasks.length === 0) return t('agentTeams.lead.forming') @@ -254,6 +284,18 @@ function memberInboxCount(member: TeamMember, messages: TeamWorkbenchMessage[]): )).length } +/** + * The chips under a member card: what it has done and is doing, then the next + * task waiting for it, within the four chips the card has room for. + */ +function memberTaskChips(ownedTasks: TeamWorkbenchTask[]): Pick { + const nextTask = ownedTasks + .filter(task => task.status === 'pending') + .sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }))[0] + const started = ownedTasks.filter(task => task.status !== 'pending') + return { recentTasks: started.slice(nextTask ? -3 : -4), nextTask } +} + function workState( member: TeamMember, snapshot: TeamWorkbenchSnapshot, @@ -479,7 +521,9 @@ function MemberNode({ : waitingDependency ? t('agentTeams.member.waitingForDependency', { task: waitingDependency }) : t('agentTeams.member.waitingForTask') - : memberStateLabel(state, member, t) + : isStalledState(state) && position.currentTask + ? t('agentTeams.member.stalledOnTask', { state: memberStateLabel(state, member, t), task: position.currentTask.id }) + : memberStateLabel(state, member, t) const characterClass = state === 'working' ? 'agent-teams-character-working' : state === 'idle' || state === 'retrying' @@ -582,14 +626,22 @@ function MemberNode({ - {position.recentTasks.map(task => ( - - #{task.id} - - ))} + {[...position.recentTasks, ...(position.nextTask ? [position.nextTask] : [])].map(task => { + const next = task === position.nextTask + return ( + + #{task.id} + + ) + })} ) : null} @@ -628,6 +680,10 @@ function TaskCard({ const owner = taskOwnerVisual(task, snapshot, members, depth) const accent = owner?.accent ?? 'var(--color-brand)' const colors = taskStateColors(state, accent) + // Still in progress on the list, but its owner stopped, failed or waits to + // retry: say so instead of animating work nobody is doing. + const stalled = owner ? stalledTaskOwnerState(task, snapshot) : undefined + const stall = stalled ? stalledColors(stalled) : undefined const progress = taskProgress(task) const dependencies = task.blockedBy.map(id => `#${id}`).join(' ') const ownerLabel = owner @@ -645,7 +701,8 @@ function TaskCard({ data-state={state} data-depth={depth} data-chain-active={focused ? 'true' : 'false'} - aria-label={`${task.subject}, ${taskStateLabel(state, t)}`} + data-stalled={stalled} + aria-label={`${task.subject}, ${stalled && owner ? memberStateLabel(stalled, owner.member, t) : taskStateLabel(state, t)}`} onClick={onSelect} onMouseEnter={onHover} onMouseLeave={onHoverEnd} @@ -656,7 +713,7 @@ function TaskCard({ left: x, top: y, backgroundColor: colors.background, - borderColor: justUnlocked ? 'var(--color-success)' : colors.border, + borderColor: justUnlocked ? 'var(--color-success)' : stall?.border ?? colors.border, opacity: dimmed ? 0.34 : state === 'blocked' ? 0.72 : 1, }} > @@ -667,12 +724,16 @@ function TaskCard({ - {justUnlocked ? t('agentTeams.task.unlocked') : taskStateLabel(state, t)} + {justUnlocked + ? t('agentTeams.task.unlocked') + : stalled && owner + ? memberStateLabel(stalled, owner.member, t) + : taskStateLabel(state, t)} @@ -706,7 +767,7 @@ function TaskCard({ : 'var(--color-surface-container-high)', }} > - {progress === null ? ( + {stalled && progress === null ? null : progress === null ? ( )} @@ -779,7 +840,7 @@ export function AgentTeamsCanvas({ total: ownedTasks.length, percent: ownedTasks.length === 0 ? 0 : Math.round((completed / ownedTasks.length) * 100), inbox: memberInboxCount(member, snapshot.messages), - recentTasks: ownedTasks.filter(task => task.status !== 'pending').slice(-4), + ...memberTaskChips(ownedTasks), }) } diff --git a/desktop/src/components/agentTeams/AgentTeamsMemberInspector.test.tsx b/desktop/src/components/agentTeams/AgentTeamsMemberInspector.test.tsx index 0ed5bf7a..b0200e8d 100644 --- a/desktop/src/components/agentTeams/AgentTeamsMemberInspector.test.tsx +++ b/desktop/src/components/agentTeams/AgentTeamsMemberInspector.test.tsx @@ -125,7 +125,59 @@ describe('AgentTeamsMemberInspector', () => { expect(row.textContent).toContain('Completed') expect(row.textContent).toContain(`${expectedStart} +7:00`) expect(row.getAttribute('data-task-state')).toBe('completed') - expect(screen.getByText('1', { selector: 'dd' })).toBeTruthy() + expect(screen.getByText('1/1', { selector: 'dd' })).toBeTruthy() + }) + + it('groups what a member is doing, will do next and has done, and says where a stopped member stopped', () => { + const memberTask = (id: string, status: TeamWorkbenchTask['status'], blockedBy: string[] = []): TeamWorkbenchTask => ({ + id, subject: `Task ${id}`, description: '', owner: 'builder', status, blocks: [], blockedBy, taskListId: 'team-a', + }) + const stopped: TeamMember = { ...builder, activity: 'stopped' } + const frame = (generatedAt: string, tasks: TeamWorkbenchTask[]): TeamWorkbenchSnapshot => ({ + version: 'v1', + generatedAt, + team: { name: 'team-a', leadAgentId: 'lead@team-a', leadSessionId: 'lead-session', members: [stopped, reviewer] }, + tasks, + messages: [], + }) + const snapshots = [ + frame('2026-08-08T07:00:00.000Z', [memberTask('1', 'pending'), memberTask('2', 'pending'), memberTask('3', 'pending', ['2']), memberTask('4', 'pending')]), + // #1 started and finished between two polls: its duration is unknown, + // not zero. + frame('2026-08-08T07:05:00.000Z', [memberTask('1', 'completed'), memberTask('2', 'in_progress'), memberTask('3', 'pending', ['2']), memberTask('4', 'pending')]), + ] + render( + , + ) + + const group = (name: string) => screen.getByTestId(`agent-teams-member-task-group-${name}`) + const ids = (name: string) => Array.from(group(name).querySelectorAll('[data-task-state]')).map(row => row.getAttribute('data-testid')) + expect(screen.getAllByTestId(/^agent-teams-member-task-group-/).map(node => node.getAttribute('data-testid'))).toEqual([ + 'agent-teams-member-task-group-running', + 'agent-teams-member-task-group-upcoming', + 'agent-teams-member-task-group-completed', + ]) + expect(ids('running')).toEqual(['agent-teams-member-task-2']) + expect(ids('upcoming')).toEqual(['agent-teams-member-task-3', 'agent-teams-member-task-4']) + expect(ids('completed')).toEqual(['agent-teams-member-task-1']) + + // The task list still says in progress, but nobody is working on it. + expect(screen.getByTestId('agent-teams-member-task-2-state').textContent).toBe('Stopped') + expect(screen.getByText('Stopped · at #2')).toBeTruthy() + expect(screen.getByTestId('agent-teams-member-task-3').textContent).toContain('Depends on #2') + expect(screen.getByTestId('agent-teams-member-task-1').textContent).toContain(formatWorkbenchMessageTime('2026-08-08T07:05:00.000Z')) + expect(screen.getByTestId('agent-teams-member-task-1').textContent).not.toContain('+0:00') + expect(screen.getByText('1/4', { selector: 'dd' })).toBeTruthy() }) it('shows message direction, renders human Markdown, and narrates protocol payloads', () => { @@ -282,7 +334,8 @@ describe('AgentTeamsMemberInspector', () => { />, ) - expect(screen.getByText('Stopped')).toBeTruthy() + // Where it stopped, not just that it stopped. + expect(screen.getByText('Stopped · at #7')).toBeTruthy() const notice = screen.getByTestId('agent-teams-member-recovery') expect(notice.getAttribute('data-member-state')).toBe('stopped') expect(screen.getByTestId('agent-teams-member-recovery-hint').textContent) diff --git a/desktop/src/components/agentTeams/AgentTeamsMemberInspector.tsx b/desktop/src/components/agentTeams/AgentTeamsMemberInspector.tsx index 5c00d2b2..1a3d4e1d 100644 --- a/desktop/src/components/agentTeams/AgentTeamsMemberInspector.tsx +++ b/desktop/src/components/agentTeams/AgentTeamsMemberInspector.tsx @@ -12,8 +12,10 @@ import { parseWorkbenchMessageBody, resolveMemberModel, resolveTeamMemberIdentity, + stalledTaskOwnerState, taskOwnedByMember, type MemberWorkState, + type StalledTaskOwnerState, type WorkbenchMessageBody, type WorkbenchTaskState, } from '@/components/agentTeams/agentTeamsModel' @@ -183,7 +185,30 @@ function formatDuration(durationMs: number): string { function formatTaskSpan(entry: TaskHistoryEntry): string { if (entry.startedAt === null || entry.durationMs === null) return '—' - return `${formatWorkbenchMessageTime(new Date(entry.startedAt).toISOString())} +${formatDuration(entry.durationMs)}` + const start = formatWorkbenchMessageTime(new Date(entry.startedAt).toISOString()) + // Times come from polled frames: a task that started and ended between two + // of them has an unknown duration, not a zero one. + return entry.durationMs < 1000 ? start : `${start} +${formatDuration(entry.durationMs)}` +} + +type TaskGroup = 'running' | 'upcoming' | 'completed' + +const TASK_GROUPS: Array<{ group: TaskGroup; label: TranslationKey }> = [ + { group: 'running', label: 'agentTeams.inspector.groupRunning' }, + { group: 'upcoming', label: 'agentTeams.inspector.groupUpcoming' }, + { group: 'completed', label: 'agentTeams.inspector.groupCompleted' }, +] + +function taskGroup(state: WorkbenchTaskState): TaskGroup { + if (state === 'running') return 'running' + if (state === 'completed') return 'completed' + return 'upcoming' +} + +function stalledTone(state: StalledTaskOwnerState): Tone { + if (state === 'error') return 'danger' + if (state === 'retrying') return 'warning' + return 'neutral' } function taskTone(state: WorkbenchTaskState): Tone { @@ -193,6 +218,15 @@ function taskTone(state: WorkbenchTaskState): Tone { return 'neutral' } +function memberStateLabel(state: MemberWorkState, member: TeamMember, t: TranslationFn): string { + return state === 'retrying' + ? t('agentTeams.member.retrying', { + attempt: member.autoRetry?.attempt ?? '?', + max: member.autoRetry?.max ?? '?', + }) + : t(`agentTeams.member.${state}` as TranslationKey) +} + function memberTone(state: MemberWorkState): Tone { if (state === 'working') return 'brand' if (state === 'error') return 'danger' @@ -346,12 +380,9 @@ export function AgentTeamsMemberInspector({ ? t('agentTeams.member.waitingForDependency', { task: waitingDependency }) : workState === 'idle' ? t('agentTeams.member.waitingForTask') - : workState === 'retrying' - ? t('agentTeams.member.retrying', { - attempt: member.autoRetry?.attempt ?? '?', - max: member.autoRetry?.max ?? '?', - }) - : t(`agentTeams.member.${workState}` as TranslationKey) + : (workState === 'stopped' || workState === 'error' || workState === 'retrying') && runningTask + ? t('agentTeams.member.stalledOnTask', { state: memberStateLabel(workState, member, t), task: runningTask.id }) + : memberStateLabel(workState, member, t) // Stopped, retrying and failed members all come back on their own or through // a message; say why they paused and what brings them back. const awaitsRecovery = !isLead && ( @@ -433,7 +464,7 @@ export function AgentTeamsMemberInspector({
{t('agentTeams.inspector.completedTasks')}
-
{completedTasks}
+
{completedTasks}/{taskHistory.length}
@@ -509,40 +540,66 @@ export function AgentTeamsMemberInspector({ {taskHistory.length > 0 ? ( -
    - {taskHistory.map(entry => { - const span = formatTaskSpan(entry) +
    + {TASK_GROUPS.map(({ group, label }) => { + const entries = taskHistory.filter(entry => taskGroup(entry.state) === group) + if (entries.length === 0) return null + // Up next reads in the order the work unblocks, not by when + // (never) it started. + if (group === 'upcoming') entries.sort((left, right) => left.task.id.localeCompare(right.task.id, undefined, { numeric: true })) return ( -
  1. - - #{entry.task.id} - - - {entry.task.subject} - - - {t(`agentTeams.task.${entry.state}` as TranslationKey)} - - -
  2. +
    +

    + {t(label)} · {entries.length} +

    +
      + {entries.map(entry => { + const stalled = stalledTaskOwnerState(entry.task, snapshot) + const openDependencies = entry.state === 'blocked' + ? entry.task.blockedBy.filter(id => snapshot.tasks.find(task => task.id === id)?.status !== 'completed') + : [] + const aside = openDependencies.length > 0 + ? `${t('agentTeams.task.dependsOn')} ${openDependencies.map(id => `#${id}`).join(' ')}` + : formatTaskSpan(entry) + return ( +
    1. + + #{entry.task.id} + + + {entry.task.subject} + + + {stalled + ? memberStateLabel(stalled, member, t) + : t(`agentTeams.task.${entry.state}` as TranslationKey)} + + +
    2. + ) + })} +
    +
    ) })} -
+
) : (

{t('agentTeams.noMemberTasks')} diff --git a/desktop/src/components/agentTeams/agentTeamsModel.test.ts b/desktop/src/components/agentTeams/agentTeamsModel.test.ts index 073d9df8..17b190ce 100644 --- a/desktop/src/components/agentTeams/agentTeamsModel.test.ts +++ b/desktop/src/components/agentTeams/agentTeamsModel.test.ts @@ -15,6 +15,7 @@ import { runningTaskForMember, shortModelLabel, snapshotWithHistoricalMembers, + stalledTaskOwnerState, taskOwnedByMember, WORKBENCH_TASK_WIDTH, } from './agentTeamsModel' @@ -446,6 +447,37 @@ describe('Agent Teams workbench model', () => { }) }) +describe('Agent Teams stalled tasks', () => { + const member = (name: string, extra: Partial): TeamMember => ({ + agentId: `${name}@team-a`, name, role: name, status: 'idle', ...extra, + }) + const withMembers = (tasks: TeamWorkbenchTask[], members: TeamMember[]): TeamWorkbenchSnapshot => { + const base = snapshot(tasks) + return { ...base, team: { ...base.team, members } } + } + + it("reports the owner's state for an in-progress task its owner is no longer working on", () => { + // The task list keeps `in_progress` until the member itself updates it, + // so a stopped or failed owner would otherwise look busy on the board. + const frame = withMembers( + [task('1', 'in_progress', [], 'stopper'), task('2', 'in_progress', [], 'failer'), task('3', 'in_progress', [], 'retrier'), task('4', 'in_progress', [], 'worker')], + [ + member('stopper', { activity: 'stopped' }), + member('failer', { status: 'error', activity: 'idle', lastError: 'Credit balance is too low' }), + member('retrier', { activity: 'idle', lastError: 'overloaded', autoRetry: { attempt: 1, max: 5, nextAt: 1 } }), + member('worker', { status: 'running', activity: 'active' }), + ], + ) + expect(frame.tasks.map(item => stalledTaskOwnerState(item, frame))).toEqual(['stopped', 'error', 'retrying', undefined]) + }) + + it('never marks finished, unstarted or unowned work as stalled', () => { + const owner = member('stopper', { activity: 'stopped' }) + const frame = withMembers([task('1', 'completed', [], 'stopper'), task('2', 'pending', [], 'stopper'), task('3', 'in_progress')], [owner]) + expect(frame.tasks.map(item => stalledTaskOwnerState(item, frame))).toEqual([undefined, undefined, undefined]) + }) +}) + describe('Agent Teams member model display', () => { const worker: TeamMember = { agentId: 'builder@team-a', diff --git a/desktop/src/components/agentTeams/agentTeamsModel.ts b/desktop/src/components/agentTeams/agentTeamsModel.ts index e2f7bd89..534c1dcd 100644 --- a/desktop/src/components/agentTeams/agentTeamsModel.ts +++ b/desktop/src/components/agentTeams/agentTeamsModel.ts @@ -314,6 +314,27 @@ export function getMemberWorkState( return member.status === 'running' ? 'working' : 'idle' } +export type StalledTaskOwnerState = Extract + +/** + * Why a task still marked in progress is not moving: its owner was stopped, + * failed, or waits for an automatic retry. Only the member updates its own + * task, so the list keeps saying `in_progress`, and the board would animate + * work nobody is doing. + */ +export function stalledTaskOwnerState( + task: TeamWorkbenchTask, + snapshot: TeamWorkbenchSnapshot, +): StalledTaskOwnerState | undefined { + if (task.status !== 'in_progress' || snapshot.deletedAt) return undefined + const owner = inferTaskOwner(task, snapshot) + if (!owner) return undefined + const { member, isLead } = resolveTeamMemberIdentity(snapshot.team, owner.identity) + if (isLead || !snapshot.team.members.includes(member)) return undefined + const state = getMemberWorkState(member) + return state === 'stopped' || state === 'error' || state === 'retrying' ? state : undefined +} + export type TaskOwnerAttribution = { identity: string /** True when the name was recovered from the mailbox rather than recorded. */ diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index 86eb9a35..8b08c1f7 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -3343,6 +3343,10 @@ Row 9, all 8 cells: continuing from straight down, turning left through lower-le 'agentTeams.member.retryNow': 'Retrying automatically now', 'agentTeams.member.stoppedHint': 'Send a message to resume it from its saved conversation', 'agentTeams.member.errorHint': 'Once the problem is fixed, send a message to let it continue', + 'agentTeams.member.stalledOnTask': '{state} · at #{task}', + 'agentTeams.inspector.groupRunning': 'Now', + 'agentTeams.inspector.groupUpcoming': 'Up next', + 'agentTeams.inspector.groupCompleted': 'Done', 'agentTeams.openMember': 'Open {name} details', 'agentTeams.resizeCommunication': 'Resize communication panel', 'agentTeams.memberTranscriptLoading': 'Loading member transcript...', diff --git a/desktop/src/i18n/locales/jp.ts b/desktop/src/i18n/locales/jp.ts index de6e4439..358e5b7a 100644 --- a/desktop/src/i18n/locales/jp.ts +++ b/desktop/src/i18n/locales/jp.ts @@ -3344,6 +3344,10 @@ export const jp: Record = { 'agentTeams.member.retryNow': 'まもなく自動で再試行', 'agentTeams.member.stoppedHint': 'メッセージを送ると、保存された会話から再開します', 'agentTeams.member.errorHint': '問題を解決してからメッセージを送ると、作業を再開します', + 'agentTeams.member.stalledOnTask': '{state} · #{task} で停止', + 'agentTeams.inspector.groupRunning': '進行中', + 'agentTeams.inspector.groupUpcoming': '次のタスク', + 'agentTeams.inspector.groupCompleted': '完了', 'agentTeams.openMember': '{name} の詳細を開く', 'agentTeams.resizeCommunication': '通信パネルの幅を変更', 'agentTeams.memberTranscriptLoading': 'メンバーの transcript を読み込み中...', diff --git a/desktop/src/i18n/locales/kr.ts b/desktop/src/i18n/locales/kr.ts index b08ff1ba..74381c10 100644 --- a/desktop/src/i18n/locales/kr.ts +++ b/desktop/src/i18n/locales/kr.ts @@ -3346,6 +3346,10 @@ export const kr: Record = { 'agentTeams.member.retryNow': '곧 자동 재시도', 'agentTeams.member.stoppedHint': '메시지를 보내면 저장된 대화에서 다시 시작합니다', 'agentTeams.member.errorHint': '문제를 해결한 뒤 메시지를 보내면 작업을 이어갑니다', + 'agentTeams.member.stalledOnTask': '{state} · #{task}에서 멈춤', + 'agentTeams.inspector.groupRunning': '진행 중', + 'agentTeams.inspector.groupUpcoming': '다음 작업', + 'agentTeams.inspector.groupCompleted': '완료', 'agentTeams.openMember': '{name} 세부 정보 열기', 'agentTeams.resizeCommunication': '통신 패널 너비 조정', 'agentTeams.memberTranscriptLoading': '멤버 transcript 불러오는 중...', diff --git a/desktop/src/i18n/locales/zh-TW.ts b/desktop/src/i18n/locales/zh-TW.ts index 2ee85bbc..62bb27dd 100644 --- a/desktop/src/i18n/locales/zh-TW.ts +++ b/desktop/src/i18n/locales/zh-TW.ts @@ -3343,6 +3343,10 @@ export const zh: Record = { 'agentTeams.member.retryNow': '即將自動重試', 'agentTeams.member.stoppedHint': '發訊息即可從已儲存的對話恢復', 'agentTeams.member.errorHint': '解決問題後,發訊息即可讓它繼續', + 'agentTeams.member.stalledOnTask': '{state} · 停在 #{task}', + 'agentTeams.inspector.groupRunning': '進行中', + 'agentTeams.inspector.groupUpcoming': '接下來', + 'agentTeams.inspector.groupCompleted': '已完成', 'agentTeams.openMember': '開啟 {name} 的詳細資料', 'agentTeams.resizeCommunication': '調整通訊面板寬度', 'agentTeams.memberTranscriptLoading': '正在載入成員 transcript...', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index fe00b469..bde2b7bc 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -3342,6 +3342,10 @@ export const zh: Record = { 'agentTeams.member.retryNow': '即将自动重试', 'agentTeams.member.stoppedHint': '发消息即可从保存的对话恢复', 'agentTeams.member.errorHint': '解决问题后,发消息即可让它继续', + 'agentTeams.member.stalledOnTask': '{state} · 停在 #{task}', + 'agentTeams.inspector.groupRunning': '进行中', + 'agentTeams.inspector.groupUpcoming': '接下来', + 'agentTeams.inspector.groupCompleted': '已完成', 'agentTeams.openMember': '打开 {name} 的详情', 'agentTeams.resizeCommunication': '调整通讯面板宽度', 'agentTeams.memberTranscriptLoading': '正在加载成员 transcript...',