Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 15 additions & 17 deletions apps/mobile/src/screens/thread/ThreadDetailHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Pressable, View } from "react-native";
import { useTheme } from "@/theme";
import { cn, Icon, Spinner, Text } from "@/ui";
import { cn, Icon, Text } from "@/ui";
import { PanelToggleButton } from "../panel/PanelToggleButton";
import type { ThreadStatusPill } from "./thread-detail-header-model";

/**
* The thread screen's native header pieces. There is one header only: the
* title (tap to rename) with a status subtitle while the thread needs
* attention or works, and two buttons on the right — the workspace panel
* attention, has an error, or waits on a host, and two buttons on the right — the workspace panel
* and the "…" menu. Everything else the old two-layer header carried
* (environment line, child roll-up, git action) lives in the menu sheet.
*/
Expand All @@ -21,13 +21,18 @@ export interface ThreadHeaderTitleProps {
onPressTitle: (() => void) | null;
}

/** Subtitle shown under the title; idle / archived threads show none. */
/**
* Subtitle shown under the title. Idle threads show none, and working threads
* show none either: the timeline's working indicator already carries that.
*/
export function headerSubtitle(
statusPill: ThreadStatusPill,
childPillLabel: ThreadHeaderTitleProps["childPillLabel"],
): string | null {
const parts: string[] = [];
if (statusPill.tone !== "idle") parts.push(statusPill.label);
if (statusPill.tone !== "idle" && statusPill.tone !== "working") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 slopcop/review — This policy change has no direct test. A table test should cover each tone and child label.

parts.push(statusPill.label);
}
if (childPillLabel) parts.push(childPillLabel);
return parts.length > 0 ? parts.join(" · ") : null;
}
Expand Down Expand Up @@ -68,21 +73,14 @@ export function ThreadHeaderTitle({
{title}
</Text>
{subtitle ? (
<View
className="flex-row items-center gap-1"
<Text
variant="caption"
numberOfLines={1}
style={{ color: subtitleColor }}
testID="thread-status-pill"
>
{statusPill.spinning ? (
<Spinner size="small" color={subtitleColor} />
) : null}
<Text
variant="caption"
numberOfLines={1}
style={{ color: subtitleColor }}
>
{subtitle}
</Text>
</View>
{subtitle}
</Text>
) : null}
</Pressable>
);
Expand Down
29 changes: 14 additions & 15 deletions apps/mobile/src/screens/thread/thread-detail-header-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { assertNever } from "@bb/thread-view";
/**
* Pure header facts for the thread detail screen: the status pill (from the
* client-core runtime display status, with the thread's own status and
* pending input layered on) and the one-line environment summary.
* pending input layered on) and the one-line environment summary. The header
* hides "working" tones; the timeline's working indicator already shows them.
*/

export type ThreadStatusPillTone =
Expand All @@ -23,8 +24,6 @@ export type ThreadStatusPillTone =
export interface ThreadStatusPill {
label: string;
tone: ThreadStatusPillTone;
/** Shows a spinner glyph instead of a static one. */
spinning: boolean;
}

export function describeThreadStatusPill({
Expand All @@ -39,33 +38,33 @@ export function describeThreadStatusPill({
archived: boolean;
}): ThreadStatusPill {
if (hasPendingInteraction) {
return { label: "Needs input", tone: "attention", spinning: false };
return { label: "Needs input", tone: "attention" };
}
if (threadStatus === "stopping") {
return { label: "Stopping", tone: "working", spinning: true };
return { label: "Stopping", tone: "working" };
}
switch (runtimeDisplayStatus) {
case "active":
return { label: "Working", tone: "working", spinning: true };
return { label: "Working", tone: "working" };
case "provisioning":
return { label: "Provisioning", tone: "working", spinning: true };
return { label: "Provisioning", tone: "working" };
case "starting":
return { label: "Starting", tone: "working", spinning: true };
return { label: "Starting", tone: "working" };
case "stopping":
return { label: "Stopping", tone: "working", spinning: true };
return { label: "Stopping", tone: "working" };
case "host-reconnecting":
return { label: "Reconnecting", tone: "working", spinning: true };
return { label: "Reconnecting", tone: "working" };
case "waiting-for-host":
return { label: "Waiting for host", tone: "muted", spinning: false };
return { label: "Waiting for host", tone: "muted" };
case "error":
return { label: "Error", tone: "error", spinning: false };
return { label: "Error", tone: "error" };
case "idle":
if (threadStatus === "error") {
return { label: "Error", tone: "error", spinning: false };
return { label: "Error", tone: "error" };
}
return archived
? { label: "Archived", tone: "muted", spinning: false }
: { label: "Idle", tone: "idle", spinning: false };
? { label: "Archived", tone: "muted" }
: { label: "Idle", tone: "idle" };
default:
return assertNever(runtimeDisplayStatus);
}
Expand Down
Loading