diff --git a/src/providers/ClaudeDevProvider.ts b/src/providers/ClaudeDevProvider.ts index 274f297..6bee06d 100644 --- a/src/providers/ClaudeDevProvider.ts +++ b/src/providers/ClaudeDevProvider.ts @@ -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, + const [apiKey, maxRequestsPerTask, claudeMessages, lastShownAnnouncementId] = await Promise.all([ this.getSecret("apiKey") as Promise, this.getGlobalState("maxRequestsPerTask") as Promise, this.getClaudeMessages(), + this.getGlobalState("lastShownAnnouncementId") as Promise, ]) this.postMessageToWebview({ type: "state", state: { - didOpenOnce: !!didOpenOnce, apiKey, maxRequestsPerTask, themeName: vscode.workspace.getConfiguration("workbench").get("colorTheme"), claudeMessages, + shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId, }, }) } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index f4110f9..b35efea 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -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 { diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 9b291a9..0a171e4 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -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" \ No newline at end of file +export type ClaudeAskResponse = "yesButtonTapped" | "noButtonTapped" | "textResponse" diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index df34c7d..3e12c11 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -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(false) const [apiKey, setApiKey] = useState("") const [maxRequestsPerTask, setMaxRequestsPerTask] = useState("") const [vscodeThemeName, setVscodeThemeName] = useState(undefined) const [claudeMessages, setClaudeMessages] = useState([]) + 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.) */} - + setShowAnnouncement(false)} + /> )} diff --git a/webview-ui/src/components/Announcement.tsx b/webview-ui/src/components/Announcement.tsx new file mode 100644 index 0000000..bad3235 --- /dev/null +++ b/webview-ui/src/components/Announcement.tsx @@ -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 ( +
+ + + +

🎉{" "}New in v1.0.0

+
    +
  • + Open in the editor (using{" "} + {" "} + or Claude Dev: Open In New Tab in command palette) to see how Claude updates your + workspace more clearly +
  • +
  • Provide feedback to tool use like terminal commands and file edits
  • +
  • + Updated max output tokens to 8192 so less lazy coding ({"// rest of code here..."}) +
  • +
  • Added ability to retry failed API requests (helpful for rate limits)
  • +
  • + Quality of life improvements like markdown rendering, memory optimizations, better theme support +
  • +
+

+ Subscribe to my new YouTube to see how to get the most out of Claude Dev!{" "} + + https://youtube.com/@saoudrizwan + +

+
+ ) +} + +export default Announcement diff --git a/webview-ui/src/components/ChatRow.tsx b/webview-ui/src/components/ChatRow.tsx index ba93a79..7ded5b4 100644 --- a/webview-ui/src/components/ChatRow.tsx +++ b/webview-ui/src/components/ChatRow.tsx @@ -97,7 +97,7 @@ const ChatRow: React.FC = ({ } } - 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 ( = ({ const { style, ...rest } = props return

}, - //p: "span", + ol(props) { + const { style, ...rest } = props + return

    + }, + ul(props) { + const { style, ...rest } = props + return
      + }, // 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 = ({ case "api_req_finished": return null // we should never see this message type case "text": - return
      {convertToMarkdown(message.text)}
      + return
      {renderMarkdown(message.text)}
      case "user_feedback": return (
      = ({ {title}
      - {convertToMarkdown(message.text)} + {renderMarkdown(message.text)}
      ) @@ -253,7 +260,7 @@ const ChatRow: React.FC = ({ {title} )} -
      {convertToMarkdown(message.text)}
      +
      {renderMarkdown(message.text)}
      ) } @@ -405,7 +412,7 @@ const ChatRow: React.FC = ({ {title}
      - {convertToMarkdown(message.text)} + {renderMarkdown(message.text)}
      ) @@ -421,7 +428,7 @@ const ChatRow: React.FC = ({ {title} )} -
      {convertToMarkdown(message.text)}
      +
      {renderMarkdown(message.text)}
      ) } diff --git a/webview-ui/src/components/ChatView.tsx b/webview-ui/src/components/ChatView.tsx index fcd490a..6777e51 100644 --- a/webview-ui/src/components/ChatView.tsx +++ b/webview-ui/src/components/ChatView.tsx @@ -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} /> ) : ( -
      -

      What can I do for you?

      -

      - Thanks to{" "} - - Claude 3.5 Sonnet's agentic coding capabilities, - {" "} - 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. -

      -
      + <> + {showAnnouncement && } +
      +

      What can I do for you?

      +

      + Thanks to{" "} + + Claude 3.5 Sonnet's agentic coding capabilities, + {" "} + 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. +

      +
      + )} = ({ 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 = ({ apiKey, setApiKey }) => { useEffect(() => { validateApiKey(apiKey) - }, []) + }, [apiKey]) return ( -
      +

      Hi, I'm Claude Dev

      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 = ({ apiKey, setApiKey }) => {

      1. Go to{" "} - - https://console.anthropic.com/ + + https://console.anthropic.com
      2. You may need to buy some credits (although Anthropic is offering $5 free credit for new users)
      3. @@ -63,7 +57,7 @@ const WelcomeView: React.FC = ({ apiKey, setApiKey }) => { style={{ flexGrow: 1, marginRight: "10px" }} placeholder="Enter API Key..." value={apiKey} - onInput={handleApiKeyChange} + onInput={(e: any) => setApiKey(e.target.value)} /> Submit