diff --git a/src/client/components/memberBoard.tsx b/src/client/components/memberBoard.tsx
index 833bd2e..6292a7c 100644
--- a/src/client/components/memberBoard.tsx
+++ b/src/client/components/memberBoard.tsx
@@ -1,15 +1,17 @@
/**
* The Fleet members board — every agent of the family in one panel, in two sections: the team's
* roster as full cards (identity, duty, latest reply, live task count), then the family's other
- * agents (plain subagents, ancestors, siblings) as compact rows. The toolbar's search, status
- * filter, and sort steer both sections. A pick pins the agent (ringing its graph node); a member
- * card's task chip filters the task board to that owner; the open button jumps to the session.
+ * agents (plain subagents, ancestors, siblings) as cards in the same form — their label in place
+ * of a roster name, and the run's tokens/duration/steps in place of the duty line. The toolbar's
+ * search, status filter, and sort steer both sections. A pick pins the agent (ringing its graph
+ * node); a member card's task chip filters the task board to that owner; the open button jumps
+ * to the session.
*/
import { useMemo, useState, type CSSProperties, type ReactElement } from 'react'
import { IconUserOutlineRegular, IconUsersOutlineRegular, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
import type { AgentForest, AgentNode } from '../agentTree'
-import { familyHue, fmtDurationCompact } from '../agentTree'
+import { familyHue, fmtDurationCompact, pressureColorOf } from '../agentTree'
import { tasksOfOwner, teamMemberOf, type FleetTeam } from '../fleetTeam'
import type { FleetDetail, FleetMemberInfo } from '../../shared/types'
import type { ViewKit } from '../viewkit'
@@ -33,12 +35,29 @@ export interface MemberBoardProps {
fmt: ViewKit['fmt']
}
-/** One member's card hue: the lead carries the brand color, teammates their golden-angled roster
- * hue (the graph's family-hue idiom). */
-function memberStyle(index: number, lead: boolean): CSSProperties {
- return {
- '--lc-member-hue': lead ? 'var(--dsw-alias-brand-primary, var(--color-blue-500))' : familyHue(index),
- } as CSSProperties
+/** One card's hue: the lead carries the brand color, every other card a golden-angled hue —
+ * teammates by roster order, other agents by their level-1 family (the graph's link hue). */
+const BRAND_HUE = 'var(--dsw-alias-brand-primary, var(--color-blue-500))'
+
+function memberStyle(hue: string): CSSProperties {
+ return { '--lc-member-hue': hue } as CSSProperties
+}
+
+/** The run's activity line above a card's foot: consumption, duration, then steps — the graph
+ * cards' own meta readout. */
+function memberMeta(node: AgentNode, t: ViewKit['t'], fmt: ViewKit['fmt']): ReactElement | null {
+ const bits: string[] = []
+ if (node.head !== null && node.head.tokens > 0) bits.push(fmt(node.head.tokens))
+ if (node.durationMs !== null) bits.push(fmtDurationCompact(node.durationMs))
+ if (node.requests > 0) bits.push(t('agents.steps', { n: node.requests }))
+ const pct = node.head !== null ? node.head.pct : null
+ if (bits.length === 0 && pct === null) return null
+ return (
+
+ {bits.join(' · ')}
+ {pct !== null ? {pct}% : null}
+
+ )
}
type StatusFilter = 'all' | 'running' | 'done' | 'idle'
@@ -183,7 +202,7 @@ export function MemberBoard(props: MemberBoardProps): ReactElement {
+ (props.pinnedId === member.id ? ' lc-member-pinned' : '')
+ (member.phase === 'failed' ? ' lc-member-failed' : '')
}
- style={memberStyle(index, member.role === 'lead')}
+ style={memberStyle(member.role === 'lead' ? BRAND_HUE : familyHue(index))}
role="button"
tabIndex={0}
onClick={() => { props.onPin(props.pinnedId === member.id ? null : member.id) }}
@@ -225,6 +244,7 @@ export function MemberBoard(props: MemberBoardProps): ReactElement {
{t('fleet.members.reply')} {detail.lastReply.text}
) : null}
+ {node !== undefined ? memberMeta(node, t, fmt) : null}
{activeTasks.length > 0 ? (
{fmt(others.length)}
-
+
{others.map((node) => {
- const pct = node.head !== null ? node.head.pct : null
- const meta: string[] = []
- if (node.head !== null) meta.push(fmt(node.head.tokens))
- if (node.model !== undefined) meta.push(node.model)
- if (node.durationMs !== null) meta.push(fmtDurationCompact(node.durationMs))
- if (node.requests > 0) meta.push(t('agents.steps', { n: node.requests }))
+ const detail = props.detailOf(node.id)?.detail ?? null
return (
-
- { props.onPin(props.pinnedId === node.id ? null : node.id) }}
- onKeyDown={(ev) => {
- if (ev.key !== 'Enter' && ev.key !== ' ') return
- ev.preventDefault()
- props.onPin(props.pinnedId === node.id ? null : node.id)
- }}
- onMouseEnter={() => { props.onHover(node.id) }}
- onMouseLeave={() => { props.onHover(null) }}
- >
- {node.running || node.completed
- ?
- :
}
-
{node.label}
-
{meta.join(' · ')}
- {pct !== null ?
{pct}% : null}
+
{ props.onPin(props.pinnedId === node.id ? null : node.id) }}
+ onKeyDown={(ev) => {
+ if (ev.key !== 'Enter' && ev.key !== ' ') return
+ ev.preventDefault()
+ props.onPin(props.pinnedId === node.id ? null : node.id)
+ }}
+ onMouseEnter={() => { props.onHover(node.id) }}
+ onMouseLeave={() => { props.onHover(null) }}
+ >
+
+
+
+
+ {/* A subagent's prompt title IS its identity, and it runs long: two clamped
+ lines where the roster name stays one. */}
+ {node.label}
+
+
+
+ {node.running ? t('agents.running') : node.completed ? t('fleet.filter.done') : t('fleet.memberInactive')}
+ {node.model !== undefined ? {node.model} : null}
+
+ {detail?.lastReply !== undefined ? (
+
+ {t('fleet.members.reply')} {detail.lastReply.text}
+
+ ) : null}
+ {memberMeta(node, t, fmt)}
+
{
ev.stopPropagation()
@@ -314,10 +348,10 @@ export function MemberBoard(props: MemberBoardProps): ReactElement {
→
-
+
)
})}
-
+
>
) : null}
diff --git a/src/client/styles/fleet.css b/src/client/styles/fleet.css
index 92125a4..3487dcb 100644
--- a/src/client/styles/fleet.css
+++ b/src/client/styles/fleet.css
@@ -29,9 +29,11 @@
.lc-members-note { margin: 2px 0 6px; font-size: 11px; color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary)); }
/* ---- Members board ---- */
-.lc-members-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(240px, 100%), 1fr)); gap: 8px; }
+/* The 200px floor keeps the two-column read the common case: the unified cards are compact
+ enough that two of them fit the boards column down to a narrow pane. */
+.lc-members-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr)); gap: 8px; }
.lc-member-card {
- display: flex; flex-direction: column; gap: 5px; padding: 10px 12px; border-radius: 10px;
+ display: flex; flex-direction: column; gap: 4px; padding: 8px 10px; border-radius: 10px;
background: var(--dsw-alias-bg-layer-2);
/* The member's roster hue (golden-angled, shared with the graph's family links) tints the icon ring. */
border: 1px solid var(--dsw-alias-border-l2);
@@ -53,13 +55,19 @@
border: 1px solid color-mix(in srgb, var(--lc-member-hue, var(--dsw-alias-border-l2)) 35%, transparent);
}
.lc-member-name { flex: 1; min-width: 0; font-size: 12.5px; color: var(--dsw-alias-label-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+/* Other agents wear their prompt title as the name: two clamped lines beat a harsher ellipsis. */
+.lc-member-name-wrap { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; white-space: normal; line-height: 1.25; }
.lc-member-dot { flex: none; }
.lc-member-status { display: flex; align-items: baseline; gap: 8px; font-size: 11px; color: var(--dsw-alias-label-secondary); }
.lc-member-model { font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lc-member-desc { font-size: 11.5px; line-height: 1.4; color: var(--dsw-alias-label-secondary); display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; overflow-wrap: anywhere; }
.lc-member-reply { font-size: 11px; line-height: 1.4; color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary)); font-style: italic; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lc-member-reply::before { content: none; }
-.lc-member-foot { display: flex; align-items: center; gap: 6px; margin-top: auto; padding-top: 3px; }
+/* The run's activity line (the graph cards' meta readout): figures left, occupancy right. */
+.lc-member-meta { display: flex; align-items: baseline; justify-content: space-between; gap: 6px; font-size: 10.5px; line-height: 1.3; color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; }
+.lc-member-meta-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.lc-member-meta-pct { flex: none; font-weight: 600; }
+.lc-member-foot { display: flex; align-items: center; gap: 6px; margin-top: auto; padding-top: 2px; }
.lc-member-tasks { border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-brand-primary, var(--color-blue-500)); font: inherit; font-size: 10.5px; padding: 1px 8px; border-radius: 999px; cursor: pointer; }
.lc-member-tasks:hover { border-color: var(--dsw-alias-brand-primary, var(--color-blue-500)); }
.lc-member-tasks-on { background: color-mix(in srgb, var(--dsw-alias-brand-primary, var(--color-blue-500)) 14%, transparent); border-color: var(--dsw-alias-brand-primary, var(--color-blue-500)); }
@@ -98,18 +106,6 @@
.lc-team-task-warning { margin-top: 5px; font-size: 10.5px; color: var(--color-amber-500); }
.lc-team-task-meta .lc-inspector-btn { margin-left: auto; }
-.lc-fleet-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
-.lc-fleet-row { display: flex; align-items: center; gap: 8px; padding: 5px 8px; border-radius: 8px; border: 1px solid transparent; cursor: pointer; font-size: 12px; }
-.lc-fleet-row:hover { background: var(--dsw-alias-bg-layer-2); }
-.lc-fleet-row:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, var(--color-blue-500)); outline-offset: 1px; }
-.lc-fleet-row-pinned { border-color: color-mix(in srgb, var(--dsw-alias-brand-primary, var(--color-blue-500)) 55%, transparent); background: var(--dsw-alias-bg-layer-2); }
-.lc-fleet-dot-idle { flex: none; width: 10px; height: 10px; border-radius: 50%; background: color-mix(in srgb, var(--dsw-alias-label-secondary) 30%, transparent); }
-.lc-fleet-row-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); }
-.lc-fleet-row-team { color: var(--dsw-alias-brand-primary, var(--color-blue-500)); }
-.lc-fleet-row-meta { flex: none; font-size: 11px; color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; }
-.lc-fleet-row-pct { flex: none; font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; }
-.lc-fleet-row-open { flex: none; padding: 1px 7px; }
-
/* ---- Comms timeline ---- */
.lc-comms-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.lc-comms-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 8px; padding: 6px 2px; font-size: 11.5px; border-bottom: 1px dashed var(--dsw-alias-border-l2); border-left: 2px solid transparent; padding-left: 8px; }
diff --git a/tests/client/components/fleetView.panels.spec.ts b/tests/client/components/fleetView.panels.spec.ts
index 5671ce3..87c9e75 100644
--- a/tests/client/components/fleetView.panels.spec.ts
+++ b/tests/client/components/fleetView.panels.spec.ts
@@ -84,7 +84,7 @@ function makeView(ctx: TestClientCtx, useKit = kit) {
}
describe('FleetView — the panels over one family', () => {
- test('the other-agents row open button jumps to the session', async () => {
+ test('the other-agents card open button jumps to the session', async () => {
stubFleetRoute()
const opened = new Array()
const ctx = new TestClientCtx({
@@ -92,7 +92,8 @@ describe('FleetView — the panels over one family', () => {
})
const View = makeView(ctx)
const m = await mount(h(View, { sessionId: 'root', useProjection: projectionsFor(richTimeline()) }))
- await click(query(query(m.container, '.lc-fleet-row'), '.lc-fleet-row-open'))
+ const cards = queryAll(m.container, '.lc-member-card')
+ await click(query(cards[cards.length - 1], '.lc-member-open'))
assert.deepEqual([...opened], ['w2'])
await m.unmount()
ctx.dispose()
@@ -112,11 +113,12 @@ describe('FleetView — the panels over one family', () => {
assert.ok(text(summary).includes('0/1'))
// The network card header carries the team chip.
assert.ok(text(query(m.container, '.lc-agents')).includes('team · 3 members · 1 tasks'))
- // The members board: team cards + the other-agents section (w2, the plain subagent).
+ // The members board: team cards (the ghost's roster row included) + the other-agents
+ // section (w2, the plain subagent) in the same card form.
const board = query(m.container, '.lc-members')
assert.ok(text(board).includes('worker-bee'))
assert.ok(text(board).includes(DICT_EN['fleet.board.others']))
- assert.deepEqual(queryAll(board, '.lc-fleet-row').length, 1)
+ assert.equal(queryAll(board, '.lc-member-card').length, 4)
assert.ok(text(query(m.container, '.lc-tasks')).includes('schema parsing'))
// The task board's completion bar.
assert.ok(text(query(m.container, '.lc-tasks-progress')).includes('0/1 done'))
@@ -187,10 +189,6 @@ describe('FleetView — the panels over one family', () => {
assert.ok(query(m.container, '[data-agent="w1"]').className.includes('lc-agent-pinned'))
await click(query(m.container, '[data-agent="w1"]'))
assert.equal(queryAll(m.container, '.lc-agent-pinned').length, 0)
-
- // The corner button still jumps.
- await click(query(query(m.container, '[data-agent="w1"]'), '.lc-agent-open'))
- assert.deepEqual([...opened], ['w1'])
await m.unmount()
ctx.dispose()
})
@@ -362,7 +360,7 @@ describe('FleetView — the panels over one family', () => {
assert.ok(text(progress).includes('1/2'))
// No team: the members board lists the subagents and the task board points at the team mode.
assert.ok(text(query(m.container, '.lc-members')).includes(DICT_EN['fleet.board.noTeam']))
- assert.deepEqual(queryAll(m.container, '.lc-fleet-row').length, 2)
+ assert.equal(queryAll(m.container, '.lc-member-card').length, 2)
assert.ok(text(query(m.container, '.lc-tasks')).includes(DICT_EN['fleet.tasks.noTeam']))
await m.unmount()
ctx.dispose()
diff --git a/tests/client/components/memberBoard.spec.ts b/tests/client/components/memberBoard.spec.ts
index 20fff00..9df71e9 100644
--- a/tests/client/components/memberBoard.spec.ts
+++ b/tests/client/components/memberBoard.spec.ts
@@ -1,6 +1,7 @@
// MemberBoard — every family agent in one panel: the team roster as full cards (identity, status,
-// model, duty, latest reply, owner-filter interlock), the other agents as compact rows, and the
-// toolbar (search / status / sort) steering both sections, down to the no-team and solo guidance.
+// model, duty, latest reply, owner-filter interlock), the other agents as cards in the same form
+// (label as the name, run meta in place of the duty line), and the toolbar (search / status / sort)
+// steering both sections, down to the no-team and solo guidance.
import { act, createElement as h, type ReactElement } from 'react'
import assert from 'node:assert/strict'
@@ -103,8 +104,9 @@ describe('MemberBoard — team cards', () => {
detailOf: id => details.get(id),
pinnedId: 'w1',
})))
+ // Four roster cards, then the three other-agents cards in the same form.
const cards = queryAll(m.container, '.lc-member-card')
- assert.equal(cards.length, 4)
+ assert.equal(cards.length, 7)
const lead = cards[0]
assert.ok(text(lead).includes('lead'))
@@ -230,87 +232,101 @@ describe('MemberBoard — team cards', () => {
})
})
-describe('MemberBoard — the other-agents rows', () => {
- function rowLabels(container: HTMLElement): string[] {
- return queryAll(container, '.lc-fleet-row-label').map(el => el.textContent ?? '')
+describe('MemberBoard — the other-agents cards', () => {
+ /** The cards of the "other agents" section: the grid after that section's own head. */
+ function otherCards(container: HTMLElement): HTMLElement[] {
+ const head = queryAll(container, '.lc-board-section-head')
+ .find(h => text(h).includes(DICT_EN['fleet.board.others']))
+ return queryAll((head?.nextElementSibling ?? container) as HTMLElement, '.lc-member-card')
+ }
+ function otherNames(container: HTMLElement): string[] {
+ return otherCards(container).map(c => text(query(c, '.lc-member-name')))
}
- test('non-member family agents list as compact rows with meta, pct, and the open jump', async () => {
+ test('non-member family agents render as member cards with status, meta, and the open jump', async () => {
const pins = new Array()
const opened: string[] = []
const hovers: (string | null)[] = []
+ // A landed detail fills the card's reply line, like the roster cards'.
+ const details = new Map([
+ ['s2', { detail: { lastReply: { time: 100, text: 'archive sealed' }, messages: [], truncated: false } }],
+ ])
const m = await mount(h(MemberBoard, propsOf({
+ detailOf: id => details.get(id),
onPin: id => { pins.push(id) },
onOpen: id => { opened.push(id) },
onHover: id => { hovers.push(id) },
})))
- // The section head, then one row per plain subagent — the current session and team members stay out.
+ // The section head, then one card per plain subagent — the current session and team members stay out.
assert.ok(text(m.container).includes(DICT_EN['fleet.board.others']))
- assert.deepEqual(rowLabels(m.container), ['scout', 'archivist', 'lounger'])
- const rows = queryAll(m.container, '.lc-fleet-row')
- assert.ok(text(rows[0]).includes('800 · deepseek-flash · 42s · 5 steps'))
- assert.ok(text(rows[0]).includes('80%'))
- assert.equal(query(rows[2], '.lc-fleet-row-meta').textContent, '', 'a stat-less row carries no meta')
- assert.equal(queryAll(rows[2], '.lc-fleet-dot-idle').length, 1, 'the idle row wears the placeholder dot')
+ assert.deepEqual(otherNames(m.container), ['scout', 'archivist', 'lounger'])
+ const cards = otherCards(m.container)
+ // The run meta rides the card: consumption, duration, steps — and the occupancy figure.
+ assert.ok(text(cards[0]).includes('800 · 42s · 5 steps'))
+ assert.ok(text(cards[0]).includes('80%'))
+ assert.ok(text(cards[0]).includes('deepseek-flash'))
+ assert.ok(text(cards[0]).includes('running'))
+ assert.ok(text(cards[1]).includes('archive sealed'), 'the landed latest reply fills the card')
+ // A stat-less agent carries no meta line and reads as inactive.
+ assert.equal(queryAll(cards[2], '.lc-member-meta').length, 0)
+ assert.ok(text(cards[2]).includes(DICT_EN['fleet.memberInactive']))
- await hover(rows[0])
- await click(rows[0])
- await unhover(rows[0])
+ await hover(cards[0])
+ await click(cards[0])
+ await unhover(cards[0])
assert.deepEqual([...pins], ['s1'])
assert.deepEqual(hovers, ['s1', null])
- await click(query(rows[0], '.lc-fleet-row-open'))
+ await click(query(cards[0], '.lc-member-open'))
assert.deepEqual(opened, ['s1'])
assert.deepEqual([...pins], ['s1'], 'open never pins')
- // Keyboard: Enter pins, Space releases, an unrelated key is inert.
- await act(async () => { rows[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })) })
- await act(async () => { rows[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'x', bubbles: true, cancelable: true })) })
+ // Keyboard: Enter pins, an unrelated key is inert.
+ await act(async () => { cards[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })) })
+ await act(async () => { cards[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'x', bubbles: true, cancelable: true })) })
assert.deepEqual([...pins], ['s1', 's1'])
await m.unmount()
- // The pinned row rings; a re-click releases, and so does the Space key.
+ // The pinned card rings; a re-click releases, and so does the Space key.
const again = new Array()
const m2 = await mount(h(MemberBoard, propsOf({ pinnedId: 's1', onPin: id => { again.push(id) } })))
- const pinnedRow = queryAll(m2.container, '.lc-fleet-row')[0]
- assert.ok(pinnedRow.className.includes('lc-fleet-row-pinned'))
- await click(pinnedRow)
- await act(async () => { pinnedRow.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true, cancelable: true })) })
+ const pinnedCard = otherCards(m2.container)[0]
+ assert.ok(pinnedCard.className.includes('lc-member-pinned'))
+ await click(pinnedCard)
+ await act(async () => { pinnedCard.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true, cancelable: true })) })
assert.deepEqual([...again], [null, null])
await m2.unmount()
})
test('the toolbar searches, filters by status, and sorts — across both sections', async () => {
const m = await mount(h(MemberBoard, propsOf()))
- // Status filter: running keeps the running members' cards and the running subagent's row.
+ // Status filter: running keeps the running members' cards and the running subagent's card.
const filters = queryAll(m.container, '.lc-fleet-filters .lc-gran-btn')
await click(filters[1])
- assert.deepEqual(queryAll(m.container, '.lc-member-card').map(c => text(query(c, '.lc-member-name'))), ['lead', 'worker-bee'])
- assert.deepEqual(rowLabels(m.container), ['scout'])
+ assert.deepEqual(queryAll(m.container, '.lc-member-card').map(c => text(query(c, '.lc-member-name'))), ['lead', 'worker-bee', 'scout'])
await click(filters[2])
- assert.deepEqual(rowLabels(m.container), ['archivist'])
- assert.equal(queryAll(m.container, '.lc-member-card').length, 0, 'no member node is done')
+ assert.deepEqual(queryAll(m.container, '.lc-member-card').map(c => text(query(c, '.lc-member-name'))), ['archivist'])
await click(filters[3])
- assert.deepEqual(rowLabels(m.container), ['lounger'])
+ assert.deepEqual(queryAll(m.container, '.lc-member-card').map(c => text(query(c, '.lc-member-name'))), ['flaky-one', 'new-joiner', 'lounger'])
await click(filters[0])
// Sort by context tokens: 800 ahead of 200 ahead of the headless idler.
await selectIn(query(m.container, '.lc-fleet-sort'), 'tokens')
- assert.deepEqual(rowLabels(m.container), ['scout', 'archivist', 'lounger'])
+ assert.deepEqual(otherNames(m.container), ['scout', 'archivist', 'lounger'])
// (Spawn order coincides here; the reverse fixture order proves the sort.)
await m.unmount()
// A headless idler mid-list exercises the sorters' null-head arms on both sides.
const flipped: AgentForest = { ...FOREST, nodes: [FOREST.nodes[0], FOREST.nodes[1], FOREST.nodes[2], FOREST.nodes[3], FOREST.nodes[5], FOREST.nodes[4]] }
const m2 = await mount(h(MemberBoard, propsOf({ forest: flipped })))
- assert.deepEqual(rowLabels(m2.container), ['scout', 'lounger', 'archivist'], 'spawn order follows the forest')
+ assert.deepEqual(otherNames(m2.container), ['scout', 'lounger', 'archivist'], 'spawn order follows the forest')
await selectIn(query(m2.container, '.lc-fleet-sort'), 'tokens')
- assert.deepEqual(rowLabels(m2.container), ['scout', 'archivist', 'lounger'])
+ assert.deepEqual(otherNames(m2.container), ['scout', 'archivist', 'lounger'])
await selectIn(query(m2.container, '.lc-fleet-sort'), 'duration')
- assert.deepEqual(rowLabels(m2.container), ['scout', 'archivist', 'lounger'])
+ assert.deepEqual(otherNames(m2.container), ['scout', 'archivist', 'lounger'])
await selectIn(query(m2.container, '.lc-fleet-sort'), 'steps')
- assert.deepEqual(rowLabels(m2.container), ['scout', 'archivist', 'lounger'])
+ assert.deepEqual(otherNames(m2.container), ['scout', 'archivist', 'lounger'])
await selectIn(query(m2.container, '.lc-fleet-sort'), 'recent')
- assert.deepEqual(rowLabels(m2.container), ['scout', 'archivist', 'lounger'])
+ assert.deepEqual(otherNames(m2.container), ['scout', 'archivist', 'lounger'])
await m2.unmount()
})
@@ -318,7 +334,6 @@ describe('MemberBoard — the other-agents rows', () => {
const m = await mount(h(MemberBoard, propsOf()))
await typeIn(query(m.container, '.lc-fleet-search'), 'worker')
assert.equal(queryAll(m.container, '.lc-member-card').length, 1)
- assert.equal(queryAll(m.container, '.lc-fleet-row').length, 0)
await typeIn(query(m.container, '.lc-fleet-search'), 'zzz-no-match')
assert.equal(queryAll(m.container, '.lc-member-card').length, 0)
assert.ok(text(m.container).includes(DICT_EN['fleet.list.empty']))
@@ -327,13 +342,12 @@ describe('MemberBoard — the other-agents rows', () => {
})
describe('MemberBoard — guidance states', () => {
- test('no team: the note names the mode and the family rows still list', async () => {
+ test('no team: the note names the mode and the family cards still list', async () => {
const m = await mount(h(MemberBoard, propsOf({ team: null })))
assert.ok(text(m.container).includes(DICT_EN['fleet.board.noTeam']))
- assert.equal(queryAll(m.container, '.lc-member-card').length, 0)
- // Without a roster every non-current node is an "other agent".
+ // Without a roster every non-current node is an "other agent" — in the same card form.
assert.deepEqual(
- queryAll(m.container, '.lc-fleet-row-label').map(el => el.textContent),
+ queryAll(m.container, '.lc-member-card .lc-member-name').map(el => el.textContent),
['w1', 'w2', 'scout', 'archivist', 'lounger'],
)
await m.unmount()
@@ -344,7 +358,7 @@ describe('MemberBoard — guidance states', () => {
const m = await mount(h(MemberBoard, propsOf({ team: null, forest: solo })))
assert.ok(text(m.container).includes(DICT_EN['fleet.board.solo']))
assert.equal(queryAll(m.container, '.lc-fleet-toolbar').length, 0)
- assert.equal(queryAll(m.container, '.lc-fleet-row').length, 0)
+ assert.equal(queryAll(m.container, '.lc-member-card').length, 0)
await m.unmount()
})
@@ -357,11 +371,11 @@ describe('MemberBoard — guidance states', () => {
await m.unmount()
})
- test('a team whose members all filtered out hides the section but keeps the rows', async () => {
+ test('a team whose members all filtered out hides the section but keeps the other cards', async () => {
const m = await mount(h(MemberBoard, propsOf()))
await typeIn(query(m.container, '.lc-fleet-search'), 'scout')
- assert.equal(queryAll(m.container, '.lc-member-card').length, 0)
- assert.equal(queryAll(m.container, '.lc-fleet-row').length, 1)
+ assert.ok(text(m.container).includes('scout'))
+ assert.equal(queryAll(m.container, '.lc-member-card').length, 1)
await m.unmount()
})
})