diff --git a/extensions/spectacular/src/HelloWorldPanel.ts b/extensions/spectacular/src/HelloWorldPanel.ts index 8820f074..e3f5f2f8 100644 --- a/extensions/spectacular/src/HelloWorldPanel.ts +++ b/extensions/spectacular/src/HelloWorldPanel.ts @@ -20,6 +20,7 @@ import { GitManager } from "./services/GitManager"; import { GitHubManager } from './services/GitHubManager'; import { TaskManager } from './services/TaskManager'; import posthog from "posthog-js"; +import { create } from 'domain'; /** * This class manages the state and behavior of HelloWorld webview panels. @@ -212,6 +213,8 @@ export class HelloWorldPanel implements WebviewViewProvider { return await this.rpcOpenWorkspaceDialog(); case "createGitRepository": return await this.rpcCreateGitRepository(); + case "createAndOpenWorkspace": + return await this.rpcCreateAndOpenWorkspace(); case "getActiveTask": return await this.rpcGetActiveTask(params.taskId); case "listMeltyFiles": @@ -252,6 +255,10 @@ export class HelloWorldPanel implements WebviewViewProvider { return await this.rpcGetAssistantDescription(params.assistantType); case "getVSCodeTheme": return this.rpcGetVSCodeTheme(); + case "checkOnboardingComplete": + return this.rpcCheckOnboardingComplete(); + case "setOnboardingComplete": + return this.rpcSetOnboardingComplete(); default: throw new Error(`Unknown RPC method: ${method}`); } @@ -284,6 +291,35 @@ export class HelloWorldPanel implements WebviewViewProvider { } } + private async rpcCreateAndOpenWorkspace(): Promise { + try { + const homedir = require('os').homedir(); + const workspacePath = vscode.Uri.file(homedir + '/melty-workspace'); + + // Create the directory + await vscode.workspace.fs.createDirectory(workspacePath); + + // Open the new workspace in the current window without prompting + const success = await vscode.commands.executeCommand('vscode.openFolder', workspacePath, { + forceNewWindow: false, + noRecentEntry: true + }); + + return success === undefined; + } catch (error) { + console.error("Failed to create and open workspace:", error); + return false; + } + } + + private async rpcCheckOnboardingComplete(): Promise { + return this.MeltyExtension.checkOnboardingComplete(); + } + + private async rpcSetOnboardingComplete(): Promise { + await this.MeltyExtension.setOnboardingComplete(); + } + private async rpcOpenWorkspaceDialog(): Promise { const result = await vscode.window.showOpenDialog({ canSelectFiles: false, diff --git a/extensions/spectacular/src/extension.ts b/extensions/spectacular/src/extension.ts index bf5cd16e..473b51c9 100644 --- a/extensions/spectacular/src/extension.ts +++ b/extensions/spectacular/src/extension.ts @@ -75,6 +75,14 @@ export class MeltyExtension { clearInterval(this.branchCheckInterval); } } + + public async checkOnboardingComplete(): Promise { + return this.context.globalState.get('onboardingComplete', false); + } + + public async setOnboardingComplete(): Promise { + await this.context.globalState.update('onboardingComplete', true); + } } let outputChannel: vscode.OutputChannel; diff --git a/extensions/spectacular/src/types.ts b/extensions/spectacular/src/types.ts index e1f45906..dd8e1af1 100644 --- a/extensions/spectacular/src/types.ts +++ b/extensions/spectacular/src/types.ts @@ -116,4 +116,7 @@ export type RpcMethod = | "getAssistantDescription" | "getVSCodeTheme" | "openWorkspaceDialog" - | "createGitRepository"; + | "createGitRepository" + | "createAndOpenWorkspace" + | "checkOnboardingComplete" + | "setOnboardingComplete"; diff --git a/extensions/spectacular/webview-ui/src/App.tsx b/extensions/spectacular/webview-ui/src/App.tsx index 4a68038f..455e7bb8 100644 --- a/extensions/spectacular/webview-ui/src/App.tsx +++ b/extensions/spectacular/webview-ui/src/App.tsx @@ -4,11 +4,12 @@ import { Route, Routes, Navigate, - useNavigate, useLocation, } from "react-router-dom"; import { Tasks } from "./components/Tasks"; import { ConversationView } from "./components/ConversationView"; +import { Help } from "./components/Help"; +import { NavBar } from "./components/NavBar"; import { Onboarding } from "./components/Onboarding"; import { EventManager } from './eventManager'; import { RpcClient } from "RpcClient"; @@ -20,45 +21,55 @@ const rpcClient = RpcClient.getInstance(); const ThemeContext = createContext<'light' | 'dark'>('light'); function AppContent() { - const navigate = useNavigate(); - const location = useLocation(); const theme = useContext(ThemeContext); - - const handleKeyDown = useCallback( - async (event: KeyboardEvent) => { - if ((event.metaKey || event.ctrlKey) && event.key === "[") { - event.preventDefault(); - if (location.pathname !== "/") { - if (location.pathname.startsWith("/task/")) { - const taskId = location.pathname.split("/")[2]; // TODO there's gotta be a less hacky way... - await rpcClient.run("deactivateTask", { taskId }) - } - navigate("/"); - } - } - }, - [navigate, location] - ); + const [showOnboarding, setShowOnboarding] = useState(null); + const location = useLocation(); useEffect(() => { - window.addEventListener("keydown", handleKeyDown); - return () => { - window.removeEventListener("keydown", handleKeyDown); + const checkOnboarding = async () => { + const onboardingComplete = await rpcClient.run("checkOnboardingComplete", {}); + console.log("onboardingComplete", onboardingComplete); + setShowOnboarding(!onboardingComplete); }; - }, [handleKeyDown]); - useEffect(() => { + checkOnboarding(); return () => EventManager.Instance.cleanup(); }, []); + useEffect(() => { + if (showOnboarding === false && location.pathname === '/onboarding') { + // Redirect to home page after onboarding is complete + window.history.pushState(null, '', '/'); + } + }, [showOnboarding, location.pathname]); + + if (showOnboarding === null) { + return
Loading...
; + } + + if (showOnboarding && location.pathname !== '/onboarding') { + return ; + } return (
+ {!showOnboarding && }
+ { + setShowOnboarding(false); + rpcClient.run("setOnboardingComplete", {}); + }} + /> + } + /> } /> - } /> } /> + } /> } />
@@ -89,7 +100,7 @@ function App() { EventManager.Instance.addListener('notification', handleNotification); return () => { - EventManager.Instance.removeListener('notification', handleNotification); // probably don't need but can't hurt + EventManager.Instance.removeListener('notification', handleNotification); EventManager.Instance.cleanup(); }; }, [initTheme]); diff --git a/extensions/spectacular/webview-ui/src/components/Ascii.tsx b/extensions/spectacular/webview-ui/src/components/Ascii.tsx new file mode 100644 index 00000000..61478c95 --- /dev/null +++ b/extensions/spectacular/webview-ui/src/components/Ascii.tsx @@ -0,0 +1,41 @@ +import React, { useState, useEffect } from 'react'; + +const Ascii = () => { + const [text, setText] = useState(''); + const fullText = ` _ _ + _ __ ___ ___| | |_ _ _ + | '_ \` _ \\ / _ \\ | __| | | | + | | | | | | __/ | |_| |_| | + |_| |_| |_|\\___|_|\\__|\\__, | + |___/`; + + useEffect(() => { + let index = 0; + const timer = setInterval(() => { + setText((prev) => prev + fullText[index]); + index++; + if (index === fullText.length) { + clearInterval(timer); + } + }, 5); // Adjust this value to control the speed + + return () => clearInterval(timer); + }, []); + + return ( +
+			{text}
+		
+ ); +}; + +export default Ascii; diff --git a/extensions/spectacular/webview-ui/src/components/Help.tsx b/extensions/spectacular/webview-ui/src/components/Help.tsx new file mode 100644 index 00000000..93100f81 --- /dev/null +++ b/extensions/spectacular/webview-ui/src/components/Help.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +export const Help: React.FC = () => { + return ( +
+

Need some help? Noticed a bug? Have a cool idea?

+ +

Just text me! I'd love to help. You're one of our early users, so your feedback is really helpful for us.

+ +

You can reach Charlie (CEO) at +1 646-761-1319.

+

Or, email us at founders@melty.sh

+
+ ); +}; diff --git a/extensions/spectacular/webview-ui/src/components/NavBar.tsx b/extensions/spectacular/webview-ui/src/components/NavBar.tsx new file mode 100644 index 00000000..b85a79ac --- /dev/null +++ b/extensions/spectacular/webview-ui/src/components/NavBar.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { HelpCircle } from 'lucide-react'; + +export const NavBar: React.FC = () => { + const location = useLocation(); + + return ( + + ); +}; diff --git a/extensions/spectacular/webview-ui/src/components/Onboarding.tsx b/extensions/spectacular/webview-ui/src/components/Onboarding.tsx index dc82503d..85857fde 100644 --- a/extensions/spectacular/webview-ui/src/components/Onboarding.tsx +++ b/extensions/spectacular/webview-ui/src/components/Onboarding.tsx @@ -1,75 +1,69 @@ -import React, { useState, useEffect } from "react"; -import { Link } from "react-router-dom"; -import { Button } from "./ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; -import { Checkbox } from "./ui/checkbox"; -const checklistItems = [ - { - id: "welcome", - title: "Get an Anthropic API key", - link: { - text: "Get Anthropic key", - href: "https://console.anthropic.com/settings/keys", - }, - }, - { - id: "welcome", - title: "Set your Anthropic API key", - description: - "Open user settings (CMD+SHIFT+P → Open User Settings), search for Melty, and set the Anthropic key.", - }, - { - id: "repo", - title: "Open a directory that has a git repo in its root", - description: "Melty uses the repo to commit changes as it goes along.", - }, - { - id: "chat", - title: "Plug into a monitor and make it big!", - description: "Melty is designed for big screens.", - }, -]; -export function Onboarding() { +import React, { useState, useEffect } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { RpcClient } from '../RpcClient'; +import { Button } from './ui/button'; +import Ascii from './Ascii'; + +export function Onboarding({ onComplete }: { onComplete: () => void }) { + + const [keyPressed, setKeyPressed] = useState(false); + const [cmdPressed, setCmdPressed] = useState(false); + const [mPressed, setMPressed] = useState(false); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.metaKey || event.ctrlKey) { + setCmdPressed(true); + } + if (event.key === "m") { + setMPressed(true); + } + if ((event.metaKey || event.ctrlKey) && event.key === "m") { + setKeyPressed(true); + onComplete(); + } + if (keyPressed && event.key === "Enter") { + onComplete(); + } + }; + + const handleKeyUp = (event: KeyboardEvent) => { + if (!(event.metaKey || event.ctrlKey)) { + setCmdPressed(false); + } + if (event.key === "m") { + setMPressed(false); + } + }; + + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("keyup", handleKeyUp); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("keyup", handleKeyUp); + }; + }, []) + return ( - - - 🫠 Welcome to Melty! - - -
    - {checklistItems.map((item) => ( -
  • -
    - - -
    +
    + +

    Hi, human.

    +

    Melty is a new kind of IDE that writes code for you.

    +

    First things first — to open and close me press ⌘ + m. Try it now.

    -

    - {item.description} -

    - {item.link && ( - - {item.link.text} → - - )} -
  • - ))} -
-
- - - -
-
-
+
+ + + {" "} + + m + +
+
+ {keyPressed && } +
+ ); -} +}; diff --git a/extensions/spectacular/webview-ui/src/components/OnboardingSection.tsx b/extensions/spectacular/webview-ui/src/components/OnboardingSection.tsx new file mode 100644 index 00000000..43bcd60a --- /dev/null +++ b/extensions/spectacular/webview-ui/src/components/OnboardingSection.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { Code, FileCode, MessageCircleQuestion } from "lucide-react"; +import { Button } from "./ui/button"; + +interface OnboardingSectionProps { + setMessageText: (text: string) => void; +} + +const OnboardingSection: React.FC = ({ setMessageText }) => { + return ( +
+
    +
  • +

    + + Code +

    +
    + + + +
    +
  • +
  • +

    + + Explain +

    + +

    Melty can understand TypeScript and JS codebases. You can also give it specific files to focus on with the `@` command.

    +
  • +
  • +

    + + Ask +

    +
    + + +
    +
  • +
+
+ ); +}; + +export default OnboardingSection; diff --git a/extensions/spectacular/webview-ui/src/components/Tasks.tsx b/extensions/spectacular/webview-ui/src/components/Tasks.tsx index 7429fef8..2e5b2f4d 100644 --- a/extensions/spectacular/webview-ui/src/components/Tasks.tsx +++ b/extensions/spectacular/webview-ui/src/components/Tasks.tsx @@ -15,6 +15,8 @@ import { LightbulbIcon, } from "lucide-react"; import { MouseEvent, KeyboardEvent } from "react"; +import Ascii from "./Ascii"; +import OnboardingSection from './OnboardingSection'; import "diff2html/bundles/css/diff2html.min.css"; import { Link, useNavigate } from "react-router-dom"; import AutoExpandingTextarea from "./AutoExpandingTextarea"; @@ -37,13 +39,19 @@ function formatDate(date: Date): string { const now = new Date(); const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); - if (diffInSeconds < 60) return `${diffInSeconds} seconds ago`; - if (diffInSeconds < 3600) - return `${Math.floor(diffInSeconds / 60)} minutes ago`; - if (diffInSeconds < 86400) - return `${Math.floor(diffInSeconds / 3600)} hours ago`; - if (diffInSeconds < 604800) - return `${Math.floor(diffInSeconds / 86400)} days ago`; + if (diffInSeconds < 60) return `${diffInSeconds} second${diffInSeconds !== 1 ? 's' : ''} ago`; + if (diffInSeconds < 3600) { + const minutes = Math.floor(diffInSeconds / 60); + return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`; + } + if (diffInSeconds < 86400) { + const hours = Math.floor(diffInSeconds / 3600); + return `${hours} hour${hours !== 1 ? 's' : ''} ago`; + } + if (diffInSeconds < 604800) { + const days = Math.floor(diffInSeconds / 86400); + return `${days} day${days !== 1 ? 's' : ''} ago`; + } return date.toLocaleDateString(); } @@ -241,12 +249,28 @@ export function Tasks({ await fetchTasks(); }, [fetchTasks, checkGitConfig]); + const handleCreateGitRepo = useCallback(async () => { const didCreate = await rpcClient.run("createGitRepository", {}); console.log("did create git repo?", didCreate); await checkGitConfig(); }, [checkGitConfig]); + const createAndOpenWorkspace = useCallback(async () => { + try { + const result = await rpcClient.run("createAndOpenWorkspace", {}); + if (result) { + // We don't need to call checkGitConfig and fetchTasks here + // because VS Code will reload the window when opening a new folder + } else { + console.log("User cancelled workspace creation or it failed"); + // We don't need to show an error message here as the user might have just cancelled + } + } catch (error) { + console.error("Error creating and opening workspace:", error); + } + }, []); + // initialization useEffect(() => { console.log("initializing tasks"); @@ -276,25 +300,32 @@ export function Tasks({ }, [fetchTasks, fetchFilePaths, checkGitConfig, addSuggestion]); // DO NOT add anything to the initialization dependency array that isn't a constant + return (
-

- melty -

+ {gitConfigError !== "" ? ( gitConfigError?.includes("Open a workspace folder") ?
-

Let's start Melting.

-

To get started, add a workspace for Melty work in.

- + + + +

Where should I work?

+

Choose a folder for Melty to work in.

+ +
+ + +
+
: gitConfigError?.includes("git init") ?

Let's start Melting.

-

To get started, create a git repo in the workspace root folder.

+

Melty needs a git repo in the workspace root folder.

@@ -430,6 +461,10 @@ export function Tasks({
+ {tasks.length === 0 && + + } + {suggestions.length > 0 && (

diff --git a/extensions/spectacular/webview-ui/src/types.ts b/extensions/spectacular/webview-ui/src/types.ts index e1f45906..dd8e1af1 100644 --- a/extensions/spectacular/webview-ui/src/types.ts +++ b/extensions/spectacular/webview-ui/src/types.ts @@ -116,4 +116,7 @@ export type RpcMethod = | "getAssistantDescription" | "getVSCodeTheme" | "openWorkspaceDialog" - | "createGitRepository"; + | "createGitRepository" + | "createAndOpenWorkspace" + | "checkOnboardingComplete" + | "setOnboardingComplete";