diff --git a/core/config/ConfigHandler.ts b/core/config/ConfigHandler.ts index bd615f2e0b..3eb55643ff 100644 --- a/core/config/ConfigHandler.ts +++ b/core/config/ConfigHandler.ts @@ -12,6 +12,7 @@ import { } from "../index.js"; import Ollama from "../llm/llms/Ollama.js"; import { GlobalContext } from "../util/GlobalContext.js"; +import { getConfigJsonPath } from "../util/paths.js"; import { ConfigResult } from "./load.js"; import { @@ -84,6 +85,18 @@ export class ConfigHandler { return this.profiles.filter((p) => p.profileId !== this.selectedProfileId); } + async openConfigProfile(profileId?: string) { + let openProfileId = profileId || this.selectedProfileId; + if (openProfileId === "local") { + await this.ide.openFile(getConfigJsonPath()); + } else { + await this.ide.openUrl( + "https://app.continue.dev/", + // `https://app.continue.dev/workspaces/${openProfileId}/chat`, + ); + } + } + private async fetchControlPlaneProfiles() { // Get the profiles and create their lifecycle managers this.controlPlaneClient diff --git a/core/core.ts b/core/core.ts index 6510add642..c6b336c7d0 100644 --- a/core/core.ts +++ b/core/core.ts @@ -28,7 +28,11 @@ import { DevDataSqliteDb } from "./util/devdataSqlite"; import { fetchwithRequestOptions } from "./util/fetchWithOptions"; import { GlobalContext } from "./util/GlobalContext"; import historyManager from "./util/history"; -import { editConfigJson, setupInitialDotContinueDirectory } from "./util/paths"; +import { + editConfigJson, + getConfigJsonPath, + setupInitialDotContinueDirectory, +} from "./util/paths"; import { Telemetry } from "./util/posthog"; import { getSymbolsForManyFiles } from "./util/treeSitter"; import { TTS } from "./util/tts"; @@ -264,6 +268,10 @@ export class Core { await this.configHandler.reloadConfig(); }); + on("config/openProfile", async (msg) => { + await this.configHandler.openConfigProfile(msg.data.profileId); + }); + on("config/reload", (msg) => { void this.configHandler.reloadConfig(); return this.configHandler.getSerializedConfig(); diff --git a/core/index.d.ts b/core/index.d.ts index 4d50e6e62b..0646edc541 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -526,6 +526,7 @@ export interface IDE { showVirtualFile(title: string, contents: string): Promise; getContinueDir(): Promise; openFile(path: string): Promise; + openUrl(url: string): Promise; runCommand(command: string): Promise; saveFile(filepath: string): Promise; readFile(filepath: string): Promise; diff --git a/core/protocol/core.ts b/core/protocol/core.ts index a53ef3f2b3..c757e5d347 100644 --- a/core/protocol/core.ts +++ b/core/protocol/core.ts @@ -69,6 +69,7 @@ export type ToCoreFromIdeOrWebviewProtocol = { "config/deleteModel": [{ title: string }, void]; "config/reload": [undefined, BrowserSerializedContinueConfig]; "config/listProfiles": [undefined, ProfileDescription[]]; + "config/openProfile": [{ profileId: string | undefined }, void]; "context/getContextItems": [ { name: string; diff --git a/core/protocol/ide.ts b/core/protocol/ide.ts index a9b1d8cf98..75f53d5a72 100644 --- a/core/protocol/ide.ts +++ b/core/protocol/ide.ts @@ -28,6 +28,7 @@ export type ToIdeFromWebviewOrCoreProtocol = { showVirtualFile: [{ name: string; content: string }, void]; getContinueDir: [undefined, string]; openFile: [{ path: string }, void]; + openUrl: [string, void]; runCommand: [{ command: string }, void]; getSearchResults: [{ query: string }, string]; subprocess: [{ command: string; cwd?: string }, [string, string]]; @@ -49,11 +50,17 @@ export type ToIdeFromWebviewOrCoreProtocol = { ]; getProblems: [{ filepath: string }, Problem[]]; getOpenFiles: [undefined, string[]]; - getCurrentFile: [undefined, undefined | { - isUntitled: boolean; - path: string; - contents: string; - }]; + getCurrentFile: [ + undefined, + ( + | undefined + | { + isUntitled: boolean; + path: string; + contents: string; + } + ), + ]; getPinnedFiles: [undefined, string[]]; showLines: [{ filepath: string; startLine: number; endLine: number }, void]; readRangeInFile: [{ filepath: string; range: Range }, string]; diff --git a/core/protocol/ideWebview.ts b/core/protocol/ideWebview.ts index c833c3a7ac..1d18e1a6f9 100644 --- a/core/protocol/ideWebview.ts +++ b/core/protocol/ideWebview.ts @@ -30,7 +30,6 @@ export type ToIdeFromWebviewProtocol = ToIdeFromWebviewOrCoreProtocol & { overwriteFile: [{ filepath: string; prevFileContent: string | null }, void]; showTutorial: [undefined, void]; showFile: [{ filepath: string }, void]; - openConfigJson: [undefined, void]; toggleDevTools: [undefined, void]; reloadWindow: [undefined, void]; focusEditor: [undefined, void]; @@ -101,7 +100,6 @@ export type ToWebviewFromIdeProtocol = ToWebviewFromIdeOrCoreProtocol & { navigateTo: [{ path: string; toggle?: boolean }, void]; addModel: [undefined, void]; - openSettings: [undefined, void]; /** * @deprecated Use navigateTo with a path instead. */ diff --git a/core/protocol/passThrough.ts b/core/protocol/passThrough.ts index 7a13926175..40d537d41b 100644 --- a/core/protocol/passThrough.ts +++ b/core/protocol/passThrough.ts @@ -21,6 +21,8 @@ export const WEBVIEW_TO_CORE_PASS_THROUGH: (keyof ToCoreFromWebviewProtocol)[] = "config/getSerializedProfileInfo", "config/deleteModel", "config/reload", + "config/listProfiles", + "config/openProfile", "context/getContextItems", "context/getSymbolsForFiles", "context/loadSubmenuItems", @@ -53,7 +55,6 @@ export const WEBVIEW_TO_CORE_PASS_THROUGH: (keyof ToCoreFromWebviewProtocol)[] = // "completeOnboarding", "addAutocompleteModel", - "config/listProfiles", "profiles/switch", "didChangeSelectedProfile", ]; @@ -71,4 +72,6 @@ export const CORE_TO_WEBVIEW_PASS_THROUGH: (keyof ToWebviewFromCoreProtocol)[] = "didChangeAvailableProfiles", "setTTSActive", "getWebviewHistoryLength", + "signInToControlPlane", + "openDialogMessage", ]; diff --git a/core/protocol/webview.ts b/core/protocol/webview.ts index d259fde9e3..0dcdb047f6 100644 --- a/core/protocol/webview.ts +++ b/core/protocol/webview.ts @@ -23,4 +23,6 @@ export type ToWebviewFromIdeOrCoreProtocol = { ]; setTTSActive: [boolean, void]; getWebviewHistoryLength: [undefined, number]; + signInToControlPlane: [undefined, void]; + openDialogMessage: ["account", void]; }; diff --git a/core/util/filesystem.ts b/core/util/filesystem.ts index 23b44eab07..5041ceb8e5 100644 --- a/core/util/filesystem.ts +++ b/core/util/filesystem.ts @@ -191,6 +191,10 @@ class FileSystemIde implements IDE { return Promise.resolve(); } + openUrl(url: string): Promise { + return Promise.resolve(); + } + runCommand(command: string): Promise { return Promise.resolve(); } diff --git a/core/util/messageIde.ts b/core/util/messageIde.ts index 49d3378b32..4c2709f231 100644 --- a/core/util/messageIde.ts +++ b/core/util/messageIde.ts @@ -163,6 +163,10 @@ export class MessageIde implements IDE { await this.request("openFile", { path }); } + async openUrl(url: string): Promise { + await this.request("openUrl", url); + } + async runCommand(command: string): Promise { await this.request("runCommand", { command }); } diff --git a/core/util/paths.ts b/core/util/paths.ts index d606f095d3..3ff5251e62 100644 --- a/core/util/paths.ts +++ b/core/util/paths.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as os from "os"; +import { pathToFileURL } from "url"; import * as path from "path"; - import * as JSONC from "comment-json"; import dotenv from "dotenv"; @@ -41,6 +41,9 @@ export function getGlobalContinueIgnorePath(): string { return continueIgnorePath; } +/* + Deprecated, replace with getContinueGlobalUri where possible +*/ export function getContinueGlobalPath(): string { // This is ~/.continue on mac/linux const continuePath = CONTINUE_GLOBAL_DIR; @@ -50,6 +53,10 @@ export function getContinueGlobalPath(): string { return continuePath; } +export function getContinueGlobalUri(): string { + return pathToFileURL(CONTINUE_GLOBAL_DIR).href; +} + export function getSessionsFolderPath(): string { const sessionsPath = path.join(getContinueGlobalPath(), "sessions"); if (!fs.existsSync(sessionsPath)) { @@ -94,6 +101,10 @@ export function getConfigJsonPath(ideType: IdeType = "vscode"): string { return p; } +export function getConfigJsonUri(): string { + return getContinueGlobalUri() + "/config.json"; +} + export function getConfigTsPath(): string { const p = path.join(getContinueGlobalPath(), "config.ts"); if (!fs.existsSync(p)) { diff --git a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/toolWindow/ContinueBrowser.kt b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/toolWindow/ContinueBrowser.kt index fc51a392e1..659ec49985 100644 --- a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/toolWindow/ContinueBrowser.kt +++ b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/toolWindow/ContinueBrowser.kt @@ -192,9 +192,6 @@ class ContinueBrowser(val project: Project, url: String) { } "reloadWindow" -> {} - "openConfigJson" -> { - ide?.setFileOpen(getConfigJsonPath()) - } "readRangeInFile" -> { val data = data.asJsonObject diff --git a/extensions/vscode/package-lock.json b/extensions/vscode/package-lock.json index 3dc6757e75..9405035051 100644 --- a/extensions/vscode/package-lock.json +++ b/extensions/vscode/package-lock.json @@ -1,12 +1,12 @@ { "name": "continue", - "version": "0.9.231", + "version": "0.9.232", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "continue", - "version": "0.9.231", + "version": "0.9.232", "license": "Apache-2.0", "dependencies": { "@electron/rebuild": "^3.2.10", diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index ef2d12c82a..811d2f86a9 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -219,9 +219,10 @@ "group": "Continue" }, { - "command": "continue.openConfigJson", + "command": "continue.openConfig", "category": "Continue", - "title": "Open config.json", + "title": "Open Continue Config", + "icon": "$(gear)", "group": "Continue" }, { @@ -250,12 +251,25 @@ "icon": "$(history)", "group": "Continue" }, + { + "command": "continue.viewLogs", + "category": "Continue", + "title": "View History", + "group": "Continue" + }, { "command": "continue.navigateTo", "category": "Continue", "title": "Navigate to a path", "group": "Continue" }, + { + "command": "continue.openMorePage", + "category": "Continue", + "title": "More", + "icon": "$(ellipsis)", + "group": "Continue" + }, { "command": "continue.writeCommentsForCode", "category": "Continue", @@ -315,6 +329,20 @@ "category": "Continue", "title": "Focus Continue Chat", "group": "Continue" + }, + { + "command": "continue.signInToControlPlane", + "title": "Sign In", + "category": "Continue", + "group": "Continue", + "icon": "$(account)" + }, + { + "command": "continue.openAccountDialog", + "title": "Account", + "category": "Continue", + "group": "Continue", + "icon": "$(account)" } ], "keybindings": [ @@ -492,6 +520,26 @@ "command": "continue.toggleFullScreen", "group": "navigation@3", "when": "view == continue.continueGUIView" + }, + { + "command": "continue.openConfig", + "group": "navigation@4", + "when": "view == continue.continueGUIView" + }, + { + "command": "continue.signInToControlPlane", + "group": "navigation@5", + "when": "(view == continue.continueGUIView) && config.continue.enableContinueForTeams && !continue.isSignedInToControlPlane" + }, + { + "command": "continue.openAccountDialog", + "group": "navigation@5", + "when": "(view == continue.continueGUIView) && config.continue.enableContinueForTeams && continue.isSignedInToControlPlane" + }, + { + "command": "continue.openMorePage", + "group": "navigation@6", + "when": "view == continue.continueGUIView" } ], "editor/title": [ diff --git a/extensions/vscode/src/VsCodeIde.ts b/extensions/vscode/src/VsCodeIde.ts index 1faace46d4..7d9536f5a7 100644 --- a/extensions/vscode/src/VsCodeIde.ts +++ b/extensions/vscode/src/VsCodeIde.ts @@ -485,6 +485,11 @@ class VsCodeIde implements IDE { return ""; } } + + async openUrl(url: string): Promise { + await vscode.env.openExternal(vscode.Uri.parse(url)); + } + async showDiff( filepath: string, newContents: string, diff --git a/extensions/vscode/src/commands.ts b/extensions/vscode/src/commands.ts index a1c46126e6..83db06586b 100644 --- a/extensions/vscode/src/commands.ts +++ b/extensions/vscode/src/commands.ts @@ -580,10 +580,6 @@ const getCommandsMap: ( vscode.commands.executeCommand("continue.continueGUIView.focus"); sidebar.webviewProtocol?.request("addModel", undefined); }, - "continue.openSettingsUI": () => { - vscode.commands.executeCommand("continue.continueGUIView.focus"); - sidebar.webviewProtocol?.request("openSettings", undefined); - }, "continue.sendMainUserInput": (text: string) => { sidebar.webviewProtocol?.request("userInput", { input: text, @@ -683,8 +679,10 @@ const getCommandsMap: ( vscode.commands.executeCommand("workbench.action.copyEditorToNewWindow"); }, - "continue.openConfigJson": () => { - ide.openFile(getConfigJsonPath()); + "continue.openConfig": () => { + core.invoke("config/openProfile", { + profileId: undefined, + }); }, "continue.selectFilesAsContext": async ( firstUri: vscode.Uri, @@ -857,7 +855,7 @@ const getCommandsMap: ( vscode.commands.executeCommand("continue.toggleFullScreen"); } else if (selectedOption === "$(question) Open help center") { focusGUI(); - vscode.commands.executeCommand("continue.navigateTo", "/more"); + vscode.commands.executeCommand("continue.navigateTo", "/more", true); } quickPick.dispose(); }); @@ -877,10 +875,19 @@ const getCommandsMap: ( client.sendFeedback(feedback, lastLines); } }, - "continue.navigateTo": (path: string) => { - sidebar.webviewProtocol?.request("navigateTo", { path }); + "continue.openMorePage": () => { + vscode.commands.executeCommand("continue.navigateTo", "/more", true); + }, + "continue.navigateTo": (path: string, toggle: boolean) => { + sidebar.webviewProtocol?.request("navigateTo", { path, toggle }); focusGUI(); }, + "continue.signInToControlPlane": () => { + sidebar.webviewProtocol?.request("signInToControlPlane", undefined); + }, + "continue.openAccountDialog": () => { + sidebar.webviewProtocol?.request("openDialogMessage", "account"); + }, }; }; diff --git a/extensions/vscode/src/extension/VsCodeExtension.ts b/extensions/vscode/src/extension/VsCodeExtension.ts index 1fa8bc6b51..ec1c5f9639 100644 --- a/extensions/vscode/src/extension/VsCodeExtension.ts +++ b/extensions/vscode/src/extension/VsCodeExtension.ts @@ -321,9 +321,13 @@ export class VsCodeExtension { // When GitHub sign-in status changes, reload config vscode.authentication.onDidChangeSessions(async (e) => { - if (e.provider.id === "github") { - this.configHandler.reloadConfig(); - } else if (e.provider.id === controlPlaneEnv.AUTH_TYPE) { + if (e.provider.id === controlPlaneEnv.AUTH_TYPE) { + vscode.commands.executeCommand( + "setContext", + "continue.isSignedInToControlPlane", + true, + ); + const sessionInfo = await getControlPlaneSessionInfo(true); this.webviewProtocolPromise.then(async (webviewProtocol) => { void webviewProtocol.request("didChangeControlPlaneSessionInfo", { @@ -336,6 +340,16 @@ export class VsCodeExtension { void this.core.invoke("didChangeControlPlaneSessionInfo", { sessionInfo, }); + } else { + vscode.commands.executeCommand( + "setContext", + "continue.isSignedInToControlPlane", + false, + ); + + if (e.provider.id === "github") { + this.configHandler.reloadConfig(); + } } }); diff --git a/extensions/vscode/src/extension/VsCodeMessenger.ts b/extensions/vscode/src/extension/VsCodeMessenger.ts index 3b5baad5f6..1267d57818 100644 --- a/extensions/vscode/src/extension/VsCodeMessenger.ts +++ b/extensions/vscode/src/extension/VsCodeMessenger.ts @@ -19,7 +19,6 @@ import { } from "core/protocol/passThrough"; import { getBasename } from "core/util"; import { InProcessMessenger, Message } from "core/util/messenger"; -import { getConfigJsonPath } from "core/util/paths"; import * as vscode from "vscode"; import { VerticalDiffManager } from "../diff/vertical/manager"; @@ -108,10 +107,6 @@ export class VsCodeMessenger { ); }); - this.onWebview("openConfigJson", (msg) => { - this.ide.openFile(getConfigJsonPath()); - }); - this.onWebview("readRangeInFile", async (msg) => { return await vscode.workspace .openTextDocument(msg.data.filepath) diff --git a/extensions/vscode/src/lang-server/codeLens/providers/ConfigPyCodeLensProvider.ts b/extensions/vscode/src/lang-server/codeLens/providers/ConfigPyCodeLensProvider.ts index 80225874c2..469426cfb2 100644 --- a/extensions/vscode/src/lang-server/codeLens/providers/ConfigPyCodeLensProvider.ts +++ b/extensions/vscode/src/lang-server/codeLens/providers/ConfigPyCodeLensProvider.ts @@ -35,7 +35,7 @@ export class ConfigPyCodeLensProvider implements vscode.CodeLensProvider { codeLenses.push( new vscode.CodeLens(range, { title: "✏️ Edit in UI", - command: "continue.openSettingsUI", + command: "continue.openConfigUI", // command likely doesn't exist anymore, check }), ); } diff --git a/extensions/vscode/src/lang-server/codeLens/registerAllCodeLensProviders.ts b/extensions/vscode/src/lang-server/codeLens/registerAllCodeLensProviders.ts index 292999f7b6..2defb97a0d 100644 --- a/extensions/vscode/src/lang-server/codeLens/registerAllCodeLensProviders.ts +++ b/extensions/vscode/src/lang-server/codeLens/registerAllCodeLensProviders.ts @@ -132,7 +132,9 @@ export function registerAllCodeLensProviders( context.subscriptions.push(verticalPerLineCodeLensProvider); context.subscriptions.push(suggestionsCodeLensDisposable); context.subscriptions.push(diffsCodeLensDisposable); - context.subscriptions.push(configPyCodeLensDisposable); + + // was opening config UI from config.json. Not currently applicable. + // context.subscriptions.push(configPyCodeLensDisposable); return { verticalDiffCodeLens }; } diff --git a/gui/src/components/AccountDialog.tsx b/gui/src/components/AccountDialog.tsx new file mode 100644 index 0000000000..5438d11df6 --- /dev/null +++ b/gui/src/components/AccountDialog.tsx @@ -0,0 +1,155 @@ +import { Listbox, Transition } from "@headlessui/react"; +import { ChevronUpDownIcon } from "@heroicons/react/24/outline"; +import { Fragment, useContext } from "react"; +import styled from "styled-components"; +import { + Button, + SecondaryButton, + vscInputBackground, + vscInputBorder, + vscListActiveBackground, + vscListActiveForeground, +} from "."; +import { IdeMessengerContext } from "../context/IdeMessenger"; +import { useAuth } from "../context/Auth"; +import { setSelectedProfileId } from "../redux/slices/stateSlice"; +import { useDispatch } from "react-redux"; +import { setDialogMessage, setShowDialog } from "../redux/slices/uiStateSlice"; +import { NewSessionButton } from "./mainInput/NewSessionButton"; + +const StyledListboxOption = styled(Listbox.Option)<{ selected: boolean }>` + background-color: ${({ selected }) => + selected ? vscListActiveBackground : vscInputBackground}; + cursor: pointer; + padding: 6px 8px; + + &:hover { + background-color: ${vscListActiveBackground}; + color: ${vscListActiveForeground}; + } +`; + +function AccountDialog() { + const ideMessenger = useContext(IdeMessengerContext); + const { + session, + logout, + login, + profiles, + selectedProfile, + controlServerBetaEnabled, + } = useAuth(); + + // These shouldn't usually show but just to be safe + if (!session?.account?.id) { + return ( +
+

Account

+

Not signed in

+ +
+ ); + } + + if (!controlServerBetaEnabled) { + return ( +
+

Account

+

+ Continue for teams is not enabled. You can enable it in your IDE + settings +

+

Using local config.

+ +
+ ); + } + + const dispatch = useDispatch(); + + const changeProfileId = (id: string) => { + ideMessenger.post("didChangeSelectedProfile", { id }); + dispatch(setSelectedProfileId(id)); + }; + + return ( +
+

Account

+ +

Select a workspace

+ + + {({ open }) => ( +
+ + {selectedProfile?.title} +
+
+
+ + + + {profiles.map((option, idx) => ( + +
+ {option.title} +
+
+ ))} + {profiles.length === 0 ? ( +
+ No workspaces found +
+ ) : null} +
+
+
+ )} +
+

+ {session.account.label === "" + ? "Signed in" + : `Signed in as ${session.account.label}`} +

+ + Sign out + +
+ +
+
+ ); +} + +export default AccountDialog; diff --git a/gui/src/components/AddModelButtonSubtext.tsx b/gui/src/components/AddModelButtonSubtext.tsx index 4a5744e7dc..11a95a711b 100644 --- a/gui/src/components/AddModelButtonSubtext.tsx +++ b/gui/src/components/AddModelButtonSubtext.tsx @@ -10,7 +10,9 @@ function AddModelButtonSubtext() { This will update your{" "} ideMessenger.post("openConfigJson", undefined)} + onClick={() => + ideMessenger.post("config/openLocalConfigFile", undefined) + } > config file diff --git a/gui/src/components/Footer.tsx b/gui/src/components/Footer.tsx index 0c094155c1..c121feeb15 100644 --- a/gui/src/components/Footer.tsx +++ b/gui/src/components/Footer.tsx @@ -1,92 +1,22 @@ -import { - Cog6ToothIcon, - EllipsisHorizontalCircleIcon, - ExclamationTriangleIcon, -} from "@heroicons/react/24/outline"; -import { useContext } from "react"; import { useSelector } from "react-redux"; -import { useLocation, useNavigate } from "react-router-dom"; -import { IdeMessengerContext } from "../context/IdeMessenger"; import { defaultModelSelector } from "../redux/selectors/modelSelectors"; -import { RootState } from "../redux/store"; import { FREE_TRIAL_LIMIT_REQUESTS } from "../util/freeTrial"; -import { ROUTES } from "../util/navigation"; -import HeaderButtonWithToolTip from "./gui/HeaderButtonWithToolTip"; import FreeTrialProgressBar from "./loaders/FreeTrialProgressBar"; -import ProfileSwitcher from "./ProfileSwitcher"; function Footer() { - const navigate = useNavigate(); - const { pathname } = useLocation(); const defaultModel = useSelector(defaultModelSelector); - const ideMessenger = useContext(IdeMessengerContext); - const selectedProfileId = useSelector( - (store: RootState) => store.state.selectedProfileId, - ); - const configError = useSelector( - (store: RootState) => store.state.configError, - ); - function onClickMore() { - navigate(pathname === ROUTES.MORE ? "/" : ROUTES.MORE); + if (defaultModel?.provider === "free-trial") { + return ( +
+ +
+ ); } - - function onClickError() { - navigate(pathname === ROUTES.CONFIG_ERROR ? "/" : ROUTES.CONFIG_ERROR); - } - - function onClickSettings() { - if (selectedProfileId === "local") { - ideMessenger.post("openConfigJson", undefined); - } else { - ideMessenger.post( - "openUrl", - `http://app.continue.dev/workspaces/${selectedProfileId}/chat`, - ); - } - } - - return ( -
-
- - {defaultModel?.provider === "free-trial" && ( - - )} -
- -
- {configError && ( - - - - )} - - - - - - - - -
-
- ); + return null; } export default Footer; diff --git a/gui/src/components/Layout.tsx b/gui/src/components/Layout.tsx index 5879bd2e0e..1b7b9222e6 100644 --- a/gui/src/components/Layout.tsx +++ b/gui/src/components/Layout.tsx @@ -12,7 +12,7 @@ import { setEditStatus, startEditMode, } from "../redux/slices/editModeState"; -import { setShowDialog } from "../redux/slices/uiStateSlice"; +import { setDialogMessage, setShowDialog } from "../redux/slices/uiStateSlice"; import { updateApplyState, updateCurCheckpoint, @@ -25,6 +25,8 @@ import TextDialog from "./dialogs"; import Footer from "./Footer"; import { isNewUserOnboarding, useOnboardingCard } from "./OnboardingCard"; import PostHogPageView from "./PosthogPageView"; +import AccountDialog from "./AccountDialog"; +import { AuthProvider } from "../context/Auth"; const LayoutTopDiv = styled(CustomScrollbarDiv)` height: 100%; @@ -50,22 +52,6 @@ const GridDiv = styled.div` overflow-x: visible; `; -const ModelDropdownPortalDiv = styled.div` - background-color: ${vscInputBackground}; - position: relative; - margin-left: 8px; - z-index: 200; - font-size: ${getFontSize()}; -`; - -const ProfileDropdownPortalDiv = styled.div` - background-color: ${vscInputBackground}; - position: relative; - margin-left: 8px; - z-index: 200; - font-size: ${getFontSize() - 2}; -`; - const Layout = () => { const navigate = useNavigate(); const location = useLocation(); @@ -109,6 +95,17 @@ const Layout = () => { }; }, [timeline]); + useWebviewListener( + "openDialogMessage", + async (message) => { + if (message === "account") { + dispatch(setShowDialog(true)); + dispatch(setDialogMessage()); + } + }, + [], + ); + useWebviewListener( "addModel", async () => { @@ -117,10 +114,6 @@ const Layout = () => { [navigate], ); - useWebviewListener("openSettings", async () => { - ideMessenger.post("openConfigJson", undefined); - }); - useWebviewListener( "viewHistory", async () => { @@ -217,56 +210,55 @@ const Layout = () => { }, [location]); return ( - - -
- { - dispatch(setShowDialog(false)); + + + +
{ - dispatch(setShowDialog(false)); - }} - message={dialogMessage} - /> + > + { + dispatch(setShowDialog(false)); + }} + onClose={() => { + dispatch(setShowDialog(false)); + }} + message={dialogMessage} + /> - - - + + + - {hasFatalErrors && pathname !== ROUTES.CONFIG_ERROR && ( -
navigate(ROUTES.CONFIG_ERROR)} - > - Error!{" "} - - Could not load config.json - -
Learn More
-
- )} - - - -
- -
-
- - + {hasFatalErrors && pathname !== ROUTES.CONFIG_ERROR && ( +
navigate(ROUTES.CONFIG_ERROR)} + > + Error!{" "} + + Could not load config.json + +
Learn More
+
+ )} +
+ +
+
+ + + ); }; diff --git a/gui/src/components/dialogs/ConfirmationDialog.tsx b/gui/src/components/dialogs/ConfirmationDialog.tsx index 6064559e0c..965d38e070 100644 --- a/gui/src/components/dialogs/ConfirmationDialog.tsx +++ b/gui/src/components/dialogs/ConfirmationDialog.tsx @@ -43,9 +43,9 @@ function ConfirmationDialog(props: ConfirmationDialogProps) { { - props.onCancel?.(); dispatch(setShowDialog(false)); dispatch(setDialogMessage(undefined)); + props.onCancel?.(); }} > Cancel diff --git a/gui/src/components/loaders/FreeTrialProgressBar.tsx b/gui/src/components/loaders/FreeTrialProgressBar.tsx index 99a2d9b69d..6363ede498 100644 --- a/gui/src/components/loaders/FreeTrialProgressBar.tsx +++ b/gui/src/components/loaders/FreeTrialProgressBar.tsx @@ -37,7 +37,7 @@ function FreeTrialProgressBar({ completed, total }: FreeTrialProgressBarProps) { return ( <>
@@ -54,21 +54,14 @@ function FreeTrialProgressBar({ completed, total }: FreeTrialProgressBarProps) { return ( <>
-
- - Free trial requests - - - - {completed} / {total} - -
- -
+ + Free trial requests + +
0.75 ? "bg-amber-500" : "bg-stone-500" @@ -78,6 +71,9 @@ function FreeTrialProgressBar({ completed, total }: FreeTrialProgressBarProps) { }} />
+ + {completed} / {total} +
{`Click to use your own API key or local LLM (required after ${FREE_TRIAL_LIMIT_REQUESTS} inputs)`} diff --git a/gui/src/components/modelSelection/ModelSelect.tsx b/gui/src/components/modelSelection/ModelSelect.tsx index c41b805240..2ce7944466 100644 --- a/gui/src/components/modelSelection/ModelSelect.tsx +++ b/gui/src/components/modelSelection/ModelSelect.tsx @@ -169,7 +169,7 @@ function ModelOption({ e.stopPropagation(); e.preventDefault(); - ideMessenger.post("openConfigJson", undefined); + ideMessenger.post("config/openLocalConfigFile", undefined); } function handleOptionClick(e) { diff --git a/gui/src/context/Auth.tsx b/gui/src/context/Auth.tsx new file mode 100644 index 0000000000..056770cf07 --- /dev/null +++ b/gui/src/context/Auth.tsx @@ -0,0 +1,184 @@ +// AuthContext.tsx +import React, { + createContext, + useContext, + useState, + useEffect, + useMemo, +} from "react"; +import { ControlPlaneSessionInfo } from "core/control-plane/client"; +import { useDispatch, useSelector } from "react-redux"; +import ConfirmationDialog from "../components/dialogs/ConfirmationDialog"; +import { IdeMessengerContext } from "./IdeMessenger"; +import { setDialogMessage, setShowDialog } from "../redux/slices/uiStateSlice"; +import { getLocalStorage, setLocalStorage } from "../util/localStorage"; +import { RootState } from "../redux/store"; +import { ProfileDescription } from "core/config/ProfileLifecycleManager"; +import { setLastControlServerBetaEnabledStatus } from "../redux/slices/miscSlice"; +import { useWebviewListener } from "../hooks/useWebviewListener"; +import AccountDialog from "../components/AccountDialog"; + +interface AuthContextType { + session: ControlPlaneSessionInfo | undefined; + logout: () => void; + login: () => void; + selectedProfile: ProfileDescription | undefined; + profiles: ProfileDescription[]; + controlServerBetaEnabled: boolean; +} + +const AuthContext = createContext(undefined); + +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [session, setSession] = useState( + undefined, + ); + const [profiles, setProfiles] = useState([]); + const selectedProfileId = useSelector( + (store: RootState) => store.state.selectedProfileId, + ); + const selectedProfile = useMemo(() => { + return profiles.find((p) => p.id === selectedProfileId); + }, [profiles, selectedProfileId]); + + const ideMessenger = useContext(IdeMessengerContext); + const dispatch = useDispatch(); + + const lastControlServerBetaEnabledStatus = useSelector( + (state: RootState) => state.misc.lastControlServerBetaEnabledStatus, + ); + + const login = () => { + ideMessenger + .request("getControlPlaneSessionInfo", { silent: false }) + .then((result) => { + if (result.status === "error") { + return; + } + const session = result.content; + setSession(session); + + // If this is the first time the user has logged in, explain how profiles work + if (!getLocalStorage("shownProfilesIntroduction")) { + dispatch(setShowDialog(true)); + dispatch( + setDialogMessage( + {}} + />, + ), + ); + setLocalStorage("shownProfilesIntroduction", true); + } + }); + }; + + const logout = () => { + dispatch(setShowDialog(true)); + dispatch( + setDialogMessage( + { + ideMessenger.post("logoutOfControlPlane", undefined); + }} + onCancel={() => { + dispatch(setDialogMessage()); + dispatch(setShowDialog(true)); + }} + />, + ), + ); + }; + + useWebviewListener("didChangeControlPlaneSessionInfo", async (data) => { + setSession(data.sessionInfo); + }); + + useWebviewListener("signInToControlPlane", async () => { + login(); + }); + + useEffect(() => { + ideMessenger + .request("getControlPlaneSessionInfo", { silent: true }) + .then( + (result) => result.status === "success" && setSession(result.content), + ); + }, []); + + const [controlServerBetaEnabled, setControlServerBetaEnabled] = + useState(false); + + useEffect(() => { + ideMessenger.ide.getIdeSettings().then(({ enableControlServerBeta }) => { + setControlServerBetaEnabled(enableControlServerBeta); + dispatch(setLastControlServerBetaEnabledStatus(enableControlServerBeta)); + + const shouldShowPopup = + !lastControlServerBetaEnabledStatus && enableControlServerBeta; + if (shouldShowPopup) { + ideMessenger.ide.showToast("info", "Continue for Teams enabled"); + } + }); + }, []); + + useWebviewListener( + "didChangeIdeSettings", + async (msg) => { + const { settings } = msg; + setControlServerBetaEnabled(settings.enableControlServerBeta); + dispatch( + setLastControlServerBetaEnabledStatus(settings.enableControlServerBeta), + ); + }, + [], + ); + + useEffect(() => { + ideMessenger + .request("config/listProfiles", undefined) + .then( + (result) => result.status === "success" && setProfiles(result.content), + ); + }, []); + + useWebviewListener( + "didChangeAvailableProfiles", + async (data) => { + setProfiles(data.profiles); + }, + [], + ); + + return ( + + {children} + + ); +}; + +export const useAuth = (): AuthContextType => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +}; diff --git a/gui/src/forms/AddModelForm.tsx b/gui/src/forms/AddModelForm.tsx index 8613335177..fdb7faf779 100644 --- a/gui/src/forms/AddModelForm.tsx +++ b/gui/src/forms/AddModelForm.tsx @@ -87,7 +87,9 @@ function AddModelForm({ }; ideMessenger.post("config/addModel", { model }); - ideMessenger.post("openConfigJson", undefined); + ideMessenger.post("config/openProfile", { + profileId: "local", + }); dispatch(setDefaultModel({ title: model.title, force: true })); diff --git a/gui/src/hooks/useAuth.tsx b/gui/src/hooks/useAuth.tsx deleted file mode 100644 index 994c8c1e7f..0000000000 --- a/gui/src/hooks/useAuth.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { ControlPlaneSessionInfo } from "core/control-plane/client"; -import { useContext, useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import ConfirmationDialog from "../components/dialogs/ConfirmationDialog"; -import { IdeMessengerContext } from "../context/IdeMessenger"; -import { setDialogMessage, setShowDialog } from "../redux/slices/uiStateSlice"; -import { getLocalStorage, setLocalStorage } from "../util/localStorage"; -import { useWebviewListener } from "./useWebviewListener"; - -export function useAuth(): { - session: ControlPlaneSessionInfo | undefined; - logout: () => void; - login: () => void; -} { - const [session, setSession] = useState( - undefined, - ); - const ideMessenger = useContext(IdeMessengerContext); - const dispatch = useDispatch(); - - useWebviewListener("didChangeControlPlaneSessionInfo", async (data) => { - setSession(data.sessionInfo); - }); - - useEffect(() => { - ideMessenger - .request("getControlPlaneSessionInfo", { silent: true }) - .then( - (result) => result.status === "success" && setSession(result.content), - ); - }, []); - - const login = () => { - ideMessenger - .request("getControlPlaneSessionInfo", { silent: false }) - .then((result) => { - if (result.status === "error") { - return; - } - const session = result.content; - setSession(session); - - // If this is the first time the user has logged in, explain how profiles work - if (!getLocalStorage("shownProfilesIntroduction")) { - dispatch(setShowDialog(true)); - dispatch( - setDialogMessage( - {}} - />, - ), - ); - setLocalStorage("shownProfilesIntroduction", true); - } - }); - }; - - const logout = () => { - dispatch(setShowDialog(true)); - dispatch( - setDialogMessage( - { - ideMessenger.post("logoutOfControlPlane", undefined); - }} - />, - ), - ); - }; - - return { - session, - logout, - login, - }; -} diff --git a/gui/src/hooks/useSetup.ts b/gui/src/hooks/useSetup.ts index 5b663717e0..4bef9da9ce 100644 --- a/gui/src/hooks/useSetup.ts +++ b/gui/src/hooks/useSetup.ts @@ -82,6 +82,7 @@ function useSetup(dispatch: Dispatch) { sessionIdRef.current = sessionId; }, [sessionId, history, ideMessenger, dispatch]); + // ON LOAD useEffect(() => { // Override persisted state dispatch(setInactive()); @@ -100,6 +101,18 @@ function useSetup(dispatch: Dispatch) { dispatch(setVscMachineId(msg.vscMachineId)); // dispatch(setVscMediaUrl(msg.vscMediaUrl)); }); + + // Save theme colors to local storage for immediate loading in JetBrains + if (isJetBrains()) { + for (const colorVar of VSC_THEME_COLOR_VARS) { + if (document.body.style.getPropertyValue(colorVar)) { + localStorage.setItem( + colorVar, + document.body.style.getPropertyValue(colorVar), + ); + } + } + } }, []); const { streamResponse } = useChatHandler(dispatch, ideMessenger); @@ -166,19 +179,7 @@ function useSetup(dispatch: Dispatch) { [defaultModelTitle], ); - // Save theme colors to local storage for immediate loading in JetBrains - useEffect(() => { - if (isJetBrains()) { - for (const colorVar of VSC_THEME_COLOR_VARS) { - if (document.body.style.getPropertyValue(colorVar)) { - localStorage.setItem( - colorVar, - document.body.style.getPropertyValue(colorVar), - ); - } - } - } - }, []); + } export default useSetup; diff --git a/gui/src/pages/AddNewModel/AddNewModel.tsx b/gui/src/pages/AddNewModel/AddNewModel.tsx index b361732c7c..46ac8fdf20 100644 --- a/gui/src/pages/AddNewModel/AddNewModel.tsx +++ b/gui/src/pages/AddNewModel/AddNewModel.tsx @@ -211,7 +211,9 @@ function AddNewModel() { className="mt-12" disabled={false} onClick={(e) => { - ideMessenger.post("openConfigJson", undefined); + ideMessenger.post("config/openProfile", { + profileId: "local", + }); }} >

diff --git a/gui/src/pages/More/IndexingProgress/IndexingProgress.tsx b/gui/src/pages/More/IndexingProgress/IndexingProgress.tsx index f3d4fea556..1f06f428c0 100644 --- a/gui/src/pages/More/IndexingProgress/IndexingProgress.tsx +++ b/gui/src/pages/More/IndexingProgress/IndexingProgress.tsx @@ -98,7 +98,9 @@ function IndexingProgress() { } break; case "disabled": - ideMessenger.post("openConfigJson", undefined); + ideMessenger.post("config/openProfile", { + profileId: undefined, + }); break; case "done": ideMessenger.post("index/forceReIndex", undefined); diff --git a/gui/src/pages/gui/Chat.tsx b/gui/src/pages/gui/Chat.tsx index a48ac244b0..a810dc87aa 100644 --- a/gui/src/pages/gui/Chat.tsx +++ b/gui/src/pages/gui/Chat.tsx @@ -54,11 +54,11 @@ import { RootState } from "../../redux/store"; import { getFontSize, getMetaKeyLabel, - isJetBrains, isMetaEquivalentKeyPressed, } from "../../util"; import { FREE_TRIAL_LIMIT_REQUESTS } from "../../util/freeTrial"; import { getLocalStorage, setLocalStorage } from "../../util/localStorage"; +import ConfigErrorIndicator from "./ConfigError"; import ChatIndexingPeeks from "../../components/indexing/ChatIndexingPeeks"; import { useFindWidget } from "../../components/find/FindWidget"; @@ -419,37 +419,29 @@ export function Chat() { pointerEvents: active ? "none" : "auto", }} > - {state.history.length > 0 ? ( -
- { - saveSession(); - }} - className="mr-auto" - > - - New Session ({getMetaKeyLabel()} {isJetBrains() ? "J" : "L"}) - - -
- ) : ( - <> - {getLastSessionId() ? ( -
+
+
+ {state.history.length === 0 && getLastSessionId() ? ( +
{ loadLastSession().catch((e) => console.error(`Failed to load last session: ${e}`), ); }} - className="mr-auto flex items-center gap-2" + className="flex items-center gap-2" > Last Session
) : null} +
+ +
+ {state.history.length === 0 && ( + <> {onboardingCard.show && (
diff --git a/gui/src/pages/gui/ConfigError.tsx b/gui/src/pages/gui/ConfigError.tsx new file mode 100644 index 0000000000..ff8d306ef1 --- /dev/null +++ b/gui/src/pages/gui/ConfigError.tsx @@ -0,0 +1,36 @@ +import { useLocation, useNavigate } from "react-router-dom"; +import HeaderButtonWithToolTip from "../../components/gui/HeaderButtonWithToolTip"; +import { useSelector } from "react-redux"; +import { ROUTES } from "../../util/navigation"; +import { RootState } from "../../redux/store"; +import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; + +const ConfigErrorIndicator = () => { + const configError = useSelector( + (store: RootState) => store.state.configError, + ); + + const navigate = useNavigate(); + const { pathname } = useLocation(); + + function onClickError() { + navigate(pathname === ROUTES.CONFIG_ERROR ? "/" : ROUTES.CONFIG_ERROR); + } + + if (!configError?.length) { + return null; + } + + // TODO: add a tooltip + return ( + + + + ); +}; + +export default ConfigErrorIndicator;