Skip to content

Commit 1192fd9

Browse files
allquixoticoz-agent
andcommitted
Improve chat privacy and draft focus
Add a display-only recent conversation privacy toggle with a keyboard shortcut, preserve queued-message editor caret position during chat updates, and keep new task drafts focused in the chat pane. Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent e00765c commit 1192fd9

5 files changed

Lines changed: 392 additions & 40 deletions

File tree

src/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "%extension.displayName%",
44
"description": "%extension.description%",
55
"publisher": "allquixotic",
6-
"version": "3.53.17",
6+
"version": "3.53.18",
77
"icon": "assets/icons/icon.png",
88
"galleryBanner": {
99
"color": "#617A91",

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 123 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@ import { appendImages } from "@src/utils/imageUtils"
1010
import { getCostBreakdownIfNeeded } from "@src/utils/costFormatting"
1111
import { batchConsecutive } from "@src/utils/batchConsecutive"
1212

13-
import type { ClineAsk, ClineSayTool, ClineMessage, ExtensionMessage, AudioType } from "@roo-code/types"
13+
import type {
14+
ClineAsk,
15+
ClineSayTool,
16+
ClineMessage,
17+
ExtensionMessage,
18+
AudioType,
19+
TodoItem,
20+
QueuedMessage,
21+
} from "@roo-code/types"
1422
import { isRetiredProvider } from "@roo-code/types"
1523

1624
import { findLast } from "@roo/array"
@@ -62,6 +70,12 @@ interface DraftConversation {
6270
title: string
6371
}
6472

73+
const EMPTY_MESSAGES: ClineMessage[] = []
74+
const EMPTY_TODOS: TodoItem[] = []
75+
const EMPTY_MESSAGE_QUEUE: QueuedMessage[] = []
76+
const RECENT_CONVERSATIONS_HIDDEN_STORAGE_KEY = "crc.recentConversationsHidden"
77+
const RECENT_CONVERSATIONS_TOGGLE_SHORTCUT = "Ctrl+Shift+H"
78+
6579
const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewProps> = ({ isHidden }, ref) => {
6680
const [audioBaseUri] = useState(() => {
6781
return (window as unknown as { AUDIO_BASE_URI?: string }).AUDIO_BASE_URI || ""
@@ -71,10 +85,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
7185
const modeShortcutText = `${isMac ? "⌘" : "Ctrl"} + . ${t("chat:forNextMode")}, ${isMac ? "⌘" : "Ctrl"} + Shift + . ${t("chat:forPreviousMode")}`
7286

7387
const {
74-
clineMessages: messages,
75-
currentTaskId,
76-
currentTaskItem,
77-
currentTaskTodos,
88+
clineMessages: contextMessages,
89+
currentTaskId: contextCurrentTaskId,
90+
currentTaskItem: contextCurrentTaskItem,
91+
currentTaskTodos: contextCurrentTaskTodos,
7892
activeConversations = [],
7993
taskHistory,
8094
apiConfiguration,
@@ -85,12 +99,34 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
8599
customModes,
86100
soundEnabled,
87101
soundVolume,
88-
messageQueue = [],
102+
messageQueue: contextMessageQueue = EMPTY_MESSAGE_QUEUE,
89103
showWorktreesInHomeScreen,
90104
currentAskDecision,
91105
} = useExtensionState()
92106
const [draftConversations, setDraftConversations] = useState<DraftConversation[]>([])
93107
const [selectedDraftId, setSelectedDraftId] = useState<string | undefined>(undefined)
108+
const [areRecentConversationsHidden, setAreRecentConversationsHidden] = useState(() => {
109+
try {
110+
return window.localStorage.getItem(RECENT_CONVERSATIONS_HIDDEN_STORAGE_KEY) === "true"
111+
} catch {
112+
return false
113+
}
114+
})
115+
const isDraftSelected = selectedDraftId !== undefined
116+
const messages = useMemo(
117+
() => (isDraftSelected ? EMPTY_MESSAGES : contextMessages),
118+
[contextMessages, isDraftSelected],
119+
)
120+
const currentTaskId = isDraftSelected ? undefined : contextCurrentTaskId
121+
const currentTaskItem = isDraftSelected ? undefined : contextCurrentTaskItem
122+
const currentTaskTodos = useMemo(
123+
() => (isDraftSelected ? EMPTY_TODOS : contextCurrentTaskTodos),
124+
[contextCurrentTaskTodos, isDraftSelected],
125+
)
126+
const messageQueue = useMemo(
127+
() => (isDraftSelected ? EMPTY_MESSAGE_QUEUE : contextMessageQueue),
128+
[contextMessageQueue, isDraftSelected],
129+
)
94130

95131
const visibleConversationIds = useMemo(
96132
() =>
@@ -112,6 +148,17 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
112148
}, [selectedDraftId, visibleConversationIds])
113149

114150
const selectedConversationId = selectedDraftId ?? currentTaskId
151+
const toggleRecentConversationsVisibility = useCallback(() => {
152+
setAreRecentConversationsHidden((prev) => {
153+
const next = !prev
154+
try {
155+
window.localStorage.setItem(RECENT_CONVERSATIONS_HIDDEN_STORAGE_KEY, String(next))
156+
} catch {
157+
// Ignore storage failures; the in-memory display toggle still works.
158+
}
159+
return next
160+
})
161+
}, [])
115162

116163
const conversations = useMemo<ConversationListItem[]>(() => {
117164
const draftItems = draftConversations
@@ -785,7 +832,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
785832
// Mark that user has responded - this prevents any pending auto-approvals.
786833
userRespondedRef.current = true
787834

788-
if (messagesRef.current.length === 0) {
835+
if (selectedDraftId || messagesRef.current.length === 0) {
789836
vscode.postMessage({ type: "newTask", taskId: selectedDraftId, text, images })
790837
} else if (clineAskRef.current) {
791838
if (clineAskRef.current === "followup") {
@@ -885,20 +932,41 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
885932
},
886933
[inputValue, selectedImages],
887934
)
888-
889-
const startNewTask = useCallback(() => {
890-
const draftId = `draft-${typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : Date.now().toString(36)}`
935+
const resetDraftSelectionState = useCallback(() => {
891936
setShowRetiredProviderWarning(false)
892937
setInputValue("")
893938
setSelectedImages([])
939+
setSendingDisabled(false)
940+
setClineAsk(undefined)
941+
setEnableButtons(false)
942+
setPrimaryButtonText(undefined)
943+
setSecondaryButtonText(undefined)
944+
setDidClickCancel(false)
945+
setExpandedRows({})
946+
setCurrentFollowUpTs(null)
947+
setCheckpointWarning(undefined)
948+
everVisibleMessagesTsRef.current.clear()
949+
950+
if (autoApproveTimeoutRef.current) {
951+
clearTimeout(autoApproveTimeoutRef.current)
952+
autoApproveTimeoutRef.current = null
953+
}
954+
955+
userRespondedRef.current = false
956+
}, [])
957+
958+
const startNewTask = useCallback(() => {
959+
const draftId = `draft-${typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : Date.now().toString(36)}`
960+
resetDraftSelectionState()
894961
setSelectedDraftId(draftId)
895962
setDraftConversations((prev) => [{ id: draftId, ts: Date.now(), title: "New conversation" }, ...prev])
896963
vscode.postMessage({ type: "clearTask" })
897-
}, [])
964+
}, [resetDraftSelectionState])
898965

899966
const handleSelectConversation = useCallback(
900967
(conversation: ConversationListItem) => {
901968
if (conversation.kind === "draft") {
969+
resetDraftSelectionState()
902970
setSelectedDraftId(conversation.activeTaskId)
903971
if (currentTaskId) {
904972
vscode.postMessage({ type: "clearTask" })
@@ -909,7 +977,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
909977
setSelectedDraftId(undefined)
910978
vscode.postMessage({ type: "showTaskWithId", text: conversation.activeTaskId })
911979
},
912-
[currentTaskId],
980+
[currentTaskId, resetDraftSelectionState],
913981
)
914982

915983
const handleDeleteConversation = useCallback((conversation: ConversationListItem) => {
@@ -983,6 +1051,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
9831051
)
9841052
if (isCompletedSubtaskForClick) {
9851053
startNewTask()
1054+
return
9861055
} else {
9871056
// Only send text/images if they exist
9881057
if (trimmedInput || (images && images.length > 0)) {
@@ -1009,7 +1078,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
10091078
case "resume_completed_task":
10101079
// Waiting for feedback, but we can just present a new task button
10111080
startNewTask()
1012-
break
1081+
return
10131082
case "command_output":
10141083
vscode.postMessage({
10151084
type: "terminalOperation",
@@ -1046,7 +1115,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
10461115
case "mistake_limit_reached":
10471116
case "resume_task":
10481117
startNewTask()
1049-
break
1118+
return
10501119
case "command":
10511120
case "tool":
10521121
case "use_mcp_server":
@@ -1762,6 +1831,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
17621831
// (PageUp, Home, ArrowUp) is handled by useScrollLifecycle.
17631832
const handleKeyDown = useCallback(
17641833
(event: KeyboardEvent) => {
1834+
if (event.ctrlKey && event.shiftKey && !event.altKey && !event.metaKey && event.key.toLowerCase() === "h") {
1835+
event.preventDefault()
1836+
toggleRecentConversationsVisibility()
1837+
return
1838+
}
17651839
if ((event.metaKey || event.ctrlKey) && event.key === ".") {
17661840
event.preventDefault()
17671841
if (event.shiftKey) {
@@ -1771,7 +1845,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
17711845
}
17721846
}
17731847
},
1774-
[switchToNextMode, switchToPreviousMode],
1848+
[switchToNextMode, switchToPreviousMode, toggleRecentConversationsVisibility],
17751849
)
17761850

17771851
useEffect(() => {
@@ -1877,7 +1951,40 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
18771951
{/* Show RooTips when authenticated or when user is new */}
18781952
{taskHistory.length < 6 && <RooTips />}
18791953
{/* Everyone should see their task history if any */}
1880-
{taskHistory.length > 0 && <HistoryPreview />}
1954+
{taskHistory.length > 0 && (
1955+
<div className="flex flex-col gap-2">
1956+
<div className="flex justify-end">
1957+
<Button
1958+
variant="secondary"
1959+
size="sm"
1960+
className="h-7 px-2 text-xs"
1961+
onClick={toggleRecentConversationsVisibility}
1962+
aria-label={
1963+
areRecentConversationsHidden
1964+
? `Show recent conversations (${RECENT_CONVERSATIONS_TOGGLE_SHORTCUT})`
1965+
: `Hide recent conversations (${RECENT_CONVERSATIONS_TOGGLE_SHORTCUT})`
1966+
}>
1967+
{areRecentConversationsHidden
1968+
? "Show recent conversations"
1969+
: "Hide recent conversations"}
1970+
<span className="ml-2 text-[10px] opacity-70">
1971+
{RECENT_CONVERSATIONS_TOGGLE_SHORTCUT}
1972+
</span>
1973+
</Button>
1974+
</div>
1975+
{areRecentConversationsHidden ? (
1976+
<div
1977+
className="rounded-md border border-vscode-editorGroup-border bg-vscode-editor-background/60 px-3 py-2 text-sm text-vscode-descriptionForeground"
1978+
data-testid="recent-conversations-hidden">
1979+
Recent conversations are hidden for privacy. Your history is still
1980+
saved; use the button or {RECENT_CONVERSATIONS_TOGGLE_SHORTCUT} to
1981+
show it again.
1982+
</div>
1983+
) : (
1984+
<HistoryPreview />
1985+
)}
1986+
</div>
1987+
)}
18811988
</div>
18821989
</div>
18831990
</div>

webview-ui/src/components/chat/QueuedMessages.tsx

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from "react"
1+
import { useLayoutEffect, useRef, useState } from "react"
22
import { useTranslation } from "react-i18next"
33

44
import { QueuedMessage } from "@roo-code/types"
@@ -14,6 +14,51 @@ interface QueuedMessagesProps {
1414
onRemove: (messageId: string) => void
1515
onUpdate: (message: QueuedMessage, updates: { text?: string; deliveryMode?: QueuedMessage["deliveryMode"] }) => void
1616
}
17+
interface QueuedMessageEditorProps {
18+
value: string
19+
onChange: (value: string) => void
20+
onSave: () => void
21+
onCancel: () => void
22+
placeholder: string
23+
rows: number
24+
}
25+
26+
const QueuedMessageEditor = ({ value, onChange, onSave, onCancel, placeholder, rows }: QueuedMessageEditorProps) => {
27+
const textareaRef = useRef<HTMLTextAreaElement | null>(null)
28+
const didPlaceInitialCaretRef = useRef(false)
29+
30+
useLayoutEffect(() => {
31+
if (didPlaceInitialCaretRef.current || !textareaRef.current) {
32+
return
33+
}
34+
35+
didPlaceInitialCaretRef.current = true
36+
const textarea = textareaRef.current
37+
textarea.focus()
38+
textarea.setSelectionRange(textarea.value.length, textarea.value.length)
39+
}, [])
40+
41+
return (
42+
<textarea
43+
ref={textareaRef}
44+
value={value}
45+
onChange={(e) => onChange(e.target.value)}
46+
onBlur={onSave}
47+
onKeyDown={(e) => {
48+
if (e.key === "Enter" && !e.shiftKey) {
49+
e.preventDefault()
50+
onSave()
51+
}
52+
if (e.key === "Escape") {
53+
onCancel()
54+
}
55+
}}
56+
className="w-full bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded px-2 py-1 resize-none focus:outline-0 focus:ring-1 focus:ring-vscode-focusBorder"
57+
placeholder={placeholder}
58+
rows={rows}
59+
/>
60+
)
61+
}
1762

1863
export const QueuedMessages = ({ queue, onRemove, onUpdate }: QueuedMessagesProps) => {
1964
const { t } = useTranslation("chat")
@@ -37,6 +82,9 @@ export const QueuedMessages = ({ queue, onRemove, onUpdate }: QueuedMessagesProp
3782
onUpdate(message, { text: newValue })
3883
setEditState(message.id, false)
3984
}
85+
const handleCancelEdit = (message: QueuedMessage) => {
86+
setEditState(message.id, false, message.text)
87+
}
4088

4189
const renderLane = (
4290
title: string,
@@ -75,30 +123,12 @@ export const QueuedMessages = ({ queue, onRemove, onUpdate }: QueuedMessagesProp
75123
</span>
76124
</div>
77125
{editState.isEditing ? (
78-
<textarea
79-
ref={(textarea) => {
80-
if (textarea) {
81-
textarea.setSelectionRange(
82-
textarea.value.length,
83-
textarea.value.length,
84-
)
85-
}
86-
}}
126+
<QueuedMessageEditor
87127
value={editState.value}
88-
onChange={(e) => setEditState(message.id, true, e.target.value)}
89-
onBlur={() => handleSaveEdit(message, editState.value)}
90-
onKeyDown={(e) => {
91-
if (e.key === "Enter" && !e.shiftKey) {
92-
e.preventDefault()
93-
handleSaveEdit(message, editState.value)
94-
}
95-
if (e.key === "Escape") {
96-
setEditState(message.id, false, message.text)
97-
}
98-
}}
99-
className="w-full bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded px-2 py-1 resize-none focus:outline-0 focus:ring-1 focus:ring-vscode-focusBorder"
128+
onChange={(value) => setEditState(message.id, true, value)}
129+
onSave={() => handleSaveEdit(message, editState.value)}
130+
onCancel={() => handleCancelEdit(message)}
100131
placeholder={t("chat:editMessage.placeholder")}
101-
autoFocus
102132
rows={Math.min(editState.value.split("\n").length, 10)}
103133
/>
104134
) : (

0 commit comments

Comments
 (0)