Skip to content

Commit 92ec009

Browse files
authored
feat(ui): refine status indicator language
1 parent 8b6869d commit 92ec009

46 files changed

Lines changed: 1644 additions & 551 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/mobile/global.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@
5656
--color-danger-border: rgba(239, 68, 68, 0.12);
5757
--color-danger-foreground: #dc2626;
5858

59+
/* Thread status */
60+
--color-warning: #fe9a00;
61+
--color-warning-foreground: #bb4d00;
62+
--color-warning-surface: #fcf4e8;
63+
--color-status-active: #1b4ed8;
64+
--color-status-info: #7622bf;
65+
5966
/* Inputs */
6067
--color-input: #ffffff;
6168
--color-input-border: rgba(0, 0, 0, 0.1);
@@ -155,6 +162,13 @@
155162
--color-danger-border: rgba(248, 113, 113, 0.18);
156163
--color-danger-foreground: #fca5a5;
157164

165+
/* Thread status */
166+
--color-warning: #fe9a00;
167+
--color-warning-foreground: #ffb900;
168+
--color-warning-surface: #312108;
169+
--color-status-active: #477cf4;
170+
--color-status-info: #e9d5ff;
171+
158172
/* Inputs */
159173
--color-input: #141414;
160174
--color-input-border: rgba(255, 255, 255, 0.08);

apps/mobile/src/features/threads/thread-list-items.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
453453
const pressedBackgroundColor = useThemeColor("--color-subtle");
454454
const selectedBackgroundColor = useThemeColor("--color-user-bubble");
455455
const selectedForegroundColor = useThemeColor("--color-user-bubble-foreground");
456+
const warningForegroundColor = useThemeColor("--color-warning-foreground");
456457

457458
const { thread, onSelectThread, onArchiveThread, onDeleteThread, onRegenerateThreadTitle } =
458459
props;
@@ -470,8 +471,10 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
470471
const effectivePressedBackground = selected
471472
? themeColorWithAlpha(String(selectedForegroundColor), 0.16)
472473
: pressedBackgroundColor;
474+
const usesWarningTreatment =
475+
status?.kind === "pending-approval" || status?.kind === "awaiting-input";
473476
const effectiveStatus =
474-
selected && status
477+
selected && status && !usesWarningTreatment
475478
? {
476479
...status,
477480
pillClassName: "bg-user-bubble-foreground/20",
@@ -515,7 +518,17 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
515518
);
516519

