Skip to content

Commit bfd5024

Browse files
authored
Merge pull request RooCodeInc#1082 from RooVetGit/switch_tab_unsaved_check
Warn when switching tabs with unsaved content
2 parents e6f3bb7 + e9e8c74 commit bfd5024

File tree

2 files changed

+56
-28
lines changed

2 files changed

+56
-28
lines changed

webview-ui/src/App.tsx

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useState } from "react"
1+
import { useCallback, useEffect, useRef, useState } from "react"
22
import { useEvent } from "react-use"
33

44
import { ExtensionMessage } from "../../src/shared/ExtensionMessage"
@@ -7,7 +7,7 @@ import { vscode } from "./utils/vscode"
77
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
88
import ChatView from "./components/chat/ChatView"
99
import HistoryView from "./components/history/HistoryView"
10-
import SettingsView from "./components/settings/SettingsView"
10+
import SettingsView, { SettingsViewRef } from "./components/settings/SettingsView"
1111
import WelcomeView from "./components/welcome/WelcomeView"
1212
import McpView from "./components/mcp/McpView"
1313
import PromptsView from "./components/prompts/PromptsView"
@@ -26,18 +26,33 @@ const App = () => {
2626
const { didHydrateState, showWelcome, shouldShowAnnouncement } = useExtensionState()
2727
const [showAnnouncement, setShowAnnouncement] = useState(false)
2828
const [tab, setTab] = useState<Tab>("chat")
29+
const settingsRef = useRef<SettingsViewRef>(null)
2930

30-
const onMessage = useCallback((e: MessageEvent) => {
31-
const message: ExtensionMessage = e.data
31+
const switchTab = useCallback(
32+
(newTab: Tab) => {
33+
if (tab === "settings" && settingsRef.current?.checkUnsaveChanges) {
34+
settingsRef.current.checkUnsaveChanges(() => setTab(newTab))
35+
} else {
36+
setTab(newTab)
37+
}
38+
},
39+
[tab],
40+
)
3241

33-
if (message.type === "action" && message.action) {
34-
const newTab = tabsByMessageAction[message.action]
42+
const onMessage = useCallback(
43+
(e: MessageEvent) => {
44+
const message: ExtensionMessage = e.data
3545

36-
if (newTab) {
37-
setTab(newTab)
46+
if (message.type === "action" && message.action) {
47+
const newTab = tabsByMessageAction[message.action]
48+
49+
if (newTab) {
50+
switchTab(newTab)
51+
}
3852
}
39-
}
40-
}, [])
53+
},
54+
[switchTab],
55+
)
4156

4257
useEvent("message", onMessage)
4358

@@ -58,15 +73,15 @@ const App = () => {
5873
<WelcomeView />
5974
) : (
6075
<>
61-
{tab === "settings" && <SettingsView onDone={() => setTab("chat")} />}
62-
{tab === "history" && <HistoryView onDone={() => setTab("chat")} />}
63-
{tab === "mcp" && <McpView onDone={() => setTab("chat")} />}
64-
{tab === "prompts" && <PromptsView onDone={() => setTab("chat")} />}
76+
{tab === "settings" && <SettingsView ref={settingsRef} onDone={() => switchTab("chat")} />}
77+
{tab === "history" && <HistoryView onDone={() => switchTab("chat")} />}
78+
{tab === "mcp" && <McpView onDone={() => switchTab("chat")} />}
79+
{tab === "prompts" && <PromptsView onDone={() => switchTab("chat")} />}
6580
<ChatView
6681
isHidden={tab !== "chat"}
6782
showAnnouncement={showAnnouncement}
6883
hideAnnouncement={() => setShowAnnouncement(false)}
69-
showHistoryView={() => setTab("history")}
84+
showHistoryView={() => switchTab("history")}
7085
/>
7186
</>
7287
)

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
2-
import { memo, useCallback, useEffect, useRef, useState } from "react"
2+
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react"
33
import { ExtensionStateContextType, useExtensionState } from "../../context/ExtensionStateContext"
44
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
55
import { vscode } from "../../utils/vscode"
@@ -25,17 +25,22 @@ type SettingsViewProps = {
2525
onDone: () => void
2626
}
2727

28-
const SettingsView = ({ onDone }: SettingsViewProps) => {
28+
export interface SettingsViewRef {
29+
checkUnsaveChanges: (then: () => void) => void
30+
}
31+
32+
const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone }, ref) => {
2933
const extensionState = useExtensionState()
3034
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
3135
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
3236
const [commandInput, setCommandInput] = useState("")
33-
const prevApiConfigName = useRef(extensionState.currentApiConfigName)
3437
const [isDiscardDialogShow, setDiscardDialogShow] = useState(false)
35-
36-
// TODO: Reduce WebviewMessage/ExtensionState complexity
3738
const [cachedState, setCachedState] = useState(extensionState)
3839
const [isChangeDetected, setChangeDetected] = useState(false)
40+
const prevApiConfigName = useRef(extensionState.currentApiConfigName)
41+
const confirmDialogHandler = useRef<() => void>()
42+
43+
// TODO: Reduce WebviewMessage/ExtensionState complexity
3944
const { currentApiConfigName } = extensionState
4045
const {
4146
apiConfiguration,
@@ -190,12 +195,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
190195
setModelIdErrorMessage(modelIdValidationResult)
191196
}, [apiConfiguration, extensionState.glamaModels, extensionState.openRouterModels])
192197

193-
const confirmDialogHandler = useRef<() => void>()
194-
const onConfirmDialogResult = useCallback((confirm: boolean) => {
195-
if (confirm) {
196-
confirmDialogHandler.current?.()
197-
}
198-
}, [])
199198
const checkUnsaveChanges = useCallback(
200199
(then: () => void) => {
201200
if (isChangeDetected) {
@@ -208,6 +207,20 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
208207
[isChangeDetected],
209208
)
210209

210+
useImperativeHandle(
211+
ref,
212+
() => ({
213+
checkUnsaveChanges,
214+
}),
215+
[checkUnsaveChanges],
216+
)
217+
218+
const onConfirmDialogResult = useCallback((confirm: boolean) => {
219+
if (confirm) {
220+
confirmDialogHandler.current?.()
221+
}
222+
}, [])
223+
211224
const handleResetState = () => {
212225
vscode.postMessage({ type: "resetState" })
213226
}
@@ -250,7 +263,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
250263
<AlertDialogHeader>
251264
<AlertDialogTitle>Unsaved changes</AlertDialogTitle>
252265
<AlertDialogDescription>
253-
<span style={{ fontSize: "2em" }} className={`codicon codicon-warning align-middle mr-1`} />
266+
<span className={`codicon codicon-warning align-middle mr-1`} />
254267
Do you want to discard changes and continue?
255268
</AlertDialogDescription>
256269
</AlertDialogHeader>
@@ -890,6 +903,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
890903
</div>
891904
</div>
892905
)
893-
}
906+
})
894907

895908
export default memo(SettingsView)

0 commit comments

Comments
 (0)