Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
26 changes: 20 additions & 6 deletions apps/mobile/src/features/threads/thread-list-v2-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,20 @@ interface StatusLabel {
readonly className: string;
readonly iconTintClassName: string;
}
const INPUT_STATUS_LABEL: StatusLabel = {
label: "Input",
icon: "questionmark.bubble",
className: "text-adaptive-indigo-600-300",
iconTintClassName: "accent-adaptive-indigo-600-300",
};
const STATUS_LABEL_BY_STATUS: Partial<Record<ThreadListV2Status, StatusLabel>> = {
approval: {
label: "Approval",
icon: "exclamationmark.shield",
className: "text-warning-foreground",
iconTintClassName: "accent-warning-foreground",
},
input: {
label: "Input",
icon: "questionmark.bubble",
className: "text-adaptive-indigo-600-300",
iconTintClassName: "accent-adaptive-indigo-600-300",
},
input: INPUT_STATUS_LABEL,
request: {
label: "Request",
icon: "person.crop.circle",
Expand Down Expand Up @@ -1131,6 +1132,19 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
<ThreadListV2Duration startedAt={thread.latestRun?.completedAt ?? null} />
) : null}
</Text>
{thread.hasPendingAsyncQuestion === true ? (
<SymbolView
name={INPUT_STATUS_LABEL.icon}
Comment thread
tzachbon marked this conversation as resolved.
Resolved
size={13}
tintColorClassName={
selected
? selectedThreadRowColors.iconTintClassName
: INPUT_STATUS_LABEL.iconTintClassName
}
type="monochrome"
weight="semibold"
/>
) : null}
</View>
) : (
<Text
Expand Down
29 changes: 23 additions & 6 deletions apps/web/src/components/sidebar/SidebarThreadRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,21 @@ function TopStatusIcon(props: { icon: keyof typeof TOP_STATUS_ICONS; className:
return <Icon aria-hidden className={cn("shrink-0", props.className)} />;
}

const INPUT_STATUS = {
label: "Input",
icon: "input",
className: "text-indigo-600 dark:text-indigo-300",
} as const;

function AsyncQuestionMarker(props: { className: string }) {
return (
<TopStatusIcon
icon={INPUT_STATUS.icon}
className={cn(INPUT_STATUS.className, props.className)}
/>
);
}

type SidebarThreadRowProps = {
thread: SidebarThreadSummary;
// "folder" is a one-line row inside a boring sidebar project folder;
Expand Down Expand Up @@ -740,11 +755,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
className: "text-warning-foreground",
}
: status === "input"
? {
label: "Input",
icon: "input" as const,
className: "text-indigo-600 dark:text-indigo-300",
}
? INPUT_STATUS
: status === "request"
? {
label: "Request",
Expand Down Expand Up @@ -777,6 +788,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
}
: null;
const isWokeStatus = topStatus?.icon === "woke";
const showsAsyncQuestion = thread.hasPendingAsyncQuestion === true && !isWokeStatus;

const branchMismatch = resolveLocalCheckoutBranchMismatch({
effectiveEnvMode: thread.worktreePath === null ? "local" : "worktree",
Expand Down Expand Up @@ -997,7 +1009,10 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP

const accessibility = resolveSidebarRowAccessibility({
title: thread.title,
statusLabel: topStatus?.label ?? null,
statusLabel:
topStatus && showsAsyncQuestion
? `${topStatus.label}, question waiting`
: (topStatus?.label ?? null),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
projectDisplayName: props.projectDisplayName,
isActive: props.isActive,
});
Expand Down Expand Up @@ -1305,6 +1320,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
>
<TopStatusIcon icon={topStatus.icon} className="size-3" />
{topStatus.label}
{showsAsyncQuestion ? <AsyncQuestionMarker className="size-3" /> : null}
</span>
) : (
<span className="text-xs">
Expand Down Expand Up @@ -1496,6 +1512,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
<WorkingDuration startedAt={thread.latestRun?.completedAt ?? null} />
</span>
) : null}
{showsAsyncQuestion ? <AsyncQuestionMarker className="size-4" /> : null}
</span>
)
) : (
Expand Down
3 changes: 3 additions & 0 deletions packages/client-runtime/src/state/entities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe("V2 client presentation", () => {
};
const active = presentThreadShell(environmentId, running);
expect(active.hasPendingUserInput).toBe(false);
expect(active.hasPendingAsyncQuestion).toBe(true);
expect(isThreadWorking(active)).toBe(true);
expect(canSnooze(active, { now: "2026-06-20T00:05:00.000Z" })).toBe(false);

Expand All @@ -49,6 +50,7 @@ describe("V2 client presentation", () => {
status: "completed",
});
expect(completed.hasPendingUserInput).toBe(true);
expect(completed.hasPendingAsyncQuestion).toBe(false);
expect(isThreadWorking(completed)).toBe(false);
expect(effectiveSnoozed(active, { now: "2026-06-20T00:05:00.000Z" })).toBe(false);
});
Expand All @@ -68,6 +70,7 @@ describe("V2 client presentation", () => {
},
});
expect(shell.hasPendingUserInput).toBe(true);
expect(shell.hasPendingAsyncQuestion).toBe(false);
expect(isThreadWorking(shell)).toBe(false);
}
});
Expand Down
10 changes: 7 additions & 3 deletions packages/client-runtime/src/state/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ export interface EnvironmentThreadShell {
readonly latestUserAuthoredMessageAt?: string | null;
readonly hasPendingApprovals: boolean;
readonly hasPendingUserInput: boolean;
/** An async question the agent keeps working past; the thread stays Working. */
readonly hasPendingAsyncQuestion?: boolean;
readonly pendingSubagentRequest?: PendingSubagentRequest | null;
/** Guest messages waiting for the owner to send or dismiss. */
readonly pendingGuestMessageCount?: number;
Expand Down Expand Up @@ -282,6 +284,9 @@ export function presentThreadShell(
runtime?.status === "preparing" ||
runtime?.status === "starting" ||
runtime?.status === "running";
const hasPendingQuestion = thread.pendingRuntimeRequest?.kind === "user_input";
const questionHoldsThread =
hasPendingQuestion && (thread.pendingRuntimeRequest?.blocking !== false || !runtimeIsWorking);
return {
environmentId,
id: thread.id,
Expand Down Expand Up @@ -315,9 +320,8 @@ export function presentThreadShell(
thread.pendingRuntimeRequest !== null &&
thread.pendingRuntimeRequest.kind !== "user_input" &&
thread.pendingRuntimeRequest.kind !== "auth_refresh",
hasPendingUserInput:
thread.pendingRuntimeRequest?.kind === "user_input" &&
(thread.pendingRuntimeRequest.blocking !== false || !runtimeIsWorking),
hasPendingUserInput: questionHoldsThread,
hasPendingAsyncQuestion: hasPendingQuestion && !questionHoldsThread,
pendingSubagentRequest,
pendingGuestMessageCount: thread.pendingGuestMessageCount ?? 0,
hasActionableProposedPlan: thread.hasActionableProposedPlan,
Expand Down
Loading