Skip to content

Commit d00165e

Browse files
committed
style: polish agent pane state headers
1 parent 9621f9e commit d00165e

6 files changed

Lines changed: 166 additions & 29 deletions

File tree

apps/web/src/features/agents/AgentWorkspaceFeature.tsx

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import type { PointerEventHandler, ReactNode } from "react";
2-
import type { Translator } from "../../i18n";
2+
import type { Locale, Translator } from "../../i18n";
33
import type { AppTheme, TerminalCompatibilityMode } from "../../types/app";
44
import type { Session, SessionPaneNode, Tab } from "../../state/workbench";
55
import { AgentSendIcon, AgentSplitHorizontalIcon, AgentSplitVerticalIcon, HeaderCloseIcon } from "../../components/icons";
66
import { AgentStreamTerminal, type XtermBaseHandle } from "../../components/terminal";
7-
import { isHiddenDraftPlaceholder, sessionCompletionRatio, sessionTone } from "../../shared/utils/session";
7+
import { isHiddenDraftPlaceholder, sessionCompletionRatio, sessionHeaderTag, sessionTone } from "../../shared/utils/session";
88
import { stripAnsi } from "../../shared/utils/ansi";
99

1010
type AgentWorkspaceFeatureProps = {
1111
visible: boolean;
12+
locale: Locale;
1213
activeTab: Tab;
1314
activePaneSession: Session;
1415
viewedSession: Session;
@@ -36,6 +37,7 @@ type AgentWorkspaceFeatureProps = {
3637

3738
export const AgentWorkspaceFeature = ({
3839
visible,
40+
locale,
3941
activeTab,
4042
activePaneSession,
4143
viewedSession,
@@ -63,6 +65,7 @@ export const AgentWorkspaceFeature = ({
6365
if (!visible) return null;
6466

6567
const viewedSessionPlainStream = stripAnsi(viewedSession.stream);
68+
const viewedHeaderTag = sessionHeaderTag(viewedSession.status, locale);
6669

6770
const renderAgentPane = (node: SessionPaneNode): ReactNode => {
6871
if (node.type === "split") {
@@ -90,6 +93,7 @@ export const AgentWorkspaceFeature = ({
9093
? "queued"
9194
: "idle";
9295
const statusTone = sessionTone(session.status);
96+
const headerTag = sessionHeaderTag(session.status, locale);
9397
const showDraftPromptInput = isHiddenDraftPlaceholder(session);
9498

9599
return (
@@ -108,33 +112,36 @@ export const AgentWorkspaceFeature = ({
108112
<span className={`session-top-dot ${statusTone} ${statusTone === "active" ? "pulse" : ""}`} />
109113
<span className="agent-pane-title">{displaySessionTitle(session.title)}</span>
110114
</div>
111-
<div className="agent-pane-actions">
112-
<button
113-
type="button"
114-
className="pane-action split"
115-
onClick={() => onSplitPane(node.id, "vertical")}
116-
title={t("splitVertical")}
117-
aria-label={t("splitVertical")}
118-
>
119-
<AgentSplitHorizontalIcon />
120-
</button>
121-
<button
122-
type="button"
123-
className="pane-action split"
124-
onClick={() => onSplitPane(node.id, "horizontal")}
125-
title={t("splitHorizontal")}
126-
aria-label={t("splitHorizontal")}
127-
>
128-
<AgentSplitVerticalIcon />
129-
</button>
130-
<button
131-
type="button"
132-
className="pane-action close"
133-
onClick={() => onCloseAgentPane(node.id, session.id)}
134-
title={t("close")}
135-
>
136-
<HeaderCloseIcon />
137-
</button>
115+
<div className="agent-pane-meta">
116+
<span className={`agent-pane-state-tag ${headerTag.tone}`}>{headerTag.label}</span>
117+
<div className="agent-pane-actions">
118+
<button
119+
type="button"
120+
className="pane-action split"
121+
onClick={() => onSplitPane(node.id, "vertical")}
122+
title={t("splitVertical")}
123+
aria-label={t("splitVertical")}
124+
>
125+
<AgentSplitHorizontalIcon />
126+
</button>
127+
<button
128+
type="button"
129+
className="pane-action split"
130+
onClick={() => onSplitPane(node.id, "horizontal")}
131+
title={t("splitHorizontal")}
132+
aria-label={t("splitHorizontal")}
133+
>
134+
<AgentSplitVerticalIcon />
135+
</button>
136+
<button
137+
type="button"
138+
className="pane-action close"
139+
onClick={() => onCloseAgentPane(node.id, session.id)}
140+
title={t("close")}
141+
>
142+
<HeaderCloseIcon />
143+
</button>
144+
</div>
138145
</div>
139146
</div>
140147
<div className="agent-pane-body" data-testid={`agent-pane-${node.id}`}>
@@ -231,6 +238,9 @@ export const AgentWorkspaceFeature = ({
231238
<span className={`session-top-dot ${sessionTone(viewedSession.status)} ${sessionTone(viewedSession.status) === "active" ? "pulse" : ""}`} />
232239
<span className="agent-pane-title">{displaySessionTitle(viewedSession.title)}</span>
233240
</div>
241+
<div className="agent-pane-meta">
242+
<span className={`agent-pane-state-tag ${viewedHeaderTag.tone}`}>{viewedHeaderTag.label}</span>
243+
</div>
234244
</div>
235245
<div className="agent-pane-body">
236246
{viewedSessionPlainStream.trim() ? (

apps/web/src/features/workspace/WorkspaceScreen.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2316,6 +2316,7 @@ export default function WorkspaceScreen({ locale, appSettings, onOpenSettings }:
23162316
const workspaceAgentPanel = (
23172317
<AgentWorkspaceFeature
23182318
visible={showAgentPanel}
2319+
locale={locale}
23192320
activeTab={activeTab}
23202321
activePaneSession={activePaneSession}
23212322
viewedSession={viewedSession}

apps/web/src/shared/utils/session.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,31 @@ export const sessionTone = (status: SessionStatus) => {
129129
return "suspended";
130130
};
131131

132+
export const sessionHeaderTag = (
133+
status: SessionStatus,
134+
locale: Locale,
135+
): {
136+
label: string;
137+
tone: "active" | "info" | "queue" | "idle" | "muted";
138+
} => {
139+
const t = createTranslator(locale);
140+
141+
if (status === "running") {
142+
return { label: t("running"), tone: "active" };
143+
}
144+
if (status === "background") {
145+
return { label: t("background"), tone: "info" };
146+
}
147+
if (status === "waiting" || status === "queued") {
148+
return { label: t("queued"), tone: "queue" };
149+
}
150+
if (status === "idle") {
151+
return { label: t("ready"), tone: "idle" };
152+
}
153+
154+
return { label: t("suspended"), tone: "muted" };
155+
};
156+
132157
export const formatRelativeSessionTime = (value: number, locale: Locale) => {
133158
const diffMs = value - Date.now();
134159
const absMs = Math.abs(diffMs);

apps/web/src/styles/app.css

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8809,3 +8809,78 @@ pre.diff,
88098809
transition: none !important;
88108810
}
88118811
}
8812+
8813+
/* ==========================================================================
8814+
Agent Pane Polish Pass
8815+
========================================================================== */
8816+
8817+
.agent-pane-header {
8818+
align-items: center;
8819+
gap: 8px;
8820+
padding: 6px 8px;
8821+
}
8822+
8823+
.agent-pane-header-copy {
8824+
min-width: 0;
8825+
gap: 6px;
8826+
}
8827+
8828+
.agent-pane-meta {
8829+
display: flex;
8830+
align-items: center;
8831+
gap: 6px;
8832+
margin-left: auto;
8833+
flex-shrink: 0;
8834+
}
8835+
8836+
.agent-pane-state-tag {
8837+
display: inline-flex;
8838+
align-items: center;
8839+
min-height: 20px;
8840+
padding: 0 7px;
8841+
border: 1px solid var(--border-subtle);
8842+
border-radius: 999px;
8843+
background: var(--surface-chip);
8844+
font-size: 10px;
8845+
font-weight: 600;
8846+
letter-spacing: 0.04em;
8847+
text-transform: uppercase;
8848+
color: var(--text-secondary);
8849+
}
8850+
8851+
.agent-pane-state-tag.active {
8852+
color: var(--accent-2);
8853+
border-color: color-mix(in srgb, var(--accent-2) 28%, var(--border-subtle));
8854+
}
8855+
8856+
.agent-pane-state-tag.info {
8857+
color: var(--accent);
8858+
border-color: color-mix(in srgb, var(--accent) 28%, var(--border-subtle));
8859+
}
8860+
8861+
.agent-pane-state-tag.queue {
8862+
color: var(--accent-3);
8863+
border-color: color-mix(in srgb, var(--accent-3) 28%, var(--border-subtle));
8864+
}
8865+
8866+
.agent-pane-state-tag.idle {
8867+
color: var(--text-secondary);
8868+
}
8869+
8870+
.agent-pane-state-tag.muted {
8871+
color: var(--text-tertiary);
8872+
}
8873+
8874+
.agent-pane-actions {
8875+
gap: 2px;
8876+
}
8877+
8878+
.pane-action {
8879+
width: 24px;
8880+
height: 24px;
8881+
border-radius: 4px;
8882+
}
8883+
8884+
.pane-action:hover {
8885+
background: var(--bg-hover);
8886+
}

tests/e2e/e2e.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,8 @@ test('flat matte UI exposes compact shell and supporting screen markers', async
493493
await expect(page.getByTestId('workspace-status-strip')).toContainText('Runtime');
494494
await expect(page.getByTestId('workspace-status-strip')).toContainText('Changes');
495495
await expect(page.getByTestId('workspace-status-strip')).toContainText('Queue');
496+
await expect(page.locator('.agent-pane-state-tag').first()).toBeVisible();
497+
await expect(page.locator('.agent-pane-state-tag').first()).toHaveText(/Ready|Queued|Suspended|Running|Background/);
496498

497499
await page.getByRole('button', { name: 'Actions' }).click();
498500
await expect(page.getByTestId('command-palette-shell')).toBeVisible();

tests/session-header-tag.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import test from "node:test";
2+
import assert from "node:assert/strict";
3+
import { sessionHeaderTag } from "../apps/web/src/shared/utils/session.ts";
4+
5+
test("sessionHeaderTag returns a running badge for active sessions", () => {
6+
assert.deepEqual(sessionHeaderTag("running", "en"), {
7+
label: "Running",
8+
tone: "active",
9+
});
10+
});
11+
12+
test("sessionHeaderTag returns a queued badge for waiting sessions", () => {
13+
assert.deepEqual(sessionHeaderTag("waiting", "en"), {
14+
label: "Queued",
15+
tone: "queue",
16+
});
17+
});
18+
19+
test("sessionHeaderTag returns a ready badge for idle sessions", () => {
20+
assert.deepEqual(sessionHeaderTag("idle", "en"), {
21+
label: "Ready",
22+
tone: "idle",
23+
});
24+
});

0 commit comments

Comments
 (0)