diff --git a/clients/vscode/package.json b/clients/vscode/package.json index be8a3a1..716582f 100644 --- a/clients/vscode/package.json +++ b/clients/vscode/package.json @@ -57,6 +57,14 @@ { "command": "tabby.gettingStarted", "title": "Tabby: Getting Started" + }, + { + "command": "tabby.openOnlineHelp", + "title": "Tabby: Online Help" + }, + { + "command": "tabby.notifications.resetMuted", + "title": "Tabby: Reset notifications marked as \"Don't Show Again\"" } ], "menus": { @@ -147,7 +155,7 @@ "tabby.usage.anonymousUsageTracking": { "type": "boolean", "default": false, - "markdownDescription": "**Disable anonymous usage tracking** \nTabby collects aggregated anonymous usage data and sends it to the Tabby team to help improve our products. \nYour code, generated completions, or any identifying information is never tracked or transmitted. \nFor more details on data collection, please check our [online documentation](https://tabby.tabbyml.com/docs/extensions/configuration#usage-collection)." + "markdownDescription": "**Disable anonymous usage tracking** \nTabby collects aggregated anonymous usage data and sends it to the Tabby team to help improve our products. \nYour code, generated completions, or any identifying information is never tracked or transmitted. \nFor more details on data collection, please check our [online documentation](https://tabby.tabbyml.com/docs/extensions/configurations#usage-collection)." } } }, diff --git a/clients/vscode/src/TabbyCompletionProvider.ts b/clients/vscode/src/TabbyCompletionProvider.ts index 0cfbe00..a9b08df 100644 --- a/clients/vscode/src/TabbyCompletionProvider.ts +++ b/clients/vscode/src/TabbyCompletionProvider.ts @@ -14,20 +14,12 @@ import { CompletionRequest, CompletionResponse, LogEventRequest } from "tabby-ag import { agent } from "./agent"; export class TabbyCompletionProvider extends EventEmitter implements InlineCompletionItemProvider { - static instance: TabbyCompletionProvider; - static getInstance(): TabbyCompletionProvider { - if (!TabbyCompletionProvider.instance) { - TabbyCompletionProvider.instance = new TabbyCompletionProvider(); - } - return TabbyCompletionProvider.instance; - } - private triggerMode: "automatic" | "manual" | "disabled" = "automatic"; private onGoingRequestAbortController: AbortController | null = null; private loading: boolean = false; private latestCompletions: CompletionResponse | null = null; - private constructor() { + public constructor() { super(); this.updateConfiguration(); workspace.onDidChangeConfiguration((event) => { diff --git a/clients/vscode/src/TabbyStatusBarItem.ts b/clients/vscode/src/TabbyStatusBarItem.ts index d3f6473..28cfe03 100644 --- a/clients/vscode/src/TabbyStatusBarItem.ts +++ b/clients/vscode/src/TabbyStatusBarItem.ts @@ -1,4 +1,4 @@ -import { StatusBarAlignment, ThemeColor, window } from "vscode"; +import { StatusBarAlignment, ThemeColor, ExtensionContext, window } from "vscode"; import { createMachine, interpret } from "@xstate/fsm"; import type { StatusChangedEvent, AuthRequiredEvent, IssuesUpdatedEvent } from "tabby-agent"; import { agent } from "./agent"; @@ -20,13 +20,26 @@ const backgroundColorWarning = new ThemeColor("statusBarItem.warningBackground") export class TabbyStatusBarItem { private item = window.createStatusBarItem(StatusBarAlignment.Right); + private extensionContext: ExtensionContext; private completionProvider: TabbyCompletionProvider; private completionResponseWarningShown = false; private subStatusForReady = [ { target: "issuesExist", - cond: () => agent().getIssues().length > 0, + cond: () => { + let issues = agent().getIssues(); + if ( + this.extensionContext.globalState + .get("notifications.muted", []) + .includes("completionResponseTimeIssues") + ) { + issues = issues.filter( + (issue) => issue !== "highCompletionTimeoutRate" && issue !== "slowCompletionResponseTime", + ); + } + return issues.length > 0; + }, }, { target: "automatic", @@ -126,18 +139,20 @@ export class TabbyStatusBarItem { private fsmService = interpret(this.fsm); - constructor(completionProvider: TabbyCompletionProvider) { + constructor(context: ExtensionContext, completionProvider: TabbyCompletionProvider) { + this.extensionContext = context; this.completionProvider = completionProvider; this.fsmService.start(); this.fsmService.send(agent().getStatus()); this.item.show(); this.completionProvider.on("triggerModeUpdated", () => { - this.fsmService.send(agent().getStatus()); + this.refresh(); }); this.completionProvider.on("loadingStatusUpdated", () => { - this.fsmService.send(agent().getStatus()); + this.refresh(); }); + agent().on("statusChanged", (event: StatusChangedEvent) => { console.debug("Tabby agent statusChanged", { event }); this.fsmService.send(event.status); @@ -158,12 +173,17 @@ export class TabbyStatusBarItem { agent().on("issuesUpdated", (event: IssuesUpdatedEvent) => { console.debug("Tabby agent issuesUpdated", { event }); this.fsmService.send(agent().getStatus()); + const showCompletionResponseWarnings = + !this.completionResponseWarningShown && + !this.extensionContext.globalState + .get("notifications.muted", []) + .includes("completionResponseTimeIssues"); if (event.issues.includes("connectionFailed")) { notifications.showInformationWhenDisconnected(); - } else if (!this.completionResponseWarningShown && event.issues.includes("highCompletionTimeoutRate")) { + } else if (showCompletionResponseWarnings && event.issues.includes("highCompletionTimeoutRate")) { this.completionResponseWarningShown = true; notifications.showInformationWhenHighCompletionTimeoutRate(); - } else if (!this.completionResponseWarningShown && event.issues.includes("slowCompletionResponseTime")) { + } else if (showCompletionResponseWarnings && event.issues.includes("slowCompletionResponseTime")) { this.completionResponseWarningShown = true; notifications.showInformationWhenSlowCompletionResponseTime(); } @@ -174,6 +194,10 @@ export class TabbyStatusBarItem { return this.item; } + public refresh() { + this.fsmService.send(agent().getStatus()); + } + private toInitializing() { this.item.color = colorNormal; this.item.backgroundColor = backgroundColorNormal; diff --git a/clients/vscode/src/commands.ts b/clients/vscode/src/commands.ts index cfe8377..d3de4cf 100644 --- a/clients/vscode/src/commands.ts +++ b/clients/vscode/src/commands.ts @@ -3,6 +3,8 @@ import { InputBoxValidationSeverity, ProgressLocation, Uri, + ThemeIcon, + ExtensionContext, workspace, window, env, @@ -12,6 +14,7 @@ import { strict as assert } from "assert"; import { agent } from "./agent"; import { notifications } from "./notifications"; import { TabbyCompletionProvider } from "./TabbyCompletionProvider"; +import { TabbyStatusBarItem } from "./TabbyStatusBarItem"; const configTarget = ConfigurationTarget.Global; @@ -175,24 +178,114 @@ const acceptInlineCompletion: Command = { }, }; -const acceptInlineCompletionNextWord: Command = { - command: "tabby.inlineCompletion.acceptNextWord", +const acceptInlineCompletionNextWord = (completionProvider: TabbyCompletionProvider): Command => { + return { + command: "tabby.inlineCompletion.acceptNextWord", + callback: () => { + completionProvider.postEvent("accept_word"); + commands.executeCommand("editor.action.inlineSuggest.acceptNextWord"); + }, + }; +}; + +const acceptInlineCompletionNextLine = (completionProvider: TabbyCompletionProvider): Command => { + return { + command: "tabby.inlineCompletion.acceptNextLine", + callback: () => { + completionProvider.postEvent("accept_line"); + // FIXME: this command move cursor to next line, but we want to move cursor to the end of current line + commands.executeCommand("editor.action.inlineSuggest.acceptNextLine"); + }, + }; +}; + +const openOnlineHelp: Command = { + command: "tabby.openOnlineHelp", callback: () => { - TabbyCompletionProvider.getInstance().postEvent("accept_word"); - commands.executeCommand("editor.action.inlineSuggest.acceptNextWord"); + window + .showQuickPick([ + { + label: "Online Documentation", + iconPath: new ThemeIcon("book"), + alwaysShow: true, + }, + { + label: "Model Registry", + description: "Explore more recommend models from Tabby's model registry", + iconPath: new ThemeIcon("library"), + alwaysShow: true, + }, + { + label: "Tabby Slack Community", + description: "Join Tabby's Slack community to get help or feed back", + iconPath: new ThemeIcon("comment-discussion"), + alwaysShow: true, + }, + { + label: "Tabby GitHub Repository", + description: "View the source code for Tabby, and open issues", + iconPath: new ThemeIcon("github"), + alwaysShow: true, + }, + ]) + .then((selection) => { + if (selection) { + switch (selection.label) { + case "Online Documentation": + env.openExternal(Uri.parse("https://tabby.tabbyml.com/")); + break; + case "Model Registry": + env.openExternal(Uri.parse("https://tabby.tabbyml.com/docs/models/")); + break; + case "Tabby Slack Community": + env.openExternal( + Uri.parse("https://join.slack.com/t/tabbycommunity/shared_invite/zt-1xeiddizp-bciR2RtFTaJ37RBxr8VxpA"), + ); + break; + case "Tabby GitHub Repository": + env.openExternal(Uri.parse("https://github.com/tabbyml/tabby")); + break; + } + } + }); }, }; -const acceptInlineCompletionNextLine: Command = { - command: "tabby.inlineCompletion.acceptNextLine", - callback: () => { - TabbyCompletionProvider.getInstance().postEvent("accept_line"); - // FIXME: this command move cursor to next line, but we want to move cursor to the end of current line - commands.executeCommand("editor.action.inlineSuggest.acceptNextLine"); - }, +const muteNotifications = (context: ExtensionContext, statusBarItem: TabbyStatusBarItem): Command => { + return { + command: "tabby.notifications.mute", + callback: (type: string) => { + const notifications = context.globalState.get("notifications.muted", []); + notifications.push(type); + context.globalState.update("notifications.muted", notifications); + statusBarItem.refresh(); + }, + }; }; -export const tabbyCommands = () => +const resetMutedNotifications = (context: ExtensionContext, statusBarItem: TabbyStatusBarItem): Command => { + return { + command: "tabby.notifications.resetMuted", + callback: (type?: string) => { + const notifications = context.globalState.get("notifications.muted", []); + if (type) { + context.globalState.update( + "notifications.muted", + notifications.filter((t) => t !== type), + ); + } else { + context.globalState.update("notifications.muted", []); + } + statusBarItem.refresh(); + }, + }; +}; + +export const tabbyCommands = ( + context: ExtensionContext, + completionProvider: TabbyCompletionProvider, + statusBarItem: TabbyStatusBarItem, +) => [ toggleInlineCompletionTriggerMode, setApiEndpoint, @@ -204,6 +297,9 @@ export const tabbyCommands = () => applyCallback, triggerInlineCompletion, acceptInlineCompletion, - acceptInlineCompletionNextWord, - acceptInlineCompletionNextLine, + acceptInlineCompletionNextWord(completionProvider), + acceptInlineCompletionNextLine(completionProvider), + openOnlineHelp, + muteNotifications(context, statusBarItem), + resetMutedNotifications(context, statusBarItem), ].map((command) => commands.registerCommand(command.command, command.callback, command.thisArg)); diff --git a/clients/vscode/src/extension.ts b/clients/vscode/src/extension.ts index 8804628..c98caaf 100644 --- a/clients/vscode/src/extension.ts +++ b/clients/vscode/src/extension.ts @@ -11,12 +11,12 @@ import { TabbyStatusBarItem } from "./TabbyStatusBarItem"; export async function activate(context: ExtensionContext) { console.debug("Activating Tabby extension", new Date()); await createAgentInstance(context); - const completionProvider = TabbyCompletionProvider.getInstance(); - const statusBarItem = new TabbyStatusBarItem(completionProvider); + const completionProvider = new TabbyCompletionProvider(); + const statusBarItem = new TabbyStatusBarItem(context, completionProvider); context.subscriptions.push( languages.registerInlineCompletionItemProvider({ pattern: "**" }, completionProvider), statusBarItem.register(), - ...tabbyCommands(), + ...tabbyCommands(context, completionProvider, statusBarItem), ); } diff --git a/clients/vscode/src/notifications.ts b/clients/vscode/src/notifications.ts index 3c34614..fd6023c 100644 --- a/clients/vscode/src/notifications.ts +++ b/clients/vscode/src/notifications.ts @@ -1,4 +1,4 @@ -import { commands, window, workspace, env, ConfigurationTarget, Uri } from "vscode"; +import { commands, window, workspace, ConfigurationTarget } from "vscode"; import type { HighCompletionTimeoutRateIssue, SlowCompletionResponseTimeIssue, @@ -100,9 +100,13 @@ function showInformationWhenDisconnected(modal: boolean = false) { detail: message, }, "Settings", + "Online Help...", ) .then((selection) => { switch (selection) { + case "Online Help...": + commands.executeCommand("tabby.openOnlineHelp"); + break; case "Settings": commands.executeCommand("tabby.openSettings"); break; @@ -165,7 +169,7 @@ function getHelpMessageForCompletionResponseTimeIssue() { helpMessageForRunningLargeModelOnCPU += `Your Tabby server is running model ${serverHealthState?.model} on CPU. ` + "This model may be performing poorly due to its large parameter size, please consider trying smaller models or switch to GPU. " + - "You can find a list of supported models in the model directory.\n"; + "You can find a list of recommend models in the online documentation.\n"; } let commonHelpMessage = ""; const host = new URL(agent().getConfig().server.endpoint).host; @@ -174,7 +178,7 @@ function getHelpMessageForCompletionResponseTimeIssue() { serverHealthState?.model ?? "" } may be performing poorly due to its large parameter size. `; commonHelpMessage += - "Please consider trying smaller models. You can find a list of supported models in the model directory.\n"; + "Please consider trying smaller models. You can find a list of recommend models in the online documentation.\n"; } if (!(host.startsWith("localhost") || host.startsWith("127.0.0.1"))) { commonHelpMessage += " - A poor network connection. Please check your network and proxy settings.\n"; @@ -212,18 +216,22 @@ function showInformationWhenSlowCompletionResponseTime(modal: boolean = false) { modal: true, detail: statsMessage + getHelpMessageForCompletionResponseTimeIssue(), }, - "Model Directory", + "Online Help...", + "Don't Show Again", ) .then((selection) => { switch (selection) { - case "Model Directory": - env.openExternal(Uri.parse("https://tabby.tabbyml.com/docs/models/")); + case "Online Help...": + commands.executeCommand("tabby.openOnlineHelp"); + break; + case "Don't Show Again": + commands.executeCommand("tabby.notifications.mute", "completionResponseTimeIssues"); break; } }); } else { window - .showWarningMessage("Completion requests appear to take too much time.", "Detail", "Settings") + .showWarningMessage("Completion requests appear to take too much time.", "Detail", "Settings", "Don't Show Again") .then((selection) => { switch (selection) { case "Detail": @@ -232,6 +240,9 @@ function showInformationWhenSlowCompletionResponseTime(modal: boolean = false) { case "Settings": commands.executeCommand("tabby.openSettings"); break; + case "Don't Show Again": + commands.executeCommand("tabby.notifications.mute", "completionResponseTimeIssues"); + break; } }); } @@ -252,26 +263,35 @@ function showInformationWhenHighCompletionTimeoutRate(modal: boolean = false) { modal: true, detail: statsMessage + getHelpMessageForCompletionResponseTimeIssue(), }, - "Model Directory", + "Online Help...", + "Don't Show Again", ) .then((selection) => { switch (selection) { - case "Model Directory": - env.openExternal(Uri.parse("https://tabby.tabbyml.com/docs/models/")); + case "Online Help...": + commands.executeCommand("tabby.openOnlineHelp"); + break; + case "Don't Show Again": + commands.executeCommand("tabby.notifications.mute", "completionResponseTimeIssues"); break; } }); } else { - window.showWarningMessage("Most completion requests timed out.", "Detail", "Settings").then((selection) => { - switch (selection) { - case "Detail": - showInformationWhenHighCompletionTimeoutRate(true); - break; - case "Settings": - commands.executeCommand("tabby.openSettings"); - break; - } - }); + window + .showWarningMessage("Most completion requests timed out.", "Detail", "Settings", "Don't Show Again") + .then((selection) => { + switch (selection) { + case "Detail": + showInformationWhenHighCompletionTimeoutRate(true); + break; + case "Settings": + commands.executeCommand("tabby.openSettings"); + break; + case "Don't Show Again": + commands.executeCommand("tabby.notifications.mute", "completionResponseTimeIssues"); + break; + } + }); } }