517520
const statusPill = effectiveStatus ? (
518-
<View className={`${effectiveStatus.pillClassName} rounded-full px-1.5 py-0.5`}>
521+
<View
522+
className={`${effectiveStatus.pillClassName} flex-row items-center gap-0.5 rounded-full px-1.5 py-0.5`}
523+
>
524+
{effectiveStatus.kind === "awaiting-input" ? (
525+
<SymbolView
526+
name="exclamationmark.triangle"
527+
size={9}
528+
tintColor={warningForegroundColor}
529+
type="monochrome"
530+
/>
531+
) : null}
519532
<Text className={`text-3xs font-t3-bold ${effectiveStatus.textClassName}`}>
520533
{effectiveStatus.label}
521534
</Text>

apps/mobile/src/features/threads/thread-list-v2-items.tsx

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@ const MONO_FONT = Platform.select({
4646
default: "monospace",
4747
});
4848

49-
// Status hues follow the system-wide convention set by sidebar v1 and the
50-
// Live Activity/widgets (amber approval, indigo input, sky working) so a
51-
// thread reads the same color everywhere it surfaces.
49+
// Active work and Plan Ready follow shared theme roles. User-blocking Input
50+
// uses the orange warning treatment instead of operational Waiting's neutral treatment.
5251
const STATUS_LABEL_BY_STATUS: Partial<
5352
Record<ThreadListV2Status, { label: string; className: string }>
5453
> = {
55-
approval: { label: "Approval", className: "text-amber-700 dark:text-amber-300" },
56-
input: { label: "Input", className: "text-indigo-600 dark:text-indigo-300" },
57-
working: { label: "Working", className: "text-sky-600 dark:text-sky-400" },
54+
approval: { label: "Approval", className: "text-warning-foreground" },
55+
input: { label: "Input", className: "text-warning-foreground" },
56+
"plan-ready": { label: "Plan Ready", className: "text-status-info" },
57+
working: { label: "Working", className: "text-status-active" },
5858
failed: { label: "Failed", className: "text-red-700 dark:text-red-300" },
5959
};
6060

@@ -423,6 +423,8 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
423423
const pressedBackgroundColor = useThemeColor("--color-subtle");
424424
const selectedBackgroundColor = useThemeColor("--color-user-bubble");
425425
const pinTintColor = useThemeColor("--color-foreground-muted");
426+
const warningForegroundColor = useThemeColor("--color-warning-foreground");
427+
const selectedForegroundColor = useThemeColor("--color-user-bubble-foreground");
426428
const sidebarPane = props.pane === "sidebar";
427429
const selected = props.selected === true;
428430

@@ -701,16 +703,35 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
701703
{pinnedRow ? (
702704
<SymbolView name="pin" size={11} tintColor={pinTintColor} type="monochrome" />
703705
) : null}
704-
<Text
705-
className={cn(
706-
"text-xs tabular-nums",
707-
selected
708-
? "text-user-bubble-foreground"
709-
: (statusLabel?.className ?? "text-foreground-tertiary"),
710-
)}
711-
>
712-
{statusLabel?.label ?? timeLabel}
713-
</Text>
706+
{statusLabel ? (
707+
<View className="flex-row items-center gap-1">
708+
{status === "input" ? (
709+
<SymbolView
710+
name="exclamationmark.triangle"
711+
size={10}
712+
tintColor={selected ? selectedForegroundColor : warningForegroundColor}
713+
type="monochrome"
714+
/>
715+
) : null}
716+
<Text
717+
className={cn(
718+
"text-xs tabular-nums",
719+
selected ? "text-user-bubble-foreground" : statusLabel.className,
720+
)}
721+
>
722+
{statusLabel.label}
723+
</Text>
724+
</View>
725+
) : (
726+
<Text
727+
className={cn(
728+
"text-xs tabular-nums",
729+
selected ? "text-user-bubble-foreground" : "text-foreground-tertiary",
730+
)}
731+
>
732+
{timeLabel}
733+
</Text>
734+
)}
714735
</View>
715736
<Text
716737
className={cn(

apps/mobile/src/features/threads/threadListV2.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,66 @@ describe("resolveThreadListV2Status", () => {
193193
"ready",
194194
);
195195
});
196+
197+
it("keeps user input distinct from operational waiting", () => {
198+
expect(
199+
resolveThreadListV2Status(
200+
makeThread({ id: ThreadId.make("t"), title: "t", hasPendingUserInput: true }),
201+
),
202+
).toBe("input");
203+
});
204+
205+
it("shows Plan Ready for an actionable settled plan", () => {
206+
expect(
207+
resolveThreadListV2Status(
208+
makeThread({
209+
id: ThreadId.make("t"),
210+
title: "t",
211+
interactionMode: "plan",
212+
hasActionableProposedPlan: true,
213+
latestTurn: {
214+
turnId: TurnId.make("plan-turn"),
215+
state: "completed",
216+
requestedAt: "2026-06-01T00:00:00.000Z",
217+
startedAt: "2026-06-01T00:00:01.000Z",
218+
completedAt: "2026-06-01T00:00:02.000Z",
219+
assistantMessageId: null,
220+
},
221+
}),
222+
),
223+
).toBe("plan-ready");
224+
});
225+
226+
it("does not expose a stale plan prompt while a new session is starting", () => {
227+
expect(
228+
resolveThreadListV2Status(
229+
makeThread({
230+
id: ThreadId.make("t"),
231+
title: "t",
232+
interactionMode: "plan",
233+
hasActionableProposedPlan: true,
234+
latestTurn: {
235+
turnId: TurnId.make("plan-turn"),
236+
state: "completed",
237+
requestedAt: "2026-06-01T00:00:00.000Z",
238+
startedAt: "2026-06-01T00:00:01.000Z",
239+
completedAt: "2026-06-01T00:00:02.000Z",
240+
assistantMessageId: null,
241+
},
242+
session: {
243+
threadId: ThreadId.make("t"),
244+
status: "starting",
245+
providerName: "Codex",
246+
providerInstanceId: ProviderInstanceId.make("codex"),
247+
runtimeMode: "full-access",
248+
activeTurnId: null,
249+
lastError: null,
250+
updatedAt: NOW,
251+
},
252+
}),
253+
),
254+
).toBe("working");
255+
});
196256
});
197257

