mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Improve Kodu promo behavior
This commit is contained in:
@@ -28,6 +28,7 @@ type GlobalStateKey =
|
|||||||
| "customInstructions"
|
| "customInstructions"
|
||||||
| "alwaysAllowReadOnly"
|
| "alwaysAllowReadOnly"
|
||||||
| "taskHistory"
|
| "taskHistory"
|
||||||
|
| "shouldShowKoduPromo"
|
||||||
|
|
||||||
export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
||||||
public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
|
public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
|
||||||
@@ -373,6 +374,10 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case "didDismissKoduPromo":
|
||||||
|
await this.updateGlobalState("shouldShowKoduPromo", false)
|
||||||
|
await this.postStateToWebview()
|
||||||
|
break
|
||||||
// Add more switch case statements here as more webview message commands
|
// Add more switch case statements here as more webview message commands
|
||||||
// are created within the webview context (i.e. inside media/main.js)
|
// are created within the webview context (i.e. inside media/main.js)
|
||||||
}
|
}
|
||||||
@@ -388,6 +393,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
await this.storeSecret("koduApiKey", apiKey)
|
await this.storeSecret("koduApiKey", apiKey)
|
||||||
await this.updateGlobalState("koduEmail", email)
|
await this.updateGlobalState("koduEmail", email)
|
||||||
await this.updateGlobalState("apiProvider", "kodu")
|
await this.updateGlobalState("apiProvider", "kodu")
|
||||||
|
await this.updateGlobalState("shouldShowKoduPromo", false)
|
||||||
await this.postStateToWebview()
|
await this.postStateToWebview()
|
||||||
await this.postMessageToWebview({ type: "action", action: "koduAuthenticated" })
|
await this.postMessageToWebview({ type: "action", action: "koduAuthenticated" })
|
||||||
this.claudeDev?.updateApi({ apiProvider: "kodu", koduApiKey: apiKey })
|
this.claudeDev?.updateApi({ apiProvider: "kodu", koduApiKey: apiKey })
|
||||||
@@ -501,6 +507,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
alwaysAllowReadOnly,
|
alwaysAllowReadOnly,
|
||||||
taskHistory,
|
taskHistory,
|
||||||
koduCredits,
|
koduCredits,
|
||||||
|
shouldShowKoduPromo,
|
||||||
} = await this.getState()
|
} = await this.getState()
|
||||||
return {
|
return {
|
||||||
version: this.context.extension?.packageJSON?.version ?? "",
|
version: this.context.extension?.packageJSON?.version ?? "",
|
||||||
@@ -514,6 +521,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts),
|
taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts),
|
||||||
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
|
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
|
||||||
koduCredits,
|
koduCredits,
|
||||||
|
shouldShowKoduPromo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,6 +628,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
customInstructions,
|
customInstructions,
|
||||||
alwaysAllowReadOnly,
|
alwaysAllowReadOnly,
|
||||||
taskHistory,
|
taskHistory,
|
||||||
|
shouldShowKoduPromo,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||||
this.getGlobalState("apiModelId") as Promise<ApiModelId | undefined>,
|
this.getGlobalState("apiModelId") as Promise<ApiModelId | undefined>,
|
||||||
@@ -636,6 +645,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
this.getGlobalState("customInstructions") as Promise<string | undefined>,
|
this.getGlobalState("customInstructions") as Promise<string | undefined>,
|
||||||
this.getGlobalState("alwaysAllowReadOnly") as Promise<boolean | undefined>,
|
this.getGlobalState("alwaysAllowReadOnly") as Promise<boolean | undefined>,
|
||||||
this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>,
|
this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>,
|
||||||
|
this.getGlobalState("shouldShowKoduPromo") as Promise<boolean | undefined>,
|
||||||
])
|
])
|
||||||
|
|
||||||
let apiProvider: ApiProvider
|
let apiProvider: ApiProvider
|
||||||
@@ -670,6 +680,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
alwaysAllowReadOnly,
|
alwaysAllowReadOnly,
|
||||||
taskHistory,
|
taskHistory,
|
||||||
koduCredits,
|
koduCredits,
|
||||||
|
shouldShowKoduPromo: shouldShowKoduPromo ?? true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export interface ExtensionState {
|
|||||||
taskHistory: HistoryItem[]
|
taskHistory: HistoryItem[]
|
||||||
shouldShowAnnouncement: boolean
|
shouldShowAnnouncement: boolean
|
||||||
koduCredits?: number
|
koduCredits?: number
|
||||||
|
shouldShowKoduPromo: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClaudeMessage {
|
export interface ClaudeMessage {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface WebviewMessage {
|
|||||||
| "exportTaskWithId"
|
| "exportTaskWithId"
|
||||||
| "didClickKoduSignOut"
|
| "didClickKoduSignOut"
|
||||||
| "fetchKoduCredits"
|
| "fetchKoduCredits"
|
||||||
|
| "didDismissKoduPromo"
|
||||||
text?: string
|
text?: string
|
||||||
askResponse?: ClaudeAskResponse
|
askResponse?: ClaudeAskResponse
|
||||||
apiConfiguration?: ApiConfiguration
|
apiConfiguration?: ApiConfiguration
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"
|
|||||||
import { useEvent } from "react-use"
|
import { useEvent } from "react-use"
|
||||||
import { ApiConfiguration } from "../../src/shared/api"
|
import { ApiConfiguration } from "../../src/shared/api"
|
||||||
import { ClaudeMessage, ExtensionMessage } from "../../src/shared/ExtensionMessage"
|
import { ClaudeMessage, ExtensionMessage } from "../../src/shared/ExtensionMessage"
|
||||||
|
import { HistoryItem } from "../../src/shared/HistoryItem"
|
||||||
import "./App.css"
|
import "./App.css"
|
||||||
import { normalizeApiConfiguration } from "./components/ApiOptions"
|
import { normalizeApiConfiguration } from "./components/ApiOptions"
|
||||||
import ChatView from "./components/ChatView"
|
import ChatView from "./components/ChatView"
|
||||||
|
import HistoryView from "./components/HistoryView"
|
||||||
import SettingsView from "./components/SettingsView"
|
import SettingsView from "./components/SettingsView"
|
||||||
import WelcomeView from "./components/WelcomeView"
|
import WelcomeView from "./components/WelcomeView"
|
||||||
import { vscode } from "./utils/vscode"
|
import { vscode } from "./utils/vscode"
|
||||||
import HistoryView from "./components/HistoryView"
|
|
||||||
import { HistoryItem } from "../../src/shared/HistoryItem"
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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 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.
|
||||||
@@ -34,7 +34,7 @@ const App: React.FC = () => {
|
|||||||
const [showAnnouncement, setShowAnnouncement] = useState(false)
|
const [showAnnouncement, setShowAnnouncement] = useState(false)
|
||||||
const [koduCredits, setKoduCredits] = useState<number | undefined>(undefined)
|
const [koduCredits, setKoduCredits] = useState<number | undefined>(undefined)
|
||||||
const [isNewUser, setIsNewUser] = useState(false)
|
const [isNewUser, setIsNewUser] = useState(false)
|
||||||
|
const [shouldShowKoduPromo, setShouldShowKoduPromo] = useState(true)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
vscode.postMessage({ type: "webviewDidLaunch" })
|
vscode.postMessage({ type: "webviewDidLaunch" })
|
||||||
}, [])
|
}, [])
|
||||||
@@ -71,6 +71,7 @@ const App: React.FC = () => {
|
|||||||
if (message.state!.shouldShowAnnouncement) {
|
if (message.state!.shouldShowAnnouncement) {
|
||||||
setShowAnnouncement(true)
|
setShowAnnouncement(true)
|
||||||
}
|
}
|
||||||
|
setShouldShowKoduPromo(message.state!.shouldShowKoduPromo)
|
||||||
setDidHydrateState(true)
|
setDidHydrateState(true)
|
||||||
break
|
break
|
||||||
case "action":
|
case "action":
|
||||||
@@ -167,6 +168,7 @@ const App: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
apiConfiguration={apiConfiguration}
|
apiConfiguration={apiConfiguration}
|
||||||
vscodeUriScheme={vscodeUriScheme}
|
vscodeUriScheme={vscodeUriScheme}
|
||||||
|
shouldShowKoduPromo={shouldShowKoduPromo}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||||
import { ApiConfiguration } from "../../../src/shared/api"
|
import { ApiConfiguration } from "../../../src/shared/api"
|
||||||
import { getKoduHomepageUrl, getKoduSignInUrl } from "../../../src/shared/kodu"
|
import { getKoduSignInUrl } from "../../../src/shared/kodu"
|
||||||
import VSCodeButtonLink from "./VSCodeButtonLink"
|
import VSCodeButtonLink from "./VSCodeButtonLink"
|
||||||
|
|
||||||
interface AnnouncementProps {
|
interface AnnouncementProps {
|
||||||
@@ -34,25 +34,19 @@ const Announcement = ({ version, hideAnnouncement, apiConfiguration, vscodeUriSc
|
|||||||
|
|
||||||
<ul style={{ margin: "0 0 8px", paddingLeft: "20px" }}>
|
<ul style={{ margin: "0 0 8px", paddingLeft: "20px" }}>
|
||||||
<li>
|
<li>
|
||||||
Excited to announce that{" "}
|
Excited to announce that we've partnered with Anthropic and are offering $20 free credits to help
|
||||||
<VSCodeLink
|
users get the most out of Claude Dev with high rate limits and prompt caching! Stay tuned for some
|
||||||
href={apiConfiguration?.koduApiKey ? getKoduHomepageUrl() : getKoduSignInUrl(vscodeUriScheme)}
|
exciting updates like easier billing, voice mode and one click deployment.
|
||||||
style={{ display: "inline" }}>
|
|
||||||
Kodu
|
|
||||||
</VSCodeLink>{" "}
|
|
||||||
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 && (
|
{apiConfiguration?.koduApiKey === undefined && (
|
||||||
<VSCodeButtonLink
|
<VSCodeButtonLink
|
||||||
appearance="secondary"
|
appearance="secondary"
|
||||||
href={getKoduSignInUrl(vscodeUriScheme)}
|
href={getKoduSignInUrl(vscodeUriScheme)}
|
||||||
style={{
|
style={{
|
||||||
transform: "scale(0.8)",
|
transform: "scale(0.85)",
|
||||||
transformOrigin: "left center",
|
transformOrigin: "left center",
|
||||||
margin: "4px 0 2px 0",
|
margin: "4px 0 2px 0",
|
||||||
}}>
|
}}>
|
||||||
Claim $10 Free Credits
|
Claim $20 Free Credits with Kodu
|
||||||
</VSCodeButtonLink>
|
</VSCodeButtonLink>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import TaskHeader from "./TaskHeader"
|
|||||||
import Thumbnails from "./Thumbnails"
|
import Thumbnails from "./Thumbnails"
|
||||||
import { HistoryItem } from "../../../src/shared/HistoryItem"
|
import { HistoryItem } from "../../../src/shared/HistoryItem"
|
||||||
import { ApiConfiguration } from "../../../src/shared/api"
|
import { ApiConfiguration } from "../../../src/shared/api"
|
||||||
|
import KoduPromo from "./KoduPromo"
|
||||||
|
|
||||||
interface ChatViewProps {
|
interface ChatViewProps {
|
||||||
version: string
|
version: string
|
||||||
@@ -31,6 +32,7 @@ interface ChatViewProps {
|
|||||||
showHistoryView: () => void
|
showHistoryView: () => void
|
||||||
apiConfiguration?: ApiConfiguration
|
apiConfiguration?: ApiConfiguration
|
||||||
vscodeUriScheme?: string
|
vscodeUriScheme?: string
|
||||||
|
shouldShowKoduPromo: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
|
const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
|
||||||
@@ -48,6 +50,7 @@ const ChatView = ({
|
|||||||
showHistoryView,
|
showHistoryView,
|
||||||
apiConfiguration,
|
apiConfiguration,
|
||||||
vscodeUriScheme,
|
vscodeUriScheme,
|
||||||
|
shouldShowKoduPromo,
|
||||||
}: ChatViewProps) => {
|
}: ChatViewProps) => {
|
||||||
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
|
//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 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)
|
||||||
@@ -495,6 +498,9 @@ const ChatView = ({
|
|||||||
vscodeUriScheme={vscodeUriScheme}
|
vscodeUriScheme={vscodeUriScheme}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{apiConfiguration?.koduApiKey === undefined && !showAnnouncement && shouldShowKoduPromo && (
|
||||||
|
<KoduPromo vscodeUriScheme={vscodeUriScheme} style={{ margin: "10px 15px -10px 15px" }} />
|
||||||
|
)}
|
||||||
<div style={{ padding: "0 20px", flexGrow: taskHistory.length > 0 ? undefined : 1 }}>
|
<div style={{ padding: "0 20px", flexGrow: taskHistory.length > 0 ? undefined : 1 }}>
|
||||||
<h2>What can I do for you?</h2>
|
<h2>What can I do for you?</h2>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
69
webview-ui/src/components/KoduPromo.tsx
Normal file
69
webview-ui/src/components/KoduPromo.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { getKoduSignInUrl } from "../../../src/shared/kodu"
|
||||||
|
import { vscode } from "../utils/vscode"
|
||||||
|
|
||||||
|
interface KoduPromoProps {
|
||||||
|
vscodeUriScheme?: string
|
||||||
|
style?: React.CSSProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
const KoduPromo: React.FC<KoduPromoProps> = ({ vscodeUriScheme, style }) => {
|
||||||
|
function onClose() {
|
||||||
|
vscode.postMessage({ type: "didDismissKoduPromo" })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ ...style }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
backgroundColor: "var(--vscode-editor-inactiveSelectionBackground)",
|
||||||
|
color: "var(--vscode-textLink-foreground)",
|
||||||
|
padding: "6px 8px",
|
||||||
|
borderRadius: "3px",
|
||||||
|
margin: "0 0 8px 0px",
|
||||||
|
fontSize: "12px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}>
|
||||||
|
<a
|
||||||
|
href={getKoduSignInUrl(vscodeUriScheme)}
|
||||||
|
style={{
|
||||||
|
textDecoration: "none",
|
||||||
|
color: "inherit",
|
||||||
|
outline: "none",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
}}>
|
||||||
|
<i
|
||||||
|
className="codicon codicon-info"
|
||||||
|
style={{
|
||||||
|
marginRight: 6,
|
||||||
|
fontSize: 16,
|
||||||
|
}}></i>
|
||||||
|
<span>Claim $20 free credits from Kodu</span>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
style={{
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
color: "var(--vscode-textLink-foreground)",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "12px",
|
||||||
|
opacity: 0.7,
|
||||||
|
padding: 0,
|
||||||
|
marginLeft: 4,
|
||||||
|
marginTop: 2,
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => (e.currentTarget.style.opacity = "1")}
|
||||||
|
onMouseLeave={(e) => (e.currentTarget.style.opacity = "0.7")}>
|
||||||
|
<i className="codicon codicon-close"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KoduPromo
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
VSCodeButton,
|
VSCodeButton,
|
||||||
|
VSCodeCheckbox,
|
||||||
VSCodeLink,
|
VSCodeLink,
|
||||||
VSCodeTextArea,
|
VSCodeTextArea,
|
||||||
VSCodeTextField,
|
VSCodeTextField,
|
||||||
VSCodeCheckbox,
|
|
||||||
} from "@vscode/webview-ui-toolkit/react"
|
} from "@vscode/webview-ui-toolkit/react"
|
||||||
import React, { useEffect, useState } from "react"
|
import React, { useEffect, useState } from "react"
|
||||||
import { ApiConfiguration } from "../../../src/shared/api"
|
import { ApiConfiguration } from "../../../src/shared/api"
|
||||||
import { validateApiConfiguration, validateMaxRequestsPerTask } from "../utils/validate"
|
import { validateApiConfiguration, validateMaxRequestsPerTask } from "../utils/validate"
|
||||||
import { vscode } from "../utils/vscode"
|
import { vscode } from "../utils/vscode"
|
||||||
import ApiOptions from "./ApiOptions"
|
import ApiOptions from "./ApiOptions"
|
||||||
import { getKoduSignInUrl } from "../../../src/shared/kodu"
|
|
||||||
|
|
||||||
type SettingsViewProps = {
|
type SettingsViewProps = {
|
||||||
version: string
|
version: string
|
||||||
@@ -106,36 +105,6 @@ const SettingsView = ({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
|
style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
|
||||||
{apiConfiguration?.koduApiKey === undefined && (
|
|
||||||
<a
|
|
||||||
href={getKoduSignInUrl(vscodeUriScheme)}
|
|
||||||
style={{
|
|
||||||
textDecoration: "none",
|
|
||||||
color: "inherit",
|
|
||||||
outline: "none",
|
|
||||||
}}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
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",
|
|
||||||
}}>
|
|
||||||
<i
|
|
||||||
className="codicon codicon-info"
|
|
||||||
style={{
|
|
||||||
marginRight: 6,
|
|
||||||
fontSize: 16,
|
|
||||||
}}></i>
|
|
||||||
<span>Claim $10 free credits from Kodu</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
<div style={{ marginBottom: 5 }}>
|
<div style={{ marginBottom: 5 }}>
|
||||||
<ApiOptions
|
<ApiOptions
|
||||||
apiConfiguration={apiConfiguration}
|
apiConfiguration={apiConfiguration}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const WelcomeView: React.FC<WelcomeViewProps> = ({ apiConfiguration, setApiConfi
|
|||||||
color: "var(--vscode-infoIcon-foreground)",
|
color: "var(--vscode-infoIcon-foreground)",
|
||||||
}}></i>
|
}}></i>
|
||||||
<span>
|
<span>
|
||||||
Explore Claude's capabilities with $10 free credits from{" "}
|
Explore Claude's capabilities with $20 free credits from{" "}
|
||||||
<VSCodeLink href={getKoduSignInUrl(vscodeUriScheme)} style={{ display: "inline" }}>
|
<VSCodeLink href={getKoduSignInUrl(vscodeUriScheme)} style={{ display: "inline" }}>
|
||||||
Kodu
|
Kodu
|
||||||
</VSCodeLink>
|
</VSCodeLink>
|
||||||
|
|||||||
Reference in New Issue
Block a user