Refactor extension state into ExtensionStateContext

This commit is contained in:
Saoud Rizwan
2024-08-27 04:44:36 -04:00
parent e95bebacda
commit 3b5e018a60
14 changed files with 155 additions and 258 deletions

View File

@@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -1,9 +0,0 @@
import React from "react"
import { render, screen } from "@testing-library/react"
import App from "./App"
test("renders learn react link", () => {
render(<App />)
const linkElement = screen.getByText(/learn react/i)
expect(linkElement).toBeInTheDocument()
})

View File

@@ -1,51 +1,27 @@
import React, { useCallback, useEffect, useMemo, useState } from "react"
import { useCallback, useMemo, useState } from "react"
import { useEvent } from "react-use"
import { ApiConfiguration } from "../../src/shared/api"
import { ClaudeMessage, ExtensionMessage } from "../../src/shared/ExtensionMessage"
import { HistoryItem } from "../../src/shared/HistoryItem"
import "./App.css"
import { ExtensionMessage } from "../../src/shared/ExtensionMessage"
import { normalizeApiConfiguration } from "./components/ApiOptions"
import ChatView from "./components/ChatView"
import HistoryView from "./components/HistoryView"
import SettingsView from "./components/SettingsView"
import WelcomeView from "./components/WelcomeView"
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
import { vscode } from "./utils/vscode"
/*
The contents of webviews however are created when the webview becomes visible and destroyed when the webview is moved into the background. Any state inside the webview will be lost when the webview is moved to a background tab.
The best way to solve this is to make your webview stateless. Use message passing to save off the webview's state and then restore the state when the webview becomes visible again.
*/
const App: React.FC = () => {
const [didHydrateState, setDidHydrateState] = useState(false)
const AppContent = () => {
const { apiConfiguration } = useExtensionState()
const [showSettings, setShowSettings] = useState(false)
const [showHistory, setShowHistory] = useState(false)
const [showWelcome, setShowWelcome] = useState<boolean>(false)
const [version, setVersion] = useState<string>("")
const [apiConfiguration, setApiConfiguration] = useState<ApiConfiguration | undefined>(undefined)
const [maxRequestsPerTask, setMaxRequestsPerTask] = useState<string>("")
const [customInstructions, setCustomInstructions] = useState<string>("")
const [alwaysAllowReadOnly, setAlwaysAllowReadOnly] = useState<boolean>(false)
const [vscodeThemeName, setVscodeThemeName] = useState<string | undefined>(undefined)
const [vscodeUriScheme, setVscodeUriScheme] = useState<string | undefined>(undefined)
const [claudeMessages, setClaudeMessages] = useState<ClaudeMessage[]>([])
const [taskHistory, setTaskHistory] = useState<HistoryItem[]>([])
const [showAnnouncement, setShowAnnouncement] = useState(false)
const [koduCredits, setKoduCredits] = useState<number | undefined>(undefined)
const [shouldShowKoduPromo, setShouldShowKoduPromo] = useState(true)
const [didAuthKoduFromWelcome, setDidAuthKoduFromWelcome] = useState<boolean>(false)
useEffect(() => {
vscode.postMessage({ type: "webviewDidLaunch" })
}, [])
const handleMessage = useCallback(
(e: MessageEvent) => {
const message: ExtensionMessage = e.data
switch (message.type) {
case "state":
setVersion(message.state!.version)
const hasKey =
message.state!.apiConfiguration?.apiKey !== undefined ||
message.state!.apiConfiguration?.openRouterApiKey !== undefined ||
@@ -55,25 +31,10 @@ const App: React.FC = () => {
if (!hasKey) {
setDidAuthKoduFromWelcome(false)
}
setApiConfiguration(message.state!.apiConfiguration)
setMaxRequestsPerTask(
message.state!.maxRequestsPerTask !== undefined
? message.state!.maxRequestsPerTask.toString()
: ""
)
setCustomInstructions(message.state!.customInstructions || "")
setAlwaysAllowReadOnly(message.state!.alwaysAllowReadOnly || false)
setVscodeThemeName(message.state!.themeName)
setVscodeUriScheme(message.state!.uriScheme)
setClaudeMessages(message.state!.claudeMessages)
setTaskHistory(message.state!.taskHistory)
setKoduCredits(message.state!.koduCredits)
// don't update showAnnouncement to false if shouldShowAnnouncement is false
if (message.state!.shouldShowAnnouncement) {
setShowAnnouncement(true)
}
setShouldShowKoduPromo(message.state!.shouldShowKoduPromo)
setDidHydrateState(true)
break
case "action":
switch (message.action!) {
@@ -89,9 +50,6 @@ const App: React.FC = () => {
setShowSettings(false)
setShowHistory(false)
break
case "koduCreditsFetched":
setKoduCredits(message.state!.koduCredits)
break
case "koduAuthenticated":
if (!didAuthKoduFromWelcome) {
setShowSettings(true)
@@ -112,49 +70,21 @@ const App: React.FC = () => {
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
if (!didHydrateState) {
return null
}
return (
<>
{showWelcome ? (
<WelcomeView
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
vscodeUriScheme={vscodeUriScheme}
setDidAuthKoduFromWelcome={setDidAuthKoduFromWelcome}
/>
<WelcomeView setDidAuthKoduFromWelcome={setDidAuthKoduFromWelcome} />
) : (
<>
{showSettings && (
<SettingsView
version={version}
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
koduCredits={koduCredits}
maxRequestsPerTask={maxRequestsPerTask}
setMaxRequestsPerTask={setMaxRequestsPerTask}
customInstructions={customInstructions}
setCustomInstructions={setCustomInstructions}
alwaysAllowReadOnly={alwaysAllowReadOnly}
setAlwaysAllowReadOnly={setAlwaysAllowReadOnly}
onDone={() => setShowSettings(false)}
vscodeUriScheme={vscodeUriScheme}
/>
)}
{showHistory && <HistoryView taskHistory={taskHistory} onDone={() => setShowHistory(false)} />}
{showSettings && <SettingsView onDone={() => setShowSettings(false)} />}
{showHistory && <HistoryView onDone={() => setShowHistory(false)} />}
{/* Do not conditionally load ChatView, it's expensive and there's state we don't want to lose (user input, disableInput, askResponse promise, etc.) */}
<ChatView
version={version}
messages={claudeMessages}
taskHistory={taskHistory}
showHistoryView={() => {
setShowSettings(false)
setShowHistory(true)
}}
isHidden={showSettings || showHistory}
vscodeThemeName={vscodeThemeName}
showAnnouncement={showAnnouncement}
selectedModelSupportsImages={selectedModelInfo.supportsImages}
selectedModelSupportsPromptCache={selectedModelInfo.supportsPromptCache}
@@ -162,10 +92,6 @@ const App: React.FC = () => {
vscode.postMessage({ type: "didCloseAnnouncement" })
setShowAnnouncement(false)
}}
apiConfiguration={apiConfiguration}
vscodeUriScheme={vscodeUriScheme}
shouldShowKoduPromo={shouldShowKoduPromo}
koduCredits={koduCredits}
/>
</>
)}
@@ -173,4 +99,12 @@ const App: React.FC = () => {
)
}
const App = () => {
return (
<ExtensionStateContextProvider>
<AppContent />
</ExtensionStateContextProvider>
)
}
export default App

View File

@@ -18,29 +18,19 @@ import { ExtensionMessage } from "../../../src/shared/ExtensionMessage"
import { getKoduAddCreditsUrl, getKoduHomepageUrl, getKoduSignInUrl } from "../../../src/shared/kodu"
import { vscode } from "../utils/vscode"
import VSCodeButtonLink from "./VSCodeButtonLink"
import { useExtensionState } from "../context/ExtensionStateContext"
interface ApiOptionsProps {
showModelOptions: boolean
apiConfiguration?: ApiConfiguration
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
koduCredits?: number
apiErrorMessage?: string
vscodeUriScheme?: string
setDidAuthKodu?: React.Dispatch<React.SetStateAction<boolean>>
}
const ApiOptions: React.FC<ApiOptionsProps> = ({
showModelOptions,
apiConfiguration,
setApiConfiguration,
koduCredits,
apiErrorMessage,
vscodeUriScheme,
setDidAuthKodu,
}) => {
const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiErrorMessage, setDidAuthKodu }) => {
const { apiConfiguration, setApiConfiguration, koduCredits, uriScheme } = useExtensionState()
const [, setDidFetchKoduCredits] = useState(false)
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
setApiConfiguration((prev) => ({ ...prev, [field]: event.target.value }))
setApiConfiguration({ ...apiConfiguration, [field]: event.target.value })
}
const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
@@ -185,7 +175,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
</span>
</div>
<VSCodeButtonLink
href={getKoduAddCreditsUrl(vscodeUriScheme)}
href={getKoduAddCreditsUrl(uriScheme)}
style={{
width: "fit-content",
}}>
@@ -194,9 +184,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
</>
) : (
<div style={{ margin: "4px 0px" }}>
<VSCodeButtonLink
href={getKoduSignInUrl(vscodeUriScheme)}
onClick={() => setDidAuthKodu?.(true)}>
<VSCodeButtonLink href={getKoduSignInUrl(uriScheme)} onClick={() => setDidAuthKodu?.(true)}>
Sign in to Kodu
</VSCodeButtonLink>
</div>

View File

@@ -5,7 +5,7 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "../../../src/shared/ExtensionMessage"
import { COMMAND_OUTPUT_STRING } from "../../../src/shared/combineCommandSequences"
import { SyntaxHighlighterStyle } from "../utils/getSyntaxHighlighterStyleFromTheme"
import CodeBlock from "./CodeBlock/CodeBlock"
import CodeBlock from "./CodeBlock"
import Thumbnails from "./Thumbnails"
import { ApiProvider } from "../../../src/shared/api"

View File

@@ -4,56 +4,50 @@ import vsDarkPlus from "react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-
import DynamicTextArea from "react-textarea-autosize"
import { useEvent, useMount } from "react-use"
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
import { ClaudeAsk, ClaudeMessage, ExtensionMessage } from "../../../src/shared/ExtensionMessage"
import { getApiMetrics } from "../../../src/shared/getApiMetrics"
import { ClaudeAsk, ExtensionMessage } from "../../../src/shared/ExtensionMessage"
import { combineApiRequests } from "../../../src/shared/combineApiRequests"
import { combineCommandSequences } from "../../../src/shared/combineCommandSequences"
import { getApiMetrics } from "../../../src/shared/getApiMetrics"
import { useExtensionState } from "../context/ExtensionStateContext"
import { getSyntaxHighlighterStyleFromTheme } from "../utils/getSyntaxHighlighterStyleFromTheme"
import { vscode } from "../utils/vscode"
import Announcement from "./Announcement"
import ChatRow from "./ChatRow"
import HistoryPreview from "./HistoryPreview"
import KoduPromo from "./KoduPromo"
import TaskHeader from "./TaskHeader"
import Thumbnails from "./Thumbnails"
import { HistoryItem } from "../../../src/shared/HistoryItem"
import { ApiConfiguration } from "../../../src/shared/api"
import KoduPromo from "./KoduPromo"
interface ChatViewProps {
version: string
messages: ClaudeMessage[]
taskHistory: HistoryItem[]
isHidden: boolean
vscodeThemeName?: string
showAnnouncement: boolean
selectedModelSupportsImages: boolean
selectedModelSupportsPromptCache: boolean
hideAnnouncement: () => void
showHistoryView: () => void
apiConfiguration?: ApiConfiguration
vscodeUriScheme?: string
shouldShowKoduPromo: boolean
koduCredits?: number
}
const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
const ChatView = ({
version,
messages,
taskHistory,
isHidden,
vscodeThemeName,
showAnnouncement,
selectedModelSupportsImages,
selectedModelSupportsPromptCache,
hideAnnouncement,
showHistoryView,
apiConfiguration,
vscodeUriScheme,
shouldShowKoduPromo,
koduCredits,
}: ChatViewProps) => {
const {
version,
claudeMessages: messages,
taskHistory,
themeName: vscodeThemeName,
apiConfiguration,
uriScheme,
shouldShowKoduPromo,
koduCredits,
} = useExtensionState()
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
const task = messages.length > 0 ? messages[0] : undefined // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see ClaudeDev.abort)
const modifiedMessages = useMemo(() => combineApiRequests(combineCommandSequences(messages.slice(1))), [messages])
@@ -490,7 +484,7 @@ const ChatView = ({
onClose={handleTaskCloseButtonClick}
isHidden={isHidden}
koduCredits={koduCredits}
vscodeUriScheme={vscodeUriScheme}
vscodeUriScheme={uriScheme}
apiProvider={apiConfiguration?.apiProvider}
/>
) : (
@@ -500,11 +494,11 @@ const ChatView = ({
version={version}
hideAnnouncement={hideAnnouncement}
apiConfiguration={apiConfiguration}
vscodeUriScheme={vscodeUriScheme}
vscodeUriScheme={uriScheme}
/>
)}
{apiConfiguration?.koduApiKey === undefined && !showAnnouncement && shouldShowKoduPromo && (
<KoduPromo vscodeUriScheme={vscodeUriScheme} style={{ margin: "10px 15px -10px 15px" }} />
<KoduPromo style={{ margin: "10px 15px -10px 15px" }} />
)}
<div style={{ padding: "0 20px", flexGrow: taskHistory.length > 0 ? undefined : 1 }}>
<h2>What can I do for you?</h2>
@@ -520,9 +514,7 @@ const ChatView = ({
permission), I can assist you in ways that go beyond simple code completion or tech support.
</p>
</div>
{taskHistory.length > 0 && (
<HistoryPreview taskHistory={taskHistory} showHistoryView={showHistoryView} />
)}
{taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
</>
)}
{task && (

View File

@@ -1,7 +1,7 @@
import { useMemo } from "react"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { getLanguageFromPath } from "../../utils/getLanguageFromPath"
import { SyntaxHighlighterStyle } from "../../utils/getSyntaxHighlighterStyleFromTheme"
import { getLanguageFromPath } from "../utils/getLanguageFromPath"
import { SyntaxHighlighterStyle } from "../utils/getSyntaxHighlighterStyleFromTheme"
/*
const vscodeSyntaxStyle: React.CSSProperties = {

View File

@@ -1,13 +1,13 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "../context/ExtensionStateContext"
import { vscode } from "../utils/vscode"
import { HistoryItem } from "../../../src/shared/HistoryItem"
type HistoryPreviewProps = {
taskHistory: HistoryItem[]
showHistoryView: () => void
}
const HistoryPreview = ({ taskHistory, showHistoryView }: HistoryPreviewProps) => {
const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
const { taskHistory } = useExtensionState()
const handleHistorySelect = (id: string) => {
vscode.postMessage({ type: "showTaskWithId", text: id })
}

View File

@@ -1,13 +1,13 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "../context/ExtensionStateContext"
import { vscode } from "../utils/vscode"
import { HistoryItem } from "../../../src/shared/HistoryItem"
type HistoryViewProps = {
taskHistory: HistoryItem[]
onDone: () => void
}
const HistoryView = ({ taskHistory, onDone }: HistoryViewProps) => {
const HistoryView = ({ onDone }: HistoryViewProps) => {
const { taskHistory } = useExtensionState()
const handleHistorySelect = (id: string) => {
vscode.postMessage({ type: "showTaskWithId", text: id })
}

View File

@@ -1,13 +1,15 @@
import React from "react"
import { getKoduSignInUrl } from "../../../src/shared/kodu"
import { vscode } from "../utils/vscode"
import { useExtensionState } from "../context/ExtensionStateContext"
interface KoduPromoProps {
vscodeUriScheme?: string
style?: React.CSSProperties
}
const KoduPromo: React.FC<KoduPromoProps> = ({ vscodeUriScheme, style }) => {
const KoduPromo: React.FC<KoduPromoProps> = ({ style }) => {
const { uriScheme } = useExtensionState()
function onClose() {
vscode.postMessage({ type: "didDismissKoduPromo" })
}
@@ -28,7 +30,7 @@ const KoduPromo: React.FC<KoduPromoProps> = ({ vscodeUriScheme, style }) => {
cursor: "pointer",
}}>
<a
href={getKoduSignInUrl(vscodeUriScheme)}
href={getKoduSignInUrl(uriScheme)}
style={{
textDecoration: "none",
color: "inherit",

View File

@@ -5,8 +5,8 @@ import {
VSCodeTextArea,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import React, { useEffect, useState } from "react"
import { ApiConfiguration } from "../../../src/shared/api"
import { useEffect, useState } from "react"
import { useExtensionState } from "../context/ExtensionStateContext"
import { validateApiConfiguration, validateMaxRequestsPerTask } from "../utils/validate"
import { vscode } from "../utils/vscode"
import ApiOptions from "./ApiOptions"
@@ -14,47 +14,35 @@ import ApiOptions from "./ApiOptions"
const IS_DEV = false // FIXME: use flags when packaging
type SettingsViewProps = {
version: string
apiConfiguration?: ApiConfiguration
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
koduCredits?: number
maxRequestsPerTask: string
setMaxRequestsPerTask: React.Dispatch<React.SetStateAction<string>>
customInstructions: string
setCustomInstructions: React.Dispatch<React.SetStateAction<string>>
onDone: () => void
alwaysAllowReadOnly: boolean
setAlwaysAllowReadOnly: React.Dispatch<React.SetStateAction<boolean>>
vscodeUriScheme?: string
}
const SettingsView = ({
version,
apiConfiguration,
setApiConfiguration,
koduCredits,
maxRequestsPerTask,
setMaxRequestsPerTask,
customInstructions,
setCustomInstructions,
onDone,
alwaysAllowReadOnly,
setAlwaysAllowReadOnly,
vscodeUriScheme,
}: SettingsViewProps) => {
const SettingsView = ({ onDone }: SettingsViewProps) => {
const {
apiConfiguration,
version,
maxRequestsPerTask,
customInstructions,
setCustomInstructions,
alwaysAllowReadOnly,
setAlwaysAllowReadOnly,
} = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [maxRequestsErrorMessage, setMaxRequestsErrorMessage] = useState<string | undefined>(undefined)
const [maxRequestsPerTaskString, setMaxRequestsPerTaskString] = useState<string>(
maxRequestsPerTask?.toString() || ""
)
const handleSubmit = () => {
const apiValidationResult = validateApiConfiguration(apiConfiguration)
const maxRequestsValidationResult = validateMaxRequestsPerTask(maxRequestsPerTask)
const maxRequestsValidationResult = validateMaxRequestsPerTask(maxRequestsPerTaskString)
setApiErrorMessage(apiValidationResult)
setMaxRequestsErrorMessage(maxRequestsValidationResult)
if (!apiValidationResult && !maxRequestsValidationResult) {
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
vscode.postMessage({ type: "maxRequestsPerTask", text: maxRequestsPerTask })
vscode.postMessage({ type: "maxRequestsPerTask", text: maxRequestsPerTaskString })
vscode.postMessage({ type: "customInstructions", text: customInstructions })
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
onDone()
@@ -112,14 +100,7 @@ const SettingsView = ({
<div
style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
<div style={{ marginBottom: 5 }}>
<ApiOptions
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
showModelOptions={true}
koduCredits={koduCredits}
apiErrorMessage={apiErrorMessage}
vscodeUriScheme={vscodeUriScheme}
/>
<ApiOptions showModelOptions={true} apiErrorMessage={apiErrorMessage} />
</div>
<div style={{ marginBottom: 5 }}>
@@ -141,13 +122,13 @@ const SettingsView = ({
<div style={{ marginBottom: 5 }}>
<VSCodeTextArea
value={customInstructions}
value={customInstructions ?? ""}
style={{ width: "100%" }}
rows={4}
placeholder={
'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'
}
onInput={(e: any) => setCustomInstructions(e.target?.value || "")}>
onInput={(e: any) => setCustomInstructions(e.target?.value ?? "")}>
<span style={{ fontWeight: "500" }}>Custom Instructions</span>
</VSCodeTextArea>
<p
@@ -162,10 +143,10 @@ const SettingsView = ({
<div>
<VSCodeTextField
value={maxRequestsPerTask}
value={maxRequestsPerTaskString}
style={{ width: "100%" }}
placeholder="20"
onInput={(e: any) => setMaxRequestsPerTask(e.target?.value)}>
onInput={(e: any) => setMaxRequestsPerTaskString(e.target?.value ?? "")}>
<span style={{ fontWeight: "500" }}>Maximum # Requests Per Task</span>
</VSCodeTextField>
<p

View File

@@ -1,24 +1,18 @@
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import React, { useEffect, useState } from "react"
import { ApiConfiguration } from "../../../src/shared/api"
import { getKoduSignInUrl } from "../../../src/shared/kodu"
import { useExtensionState } from "../context/ExtensionStateContext"
import { validateApiConfiguration } from "../utils/validate"
import { vscode } from "../utils/vscode"
import ApiOptions from "./ApiOptions"
import { getKoduSignInUrl } from "../../../src/shared/kodu"
interface WelcomeViewProps {
apiConfiguration?: ApiConfiguration
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
vscodeUriScheme?: string
setDidAuthKoduFromWelcome: React.Dispatch<React.SetStateAction<boolean>>
}
const WelcomeView: React.FC<WelcomeViewProps> = ({
apiConfiguration,
setApiConfiguration,
vscodeUriScheme,
setDidAuthKoduFromWelcome,
}) => {
const WelcomeView: React.FC<WelcomeViewProps> = ({ setDidAuthKoduFromWelcome }) => {
const { apiConfiguration, uriScheme } = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const disableLetsGoButton = apiErrorMessage != null
@@ -67,20 +61,14 @@ const WelcomeView: React.FC<WelcomeViewProps> = ({
}}></i>
<span>
Explore Claude's capabilities with $20 free credits from{" "}
<VSCodeLink href={getKoduSignInUrl(vscodeUriScheme)} style={{ display: "inline" }}>
<VSCodeLink href={getKoduSignInUrl(uriScheme)} style={{ display: "inline" }}>
Kodu
</VSCodeLink>
</span>
</div>
<div style={{ marginTop: "10px" }}>
<ApiOptions
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
showModelOptions={false}
vscodeUriScheme={vscodeUriScheme}
setDidAuthKodu={setDidAuthKoduFromWelcome}
/>
<ApiOptions showModelOptions={false} setDidAuthKodu={setDidAuthKoduFromWelcome} />
{apiConfiguration?.apiProvider !== "kodu" && (
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton} style={{ marginTop: "3px" }}>
Let's go!

View File

@@ -0,0 +1,71 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
import { useEvent } from "react-use"
import { ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage"
import { ApiConfiguration } from "../../../src/shared/api"
import { vscode } from "../utils/vscode"
interface ExtensionStateContextType extends ExtensionState {
setApiConfiguration: (config: ApiConfiguration) => void
setMaxRequestsPerTask: (value?: number) => void
setCustomInstructions: (value?: string) => void
setAlwaysAllowReadOnly: (value: boolean) => void
setShowAnnouncement: (value: boolean) => void
}
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, setState] = useState<ExtensionState>({
version: "",
claudeMessages: [],
taskHistory: [],
shouldShowAnnouncement: false,
shouldShowKoduPromo: false,
})
const [didHydrateState, setDidHydrateState] = useState(false)
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
if (message.type === "state" && message.state) {
setState(message.state)
setDidHydrateState(true)
}
if (message.type === "action" && message.action) {
switch (message.action) {
case "koduCreditsFetched":
// special case where we only want to update one part of state in case user is in the middle of modifying settings
setState((prevState) => ({ ...prevState, koduCredits: message.state?.koduCredits }))
break
}
}
}, [])
useEvent("message", handleMessage)
useEffect(() => {
vscode.postMessage({ type: "webviewDidLaunch" })
}, [])
const contextValue: ExtensionStateContextType = {
...state,
setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value })),
setMaxRequestsPerTask: (value) => setState((prevState) => ({ ...prevState, maxRequestsPerTask: value })),
setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
}
if (!didHydrateState) {
return null
}
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
}
export const useExtensionState = () => {
const context = useContext(ExtensionStateContext)
if (context === undefined) {
throw new Error("useExtensionState must be used within an ExtensionStateContextProvider")
}
return context
}

View File

@@ -1,15 +1,3 @@
/* body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
} */
textarea:focus {
outline: 1.5px solid var(--vscode-focusBorder, #007fd4);
}