Skip to content

Commit f13e189

Browse files
agent-era-aiclaude
andauthored
Show running-status chips on tracker board cards (#225)
* tracker: seed item files for running-status-chips * add running-status chips to tracker board cards Surface which of the agent / shell / run sessions are active for each tracker item directly on the kanban card, mirroring the a/s/x cells in the worktree mainview. Chips render only for active sessions, in fixed order, with full-word labels (cyan/green/magenta) so they're readable without a column header. The previous ◆ "has session" glyph is dropped since the chips communicate that signal more precisely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * move running-status chip row below secondary text Render the chip row at the bottom of each card (after the secondary status text and the approve-hint row) so the textual signals stay prominent and the chips read as a footer rather than competing with the slug for visual weight. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * hoist worktree lookup; use marginLeft for chip indent Pulls a single getWorktreeForItem() call up to the top of the card render block so prMerged, the chip computation, and the inlined session-status filter all share one Map.get() instead of three. Also swaps the four-space Text spacer for marginLeft={4} on the chip row, matching how Ink Boxes are normally indented elsewhere. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * tracker: implementation notes + cleanup-stage status Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * update display-state test for dropped ◆ branch The session-only assertion expected ◆/gray/'session idle', but with the running-status chips taking over the session-presence signal the helper now collapses session-only items to the idle/empty branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3eb3e4d commit f13e189

8 files changed

Lines changed: 217 additions & 21 deletions

File tree

src/screens/TrackerBoardScreen.tsx

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {startIntervalIfEnabled} from '../shared/utils/intervals.js';
1313
import {VISIBLE_STATUS_REFRESH_DURATION} from '../constants.js';
1414
import TrackerProjectPickerDialog from '../components/dialogs/TrackerProjectPickerDialog.js';
1515
import AIToolDialog from '../components/dialogs/AIToolDialog.js';
16+
import StatusChip from '../components/common/StatusChip.js';
17+
import {computeRunningChips} from './runningChips.js';
1618

1719
interface TrackerBoardScreenProps {
1820
project: string;
@@ -152,20 +154,6 @@ export function getTrackerCardDisplayState({
152154
};
153155
}
154156

155-
if (hasSession) {
156-
return {
157-
statusGlyph: '◆',
158-
statusColor: 'gray',
159-
titleColor: inactive ? 'gray' : undefined,
160-
titleBold: false,
161-
secondaryText: itemStatusDescription || 'session idle',
162-
secondaryColor: inactive ? 'gray' : undefined,
163-
secondaryBold: false,
164-
secondaryDim: true,
165-
showApproveHint: false,
166-
};
167-
}
168-
169157
return {
170158
statusGlyph: ' ',
171159
statusColor: inactive ? 'gray' : undefined,
@@ -756,7 +744,8 @@ export default function TrackerBoardScreen({
756744
{visibleItems.map((item, sliceIndex) => {
757745
const itemIndex = scrollTop + sliceIndex;
758746
const isSelected = isActiveColumn && selectedRow === itemIndex;
759-
const sessWt = getSessionForItem(item);
747+
const wt = getWorktreeForItem(item);
748+
const sessWt = (wt?.session?.ai_status && wt.session.ai_status !== 'not_running') ? wt : null;
760749
const aiStatus: AIStatus | undefined = sessWt?.session?.ai_status;
761750
const aiWaiting = aiStatus === 'waiting';
762751
const isWorking = aiStatus === 'working' || aiStatus === 'active';
@@ -767,11 +756,15 @@ export default function TrackerBoardScreen({
767756
// "ready to advance" treatment so it's spottable at a glance and
768757
// can be acted on with the `m` shortcut from the board.
769758
const itemStatus = service.getItemStatus(projectPath, item.slug);
770-
const prMerged = getWorktreeForItem(item)?.pr?.is_merged === true;
759+
const prMerged = wt?.pr?.is_merged === true;
771760
const readyToAdvance = !prMerged && service.isItemReadyToAdvance(itemStatus);
772761
const ralphWaiting = !!itemStatus && !readyToAdvance && service.isItemWaiting(itemStatus);
773762
const isWaiting = aiWaiting || ralphWaiting;
774763

764+
// Session presence is now signalled by the running-status chip row
765+
// (rendered separately below); the ◆ branch in
766+
// getTrackerCardDisplayState is dropped to avoid duplicating that
767+
// signal.
775768
const display = getTrackerCardDisplayState({
776769
prMerged,
777770
readyToAdvance,
@@ -781,6 +774,7 @@ export default function TrackerBoardScreen({
781774
inactive: item.inactive,
782775
itemStatusDescription: itemStatus?.brief_description,
783776
});
777+
const runningChips = computeRunningChips(wt);
784778

785779
// Slug row eats: 2 (border) + 2 (paddingX) + 2 (cursor) + 2 (status glyph) = 8 chars
786780
const slug = truncateDisplay(item.slug, Math.max(4, colWidth - 8));
@@ -811,7 +805,9 @@ export default function TrackerBoardScreen({
811805
if (!text) return null;
812806
// Focused card gets more lines so the full (up to 200-char)
813807
// brief_description is readable; other cards stay compact.
814-
const maxLines = isSelected ? 4 : SECONDARY_MAX_LINES;
808+
// Chip row eats one of those lines when present.
809+
const baseMax = isSelected ? 4 : SECONDARY_MAX_LINES;
810+
const maxLines = Math.max(1, baseMax - (runningChips.length > 0 ? 1 : 0));
815811
const lines = wrapToLines(text, secMax, maxLines);
816812
return lines.map((line, lineIdx) => (
817813
<Text
@@ -835,6 +831,22 @@ export default function TrackerBoardScreen({
835831
{` press [m] to approve and advance`}
836832
</Text>
837833
)}
834+
{/* Running-status chips: one per active tmux session, rendered
835+
last so the card's textual signals (ready/waiting/working)
836+
stay above. Indented to match the secondary-text gutter.
837+
Eats one of the four rows budgeted per item, so secondary
838+
maxLines drops by 1 when chips render to keep scroll math
839+
intact. */}
840+
{runningChips.length > 0 && (
841+
<Box marginLeft={4}>
842+
{runningChips.map((chip, idx) => (
843+
<React.Fragment key={chip.label}>
844+
{idx > 0 && <Text> </Text>}
845+
<StatusChip label={chip.label} color={chip.color} fg="white" />
846+
</React.Fragment>
847+
))}
848+
</Box>
849+
)}
838850
</Box>
839851
);
840852
})}

src/screens/runningChips.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type {WorktreeInfo} from '../models.js';
2+
3+
export interface RunningChip {
4+
label: string;
5+
color: string;
6+
}
7+
8+
export function computeRunningChips(worktree: WorktreeInfo | null | undefined): RunningChip[] {
9+
const session = worktree?.session;
10+
if (!session) return [];
11+
const chips: RunningChip[] = [];
12+
if (session.attached) chips.push({label: 'agent', color: 'cyan'});
13+
if (session.shell_attached) chips.push({label: 'shell', color: 'green'});
14+
if (session.run_attached) chips.push({label: 'run', color: 'magenta'});
15+
return chips;
16+
}

tests/unit/TrackerBoardScreen.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,17 @@ describe('getTrackerCardDisplayState', () => {
104104
});
105105
});
106106

107-
test('session-only item is dim with no secondary color', () => {
107+
test('session-only item collapses to the idle/empty branch (chips below the card carry the session signal)', () => {
108108
const display = getTrackerCardDisplayState({
109109
...baseFlags,
110110
hasSession: true,
111111
});
112112

113113
expect(display).toMatchObject({
114-
statusGlyph: '',
115-
statusColor: 'gray',
114+
statusGlyph: ' ',
115+
statusColor: undefined,
116116
titleColor: undefined,
117-
secondaryText: 'session idle',
117+
secondaryText: '',
118118
secondaryColor: undefined,
119119
secondaryDim: true,
120120
showApproveHint: false,

tests/unit/runningChips.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {describe, test, expect} from '@jest/globals';
2+
import {computeRunningChips} from '../../src/screens/runningChips.js';
3+
import {WorktreeInfo, SessionInfo} from '../../src/models.js';
4+
5+
function wt(session: Partial<SessionInfo>): WorktreeInfo {
6+
return new WorktreeInfo({
7+
project: 'p',
8+
feature: 'f',
9+
path: '/p/f',
10+
session: new SessionInfo({session_name: 's', ai_status: 'idle', ai_tool: 'none', ...session}),
11+
});
12+
}
13+
14+
describe('computeRunningChips', () => {
15+
test('all three flags active → agent, shell, run in fixed order', () => {
16+
const chips = computeRunningChips(wt({attached: true, shell_attached: true, run_attached: true}));
17+
expect(chips).toEqual([
18+
{label: 'agent', color: 'cyan'},
19+
{label: 'shell', color: 'green'},
20+
{label: 'run', color: 'magenta'},
21+
]);
22+
});
23+
24+
test('only shell_attached → only shell chip', () => {
25+
const chips = computeRunningChips(wt({attached: false, shell_attached: true, run_attached: false}));
26+
expect(chips).toEqual([{label: 'shell', color: 'green'}]);
27+
});
28+
29+
test('only attached (agent) → only agent chip', () => {
30+
const chips = computeRunningChips(wt({attached: true, shell_attached: false, run_attached: false}));
31+
expect(chips).toEqual([{label: 'agent', color: 'cyan'}]);
32+
});
33+
34+
test('only run_attached → only run chip', () => {
35+
const chips = computeRunningChips(wt({attached: false, shell_attached: false, run_attached: true}));
36+
expect(chips).toEqual([{label: 'run', color: 'magenta'}]);
37+
});
38+
39+
test('agent + run (no shell) preserves fixed order with shell skipped', () => {
40+
const chips = computeRunningChips(wt({attached: true, shell_attached: false, run_attached: true}));
41+
expect(chips).toEqual([
42+
{label: 'agent', color: 'cyan'},
43+
{label: 'run', color: 'magenta'},
44+
]);
45+
});
46+
47+
test('worktree linked but no sessions running → []', () => {
48+
const chips = computeRunningChips(wt({attached: false, shell_attached: false, run_attached: false}));
49+
expect(chips).toEqual([]);
50+
});
51+
52+
test('null worktree (no link) → []', () => {
53+
expect(computeRunningChips(null)).toEqual([]);
54+
});
55+
56+
test('undefined worktree → []', () => {
57+
expect(computeRunningChips(undefined)).toEqual([]);
58+
});
59+
60+
test('worktree with no session → []', () => {
61+
const w = new WorktreeInfo({project: 'p', feature: 'f', path: '/p/f'});
62+
(w as any).session = undefined;
63+
expect(computeRunningChips(w)).toEqual([]);
64+
});
65+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Implementation — running-status-chips
2+
3+
## What was built
4+
5+
- `src/screens/runningChips.ts` — pure helper `computeRunningChips(worktree)` returning `[]` or an ordered subset of `[{label: 'agent', color: 'cyan'}, {label: 'shell', color: 'green'}, {label: 'run', color: 'magenta'}]` based on `session.attached / shell_attached / run_attached`. Pure and unit-tested.
6+
- `src/screens/TrackerBoardScreen.tsx`:
7+
- Imports `computeRunningChips` and the existing `StatusChip` common component.
8+
- Renders a chip row directly under each card's slug row, indented to the same ` ` (four-space) gutter as the existing secondary text. Hidden when `computeRunningChips` returns `[]` (covers both "no linked worktree" and "all flags false").
9+
- When a chip row is present, the secondary-text `maxLines` is reduced by 1 (to `1` for unselected, `3` for selected) so each card stays within the existing 4-row scroll budget. Long `brief_description` strings still get most of the space.
10+
- Drops the `` "has session" branch from `statusGlyph` / `statusColor`. The `` / `!` / `` branches (and their colors) are unchanged.
11+
- `tests/unit/runningChips.test.ts` — nine cases covering all-three / single-flag / agent+run / no sessions / null / undefined / no-session worktree.
12+
13+
## Key decisions
14+
15+
1. **Reused `StatusChip` rather than `SessionCell`.** SessionCell hard-codes `#005f87` blue and is single-letter only. Per the user's requirements (full word labels, distinct color per session type), StatusChip's flexible `color`/`fg`/`label` props were the better fit, and it already produces a centered `␣label␣` rendering with a colored background. SessionCell is left untouched for the mainview.
16+
2. **Active-only rendering.** No bracket/placeholder for inactive sessions. With three single-color chips removed, "absent" reads as "not running" cleanly. Combined with hiding the row entirely when nothing is active, cards stay compact.
17+
3. **4-row budget preserved.** Adding the chip row would have pushed each card to 5 rows and broken the column scroll math (`ROWS_PER_ITEM = 4` drives `visibleItemSlots`). Trading one secondary-text line for the chip row keeps everything aligned. The first secondary line — typically the most useful — is kept.
18+
4. **No data plumbing changes.** `getWorktreeForItem` was already on the screen and returns the unfiltered linked worktree (unlike `getSessionForItem`, which filters to `ai_status !== 'not_running'`). Using it means a card with only a shell or run session still lights up its chips even when the agent is idle.
19+
20+
## Notes for cleanup
21+
22+
- `hasSession` is still used by the secondary-text logic (line ~720) to decide between `'session idle'` vs `secondary` when there's no ralph status. That's intentional and out of scope for this item — the chips handle the visual signal, the secondary text still uses session presence as a fallback content selector.
23+
- `secondary = !hasSession ? renderSecondary(item) : ''` continues to suppress secondary metadata when a session exists; that behavior is unchanged.
24+
- I did not manually launch the CLI to view the chips against a live tmux session — only unit tests + `npm run typecheck` were run. Pure helper covers the chip-list logic exhaustively; the rendering is a thin `runningChips.map(...)` over StatusChip with no conditional logic worth eyeballing.
25+
26+
## Stage review
27+
28+
Built the chip helper, wired it into the board card, dropped the redundant ◆ glyph, and added 9 unit tests for the helper. `npm test` (702 tests across 72 suites) and `npm run typecheck` both pass. Committed as e7bdaa1.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Discovery — running-status-chips
2+
3+
## Problem
4+
5+
Tracker board cards don't show which of the three tmux sessions (agent / shell / execution) are actually running for an item. The mainview already exposes this with the `[a] [s] [x]` chips, but the kanban only shows a single status glyph (✓ / ! / ⟳ / ◆ / blank), which collapses three independent flags into one.
6+
7+
## Why
8+
9+
A user scanning the kanban can't tell at a glance whether a card has only an agent attached, only a shell, both, plus a run session, etc. That's information they already get on the worktree list and would expect to see on the board, especially when triaging which item to attach to.
10+
11+
## Findings
12+
13+
### Existing chip pattern (mainview)
14+
- The a/s/x indicators in MainView are *not* `StatusChip` — they're rendered by `renderSessionCell()` in `src/components/views/MainView/SessionCell.tsx:7-27`.
15+
- Active: solid `#005f87` blue bg, bold white ` a `/` s `/` x `.
16+
- Inactive: dimmed `[a]/[s]/[x]` brackets (or inverted on selected/dimmed rows).
17+
- WorktreeRow wires three flags from `worktree.session`: `attached` (agent), `shell_attached` (shell), `run_attached` (run/execution) — `WorktreeRow.tsx:63-65`.
18+
19+
### Data linkage already exists on the board
20+
- `TrackerBoardScreen.tsx:146-159` builds `sessionMap: slug → WorktreeInfo`. Tracker item slug == worktree feature name.
21+
- `getWorktreeForItem(item)` returns the linked WorktreeInfo regardless of AI status.
22+
- `getSessionForItem(item)` is the *filtered* version (only when `ai_status !== 'not_running'`) — used for the existing ◆/⟳/! glyph logic.
23+
- For chips we want the unfiltered `getWorktreeForItem` so a shell-only or run-only worktree still lights up s/x even when the agent is idle.
24+
25+
### Card layout & space budget
26+
- Cards are columns; column width is variable but tight. Slug row currently eats ~8 chars: `` + glyph + space + slug. `TrackerBoardScreen.tsx:677-678`.
27+
- Three SessionCell-style chips at width 3 each = 9 chars (or 6 if we tighten to ` a ` without bracket padding for inactive).
28+
- Two reasonable placements: (a) next to the slug on the same row (eats more of the slug width); (b) dedicated mini-row beneath the slug, before/replacing the secondary description line for cards with sessions.
29+
30+
### Open design questions (for requirements stage)
31+
1. Placement: same row as slug, or dedicated row?
32+
2. When no worktree is linked (item never had a session): hide chips entirely, or show three dim placeholders?
33+
3. Coexistence with existing status glyph (✓/!/⟳/◆): keep both, or do chips subsume ◆ ("has session")?
34+
35+
## Recommendation
36+
37+
Reuse `renderSessionCell` directly (it's the same primitive the mainview uses, so visual consistency is free). Render in a dedicated mini-row right under the slug, only when the item has a linked worktree. Keep the existing ✓/!/⟳ status glyph since it conveys ralph/AI state, but drop the redundant ◆ glyph (chips communicate "session present" more precisely). These are tentative; will confirm in requirements.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
title: "items should show which of the agent, session, execution are running. similar to the mainview. maybe use the same kind of 'chips' with colored bgs"
3+
slug: running-status-chips
4+
updated: 2026-04-26
5+
---
6+
7+
## Problem
8+
9+
Tracker board cards don't show which of the three tmux sessions (agent / shell / execution) are actually running for an item. The mainview already exposes this with the `[a] [s] [x]` chips, but the kanban only shows a single status glyph (✓ / ! / ⟳ / ◆ / blank), which collapses three independent flags into one.
10+
11+
## Why
12+
13+
A user scanning the kanban can't tell at a glance whether a card has only an agent attached, only a shell, both, plus a run session, etc. That's information they already get on the worktree list and would expect to see on the board, especially when triaging which item to attach to.
14+
15+
## Summary
16+
17+
Add a per-card "running" mini-row to `TrackerBoardScreen` that renders one labeled, colored chip per active tmux session — `agent` (cyan), `shell` (green), `run` (magenta) — using the same `getWorktreeForItem` linkage the board already builds. The row is active-only: hidden when the item has no linked worktree, and hidden again when all three session flags are false. The existing `` "has session" status glyph is dropped because the chips communicate the same thing more precisely; the other status glyphs (`` / `!` / ``) remain since they convey ralph/AI state, not session presence.
18+
19+
## Acceptance criteria
20+
21+
1. A tracker card on `TrackerBoardScreen` whose linked worktree has `session.attached === true` renders a chip labeled `agent` with a cyan (`cyan`) background and white foreground.
22+
2. A tracker card whose linked worktree has `session.shell_attached === true` renders a chip labeled `shell` with a green (`green`) background and white foreground.
23+
3. A tracker card whose linked worktree has `session.run_attached === true` renders a chip labeled `run` with a magenta (`magenta`) background and white foreground.
24+
4. Chips render in fixed order — `agent`, `shell`, `run` — with a single space between adjacent chips. Inactive sessions are omitted (no placeholder chip, no bracketed `[a]/[s]/[x]` form).
25+
5. The chip row is rendered as a dedicated line directly beneath the slug row and above the existing secondary/description text. It is indented to the same ` ` (four-space) gutter the secondary lines already use.
26+
6. The chip row is hidden entirely when no worktree is linked to the item (i.e., `getWorktreeForItem(item)` returns `undefined`).
27+
7. The chip row is hidden entirely when a worktree is linked but all three of `attached`, `shell_attached`, `run_attached` are false.
28+
8. Chip backgrounds remain at their session colors regardless of whether the card is currently selected — mirroring how `SessionCell` keeps `#005f87` for active sessions across selection states. The slug row's existing selection/inverse treatment is unchanged.
29+
9. The `` "has session" branch of the existing status-glyph logic in `TrackerBoardScreen.tsx` is removed: when the only positive signal would have been `hasSession`, the glyph cell renders a space (as it already does for fully-inactive cards). The ``, `!`, and `` branches and their associated colors are unchanged.
30+
10. The wider behavior of `getSessionForItem` (used elsewhere on the board for ralph/AI status decisions) is untouched. The chip row uses `getWorktreeForItem` so a worktree with `ai_status === 'not_running'` but a live shell or run session still surfaces its chips.
31+
11. Unit / e2e tests cover: (a) all three sessions active → all three chips render in order; (b) only `shell_attached` → only the `shell` chip renders; (c) no linked worktree → no chip row in the card output; (d) worktree linked but no sessions → no chip row.
32+
12. `npm test` and `npm run typecheck` both pass.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"stage": "cleanup",
3+
"state": "waiting_for_approval",
4+
"brief_description": "feature shipped on commit e7bdaa1; 702 tests pass; awaiting approval to archive + open PR",
5+
"timestamp": "2026-04-26T00:40:00Z"
6+
}

0 commit comments

Comments
 (0)