- {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 (
-
-
- #{entry.task.id}
-
-
- {entry.task.subject}
-
-
- {t(`agentTeams.task.${entry.state}` as TranslationKey)}
-
-
- {span}
-
-
+
+
+ {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 (
+
+
+ #{entry.task.id}
+
+
+ {entry.task.subject}
+
+
+ {stalled
+ ? memberStateLabel(stalled, member, t)
+ : t(`agentTeams.task.${entry.state}` as TranslationKey)}
+
+
+ {aside}
+
+
+ )
+ })}
+
+
)
})}
-
+
) : (
{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...',