Skip to content

Commit 1a593ff

Browse files
authored
fix: show indicator on user input request (#411)
1 parent a752c3a commit 1a593ff

9 files changed

Lines changed: 295 additions & 2 deletions

File tree

src/features/app/components/PinnedThreadList.test.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,24 @@ describe("PinnedThreadList", () => {
101101
true,
102102
);
103103
});
104+
105+
it("shows blue unread-style status when a pinned thread is waiting for user input", () => {
106+
const { container } = render(
107+
<PinnedThreadList
108+
{...baseProps}
109+
rows={[{ thread: otherThread, depth: 0, workspaceId: "ws-2" }]}
110+
threadStatusById={{
111+
"thread-1": { isProcessing: false, hasUnread: false, isReviewing: true },
112+
"thread-2": { isProcessing: true, hasUnread: false, isReviewing: false },
113+
}}
114+
pendingUserInputKeys={new Set(["ws-2:thread-2"])}
115+
/>,
116+
);
117+
118+
const row = container.querySelector(".thread-row");
119+
expect(row).toBeTruthy();
120+
expect(row?.querySelector(".thread-name")?.textContent).toBe("Pinned Beta");
121+
expect(row?.querySelector(".thread-status")?.className).toContain("unread");
122+
expect(row?.querySelector(".thread-status")?.className).not.toContain("processing");
123+
});
104124
});

