fix(cli): widen LiveAgentPanel, drop [in turn] marker, point overflow at dialog

Three usability fixes from review:

1. Use `terminalWidth` instead of `mainAreaWidth`. The latter is
   capped at 100 cols (intended for markdown / code where soft-wrap
   matters), which on a 200-col terminal left half the screen empty
   to the right of an already-truncating row. Live progress lines
   have nothing to soft-wrap, so the panel wants the full width.

2. Drop the `[in turn]` foreground marker. The flavor distinction
   matters in BackgroundTasksDialog (cancel semantics differ for
   foreground vs background entries) but in the glance panel the
   marker reads as cryptic noise — users asked what it meant. Keep
   the dialog as the surface that surfaces it.

3. Annotate the overflow callout with `(↓ to view all)`. The panel
   is intentionally read-only (it has no keyboard focus so it can't
   steal input from the composer), so when the roster outgrows the
   row budget we point users at the existing dialog — same keystroke
   the footer pill uses, kept in sync so users only learn one
   gesture.
This commit is contained in:
wenshao 2026-05-07 18:20:20 +08:00
parent 860c4ee212
commit bfecc094c8
3 changed files with 37 additions and 7 deletions

View file

@ -156,7 +156,10 @@ describe('<LiveAgentPanel />', () => {
expect(frame).toContain('5s');
});
it('marks foreground agents with the [in turn] prefix', () => {
it('does NOT surface a flavor marker on foreground agents', () => {
// Foreground vs background distinction stays with BackgroundTasksDialog
// (where cancel semantics differ); the panel reads as a glance roster
// and the marker added more confusion than signal.
const { lastFrame } = renderPanel({
entries: [
agentEntry({
@ -167,7 +170,10 @@ describe('<LiveAgentPanel />', () => {
}),
],
});
expect(lastFrame() ?? '').toContain('[in turn]');
const frame = lastFrame() ?? '';
expect(frame).not.toContain('[in turn]');
expect(frame).toContain('editor');
expect(frame).toContain('tighten import order');
});
it('windows from the tail when entries exceed maxRows', () => {
@ -190,8 +196,11 @@ describe('<LiveAgentPanel />', () => {
];
const { lastFrame } = renderPanel({ entries, maxRows: 2 });
const frame = lastFrame() ?? '';
// `more above` callout flagged with the dropped count.
// `more above` callout flagged with the dropped count and points
// at the dialog (the only surface where the user can scroll
// through the full roster + take action).
expect(frame).toContain('1 more above');
expect(frame).toContain('to view all');
// Tail window keeps the newest two rows.
expect(frame).toContain('mid-agent');
expect(frame).toContain('fresh-agent');

View file

@ -237,9 +237,17 @@ export const LiveAgentPanel: React.FC<LiveAgentPanelProps> = ({
</Box>
{overflow > 0 && (
<Box>
{/*
The panel is read-only (no keyboard focus that would
steal input from the composer), so when the roster
overflows the row budget we point users at the dialog
that DOES support selection / scroll / cancel / resume.
Same keystroke the footer pill uses, kept in sync so
users only have to learn one thing.
*/}
<Text
color={theme.text.secondary}
>{` ^ ${overflow} more above`}</Text>
>{` ^ ${overflow} more above (↓ to view all)`}</Text>
</Box>
)}
{visible.map((entry) => (
@ -255,7 +263,13 @@ const AgentRow: React.FC<{ entry: AgentDialogEntry; now: number }> = ({
}) => {
const { glyph, color } = statusIcon(entry);
const label = descriptionWithoutPrefix(entry);
const flavorPrefix = entry.flavor === 'foreground' ? '[in turn] ' : '';
// Note: foreground vs background is intentionally not surfaced here.
// Earlier iterations prefixed foreground rows with `[in turn]` (the
// BackgroundTasksDialog convention), but in the panel context the
// marker reads as cryptic — the foreground / background distinction
// matters in the dialog (where cancel semantics differ) but the
// glance roster just needs identity + intent + cost. Keep the
// dialog as the place that surfaces the flavor distinction.
const activity = activityLabel(entry);
const elapsed = elapsedLabel(entry, now);
const showType =
@ -296,7 +310,7 @@ const AgentRow: React.FC<{ entry: AgentDialogEntry; now: number }> = ({
<Text color={theme.text.secondary}>{': '}</Text>
</>
)}
<Text color={theme.text.secondary}>{`${flavorPrefix}${label}`}</Text>
<Text color={theme.text.secondary}>{label}</Text>
{activity && (
<Text color={theme.text.secondary}>{` (${activity})`}</Text>
)}

View file

@ -115,9 +115,16 @@ export const DefaultAppLayout: React.FC = () => {
open (auth / permission / background tasks / etc.) so the modal
surface doesn't compete with the live roster, and the panel's
own internal self-hide handles the empty-roster case.
Panel uses `terminalWidth`, not `mainAreaWidth` `mainAreaWidth`
is hard-capped at 100 cols (intended for markdown / code blocks
where soft-wrap matters), which on wider terminals leaves a
large empty gutter to the right of an already-truncating row.
Live progress lines have nothing to soft-wrap, so the panel
wants the full terminal width.
*/}
{!isAgentTab && !uiState.dialogsVisible && (
<LiveAgentPanel width={uiState.mainAreaWidth} />
<LiveAgentPanel width={uiState.terminalWidth} />
)}
</Box>
);