198258
describe("resolveThreadListV2SwipeActions", () => {

apps/mobile/src/features/threads/threadListV2.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,16 @@ export { snoozeWakeLabel };
2626
* Thread List v2 model, ported from the web sidebar v2
2727
* (apps/web/src/components/Sidebar.logic.ts + SidebarV2.tsx).
2828
*
29-
* Four visual states, three colors: color is reserved for "act now"
30-
* (approval), "in motion" (working), and "broken" (failed). Ready is the
31-
* unlabeled resting state.
29+
* Actionable attention, active work, failure, and Plan Ready receive explicit
30+
* labels. Ready is the unlabeled resting state.
3231
*/
33-
export type ThreadListV2Status = "approval" | "input" | "working" | "failed" | "ready";
32+
export type ThreadListV2Status =
33+
| "approval"
34+
| "input"
35+
| "working"
36+
| "failed"
37+
| "plan-ready"
38+
| "ready";
3439
export type ThreadListV2SwipeAction = "archive" | "settle" | "unsettle" | "snooze" | "unsnooze";
3540

3641
export interface ThreadListV2ChangeRequestState extends ChangeRequestSettleSource {
@@ -157,8 +162,23 @@ export function resolveThreadListV2Enabled(input: {
157162
return input.legacyPreference !== true;
158163
}
159164

165+
function isThreadListV2LatestTurnSettled(
166+
thread: Pick<EnvironmentThreadShell, "latestTurn" | "session">,
167+
): boolean {
168+
if (!thread.latestTurn?.startedAt || !thread.latestTurn.completedAt) return false;
169+
return thread.session?.status !== "running";
170+
}
171+
160172
export function resolveThreadListV2Status(
161-
thread: Pick<EnvironmentThreadShell, "hasPendingApprovals" | "hasPendingUserInput" | "session">,
173+
thread: Pick<
174+
EnvironmentThreadShell,
175+
| "hasActionableProposedPlan"
176+
| "hasPendingApprovals"
177+
| "hasPendingUserInput"
178+
| "interactionMode"
179+
| "latestTurn"
180+
| "session"
181+
>,
162182
): ThreadListV2Status {
163183
if (thread.hasPendingApprovals) {
164184
return "approval";
@@ -172,6 +192,13 @@ export function resolveThreadListV2Status(
172192
if (thread.session?.status === "error") {
173193
return "failed";
174194
}
195+
if (
196+
thread.interactionMode === "plan" &&
197+
thread.hasActionableProposedPlan &&
198+
isThreadListV2LatestTurnSettled(thread)
199+
) {
200+
return "plan-ready";
201+
}
175202
return "ready";
176203
}
177204

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { describe, expect, it } from "vite-plus/test";
2+
3+
import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
4+
5+
import { resolveThreadStatus } from "./threadPresentation";
6+
7+
const baseThread = {
8+
interactionMode: "default",
9+
hasActionableProposedPlan: false,
10+
hasPendingApprovals: false,
11+
hasPendingUserInput: false,
12+
latestTurn: null,
13+
session: null,
14+
} as EnvironmentThreadShell;
15+
16+
describe("resolveThreadStatus", () => {
17+
it("uses the orange warning treatment for user input", () => {
18+
expect(resolveThreadStatus({ ...baseThread, hasPendingUserInput: true })).toMatchObject({
19+
kind: "awaiting-input",
20+
pillClassName: "bg-warning-surface",
21+
textClassName: "text-warning-foreground",
22+
});
23+
});
24+
25+
it("uses the purple info role for plan-ready information", () => {
26+
expect(
27+
resolveThreadStatus({
28+
...baseThread,
29+
interactionMode: "plan",
30+
hasActionableProposedPlan: true,
31+
latestTurn: {
32+
startedAt: "2026-08-27T12:00:00.000Z",
33+
completedAt: "2026-08-27T12:01:00.000Z",
34+
},
35+
} as EnvironmentThreadShell),
36+
).toMatchObject({
37+
kind: "plan-ready",
38+
pillClassName: "bg-screen",
39+
textClassName: "text-status-info",
40+
});
41+
});
42+
43+
it.each(["running", "starting"] as const)(
44+
"uses the active theme role while the session is %s",
45+
(status) => {
46+
expect(
47+
resolveThreadStatus({
48+
...baseThread,
49+
session: { status },
50+
} as EnvironmentThreadShell),
51+
).toMatchObject({
52+
pillClassName: "bg-screen",
53+
textClassName: "text-status-active",
54+
});
55+
},
56+
);
57+
});

0 commit comments

Comments
 (0)