src/features/app/components/PinnedThreadList.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type PinnedThreadListProps = {
1818
activeWorkspaceId: string | null;
1919
activeThreadId: string | null;
2020
threadStatusById: ThreadStatusMap;
21+
pendingUserInputKeys?: Set<string>;
2122
getThreadTime: (thread: ThreadSummary) => string | null;
2223
isThreadPinned: (workspaceId: string, threadId: string) => boolean;
2324
onSelectThread: (workspaceId: string, threadId: string) => void;
@@ -34,6 +35,7 @@ export function PinnedThreadList({
3435
activeWorkspaceId,
3536
activeThreadId,
3637
threadStatusById,
38+
pendingUserInputKeys,
3739
getThreadTime,
3840
isThreadPinned,
3941
onSelectThread,
@@ -48,7 +50,12 @@ export function PinnedThreadList({
4850
? ({ "--thread-indent": `${depth * 14}px` } as CSSProperties)
4951
: undefined;
5052
const status = threadStatusById[thread.id];
51-
const statusClass = status?.isReviewing
53+
const hasPendingUserInput = Boolean(
54+
pendingUserInputKeys?.has(`${workspaceId}:${thread.id}`),
55+
);
56+
const statusClass = hasPendingUserInput
57+
? "unread"
58+
: status?.isReviewing
5259
? "reviewing"
5360
: status?.isProcessing
5461
? "processing"

src/features/app/components/Sidebar.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {
22
AccountSnapshot,
3+
RequestUserInputRequest,
34
RateLimitSnapshot,
45
ThreadListSortKey,
56
ThreadSummary,
@@ -66,6 +67,7 @@ type SidebarProps = {
6667
onRefreshAllThreads: () => void;
6768
activeWorkspaceId: string | null;
6869
activeThreadId: string | null;
70+
userInputRequests?: RequestUserInputRequest[];
6971
accountRateLimits: RateLimitSnapshot | null;
7072
usageShowRemaining: boolean;
7173
accountInfo: AccountSnapshot | null;
@@ -122,6 +124,7 @@ export const Sidebar = memo(function Sidebar({
122124
onRefreshAllThreads,
123125
activeWorkspaceId,
124126
activeThreadId,
127+
userInputRequests = [],
125128
accountRateLimits,
126129
usageShowRemaining,
127130
accountInfo,
@@ -197,6 +200,19 @@ export const Sidebar = memo(function Sidebar({
197200
} = getUsageLabels(accountRateLimits, usageShowRemaining);
198201
const debouncedQuery = useDebouncedValue(searchQuery, 150);
199202
const normalizedQuery = debouncedQuery.trim().toLowerCase();
203+
const pendingUserInputKeys = useMemo(
204+
() =>
205+
new Set(
206+
userInputRequests
207+
.map((request) => {
208+
const workspaceId = request.workspace_id.trim();
209+
const threadId = request.params.thread_id.trim();
210+
return workspaceId && threadId ? `${workspaceId}:${threadId}` : "";
211+
})
212+
.filter(Boolean),
213+
),
214+
[userInputRequests],
215+
);
200216

201217
const isWorkspaceMatch = useCallback(
202218
(workspace: WorkspaceInfo) => {
@@ -484,6 +500,7 @@ export const Sidebar = memo(function Sidebar({
484500
activeWorkspaceId={activeWorkspaceId}
485501
activeThreadId={activeThreadId}
486502
threadStatusById={threadStatusById}
503+
pendingUserInputKeys={pendingUserInputKeys}
487504
getThreadTime={getThreadTime}
488505
isThreadPinned={isThreadPinned}
489506
onSelectThread={onSelectThread}
@@ -635,6 +652,7 @@ export const Sidebar = memo(function Sidebar({
635652
expandedWorkspaces={expandedWorkspaces}
636653
activeWorkspaceId={activeWorkspaceId}
637654
activeThreadId={activeThreadId}
655+
pendingUserInputKeys={pendingUserInputKeys}
638656
getThreadRows={getThreadRows}
639657
getThreadTime={getThreadTime}
640658
isThreadPinned={isThreadPinned}
@@ -661,6 +679,7 @@ export const Sidebar = memo(function Sidebar({
661679
activeWorkspaceId={activeWorkspaceId}
662680
activeThreadId={activeThreadId}
663681
threadStatusById={threadStatusById}
682+
pendingUserInputKeys={pendingUserInputKeys}
664683
getThreadTime={getThreadTime}
665684
isThreadPinned={isThreadPinned}
666685
onToggleExpanded={handleToggleExpanded}

src/features/app/components/ThreadList.test.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,23 @@ describe("ThreadList", () => {
133133
false,
134134
);
135135
});
136+
137+
it("shows blue unread-style status when a thread is waiting for user input", () => {
138+
const { container } = render(
139+
<ThreadList
140+
{...baseProps}
141+
threadStatusById={{
142+
"thread-1": { isProcessing: true, hasUnread: false, isReviewing: false },
143+
"thread-2": { isProcessing: false, hasUnread: false, isReviewing: false },
144+
}}
145+
pendingUserInputKeys={new Set(["ws-1:thread-1"])}
146+
/>,
147+
);
148+
149+
const row = container.querySelector(".thread-row");
150+
expect(row).toBeTruthy();
151+
expect(row?.querySelector(".thread-name")?.textContent).toBe("Alpha");
152+
expect(row?.querySelector(".thread-status")?.className).toContain("unread");
153+
expect(row?.querySelector(".thread-status")?.className).not.toContain("processing");
154+
});
136155
});

src/features/app/components/ThreadList.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type ThreadListProps = {
2525
activeWorkspaceId: string | null;
2626
activeThreadId: string | null;
2727
threadStatusById: ThreadStatusMap;
28+
pendingUserInputKeys?: Set<string>;
2829
getThreadTime: (thread: ThreadSummary) => string | null;
2930
isThreadPinned: (workspaceId: string, threadId: string) => boolean;
3031
onToggleExpanded: (workspaceId: string) => void;
@@ -51,6 +52,7 @@ export function ThreadList({
5152
activeWorkspaceId,
5253
activeThreadId,
5354
threadStatusById,
55+
pendingUserInputKeys,
5456
getThreadTime,
5557
isThreadPinned,
5658
onToggleExpanded,
@@ -66,7 +68,12 @@ export function ThreadList({
6668
? ({ "--thread-indent": `${depth * indentUnit}px` } as CSSProperties)
6769
: undefined;
6870
const status = threadStatusById[thread.id];
69-
const statusClass = status?.isReviewing
71+
const hasPendingUserInput = Boolean(
72+
pendingUserInputKeys?.has(`${workspaceId}:${thread.id}`),
73+
);
74+
const statusClass = hasPendingUserInput
75+
? "unread"
76+
: status?.isReviewing
7077
? "reviewing"
7178
: status?.isProcessing
7279
? "processing"

src/features/app/components/WorktreeSection.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type WorktreeSectionProps = {
2929
expandedWorkspaces: Set<string>;
3030
activeWorkspaceId: string | null;
3131
activeThreadId: string | null;
32+
pendingUserInputKeys?: Set<string>;
3233
getThreadRows: (
3334
threads: ThreadSummary[],
3435
isExpanded: boolean,
@@ -64,6 +65,7 @@ export function WorktreeSection({
6465
expandedWorkspaces,
6566
activeWorkspaceId,
6667
activeThreadId,
68+
pendingUserInputKeys,
6769
getThreadRows,
6870
getThreadTime,
6971
isThreadPinned,
@@ -136,6 +138,7 @@ export function WorktreeSection({
136138
activeWorkspaceId={activeWorkspaceId}
137139
activeThreadId={activeThreadId}
138140
threadStatusById={threadStatusById}
141+
pendingUserInputKeys={pendingUserInputKeys}
139142
getThreadTime={getThreadTime}
140143
isThreadPinned={isThreadPinned}
141144
onToggleExpanded={onToggleExpanded}

src/features/layout/hooks/layoutNodes/buildPrimaryNodes.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export function buildPrimaryNodes(options: LayoutNodesOptions): PrimaryLayoutNod
5050
onRefreshAllThreads={options.onRefreshAllThreads}
5151
activeWorkspaceId={options.activeWorkspaceId}
5252
activeThreadId={options.activeThreadId}
53+
userInputRequests={options.userInputRequests}
5354
accountRateLimits={options.activeRateLimits}
5455
usageShowRemaining={options.usageShowRemaining}
5556
accountInfo={options.accountInfo}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// @vitest-environment jsdom
2+
import { act, renderHook } from "@testing-library/react";
3+
import { describe, expect, it, vi } from "vitest";
4+
import { useThreadUserInputEvents } from "./useThreadUserInputEvents";
5+
6+
describe("useThreadUserInputEvents", () => {
7+
it("queues request user input without clearing turn state", () => {
8+
const dispatch = vi.fn();
9+
10+
const { result } = renderHook(() =>
11+
useThreadUserInputEvents({
12+
dispatch,
13+
}),
14+
);
15+
16+
const request = {
17+
workspace_id: "ws-1",
18+
request_id: "req-1",
19+
params: {
20+
thread_id: "thread-1",
21+
turn_id: "turn-1",
22+
item_id: "item-1",
23+
questions: [],
24+
},
25+
};
26+
27+
act(() => {
28+
result.current(request);
29+
});
30+
31+
expect(dispatch).toHaveBeenCalledWith({ type: "addUserInputRequest", request });
32+
});
33+
});

0 commit comments

Comments
 (0)