Add Announcement component to update users on new features

This commit is contained in:
Saoud Rizwan
2024-07-26 13:33:58 -04:00
parent c1e9ceccb9
commit f4b77d5066
8 changed files with 135 additions and 45 deletions

View File

@@ -18,6 +18,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
private view?: vscode.WebviewView | vscode.WebviewPanel
private providerInstanceIdentifier = Date.now()
private claudeDev?: ClaudeDev
private latestAnnouncementId = "jul-25-2024" // update to some unique identifier when we add a new announcement
constructor(private readonly context: vscode.ExtensionContext) {}
@@ -227,7 +228,6 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
async (message: WebviewMessage) => {
switch (message.type) {
case "webviewDidLaunch":
await this.updateGlobalState("didOpenOnce", true)
await this.postStateToWebview()
break
case "newTask":
@@ -266,6 +266,10 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
await this.clearTask()
await this.postStateToWebview()
break
case "didShowAnnouncement":
await this.updateGlobalState("lastShownAnnouncementId", this.latestAnnouncementId)
await this.postStateToWebview()
break
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
}
@@ -276,20 +280,20 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
}
async postStateToWebview() {
const [didOpenOnce, apiKey, maxRequestsPerTask, claudeMessages] = await Promise.all([
this.getGlobalState("didOpenOnce") as Promise<boolean | undefined>,
const [apiKey, maxRequestsPerTask, claudeMessages, lastShownAnnouncementId] = await Promise.all([
this.getSecret("apiKey") as Promise<string | undefined>,
this.getGlobalState("maxRequestsPerTask") as Promise<number | undefined>,
this.getClaudeMessages(),
this.getGlobalState("lastShownAnnouncementId") as Promise<string | undefined>,
])
this.postMessageToWebview({
type: "state",
state: {
didOpenOnce: !!didOpenOnce,
apiKey,
maxRequestsPerTask,
themeName: vscode.workspace.getConfiguration("workbench").get<string>("colorTheme"),
claudeMessages,
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
},
})
}

View File

@@ -9,11 +9,11 @@ export interface ExtensionMessage {
}
export interface ExtensionState {
didOpenOnce: boolean
apiKey?: string
maxRequestsPerTask?: number
themeName?: string
claudeMessages: ClaudeMessage[]
shouldShowAnnouncement: boolean
}
export interface ClaudeMessage {

View File

@@ -1,7 +1,14 @@
export interface WebviewMessage {
type: "apiKey" | "maxRequestsPerTask" | "webviewDidLaunch" | "newTask" | "askResponse" | "clearTask"
text?: string
askResponse?: ClaudeAskResponse
type:
| "apiKey"
| "maxRequestsPerTask"
| "webviewDidLaunch"
| "newTask"
| "askResponse"
| "clearTask"
| "didShowAnnouncement"
text?: string
askResponse?: ClaudeAskResponse
}
export type ClaudeAskResponse = "yesButtonTapped" | "noButtonTapped" | "textResponse"

View File

@@ -16,12 +16,14 @@ The best way to solve this is to make your webview stateless. Use message passin
*/
const App: React.FC = () => {
const [didHydrateState, setDidHydrateState] = useState(false)
const [showSettings, setShowSettings] = useState(false)
const [showWelcome, setShowWelcome] = useState<boolean>(false)
const [apiKey, setApiKey] = useState<string>("")
const [maxRequestsPerTask, setMaxRequestsPerTask] = useState<string>("")
const [vscodeThemeName, setVscodeThemeName] = useState<string | undefined>(undefined)
const [claudeMessages, setClaudeMessages] = useState<ClaudeMessage[]>([])
const [showAnnouncement, setShowAnnouncement] = useState(false)
useEffect(() => {
vscode.postMessage({ type: "webviewDidLaunch" })
@@ -31,14 +33,19 @@ const App: React.FC = () => {
const message: ExtensionMessage = e.data
switch (message.type) {
case "state":
const shouldShowWelcome = !message.state!.didOpenOnce || !message.state!.apiKey
setShowWelcome(shouldShowWelcome)
setShowWelcome(!message.state!.apiKey)
setApiKey(message.state!.apiKey || "")
setMaxRequestsPerTask(
message.state!.maxRequestsPerTask !== undefined ? message.state!.maxRequestsPerTask.toString() : ""
)
setVscodeThemeName(message.state!.themeName)
setClaudeMessages(message.state!.claudeMessages)
// don't update showAnnouncement to false if shouldShowAnnouncement is false
if (message.state!.shouldShowAnnouncement) {
setShowAnnouncement(true)
vscode.postMessage({ type: "didShowAnnouncement" })
}
setDidHydrateState(true)
break
case "action":
switch (message.action!) {
@@ -56,6 +63,10 @@ const App: React.FC = () => {
useEvent("message", handleMessage)
if (!didHydrateState) {
return null
}
return (
<>
{showWelcome ? (
@@ -72,7 +83,13 @@ const App: React.FC = () => {
/>
)}
{/* 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 messages={claudeMessages} isHidden={showSettings} vscodeThemeName={vscodeThemeName} />
<ChatView
messages={claudeMessages}
isHidden={showSettings}
vscodeThemeName={vscodeThemeName}
showAnnouncement={showAnnouncement}
hideAnnouncement={() => setShowAnnouncement(false)}
/>
</>
)}
</>

View File

@@ -0,0 +1,54 @@
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
interface AnnouncementProps {
hideAnnouncement: () => void
}
/*
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 = ({ hideAnnouncement }: AnnouncementProps) => {
return (
<div
style={{
backgroundColor: "var(--vscode-editor-inactiveSelectionBackground)",
borderRadius: "3px",
padding: "12px 16px",
margin: "5px 15px 5px 15px",
position: "relative",
}}>
<VSCodeButton
appearance="icon"
onClick={hideAnnouncement}
style={{ position: "absolute", top: "8px", right: "8px" }}>
<span className="codicon codicon-close"></span>
</VSCodeButton>
<h3 style={{ margin: "0 0 8px" }}>🎉{" "}New in v1.0.0</h3>
<ul style={{ margin: "0 0 8px", paddingLeft: "20px" }}>
<li>
Open in the editor (using{" "}
<span
className="codicon codicon-link-external"
style={{ display: "inline", fontSize: "12.5px", verticalAlign: "text-bottom" }}></span>{" "}
or <code>Claude Dev: Open In New Tab</code> in command palette) to see how Claude updates your
workspace more clearly
</li>
<li>Provide feedback to tool use like terminal commands and file edits</li>
<li>
Updated max output tokens to 8192 so less lazy coding (<code>{"// rest of code here..."}</code>)
</li>
<li>Added ability to retry failed API requests (helpful for rate limits)</li>
<li>
Quality of life improvements like markdown rendering, memory optimizations, better theme support
</li>
</ul>
<p style={{ margin: "0" }}>
Subscribe to my new YouTube to see how to get the most out of Claude Dev!{" "}
<VSCodeLink href="https://youtube.com/@saoudrizwan" style={{ display: "inline" }}>
https://youtube.com/@saoudrizwan
</VSCodeLink>
</p>
</div>
)
}
export default Announcement

View File

@@ -97,7 +97,7 @@ const ChatRow: React.FC<ChatRowProps> = ({
}
}
const convertToMarkdown = (markdown: string = "") => {
const renderMarkdown = (markdown: string = "") => {
// react-markdown lets us customize elements, so here we're using their example of replacing code blocks with SyntaxHighlighter. However when there are no language matches (` or ``` without a language specifier) then we default to a normal code element for inline code. Code blocks without a language specifier shouldn't be a common occurrence as we prompt Claude to always use a language specifier.
return (
<Markdown
@@ -107,7 +107,14 @@ const ChatRow: React.FC<ChatRowProps> = ({
const { style, ...rest } = props
return <p style={{ ...style, margin: 0, marginTop: 0, marginBottom: 0 }} {...rest} />
},
//p: "span",
ol(props) {
const { style, ...rest } = props
return <ol style={{ ...style, padding: "0 0 0 20px", margin: "10px 0" }} {...rest} />
},
ul(props) {
const { style, ...rest } = props
return <ul style={{ ...style, padding: "0 0 0 20px", margin: "10px 0" }} {...rest} />
},
// https://github.com/remarkjs/react-markdown?tab=readme-ov-file#use-custom-components-syntax-highlight
code(props) {
const { children, className, node, ...rest } = props
@@ -205,7 +212,7 @@ const ChatRow: React.FC<ChatRowProps> = ({
case "api_req_finished":
return null // we should never see this message type
case "text":
return <div>{convertToMarkdown(message.text)}</div>
return <div>{renderMarkdown(message.text)}</div>
case "user_feedback":
return (
<div
@@ -240,7 +247,7 @@ const ChatRow: React.FC<ChatRowProps> = ({
{title}
</div>
<div style={{ color: "var(--vscode-testing-iconPassed)" }}>
{convertToMarkdown(message.text)}
{renderMarkdown(message.text)}
</div>
</>
)
@@ -253,7 +260,7 @@ const ChatRow: React.FC<ChatRowProps> = ({
{title}
</div>
)}
<div>{convertToMarkdown(message.text)}</div>
<div>{renderMarkdown(message.text)}</div>
</>
)
}
@@ -405,7 +412,7 @@ const ChatRow: React.FC<ChatRowProps> = ({
{title}
</div>
<div style={{ color: "var(--vscode-testing-iconPassed)" }}>
{convertToMarkdown(message.text)}
{renderMarkdown(message.text)}
</div>
</div>
)
@@ -421,7 +428,7 @@ const ChatRow: React.FC<ChatRowProps> = ({
{title}
</div>
)}
<div>{convertToMarkdown(message.text)}</div>
<div>{renderMarkdown(message.text)}</div>
</>
)
}

View File

@@ -12,14 +12,17 @@ import { vscode } from "../utilities/vscode"
import ChatRow from "./ChatRow"
import TaskHeader from "./TaskHeader"
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
import Announcement from "./Announcement"
interface ChatViewProps {
messages: ClaudeMessage[]
isHidden: boolean
vscodeThemeName?: string
showAnnouncement: boolean
hideAnnouncement: () => void
}
// maybe instead of storing state in App, just make chatview always show so dont conditionally load/unload? need to make sure messages are persisted (i remember seeing something about how webviews can be frozen in docs)
const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => {
const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideAnnouncement }: ChatViewProps) => {
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : 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])
@@ -337,20 +340,24 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => {
isHidden={isHidden}
/>
) : (
<div style={{ padding: "0 25px" }}>
<h2>What can I do for you?</h2>
<p>
Thanks to{" "}
<VSCodeLink
href="https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf"
style={{ display: "inline" }}>
Claude 3.5 Sonnet's agentic coding capabilities,
</VSCodeLink>{" "}
I can handle complex software development tasks step-by-step. With tools that let me read &
write files, create entire projects from scratch, and execute terminal commands (after you grant
permission), I can assist you in ways that go beyond simple code completion or tech support.
</p>
</div>
<>
{showAnnouncement && <Announcement hideAnnouncement={hideAnnouncement} />}
<div style={{ padding: "0 20px" }}>
<h2>What can I do for you?</h2>
<p>
Thanks to{" "}
<VSCodeLink
href="https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf"
style={{ display: "inline" }}>
Claude 3.5 Sonnet's agentic coding capabilities,
</VSCodeLink>{" "}
I can handle complex software development tasks step-by-step. With tools that let me read &
write files, create entire projects from scratch, and execute terminal commands (after you
grant permission), I can assist you in ways that go beyond simple code completion or tech
support.
</p>
</div>
</>
)}
<Virtuoso
ref={virtuosoRef}

View File

@@ -12,12 +12,6 @@ const WelcomeView: React.FC<WelcomeViewProps> = ({ apiKey, setApiKey }) => {
const disableLetsGoButton = apiKeyErrorMessage != null
const handleApiKeyChange = (event: any) => {
const input = event.target.value
setApiKey(input)
validateApiKey(input)
}
const validateApiKey = (value: string) => {
if (value.trim() === "") {
setApiKeyErrorMessage("API Key cannot be empty")
@@ -32,10 +26,10 @@ const WelcomeView: React.FC<WelcomeViewProps> = ({ apiKey, setApiKey }) => {
useEffect(() => {
validateApiKey(apiKey)
}, [])
}, [apiKey])
return (
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, padding: "0 15px" }}>
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, padding: "0 20px" }}>
<h2>Hi, I'm Claude Dev</h2>
<p>
I can do all kinds of tasks thanks to the latest breakthroughs in Claude Sonnet 3.5's agentic coding
@@ -48,8 +42,8 @@ const WelcomeView: React.FC<WelcomeViewProps> = ({ apiKey, setApiKey }) => {
<ol style={{ paddingLeft: "15px" }}>
<li>
Go to{" "}
<VSCodeLink href="https://console.anthropic.com/" style={{ display: "inline" }}>
https://console.anthropic.com/
<VSCodeLink href="https://console.anthropic.com" style={{ display: "inline" }}>
https://console.anthropic.com
</VSCodeLink>
</li>
<li>You may need to buy some credits (although Anthropic is offering $5 free credit for new users)</li>
@@ -63,7 +57,7 @@ const WelcomeView: React.FC<WelcomeViewProps> = ({ apiKey, setApiKey }) => {
style={{ flexGrow: 1, marginRight: "10px" }}
placeholder="Enter API Key..."
value={apiKey}
onInput={handleApiKeyChange}
onInput={(e: any) => setApiKey(e.target.value)}
/>
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton}>
Submit