) : null}
+ {/* Why: the card behind it is amber now, so the pill needs its own edge
+ to stay a distinct chip instead of a flat block of tint. */}
{card.askSummary ? (
-
+
{card.askSummary}
) : null}
- {card.repoName}
+ {/* Why: the repo's own icon never truncates and never competes with
+ the worktree name; the name itself lives in the tooltip. */}
+
+
+
+
+
+
+
+ {card.repoName}
+
+
{displayTimestamp(card) > 0 ? (
{formatStartedAgo(displayTimestamp(card), now)}
@@ -146,6 +208,7 @@ export const AgentKanbanCard = memo(
(previous, next) =>
previous.onOpenTerminal === next.onOpenTerminal &&
sameCard(previous.card, next.card) &&
+ sameRepoIcon(previous.repoIcon, next.repoIcon) &&
(displayTimestamp(previous.card) <= 0 ||
formatStartedAgo(displayTimestamp(previous.card), previous.now) ===
formatStartedAgo(displayTimestamp(next.card), next.now))
diff --git a/src/renderer/src/components/dashboard/build-dashboard-snapshot.test.ts b/src/renderer/src/components/dashboard/build-dashboard-snapshot.test.ts
index fadd385afbb..6da673ecbe8 100644
--- a/src/renderer/src/components/dashboard/build-dashboard-snapshot.test.ts
+++ b/src/renderer/src/components/dashboard/build-dashboard-snapshot.test.ts
@@ -114,6 +114,73 @@ describe('buildDashboardSnapshot', () => {
expect(card.unseen).toBe(true)
})
+ it('carries the tab conversation name and drops status-only titles', () => {
+ const named = buildDashboardSnapshot(
+ baseState({
+ agentStatusByPaneKey: { [PANE_KEY]: entry({}) },
+ tabsByWorktree: { w1: [{ ...tab(), customTitle: 'Sparse-checkout parser' }] }
+ }),
+ NOW
+ )
+ expect(named.cards[0].conversationName).toBe('Sparse-checkout parser')
+
+ // The fixture tab's title is the 'agent' placeholder — not a name.
+ const unnamed = buildDashboardSnapshot(
+ baseState({ agentStatusByPaneKey: { [PANE_KEY]: entry({}) } }),
+ NOW
+ )
+ expect(unnamed.cards[0].conversationName).toBeUndefined()
+ })
+
+ it('withholds generated titles until the setting enables them', () => {
+ const tabs = { w1: [{ ...tab(), generatedTitle: 'Fix the flaky pty test' }] }
+ const off = buildDashboardSnapshot(
+ baseState({ agentStatusByPaneKey: { [PANE_KEY]: entry({}) }, tabsByWorktree: tabs }),
+ NOW
+ )
+ expect(off.cards[0].conversationName).toBeUndefined()
+
+ const on = buildDashboardSnapshot(
+ baseState({
+ agentStatusByPaneKey: { [PANE_KEY]: entry({}) },
+ tabsByWorktree: tabs,
+ settings: { tabAutoGenerateTitle: true }
+ } as unknown as Partial),
+ NOW
+ )
+ expect(on.cards[0].conversationName).toBe('Fix the flaky pty test')
+ })
+
+ it('ships one icon per card-bearing repo, and none for repos without cards', () => {
+ const snapshot = buildDashboardSnapshot(
+ baseState({
+ repos: [
+ {
+ id: 'r1',
+ path: '/r1',
+ displayName: 'Repo One',
+ badgeColor: '#000',
+ repoIcon: { type: 'lucide', name: 'Rocket' }
+ },
+ { id: 'r2', path: '/r2', displayName: 'Repo Two', badgeColor: '#000' }
+ ],
+ worktreesByRepo: { r1: [worktree()], r2: [worktree('w2', 'wt-two')] },
+ agentStatusByPaneKey: { [PANE_KEY]: entry({}) }
+ } as unknown as Partial),
+ NOW
+ )
+ // r2 has a worktree but no agent card, so its icon never ships.
+ expect(snapshot.repoIconsByRepoId).toEqual({ r1: { type: 'lucide', name: 'Rocket' } })
+ })
+
+ it('records a null icon for a card-bearing repo that has none', () => {
+ const snapshot = buildDashboardSnapshot(
+ baseState({ agentStatusByPaneKey: { [PANE_KEY]: entry({}) } }),
+ NOW
+ )
+ expect(snapshot.repoIconsByRepoId).toEqual({ r1: null })
+ })
+
it('nulls ptyId when the layout entry points at a dead pty', () => {
const snapshot = buildDashboardSnapshot(
baseState({
diff --git a/src/renderer/src/components/dashboard/build-dashboard-snapshot.ts b/src/renderer/src/components/dashboard/build-dashboard-snapshot.ts
index 8554cf85e74..d860f646fb0 100644
--- a/src/renderer/src/components/dashboard/build-dashboard-snapshot.ts
+++ b/src/renderer/src/components/dashboard/build-dashboard-snapshot.ts
@@ -5,7 +5,9 @@ import type {
DashboardCardDotState,
DashboardSnapshot
} from '../../../../shared/dashboard-snapshot'
+import type { RepoIcon } from '../../../../shared/repo-icon'
import { parsePaneKey } from '../../../../shared/stable-pane-id'
+import { getAgentRowConversationName } from '../../../../shared/agent-row-conversation-name'
import { migrationUnsupportedToAgentStatusEntry } from '@/lib/migration-unsupported-agent-entry'
import { applyAgentRowLineage } from './agent-row-lineage'
import { lastEnteredDoneAt } from './agent-finished-timestamp'
@@ -43,6 +45,7 @@ export type DashboardSnapshotState = Pick<
| 'ptyIdsByTabId'
| 'runtimePaneTitlesByTabId'
| 'acknowledgedAgentsByPaneKey'
+ | 'settings'
>
function bucketForState(state: DashboardAgentRow['state']): DashboardBucket {
@@ -70,6 +73,24 @@ function nonEmpty(value: string | undefined): string | undefined {
return trimmed.length > 0 ? trimmed : undefined
}
+/** Mirrors useAgentRowConversationName so the board and the sidebar label the
+ * same agent with the same name. */
+function rowConversationName(
+ row: DashboardAgentRow,
+ generatedTitlesEnabled: boolean
+): string | undefined {
+ const parentPaneKey = row.entry.orchestration?.parentPaneKey
+ // Why: a child row rendered on its parent's tab does not own that tab's name.
+ if (
+ row.lineage?.depth === 1 &&
+ parentPaneKey !== undefined &&
+ parsePaneKey(parentPaneKey)?.tabId === row.tab.id
+ ) {
+ return undefined
+ }
+ return getAgentRowConversationName(row.tab, row.agentType, generatedTitlesEnabled) ?? undefined
+}
+
/**
* Derive the serializable dashboard snapshot from the live renderer store.
* Reuses the exact per-worktree row machinery the sidebar uses
@@ -82,6 +103,8 @@ export function buildDashboardSnapshot(
now: number
): DashboardSnapshot {
const cards: DashboardCard[] = []
+ const repoIconsByRepoId: Record = {}
+ const generatedTitlesEnabled = state.settings?.tabAutoGenerateTitle === true
const activeWorktrees: {
repo: AppState['repos'][number]
worktree: AppState['worktreesByRepo'][string][number]
@@ -169,6 +192,8 @@ export function buildDashboardSnapshot(
: null
const dotState = row.state as DashboardCardDotState
const bucket = bucketForState(row.state)
+ // Only repos that actually contribute a card ship their icon.
+ repoIconsByRepoId[repo.id] = repo.repoIcon ?? null
cards.push({
paneKey: row.paneKey,
@@ -193,10 +218,11 @@ export function buildDashboardSnapshot(
unseen:
!isTitleDerived &&
(state.acknowledgedAgentsByPaneKey?.[row.paneKey] ?? 0) < row.entry.stateStartedAt,
- askSummary: bucket === 'attention' ? (row.entry.interactivePrompt ?? undefined) : undefined
+ askSummary: bucket === 'attention' ? (row.entry.interactivePrompt ?? undefined) : undefined,
+ conversationName: rowConversationName(row, generatedTitlesEnabled)
})
}
}
- return { generatedAt: now, cards }
+ return { generatedAt: now, cards, repoIconsByRepoId }
}
diff --git a/src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts b/src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts
index 3a8e8c2c04e..e1609f8666b 100644
--- a/src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts
+++ b/src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts
@@ -26,6 +26,8 @@ export function dashboardSnapshotInputsChanged(
state.ptyIdsByTabId !== previousState.ptyIdsByTabId ||
state.runtimePaneTitlesByTabId !== previousState.runtimePaneTitlesByTabId ||
state.acknowledgedAgentsByPaneKey !== previousState.acknowledgedAgentsByPaneKey ||
+ // Why: tabAutoGenerateTitle decides whether cards may show generated names.
+ state.settings !== previousState.settings ||
// Why: freshness can change a bucket without replacing any backing map.
state.agentStatusEpoch !== previousState.agentStatusEpoch
)
diff --git a/src/shared/dashboard-snapshot.ts b/src/shared/dashboard-snapshot.ts
index 0f3296c4e1d..1091a911b55 100644
--- a/src/shared/dashboard-snapshot.ts
+++ b/src/shared/dashboard-snapshot.ts
@@ -1,4 +1,5 @@
import type { AgentType } from './agent-status-types'
+import type { RepoIcon } from './repo-icon'
/**
* Serializable contract for the pop-out agent dashboard. The main renderer owns
@@ -58,14 +59,26 @@ export type DashboardCard = {
unseen: boolean
/** Short summary of the pending question when bucket === 'attention'. */
askSummary?: string
+ /** The tab's conversation name, resolved exactly as the sidebar's agent rows
+ * resolve it. Undefined when no usable name exists (status-only titles). */
+ conversationName?: string
}
export type DashboardSnapshot = {
generatedAt: number
cards: DashboardCard[]
+ /** Icons for the repos the cards belong to. Keyed by repoId rather than
+ * carried per card: image icons are data URLs up to 400KB, and the snapshot
+ * is republished several times a second. Optional so a pop-out running
+ * pre-upgrade code still accepts the payload. */
+ repoIconsByRepoId?: Record
}
-export const EMPTY_DASHBOARD_SNAPSHOT: DashboardSnapshot = { generatedAt: 0, cards: [] }
+export const EMPTY_DASHBOARD_SNAPSHOT: DashboardSnapshot = {
+ generatedAt: 0,
+ cards: [],
+ repoIconsByRepoId: {}
+}
/** Routing payload for click-to-focus: reveal this agent's pane in the main
* window. leafId is null when the pane could not be resolved (best-effort: