From 7fa7589ed0e324aa7aec444c47a8b4be766c45ff Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Sun, 25 Aug 2024 16:08:57 -0400 Subject: [PATCH] Refactor Kodu links --- src/api/kodu.ts | 20 ++------ src/providers/ClaudeDevProvider.ts | 9 +--- src/shared/ExtensionMessage.ts | 1 + src/shared/WebviewMessage.ts | 2 - src/shared/kodu.ts | 17 +++++++ webview-ui/src/App.tsx | 10 +++- webview-ui/src/components/Announcement.tsx | 14 +++--- webview-ui/src/components/ApiOptions.tsx | 31 +++++------- webview-ui/src/components/ChatView.tsx | 3 ++ webview-ui/src/components/SettingsView.tsx | 49 ++++++++++++------- .../src/components/VSCodeButtonLink.tsx | 23 +++++++++ webview-ui/src/components/WelcomeView.tsx | 7 ++- 12 files changed, 115 insertions(+), 71 deletions(-) create mode 100644 src/shared/kodu.ts create mode 100644 webview-ui/src/components/VSCodeButtonLink.tsx diff --git a/src/api/kodu.ts b/src/api/kodu.ts index f406946..ecba2ea 100644 --- a/src/api/kodu.ts +++ b/src/api/kodu.ts @@ -1,23 +1,11 @@ import { Anthropic } from "@anthropic-ai/sdk" +import axios from "axios" import { ApiHandler, withoutImageData } from "." import { ApiHandlerOptions, koduDefaultModelId, KoduModelId, koduModels, ModelInfo } from "../shared/api" -import axios from "axios" -import * as vscode from "vscode" - -const KODU_BASE_URL = "https://claude-dev.com" - -export function didClickKoduSignIn() { - const loginUrl = `${KODU_BASE_URL}/auth/login?redirectTo=${vscode.env.uriScheme}://saoudrizwan.claude-dev&ext=1` - vscode.env.openExternal(vscode.Uri.parse(loginUrl)) -} - -export function didClickKoduAddCredits() { - const addCreditsUrl = `${KODU_BASE_URL}/user/addCredits?redirectTo=${vscode.env.uriScheme}://saoudrizwan.claude-dev&ext=1` - vscode.env.openExternal(vscode.Uri.parse(addCreditsUrl)) -} +import { getKoduCreditsUrl, getKoduInferenceUrl } from "../shared/kodu" export async function fetchKoduCredits({ apiKey }: { apiKey: string }) { - const response = await axios.get(`${KODU_BASE_URL}/api/credits`, { + const response = await axios.get(getKoduCreditsUrl(), { headers: { "x-api-key": apiKey, }, @@ -89,7 +77,7 @@ export class KoduHandler implements ApiHandler { tool_choice: { type: "auto" }, } } - const response = await axios.post(`${KODU_BASE_URL}/api/inference`, requestBody, { + const response = await axios.post(getKoduInferenceUrl(), requestBody, { headers: { "x-api-key": this.options.koduApiKey, }, diff --git a/src/providers/ClaudeDevProvider.ts b/src/providers/ClaudeDevProvider.ts index ebb0e54..796cac0 100644 --- a/src/providers/ClaudeDevProvider.ts +++ b/src/providers/ClaudeDevProvider.ts @@ -8,7 +8,7 @@ import { downloadTask, getNonce, getUri, selectImages } from "../utils" import * as path from "path" import fs from "fs/promises" import { HistoryItem } from "../shared/HistoryItem" -import { didClickKoduAddCredits, didClickKoduSignIn, fetchKoduCredits } from "../api/kodu" +import { fetchKoduCredits } from "../api/kodu" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -358,15 +358,9 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { case "exportTaskWithId": this.exportTaskWithId(message.text!) break - case "didClickKoduSignIn": - didClickKoduSignIn() - break case "didClickKoduSignOut": await this.signOutKodu() break - case "didClickKoduAddCredits": - didClickKoduAddCredits() - break case "fetchKoduCredits": const koduApiKey = await this.getSecret("koduApiKey") if (koduApiKey) { @@ -515,6 +509,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { customInstructions, alwaysAllowReadOnly, themeName: vscode.workspace.getConfiguration("workbench").get("colorTheme"), + uriScheme: vscode.env.uriScheme, claudeMessages: this.claudeDev?.claudeMessages || [], taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts), shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId, diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 5e60256..65ef170 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -25,6 +25,7 @@ export interface ExtensionState { customInstructions?: string alwaysAllowReadOnly?: boolean themeName?: string + uriScheme?: string claudeMessages: ClaudeMessage[] taskHistory: HistoryItem[] shouldShowAnnouncement: boolean diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 1fd5c78..6286c5d 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -16,9 +16,7 @@ export interface WebviewMessage { | "showTaskWithId" | "deleteTaskWithId" | "exportTaskWithId" - | "didClickKoduSignIn" | "didClickKoduSignOut" - | "didClickKoduAddCredits" | "fetchKoduCredits" text?: string askResponse?: ClaudeAskResponse diff --git a/src/shared/kodu.ts b/src/shared/kodu.ts new file mode 100644 index 0000000..1b8416f --- /dev/null +++ b/src/shared/kodu.ts @@ -0,0 +1,17 @@ +const KODU_BASE_URL = "https://claude-dev.com" + +export function getKoduSignInUrl(uriScheme?: string) { + return `${KODU_BASE_URL}/auth/login?redirectTo=${uriScheme}://saoudrizwan.claude-dev&ext=1` +} + +export function getKoduAddCreditsUrl(uriScheme?: string) { + return `${KODU_BASE_URL}/user/addCredits?redirectTo=${uriScheme}://saoudrizwan.claude-dev&ext=1` +} + +export function getKoduCreditsUrl() { + return `${KODU_BASE_URL}/api/credits` +} + +export function getKoduInferenceUrl() { + return `${KODU_BASE_URL}/api/inference` +} diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 971926b..21d38c2 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -28,6 +28,7 @@ const App: React.FC = () => { const [customInstructions, setCustomInstructions] = useState("") const [alwaysAllowReadOnly, setAlwaysAllowReadOnly] = useState(false) const [vscodeThemeName, setVscodeThemeName] = useState(undefined) + const [vscodeUriScheme, setVscodeUriScheme] = useState(undefined) const [claudeMessages, setClaudeMessages] = useState([]) const [taskHistory, setTaskHistory] = useState([]) const [showAnnouncement, setShowAnnouncement] = useState(false) @@ -62,6 +63,7 @@ const App: React.FC = () => { 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) @@ -121,7 +123,11 @@ const App: React.FC = () => { return ( <> {showWelcome ? ( - + ) : ( <> {showSettings && ( @@ -137,6 +143,7 @@ const App: React.FC = () => { alwaysAllowReadOnly={alwaysAllowReadOnly} setAlwaysAllowReadOnly={setAlwaysAllowReadOnly} onDone={() => setShowSettings(false)} + vscodeUriScheme={vscodeUriScheme} /> )} {showHistory && setShowHistory(false)} />} @@ -159,6 +166,7 @@ const App: React.FC = () => { setShowAnnouncement(false) }} apiConfiguration={apiConfiguration} + vscodeUriScheme={vscodeUriScheme} /> )} diff --git a/webview-ui/src/components/Announcement.tsx b/webview-ui/src/components/Announcement.tsx index 582ce44..89a9960 100644 --- a/webview-ui/src/components/Announcement.tsx +++ b/webview-ui/src/components/Announcement.tsx @@ -1,16 +1,18 @@ import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import { vscode } from "../utils/vscode" import { ApiConfiguration } from "../../../src/shared/api" +import { getKoduSignInUrl } from "../../../src/shared/kodu" +import VSCodeButtonLink from "./VSCodeButtonLink" interface AnnouncementProps { version: string hideAnnouncement: () => void apiConfiguration?: ApiConfiguration + vscodeUriScheme?: string } /* You must update the latestAnnouncementId in ClaudeDevProvider for new announcements to show to users. This new id will be compared with whats in state for the 'last announcement shown', and if it's different then the announcement will render. As soon as an announcement is shown, the id will be updated in state. This ensures that announcements are not shown more than once, even if the user doesn't close it themselves. */ -const Announcement = ({ version, hideAnnouncement, apiConfiguration }: AnnouncementProps) => { +const Announcement = ({ version, hideAnnouncement, apiConfiguration, vscodeUriScheme }: AnnouncementProps) => { return (
  • Excited to announce that{" "} - + Kodu {" "} is offering $10 free credits to help new users get the most out of Claude Dev with high rate limits and prompt caching! Stay tuned for some exciting updates like easier billing and deploying live websites. {apiConfiguration?.koduApiKey === undefined && ( - vscode.postMessage({ type: "didClickKoduSignIn" })} + href={getKoduSignInUrl(vscodeUriScheme)} style={{ transform: "scale(0.8)", transformOrigin: "left center", margin: "4px 0 2px 0", }}> Claim $10 Free Credits - + )}
  • diff --git a/webview-ui/src/components/ApiOptions.tsx b/webview-ui/src/components/ApiOptions.tsx index a4b5c05..d9906f5 100644 --- a/webview-ui/src/components/ApiOptions.tsx +++ b/webview-ui/src/components/ApiOptions.tsx @@ -1,11 +1,6 @@ -import { - VSCodeButton, - VSCodeDropdown, - VSCodeLink, - VSCodeOption, - VSCodeTextField, -} from "@vscode/webview-ui-toolkit/react" +import { VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import React, { useCallback, useEffect, useMemo, useState } from "react" +import { useEvent } from "react-use" import { ApiConfiguration, ApiModelId, @@ -19,9 +14,10 @@ import { openRouterDefaultModelId, openRouterModels, } from "../../../src/shared/api" -import { vscode } from "../utils/vscode" -import { useEvent } from "react-use" import { ExtensionMessage } from "../../../src/shared/ExtensionMessage" +import { getKoduAddCreditsUrl, getKoduSignInUrl } from "../../../src/shared/kodu" +import { vscode } from "../utils/vscode" +import VSCodeButtonLink from "./VSCodeButtonLink" interface ApiOptionsProps { showModelOptions: boolean @@ -29,6 +25,7 @@ interface ApiOptionsProps { setApiConfiguration: React.Dispatch> koduCredits?: number apiErrorMessage?: string + vscodeUriScheme?: string } const ApiOptions: React.FC = ({ @@ -37,6 +34,7 @@ const ApiOptions: React.FC = ({ setApiConfiguration, koduCredits, apiErrorMessage, + vscodeUriScheme, }) => { const [didFetchKoduCredits, setDidFetchKoduCredits] = useState(false) const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => { @@ -184,22 +182,19 @@ const ApiOptions: React.FC = ({ {formatPrice(koduCredits || 0)}
  • - vscode.postMessage({ type: "didClickKoduAddCredits" })} + Add Credits - + ) : (
    - vscode.postMessage({ type: "didClickKoduSignIn" })}> + Sign in to Kodu - +
    )}

    = ({ }}> Kodu is recommended for its high rate limits and access to the latest features like prompt caching. - + Learn more about Kodu here.

    diff --git a/webview-ui/src/components/ChatView.tsx b/webview-ui/src/components/ChatView.tsx index 697198e..c0162d3 100644 --- a/webview-ui/src/components/ChatView.tsx +++ b/webview-ui/src/components/ChatView.tsx @@ -30,6 +30,7 @@ interface ChatViewProps { hideAnnouncement: () => void showHistoryView: () => void apiConfiguration?: ApiConfiguration + vscodeUriScheme?: string } const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images @@ -46,6 +47,7 @@ const ChatView = ({ hideAnnouncement, showHistoryView, apiConfiguration, + vscodeUriScheme, }: ChatViewProps) => { //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) @@ -490,6 +492,7 @@ const ChatView = ({ version={version} hideAnnouncement={hideAnnouncement} apiConfiguration={apiConfiguration} + vscodeUriScheme={vscodeUriScheme} /> )}
    0 ? undefined : 1 }}> diff --git a/webview-ui/src/components/SettingsView.tsx b/webview-ui/src/components/SettingsView.tsx index e9d28f0..9e8d7e5 100644 --- a/webview-ui/src/components/SettingsView.tsx +++ b/webview-ui/src/components/SettingsView.tsx @@ -10,6 +10,7 @@ import { ApiConfiguration } from "../../../src/shared/api" import { validateApiConfiguration, validateMaxRequestsPerTask } from "../utils/validate" import { vscode } from "../utils/vscode" import ApiOptions from "./ApiOptions" +import { getKoduSignInUrl } from "../../../src/shared/kodu" type SettingsViewProps = { version: string @@ -23,6 +24,7 @@ type SettingsViewProps = { onDone: () => void alwaysAllowReadOnly: boolean setAlwaysAllowReadOnly: React.Dispatch> + vscodeUriScheme?: string } const SettingsView = ({ @@ -37,6 +39,7 @@ const SettingsView = ({ onDone, alwaysAllowReadOnly, setAlwaysAllowReadOnly, + vscodeUriScheme, }: SettingsViewProps) => { const [apiErrorMessage, setApiErrorMessage] = useState(undefined) const [maxRequestsErrorMessage, setMaxRequestsErrorMessage] = useState(undefined) @@ -104,27 +107,34 @@ const SettingsView = ({
    {apiConfiguration?.koduApiKey === undefined && ( -
    vscode.postMessage({ type: "didClickKoduSignIn" })}> - +
    - Claim $10 free credits from Kodu -
    + display: "flex", + alignItems: "center", + backgroundColor: "var(--vscode-editor-inactiveSelectionBackground)", + color: "var(--vscode-textLink-foreground)", + padding: "6px 8px", + borderRadius: "3px", + margin: "0 0 8px 0px", + fontSize: "12px", + cursor: "pointer", + }}> + + Claim $10 free credits from Kodu +
    + )}
    diff --git a/webview-ui/src/components/VSCodeButtonLink.tsx b/webview-ui/src/components/VSCodeButtonLink.tsx new file mode 100644 index 0000000..8d0c87d --- /dev/null +++ b/webview-ui/src/components/VSCodeButtonLink.tsx @@ -0,0 +1,23 @@ +import React from "react" +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" + +interface VSCodeButtonLinkProps { + href: string + children: React.ReactNode + [key: string]: any +} + +const VSCodeButtonLink: React.FC = ({ href, children, ...props }) => { + return ( + + {children} + + ) +} + +export default VSCodeButtonLink diff --git a/webview-ui/src/components/WelcomeView.tsx b/webview-ui/src/components/WelcomeView.tsx index db13a06..7724597 100644 --- a/webview-ui/src/components/WelcomeView.tsx +++ b/webview-ui/src/components/WelcomeView.tsx @@ -4,13 +4,15 @@ import { ApiConfiguration } from "../../../src/shared/api" 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> + vscodeUriScheme?: string } -const WelcomeView: React.FC = ({ apiConfiguration, setApiConfiguration }) => { +const WelcomeView: React.FC = ({ apiConfiguration, setApiConfiguration, vscodeUriScheme }) => { const [apiErrorMessage, setApiErrorMessage] = useState(undefined) const disableLetsGoButton = apiErrorMessage != null @@ -59,7 +61,7 @@ const WelcomeView: React.FC = ({ apiConfiguration, setApiConfi }}> Explore Claude's capabilities with $10 free credits from{" "} - + Kodu @@ -70,6 +72,7 @@ const WelcomeView: React.FC = ({ apiConfiguration, setApiConfi apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} showModelOptions={false} + vscodeUriScheme={vscodeUriScheme} /> {apiConfiguration?.apiProvider !== "kodu" && (