Skip to content

Commit e8cbd0f

Browse files
authored
feat: Add ability to dismiss individual queued messages (#927)
1 parent a0bd386 commit e8cbd0f

File tree

7 files changed

+76
-89
lines changed

7 files changed

+76
-89
lines changed

apps/twig/src/renderer/components/action-selector/ActionSelector.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,7 @@ export function ActionSelector({
195195
tabIndex={0}
196196
p="3"
197197
onClick={(e) => {
198-
if (
199-
e.target instanceof HTMLElement &&
200-
e.target.closest("[contenteditable]")
201-
) {
198+
if (e.target instanceof HTMLInputElement) {
202199
return;
203200
}
204201
containerRef.current?.focus();
Lines changed: 21 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { Box, Text } from "@radix-ui/themes";
2-
import { useCallback, useEffect } from "react";
1+
import { useCallback, useEffect, useRef } from "react";
32

43
interface InlineEditableTextProps {
54
value: string;
@@ -9,7 +8,6 @@ interface InlineEditableTextProps {
98
onNavigateDown: () => void;
109
onEscape: () => void;
1110
onSubmit: () => void;
12-
inputRef: React.RefObject<HTMLSpanElement | null>;
1311
}
1412

1513
export function InlineEditableText({
@@ -20,33 +18,15 @@ export function InlineEditableText({
2018
onNavigateDown,
2119
onEscape,
2220
onSubmit,
23-
inputRef,
2421
}: InlineEditableTextProps) {
25-
useEffect(() => {
26-
if (inputRef.current) {
27-
inputRef.current.textContent = value || "";
28-
inputRef.current.focus();
29-
if (value) {
30-
const range = document.createRange();
31-
range.selectNodeContents(inputRef.current);
32-
range.collapse(false);
33-
const sel = window.getSelection();
34-
sel?.removeAllRanges();
35-
sel?.addRange(range);
36-
}
37-
}
38-
}, [inputRef, value]);
22+
const nativeInputRef = useRef<HTMLInputElement>(null);
3923

40-
const handleInput = useCallback(
41-
(e: React.FormEvent<HTMLSpanElement>) => {
42-
const text = e.currentTarget.textContent ?? "";
43-
onChange(text);
44-
},
45-
[onChange],
46-
);
24+
useEffect(() => {
25+
nativeInputRef.current?.focus();
26+
}, []);
4727

4828
const handleKeyDown = useCallback(
49-
(e: React.KeyboardEvent<HTMLSpanElement>) => {
29+
(e: React.KeyboardEvent<HTMLInputElement>) => {
5030
if (e.key === "Escape") {
5131
e.preventDefault();
5232
onEscape();
@@ -65,54 +45,23 @@ export function InlineEditableText({
6545
);
6646

6747
return (
68-
<Box
48+
<input
49+
ref={nativeInputRef}
50+
type="text"
51+
value={value}
52+
placeholder={placeholder}
53+
onChange={(e) => onChange(e.target.value)}
54+
onKeyDown={handleKeyDown}
55+
onClick={(e) => e.stopPropagation()}
56+
className="text-gray-12 placeholder:text-gray-10"
6957
style={{
70-
display: "inline-grid",
58+
all: "unset",
59+
fontSize: "var(--font-size-1)",
60+
lineHeight: "var(--line-height-1)",
61+
fontWeight: 500,
7162
minWidth: "200px",
63+
display: "inline-block",
7264
}}
73-
>
74-
{!value && (
75-
<Text
76-
size="1"
77-
weight="medium"
78-
className="text-gray-10"
79-
style={{
80-
gridRow: 1,
81-
gridColumn: 1,
82-
pointerEvents: "none",
83-
userSelect: "none",
84-
whiteSpace: "pre-wrap",
85-
wordBreak: "break-word",
86-
}}
87-
>
88-
{placeholder}
89-
</Text>
90-
)}
91-
<Text
92-
asChild
93-
size="1"
94-
weight="medium"
95-
className={value ? "text-gray-12" : ""}
96-
>
97-
{/* biome-ignore lint/a11y/useSemanticElements: contentEditable span needed for inline editing UX */}
98-
<span
99-
ref={inputRef}
100-
role="textbox"
101-
tabIndex={0}
102-
contentEditable
103-
suppressContentEditableWarning
104-
onClick={(e) => e.stopPropagation()}
105-
onInput={handleInput}
106-
onKeyDown={handleKeyDown}
107-
style={{
108-
gridRow: 1,
109-
gridColumn: 1,
110-
outline: "none",
111-
whiteSpace: "pre-wrap",
112-
wordBreak: "break-word",
113-
}}
114-
/>
115-
</Text>
116-
</Box>
65+
/>
11766
);
11867
}

apps/twig/src/renderer/components/action-selector/OptionRow.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Box, Checkbox, Flex, Text } from "@radix-ui/themes";
22
import { compactHomePath } from "@utils/path";
3-
import { useRef } from "react";
43
import { isOtherOption, isSubmitOption } from "./constants";
54
import { InlineEditableText } from "./InlineEditableText";
65
import type { SelectorOption } from "./types";
@@ -56,8 +55,6 @@ export function OptionRow({
5655
onClick,
5756
onMouseEnter,
5857
}: OptionRowProps) {
59-
const inputRef = useRef<HTMLSpanElement>(null);
60-
6158
if (isSubmitOption(option.id)) {
6259
return (
6360
<Flex
@@ -102,7 +99,6 @@ export function OptionRow({
10299
onNavigateDown={onNavigateDown}
103100
onEscape={onEscape}
104101
onSubmit={onInlineSubmit}
105-
inputRef={inputRef}
106102
/>
107103
);
108104
}

apps/twig/src/renderer/features/sessions/components/ConversationView.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
} from "@agentclientprotocol/sdk";
55
import {
66
type QueuedMessage,
7+
sessionStoreSetters,
78
usePendingPermissionsForTask,
89
useQueuedMessagesForTask,
910
} from "@features/sessions/stores/sessionStore";
@@ -170,10 +171,23 @@ export function ConversationView({
170171
case "user_shell_execute":
171172
return <UserShellExecuteView item={item} />;
172173
case "queued":
173-
return <QueuedMessageView message={item.message} />;
174+
return (
175+
<QueuedMessageView
176+
message={item.message}
177+
onRemove={
178+
taskId
179+
? () =>
180+
sessionStoreSetters.removeQueuedMessage(
181+
taskId,
182+
item.message.id,
183+
)
184+
: undefined
185+
}
186+
/>
187+
);
174188
}
175189
},
176-
[repoPath],
190+
[repoPath, taskId],
177191
);
178192

179193
const getItemKey = useCallback((item: VirtualizedItem) => item.id, []);

apps/twig/src/renderer/features/sessions/components/session-update/QueuedMessageView.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,38 @@
11
import { MarkdownRenderer } from "@features/editor/components/MarkdownRenderer";
22
import type { QueuedMessage } from "@features/sessions/stores/sessionStore";
3-
import { Clock } from "@phosphor-icons/react";
4-
import { Box, Flex, Text } from "@radix-ui/themes";
3+
import { Clock, X } from "@phosphor-icons/react";
4+
import { Box, Flex, IconButton, Text } from "@radix-ui/themes";
55

66
interface QueuedMessageViewProps {
77
message: QueuedMessage;
8+
onRemove?: () => void;
89
}
910

10-
export function QueuedMessageView({ message }: QueuedMessageViewProps) {
11+
export function QueuedMessageView({
12+
message,
13+
onRemove,
14+
}: QueuedMessageViewProps) {
1115
return (
1216
<Box
13-
className="border-l-2 border-dashed bg-gray-2 py-2 pr-2 pl-3 opacity-70"
17+
className="group relative border-l-2 border-dashed bg-gray-2 py-2 pr-2 pl-3 opacity-70"
1418
style={{ borderColor: "var(--gray-8)" }}
1519
>
16-
<Box className="font-medium [&>*:last-child]:mb-0">
17-
<MarkdownRenderer content={message.content} />
18-
</Box>
20+
<Flex justify="between" align="start" gap="2">
21+
<Box className="min-w-0 flex-1 font-medium [&>*:last-child]:mb-0">
22+
<MarkdownRenderer content={message.content} />
23+
</Box>
24+
{onRemove && (
25+
<IconButton
26+
size="1"
27+
variant="ghost"
28+
color="gray"
29+
className="shrink-0 opacity-0 group-hover:opacity-100"
30+
onClick={onRemove}
31+
>
32+
<X size={12} />
33+
</IconButton>
34+
)}
35+
</Flex>
1936
<Flex align="center" gap="1" mt="1">
2037
<Clock size={12} className="text-gray-9" />
2138
<Text size="1" color="gray">

apps/twig/src/renderer/features/sessions/service/service.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const mockSessionStoreSetters = vi.hoisted(() => ({
4040
updateSession: vi.fn(),
4141
appendEvents: vi.fn(),
4242
enqueueMessage: vi.fn(),
43+
removeQueuedMessage: vi.fn(),
4344
clearMessageQueue: vi.fn(),
4445
dequeueMessagesAsText: vi.fn(() => null),
4546
setPendingPermissions: vi.fn(),

apps/twig/src/renderer/features/sessions/stores/sessionStore.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,19 @@ export const sessionStoreSetters = {
251251
});
252252
},
253253

254+
removeQueuedMessage: (taskId: string, messageId: string) => {
255+
useSessionStore.setState((state) => {
256+
const taskRunId = state.taskIdIndex[taskId];
257+
if (!taskRunId) return;
258+
const session = state.sessions[taskRunId];
259+
if (session) {
260+
session.messageQueue = session.messageQueue.filter(
261+
(msg) => msg.id !== messageId,
262+
);
263+
}
264+
});
265+
},
266+
254267
clearMessageQueue: (taskId: string) => {
255268
useSessionStore.setState((state) => {
256269
const taskRunId = state.taskIdIndex[taskId];

0 commit comments

Comments
 (0)