feat(vscode): add manual trigger supporting. (#459)

* feat(vscode): add manual trigger supporting.

* fix: update documents and fix minor bugs.

* fix: lint.
release-0.2
Zhiming Ma 2023-09-19 17:01:36 +08:00 committed by GitHub
parent 61ade26545
commit 0aa6c5fcfb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 515 additions and 351 deletions

View File

@ -20,9 +20,11 @@ Try our online demo [here](https://tabby.tabbyml.com/playground).
## Get Started
If you have installed the Tabby VSCode extension, you can follow the built-in walkthrough guides to get started. You can also reopen walkthrough page anytime by using command `Tabby: Getting Started`.
Once you have installed the Tabby VSCode extension, you can easily get started by following the built-in walkthrough guides. You can access the walkthrough page at any time by using the command `Tabby: Getting Started`.
1. Setup the Tabby server: you can get a Tabby Cloud hosted server [here](https://app.tabbyml.com), or build your self-hosted Tabby server following [this guide](https://tabby.tabbyml.com/docs/installation).
2. Use the command `Tabby: Specify API Endpoint of Tabby` to connect the extension to your Tabby server. If you are using a Tabby Cloud server endpoint, please follow the popup messages to complete authorization.
1. **Setup the Tabby server**: You have two options to set up your Tabby server. You can either get a Tabby Cloud hosted server [here](https://app.tabbyml.com) or build your own self-hosted Tabby server following [this guide](https://tabby.tabbyml.com/docs/installation).
2. **Connect the extension to your Tabby server**: Use the command `Tabby: Specify API Endpoint of Tabby` to connect the extension to your Tabby server. If you are using a Tabby Cloud server endpoint, follow the instructions provided in the popup messages to complete the authorization process.
Once setup is complete, Tabby will provide inline suggestions automatically, and you can accept suggestions by just pressing the Tab key. You can hover on the inline suggestion text to find more useful actions such as partially accepting by word or by line.
Once the setup is complete, Tabby will automatically provide inline suggestions. You can accept the suggestions by simply pressing the `Tab` key. Hovering over the inline suggestion text will display additional useful actions, such as partially accepting suggestions by word or by line.
If you prefer to trigger code completion manually, you can select the manual trigger option in the settings. After that, use the shortcut `Alt + \` to trigger code completion. To access the settings page, use the command `Tabby: Open Settings`.

View File

@ -6,6 +6,10 @@ Tabby will show inline suggestions when you stop typing, and you can accept sugg
![Demo](https://tabby.tabbyml.com/img/demo.gif)
## Manual Trigger
If you select manual trigger in the [settings](command:tabby.openSettings), you can trigger code completion by pressing `Alt + \`.
## Cycling Through Choices
When multiple choices are available, you can cycle through them by pressing `Alt + [` and `Alt + ]`.

View File

@ -0,0 +1,3 @@
# Tabby Commands
![Commands](./commands.png)

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cefad8151a4e659cf21cf02cd1211eff8f965ede107642233badb5828904b5a5
size 43420
oid sha256:90b67bc6280cfd0e4d73d61c2cad5bd22c634b864cb8dd941dfaeaddcaa03073
size 68239

View File

@ -0,0 +1,9 @@
# Connect to Tabby Server
When you execute the command `Tabby: Specify API Endpoint of Tabby`, you will see a prompt asking you to enter the API endpoint of your Tabby server.
![setApiEndpoint](./setApiEndpoint.png)
Once the connection is established, you will see the status bar showing the check mark.
If you are using a Tabby Cloud server endpoint, please follow the popup messages to complete authorization.

View File

@ -7,7 +7,7 @@
"repository": "https://github.com/TabbyML/tabby",
"bugs": "https://github.com/TabbyML/tabby/issues",
"license": "Apache-2.0",
"version": "0.5.0",
"version": "0.6.0-dev",
"keywords": [
"ai",
"autocomplete",
@ -35,8 +35,12 @@
"contributes": {
"commands": [
{
"command": "tabby.toggleEnabled",
"title": "Tabby: Toggle Code Suggestion On/Off"
"command": "tabby.toggleInlineCompletionTriggerMode",
"title": "Tabby: Toggle Code Completion Trigger Mode (Automatic/Manual)"
},
{
"command": "tabby.inlineCompletion.trigger",
"title": "Tabby: Trigger Code Completion Manually"
},
{
"command": "tabby.setApiEndpoint",
@ -57,6 +61,10 @@
],
"menus": {
"commandPalette": [
{
"command": "tabby.inlineCompletion.trigger",
"when": "config.tabby.inlineCompletion.triggerMode === 'manual' && !editorHasSelection && !inlineSuggestionsVisible"
},
{
"command": "tabby.openTabbyAgentSettings",
"when": "!isWeb"
@ -80,16 +88,15 @@
{
"id": "connectToTabbyServer",
"title": "Connect to Tabby Server",
"description": "Once your Tabby server is ready, specify the server API endpoint here. If you are using a Tabby Cloud endpoint, please follow the popup messages to complete authorization. \n[Specify API Endpoint](command:tabby.setApiEndpoint)",
"description": "Once your Tabby server is ready, specify the server API endpoint here. \n[Specify API Endpoint](command:tabby.setApiEndpoint)",
"media": {
"image": "assets/walkthroughs/setApiEndpoint.png",
"altText": "Tabby: Specify API Endpoint of Tabby"
"markdown": "assets/walkthroughs/setApiEndpoint.md"
}
},
{
"id": "codeCompletion",
"title": "Code Completion",
"description": "Tabby provides inline suggestions automatically, and you can accept suggestions by just pressing the Tab key. Follow this guide to find more useful actions.",
"description": "Tabby provides inline suggestions automatically by default, and you can accept suggestions by just pressing the Tab key.",
"media": {
"markdown": "assets/walkthroughs/codeCompletion.md"
}
@ -99,8 +106,7 @@
"title": "Commands",
"description": "Type `>Tabby:` in command palette to list all Tabby commands. \n[Tabby commands](command:workbench.action.quickOpen?%5B%22%3ETabby%3A%22%5D)",
"media": {
"image": "assets/walkthroughs/commands.png",
"altText": "Tabby Commands"
"markdown": "assets/walkthroughs/commands.md"
}
}
]
@ -116,10 +122,18 @@
"patternErrorMessage": "Please enter a validate http or https URL.",
"markdownDescription": "Specify API Endpoint of Tabby. \nIf leave empty, server endpoint in [Tabby Agent Settings](command:tabby.openTabbyAgentSettings) will be used."
},
"tabby.codeCompletion": {
"type": "boolean",
"default": true,
"description": "Enable Tabby code completion or not."
"tabby.inlineCompletion.triggerMode": {
"type": "string",
"enum": [
"automatic",
"manual"
],
"default": "automatic",
"description": "Select the code completion trigger mode.",
"enumDescriptions": [
"Automatic trigger when you stop typing",
"Manual trigger by pressing `Alt + \\`"
]
},
"tabby.keybindings": {
"type": "string",
@ -128,7 +142,7 @@
"tabby-style"
],
"default": "vscode-style",
"markdownDescription": "Select keybinding profile to accept current inline completion. \n | | Next Line | Full Completion | Next Word | \n |:---:|:---:|:---:|:---:| \n | _vscode-style_ | - | Tab | Ctrl + RightArrow | \n | _tabby-style_ <br/> _(experimental)_ | Tab | Ctrl + Tab | Ctrl + RightArrow | \n"
"markdownDescription": "Select the keybinding profile to accept shown inline completion. \n | | Next Line | Full Completion | Next Word | \n |:---:|:---:|:---:|:---:| \n | _vscode-style_ | - | Tab | Ctrl + RightArrow | \n | _tabby-style_ <br/> _(experimental)_ | Tab | Ctrl + Tab | Ctrl + RightArrow | \n"
},
"tabby.usage.anonymousUsageTracking": {
"type": "boolean",
@ -138,30 +152,35 @@
}
},
"keybindings": [
{
"key": "alt+\\",
"command": "tabby.inlineCompletion.trigger",
"when": "config.tabby.inlineCompletion.triggerMode === 'manual' && editorTextFocus && !editorHasSelection && !inlineSuggestionsVisible"
},
{
"command": "tabby.inlineCompletion.accept",
"key": "tab",
"when": "config.tabby.codeCompletion && config.tabby.keybindings === 'vscode-style' && inlineSuggestionVisible && !editorReadonly && !editorTabMovesFocus && inlineSuggestionHasIndentationLessThanTabSize"
"when": "config.tabby.keybindings === 'vscode-style' && inlineSuggestionVisible && !editorReadonly && !suggestWidgetVisible && !editorHoverFocused && !editorTabMovesFocus && inlineSuggestionHasIndentationLessThanTabSize"
},
{
"command": "tabby.inlineCompletion.acceptNextWord",
"key": "ctrl+right",
"when": "config.tabby.codeCompletion && config.tabby.keybindings === 'vscode-style' && inlineSuggestionVisible && !editorReadonly"
"when": "config.tabby.keybindings === 'vscode-style' && inlineSuggestionVisible && !editorReadonly && !suggestWidgetVisible"
},
{
"command": "tabby.inlineCompletion.accept",
"key": "ctrl+tab",
"when": "config.tabby.codeCompletion && config.tabby.keybindings === 'tabby-style' && inlineSuggestionVisible && !editorReadonly"
"when": "config.tabby.keybindings === 'tabby-style' && inlineSuggestionVisible && !editorReadonly && !suggestWidgetVisible"
},
{
"command": "tabby.inlineCompletion.acceptNextWord",
"key": "ctrl+right",
"when": "config.tabby.codeCompletion && config.tabby.keybindings === 'tabby-style' && inlineSuggestionVisible && !editorReadonly"
"when": "config.tabby.keybindings === 'tabby-style' && inlineSuggestionVisible && !editorReadonly && !suggestWidgetVisible"
},
{
"command": "tabby.inlineCompletion.acceptNextLine",
"key": "tab",
"when": "config.tabby.codeCompletion && config.tabby.keybindings === 'tabby-style' && inlineSuggestionVisible && !editorReadonly && !editorTabMovesFocus"
"when": "config.tabby.keybindings === 'tabby-style' && inlineSuggestionVisible && !editorReadonly && !suggestWidgetVisible && !editorHoverFocused && !editorTabMovesFocus"
}
]
},

View File

@ -9,15 +9,17 @@ import {
TextDocument,
workspace,
} from "vscode";
import { EventEmitter } from "events";
import { CompletionResponse } from "tabby-agent";
import { agent } from "./agent";
import { notifications } from "./notifications";
export class TabbyCompletionProvider implements InlineCompletionItemProvider {
// User Settings
private enabled: boolean = true;
export class TabbyCompletionProvider extends EventEmitter implements InlineCompletionItemProvider {
private triggerMode: "automatic" | "manual" | "disabled" = "automatic";
private onGoingRequestAbortController: AbortController | null = null;
private loading: boolean = false;
constructor() {
super();
this.updateConfiguration();
workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration("tabby") || event.affectsConfiguration("editor.inlineSuggest")) {
@ -26,27 +28,37 @@ export class TabbyCompletionProvider implements InlineCompletionItemProvider {
});
}
public getTriggerMode(): "automatic" | "manual" | "disabled" {
return this.triggerMode;
}
public isLoading(): boolean {
return this.loading;
}
public async provideInlineCompletionItems(
document: TextDocument,
position: Position,
context: InlineCompletionContext,
token: CancellationToken,
): Promise<InlineCompletionItem[]> {
const emptyResponse = Promise.resolve([] as InlineCompletionItem[]);
if (!this.enabled) {
console.debug("Extension not enabled, skipping.");
return emptyResponse;
): Promise<InlineCompletionItem[] | null> {
if (context.triggerKind === InlineCompletionTriggerKind.Automatic && this.triggerMode === "manual") {
return null;
}
if (context.triggerKind === InlineCompletionTriggerKind.Invoke && this.triggerMode === "automatic") {
return null;
}
// Check if autocomplete widget is visible
if (context.selectedCompletionInfo !== undefined) {
console.debug("Autocomplete widget is visible, skipping.");
return emptyResponse;
return null;
}
if (token?.isCancellationRequested) {
console.debug("Cancellation was requested.");
return emptyResponse;
return null;
}
const replaceRange = this.calculateReplaceRange(document, position);
@ -60,33 +72,40 @@ export class TabbyCompletionProvider implements InlineCompletionItemProvider {
};
const abortController = new AbortController();
this.onGoingRequestAbortController = abortController;
token?.onCancellationRequested(() => {
console.debug("Cancellation requested.");
abortController.abort();
});
const completion = await agent()
.provideCompletions(request, { signal: abortController.signal })
.catch((_) => {
return null;
});
try {
this.loading = true;
this.emit("loadingStatusUpdated");
const result = await agent().provideCompletions(request, { signal: abortController.signal });
this.loading = false;
this.emit("loadingStatusUpdated");
return this.toInlineCompletions(result, replaceRange);
} catch (error: any) {
if (this.onGoingRequestAbortController === abortController) {
// the request was not replaced by a new request, set loading to false safely
this.loading = false;
this.emit("loadingStatusUpdated");
}
if (error.name !== "AbortError") {
console.debug("Error when providing completions", { error });
}
}
const completions = this.toInlineCompletions(completion, replaceRange);
return Promise.resolve(completions);
return null;
}
private updateConfiguration() {
const configuration = workspace.getConfiguration("tabby");
this.enabled = configuration.get("codeCompletion", true);
this.checkInlineCompletionEnabled();
}
private checkInlineCompletionEnabled() {
const configuration = workspace.getConfiguration("editor.inlineSuggest");
const inlineSuggestEnabled = configuration.get("enabled", true);
if (this.enabled && !inlineSuggestEnabled) {
console.debug("Tabby code completion is enabled but inline suggest is disabled.");
notifications.showInformationWhenInlineSuggestDisabled();
if (!workspace.getConfiguration("editor").get("inlineSuggest.enabled", true)) {
this.triggerMode = "disabled";
this.emit("triggerModeUpdated");
} else {
this.triggerMode = workspace.getConfiguration("tabby").get("inlineCompletion.triggerMode", "automatic");
this.emit("triggerModeUpdated");
}
}

View File

@ -0,0 +1,316 @@
import { StatusBarAlignment, ThemeColor, window } from "vscode";
import { createMachine, interpret } from "@xstate/fsm";
import { agent } from "./agent";
import { notifications } from "./notifications";
import { TabbyCompletionProvider } from "./TabbyCompletionProvider";
const label = "Tabby";
const iconLoading = "$(loading~spin)";
const iconAutomatic = "$(check)";
const iconManual = "$(chevron-right)";
const iconDisabled = "$(x)";
const iconDisconnected = "$(plug)";
const iconUnauthorized = "$(key)";
const iconIssueExist = "$(warning)";
const colorNormal = new ThemeColor("statusBar.foreground");
const colorWarning = new ThemeColor("statusBarItem.warningForeground");
const backgroundColorNormal = new ThemeColor("statusBar.background");
const backgroundColorWarning = new ThemeColor("statusBarItem.warningBackground");
export class TabbyStatusBarItem {
private item = window.createStatusBarItem(StatusBarAlignment.Right);
private completionProvider: TabbyCompletionProvider;
private transitionsForCompletionProviderStatus = [
{
target: "automatic",
cond: () => this.completionProvider.getTriggerMode() === "automatic",
},
{
target: "manual",
cond: () => this.completionProvider.getTriggerMode() === "manual" && !this.completionProvider.isLoading(),
},
{
target: "loading",
cond: () => this.completionProvider.getTriggerMode() === "manual" && this.completionProvider.isLoading(),
},
{
target: "disabled",
cond: () => this.completionProvider.getTriggerMode() === "disabled",
},
];
private fsm = createMachine({
id: "statusBarItem",
initial: "initializing",
states: {
initializing: {
on: {
ready: this.transitionsForCompletionProviderStatus,
disconnected: "disconnected",
unauthorized: "unauthorized",
issuesExist: "issuesExist",
},
entry: () => this.toInitializing(),
},
automatic: {
on: {
completionStatusChanged: this.transitionsForCompletionProviderStatus,
ready: this.transitionsForCompletionProviderStatus,
disconnected: "disconnected",
unauthorized: "unauthorized",
issuesExist: "issuesExist",
},
entry: () => this.toAutomatic(),
},
manual: {
on: {
completionStatusChanged: this.transitionsForCompletionProviderStatus,
ready: this.transitionsForCompletionProviderStatus,
disconnected: "disconnected",
unauthorized: "unauthorized",
issuesExist: "issuesExist",
},
entry: () => this.toManual(),
},
loading: {
on: {
completionStatusChanged: this.transitionsForCompletionProviderStatus,
ready: this.transitionsForCompletionProviderStatus,
disconnected: "disconnected",
unauthorized: "unauthorized",
issuesExist: "issuesExist",
},
entry: () => this.toLoading(),
},
disabled: {
on: {
completionStatusChanged: this.transitionsForCompletionProviderStatus,
ready: this.transitionsForCompletionProviderStatus,
disconnected: "disconnected",
unauthorized: "unauthorized",
issuesExist: "issuesExist",
},
entry: () => this.toDisabled(),
},
disconnected: {
on: {
ready: this.transitionsForCompletionProviderStatus,
unauthorized: "unauthorized",
issuesExist: "issuesExist",
},
entry: () => this.toDisconnected(),
},
unauthorized: {
on: {
ready: this.transitionsForCompletionProviderStatus,
disconnected: "disconnected",
issuesExist: "issuesExist",
authStart: "unauthorizedAndAuthInProgress",
},
entry: () => this.toUnauthorized(),
},
unauthorizedAndAuthInProgress: {
on: {
ready: this.transitionsForCompletionProviderStatus,
disconnected: "disconnected",
issuesExist: "issuesExist",
authEnd: "unauthorized", // if auth succeeds, we will get `ready` before `authEnd` event
},
entry: () => this.toUnauthorizedAndAuthInProgress(),
},
issuesExist: {
on: {
ready: this.transitionsForCompletionProviderStatus,
disconnected: "disconnected",
unauthorized: "unauthorized",
},
entry: () => this.toIssuesExist(),
},
},
});
private fsmService = interpret(this.fsm);
constructor(completionProvider: TabbyCompletionProvider) {
this.completionProvider = completionProvider;
this.fsmService.start();
this.fsmService.send(agent().getStatus());
this.fsmService.send("completionStatusChanged");
this.item.show();
this.completionProvider.on("triggerModeUpdated", () => {
this.fsmService.send("completionStatusChanged");
});
this.completionProvider.on("loadingStatusUpdated", () => {
this.fsmService.send("completionStatusChanged");
});
agent().on("statusChanged", (event) => {
console.debug("Tabby agent statusChanged", { event });
this.fsmService.send(event.status);
});
agent().on("authRequired", (event) => {
console.debug("Tabby agent authRequired", { event });
notifications.showInformationStartAuth({
onAuthStart: () => {
this.fsmService.send("authStart");
},
onAuthEnd: () => {
this.fsmService.send("authEnd");
},
});
});
agent().on("newIssue", (event) => {
console.debug("Tabby agent newIssue", { event });
if (event.issue.name === "slowCompletionResponseTime") {
notifications.showInformationWhenSlowCompletionResponseTime();
} else if (event.issue.name === "highCompletionTimeoutRate") {
notifications.showInformationWhenHighCompletionTimeoutRate();
}
});
}
public register() {
return this.item;
}
private toInitializing() {
this.item.color = colorNormal;
this.item.backgroundColor = backgroundColorNormal;
this.item.text = `${iconLoading} ${label}`;
this.item.tooltip = "Tabby is initializing.";
this.item.command = {
title: "",
command: "tabby.applyCallback",
arguments: [() => notifications.showInformationWhenInitializing()],
};
}
private toAutomatic() {
this.item.color = colorNormal;
this.item.backgroundColor = backgroundColorNormal;
this.item.text = `${iconAutomatic} ${label}`;
this.item.tooltip = "Tabby automatic code completion is enabled.";
this.item.command = {
title: "",
command: "tabby.applyCallback",
arguments: [() => notifications.showInformationWhenAutomaticTrigger()],
};
}
private toManual() {
this.item.color = colorNormal;
this.item.backgroundColor = backgroundColorNormal;
this.item.text = `${iconManual} ${label}`;
this.item.tooltip = "Tabby is standing by, click or press `Alt + \\` to trigger code completion.";
this.item.command = {
title: "",
command: "tabby.applyCallback",
arguments: [() => notifications.showInformationWhenManualTrigger()],
};
}
private toLoading() {
this.item.color = colorNormal;
this.item.backgroundColor = backgroundColorNormal;
this.item.text = `${iconLoading} ${label}`;
this.item.tooltip = "Tabby is generating code completions.";
this.item.command = {
title: "",
command: "tabby.applyCallback",
arguments: [() => notifications.showInformationWhenManualTriggerLoading()],
};
}
private toDisabled() {
this.item.color = colorWarning;
this.item.backgroundColor = backgroundColorWarning;
this.item.text = `${iconDisabled} ${label}`;
this.item.tooltip = "Tabby is disabled. Click to check settings.";
this.item.command = {
title: "",
command: "tabby.applyCallback",
arguments: [() => notifications.showInformationWhenInlineSuggestDisabled()],
};
console.debug("Tabby code completion is enabled but inline suggest is disabled.");
notifications.showInformationWhenInlineSuggestDisabled();
}
private toDisconnected() {
this.item.color = colorWarning;
this.item.backgroundColor = backgroundColorWarning;
this.item.text = `${iconDisconnected} ${label}`;
this.item.tooltip = "Cannot connect to Tabby Server. Click to open settings.";
this.item.command = {
title: "",
command: "tabby.applyCallback",
arguments: [() => notifications.showInformationWhenDisconnected()],
};
}
private toUnauthorized() {
this.item.color = colorWarning;
this.item.backgroundColor = backgroundColorWarning;
this.item.text = `${iconUnauthorized} ${label}`;
this.item.tooltip = "Tabby Server requires authorization. Click to continue.";
this.item.command = {
title: "",
command: "tabby.applyCallback",
arguments: [
() =>
notifications.showInformationStartAuth({
onAuthStart: () => {
this.fsmService.send("authStart");
},
onAuthEnd: () => {
this.fsmService.send("authEnd");
},
}),
],
};
}
private toUnauthorizedAndAuthInProgress() {
this.item.color = colorWarning;
this.item.backgroundColor = backgroundColorWarning;
this.item.text = `${iconUnauthorized} ${label}`;
this.item.tooltip = "Waiting for authorization.";
this.item.command = undefined;
}
private toIssuesExist() {
this.item.color = colorWarning;
this.item.backgroundColor = backgroundColorWarning;
this.item.text = `${iconIssueExist} ${label}`;
switch (agent().getIssues()[0]?.name) {
case "slowCompletionResponseTime":
this.item.tooltip = "Completion requests appear to take too much time.";
break;
case "highCompletionTimeoutRate":
this.item.tooltip = "Most completion requests timed out.";
break;
default:
this.item.tooltip = "";
break;
}
this.item.command = {
title: "",
command: "tabby.applyCallback",
arguments: [
() => {
switch (agent().getIssues()[0]?.name) {
case "slowCompletionResponseTime":
notifications.showInformationWhenSlowCompletionResponseTime();
break;
case "highCompletionTimeoutRate":
notifications.showInformationWhenHighCompletionTimeoutRate();
break;
}
},
],
};
}
}

View File

@ -20,13 +20,20 @@ type Command = {
thisArg?: any;
};
const toggleEnabled: Command = {
command: "tabby.toggleEnabled",
callback: () => {
const toggleInlineCompletionTriggerMode: Command = {
command: "tabby.toggleInlineCompletionTriggerMode",
callback: (value: "automatic" | "manual" | undefined) => {
const configuration = workspace.getConfiguration("tabby");
const enabled = configuration.get("codeCompletion", true);
console.debug(`Toggle Enabled: ${enabled} -> ${!enabled}.`);
configuration.update("codeCompletion", !enabled, configTarget, false);
let target = value;
if (!target) {
const current = configuration.get("inlineCompletion.triggerMode", "automatic");
if (current === "automatic") {
target = "manual";
} else {
target = "automatic";
}
}
configuration.update("inlineCompletion.triggerMode", target, configTarget, false);
},
};
@ -154,42 +161,17 @@ const openAuthPage: Command = {
},
};
const statusBarItemClicked: Command = {
command: "tabby.statusBarItemClicked",
callback: (status) => {
switch (status) {
case "loading":
notifications.showInformationWhenLoading();
break;
case "ready":
notifications.showInformationWhenReady();
break;
case "disconnected":
notifications.showInformationWhenDisconnected();
break;
case "unauthorized":
notifications.showInformationStartAuth();
break;
case "issuesExist":
switch (agent().getIssues()[0]?.name) {
case "slowCompletionResponseTime":
notifications.showInformationWhenSlowCompletionResponseTime();
break;
case "highCompletionTimeoutRate":
notifications.showInformationWhenHighCompletionTimeoutRate();
break;
}
break;
case "disabled":
const enabled = workspace.getConfiguration("tabby").get("codeCompletion", true);
const inlineSuggestEnabled = workspace.getConfiguration("editor").get("inlineSuggest.enabled", true);
if (enabled && !inlineSuggestEnabled) {
notifications.showInformationWhenInlineSuggestDisabled();
} else {
notifications.showInformationWhenDisabled();
}
break;
}
const applyCallback: Command = {
command: "tabby.applyCallback",
callback: (callback) => {
callback?.();
},
};
const triggerInlineCompletion: Command = {
command: "tabby.inlineCompletion.trigger",
callback: () => {
commands.executeCommand("editor.action.inlineSuggest.trigger");
},
};
@ -218,7 +200,7 @@ const acceptInlineCompletionNextLine: Command = {
export const tabbyCommands = () =>
[
toggleEnabled,
toggleInlineCompletionTriggerMode,
setApiEndpoint,
openSettings,
openTabbyAgentSettings,
@ -226,7 +208,8 @@ export const tabbyCommands = () =>
gettingStarted,
emitEvent,
openAuthPage,
statusBarItemClicked,
applyCallback,
triggerInlineCompletion,
acceptInlineCompletion,
acceptInlineCompletionNextWord,
acceptInlineCompletionNextLine,

View File

@ -4,16 +4,18 @@ import { ExtensionContext, languages } from "vscode";
import { createAgentInstance } from "./agent";
import { tabbyCommands } from "./commands";
import { TabbyCompletionProvider } from "./TabbyCompletionProvider";
import { tabbyStatusBarItem } from "./statusBarItem";
import { TabbyStatusBarItem } from "./TabbyStatusBarItem";
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export async function activate(context: ExtensionContext) {
console.debug("Activating Tabby extension", new Date());
await createAgentInstance(context);
const completionProvider = new TabbyCompletionProvider();
const statusBarItem = new TabbyStatusBarItem(completionProvider);
context.subscriptions.push(
languages.registerInlineCompletionItemProvider({ pattern: "**" }, new TabbyCompletionProvider()),
tabbyStatusBarItem(),
languages.registerInlineCompletionItemProvider({ pattern: "**" }, completionProvider),
statusBarItem.register(),
...tabbyCommands(),
);
}

View File

@ -1,7 +1,7 @@
import { commands, window, workspace, env, ConfigurationTarget, Uri } from "vscode";
import { agent } from "./agent";
function showInformationWhenLoading() {
function showInformationWhenInitializing() {
window.showInformationMessage("Tabby is initializing.", "Settings").then((selection) => {
switch (selection) {
case "Settings":
@ -11,13 +11,17 @@ function showInformationWhenLoading() {
});
}
function showInformationWhenDisabled() {
function showInformationWhenAutomaticTrigger() {
window
.showInformationMessage("Tabby code completion is disabled. Enable it?", "Enable", "Settings")
.showInformationMessage(
"Tabby automatic code completion is enabled. Switch to manual trigger mode?",
"Manual Mode",
"Settings",
)
.then((selection) => {
switch (selection) {
case "Enable":
commands.executeCommand("tabby.toggleEnabled");
case "Manual Mode":
commands.executeCommand("tabby.toggleInlineCompletionTriggerMode", "manual");
break;
case "Settings":
commands.executeCommand("tabby.openSettings");
@ -26,13 +30,21 @@ function showInformationWhenDisabled() {
});
}
function showInformationWhenReady() {
function showInformationWhenManualTrigger() {
window
.showInformationMessage("Tabby is providing code suggestions for you. Disable it?", "Disable", "Settings")
.showInformationMessage(
"Tabby is standing by. Trigger code completion manually?",
"Trigger",
"Automatic Mode",
"Settings",
)
.then((selection) => {
switch (selection) {
case "Disable":
commands.executeCommand("tabby.toggleEnabled");
case "Trigger":
commands.executeCommand("editor.action.inlineSuggest.trigger");
break;
case "Automatic Mode":
commands.executeCommand("tabby.toggleInlineCompletionTriggerMode", "automatic");
break;
case "Settings":
commands.executeCommand("tabby.openSettings");
@ -41,6 +53,37 @@ function showInformationWhenReady() {
});
}
function showInformationWhenManualTriggerLoading() {
window.showInformationMessage("Tabby is generating code completions.", "Settings").then((selection) => {
switch (selection) {
case "Settings":
commands.executeCommand("tabby.openSettings");
break;
}
});
}
function showInformationWhenInlineSuggestDisabled() {
window
.showWarningMessage(
"Tabby's suggestion is not showing because inline suggestion is disabled. Please enable it first.",
"Enable",
"Settings",
)
.then((selection) => {
switch (selection) {
case "Enable":
const configuration = workspace.getConfiguration("editor");
console.debug(`Set editor.inlineSuggest.enabled: true.`);
configuration.update("inlineSuggest.enabled", true, ConfigurationTarget.Global, false);
break;
case "Settings":
commands.executeCommand("workbench.action.openSettings", "@id:editor.inlineSuggest.enabled");
break;
}
});
}
function showInformationWhenDisconnected() {
window
.showInformationMessage("Cannot connect to Tabby Server. Please check settings.", "Settings")
@ -89,27 +132,6 @@ function showInformationWhenAuthFailed() {
});
}
function showInformationWhenInlineSuggestDisabled() {
window
.showWarningMessage(
"Tabby's suggestion is not showing because inline suggestion is disabled. Please enable it first.",
"Enable",
"Settings",
)
.then((selection) => {
switch (selection) {
case "Enable":
const configuration = workspace.getConfiguration("editor");
console.debug(`Set editor.inlineSuggest.enabled: true.`);
configuration.update("inlineSuggest.enabled", true, ConfigurationTarget.Global, false);
break;
case "Settings":
commands.executeCommand("workbench.action.openSettings", "@id:editor.inlineSuggest.enabled");
break;
}
});
}
function getHelpMessageForCompletionResponseTimeIssue() {
let helpMessageForRunningLargeModelOnCPU = "";
const serverHealthState = agent().getServerHealthState();
@ -218,15 +240,16 @@ function showInformationWhenHighCompletionTimeoutRate(modal: boolean = false) {
}
export const notifications = {
showInformationWhenLoading,
showInformationWhenDisabled,
showInformationWhenReady,
showInformationWhenInitializing,
showInformationWhenAutomaticTrigger,
showInformationWhenManualTrigger,
showInformationWhenManualTriggerLoading,
showInformationWhenInlineSuggestDisabled,
showInformationWhenDisconnected,
showInformationStartAuth,
showInformationAuthSuccess,
showInformationWhenStartAuthButAlreadyAuthorized,
showInformationWhenAuthFailed,
showInformationWhenInlineSuggestDisabled,
showInformationWhenSlowCompletionResponseTime,
showInformationWhenHighCompletionTimeoutRate,
};

View File

@ -1,216 +0,0 @@
import { StatusBarAlignment, ThemeColor, window, workspace } from "vscode";
import { createMachine, interpret } from "@xstate/fsm";
import { agent } from "./agent";
import { notifications } from "./notifications";
const label = "Tabby";
const iconLoading = "$(loading~spin)";
const iconReady = "$(check)";
const iconDisconnected = "$(plug)";
const iconUnauthorized = "$(key)";
const iconIssueExist = "$(warning)";
const iconDisabled = "$(x)";
const colorNormal = new ThemeColor("statusBar.foreground");
const colorWarning = new ThemeColor("statusBarItem.warningForeground");
const backgroundColorNormal = new ThemeColor("statusBar.background");
const backgroundColorWarning = new ThemeColor("statusBarItem.warningBackground");
const item = window.createStatusBarItem(StatusBarAlignment.Right);
const fsm = createMachine({
id: "statusBarItem",
initial: "loading",
states: {
loading: {
on: {
ready: "ready",
disconnected: "disconnected",
unauthorized: "unauthorized",
issuesExist: "issuesExist",
disabled: "disabled",
},
entry: () => toLoading(),
},
ready: {
on: {
loading: "loading",
disconnected: "disconnected",
unauthorized: "unauthorized",
issuesExist: "issuesExist",
disabled: "disabled",
},
entry: () => toReady(),
},
disconnected: {
on: {
loading: "loading",
ready: "ready",
unauthorized: "unauthorized",
issuesExist: "issuesExist",
disabled: "disabled",
},
entry: () => toDisconnected(),
},
unauthorized: {
on: {
ready: "ready",
disconnected: "disconnected",
disabled: "disabled",
issuesExist: "issuesExist",
authStart: "unauthorizedAndAuthInProgress",
},
entry: () => toUnauthorized(),
},
unauthorizedAndAuthInProgress: {
on: {
ready: "ready",
disconnected: "disconnected",
issuesExist: "issuesExist",
disabled: "disabled",
authEnd: "unauthorized", // if auth succeeds, we will get `ready` before `authEnd` event
},
entry: () => toUnauthorizedAndAuthInProgress(),
},
issuesExist: {
on: {
loading: "loading",
ready: "ready",
disconnected: "disconnected",
unauthorized: "unauthorized",
disabled: "disabled",
},
entry: () => toIssuesExist(),
},
disabled: {
on: {
loading: "loading",
ready: "ready",
disconnected: "disconnected",
unauthorized: "unauthorized",
issuesExist: "issuesExist",
},
entry: () => toDisabled(),
},
},
});
const fsmService = interpret(fsm);
function toLoading() {
item.color = colorNormal;
item.backgroundColor = backgroundColorNormal;
item.text = `${iconLoading} ${label}`;
item.tooltip = "Tabby is initializing.";
item.command = { title: "", command: "tabby.statusBarItemClicked", arguments: ["loading"] };
}
function toReady() {
item.color = colorNormal;
item.backgroundColor = backgroundColorNormal;
item.text = `${iconReady} ${label}`;
item.tooltip = "Tabby is providing code suggestions for you.";
item.command = { title: "", command: "tabby.statusBarItemClicked", arguments: ["ready"] };
}
function toDisconnected() {
item.color = colorWarning;
item.backgroundColor = backgroundColorWarning;
item.text = `${iconDisconnected} ${label}`;
item.tooltip = "Cannot connect to Tabby Server. Click to open settings.";
item.command = { title: "", command: "tabby.statusBarItemClicked", arguments: ["disconnected"] };
}
function toUnauthorized() {
item.color = colorWarning;
item.backgroundColor = backgroundColorWarning;
item.text = `${iconUnauthorized} ${label}`;
item.tooltip = "Tabby Server requires authorization. Click to continue.";
item.command = { title: "", command: "tabby.statusBarItemClicked", arguments: ["unauthorized"] };
}
function toUnauthorizedAndAuthInProgress() {
item.color = colorWarning;
item.backgroundColor = backgroundColorWarning;
item.text = `${iconUnauthorized} ${label}`;
item.tooltip = "Waiting for authorization.";
item.command = undefined;
}
function toIssuesExist() {
item.color = colorWarning;
item.backgroundColor = backgroundColorWarning;
item.text = `${iconIssueExist} ${label}`;
switch (agent().getIssues()[0]?.name) {
case "slowCompletionResponseTime":
item.tooltip = "Completion requests appear to take too much time.";
break;
case "highCompletionTimeoutRate":
item.tooltip = "Most completion requests timed out.";
break;
default:
item.tooltip = "";
break;
}
item.command = { title: "", command: "tabby.statusBarItemClicked", arguments: ["issuesExist"] };
}
function toDisabled() {
item.color = colorWarning;
item.backgroundColor = backgroundColorWarning;
item.text = `${iconDisabled} ${label}`;
item.tooltip = "Tabby is disabled.";
item.command = { title: "", command: "tabby.statusBarItemClicked", arguments: ["disabled"] };
}
function updateStatusBarItem() {
const enabled = workspace.getConfiguration("tabby").get("codeCompletion", true);
const inlineSuggestEnabled = workspace.getConfiguration("editor").get("inlineSuggest.enabled", true);
if (!enabled || !inlineSuggestEnabled) {
fsmService.send("disabled");
} else {
const status = agent().getStatus();
switch (status) {
case "notInitialized":
fsmService.send("loading");
break;
case "ready":
case "disconnected":
case "unauthorized":
case "issuesExist":
fsmService.send(status);
break;
}
}
}
export const tabbyStatusBarItem = () => {
fsmService.start();
updateStatusBarItem();
workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration("tabby") || event.affectsConfiguration("editor.inlineSuggest")) {
updateStatusBarItem();
}
});
agent().on("statusChanged", updateStatusBarItem);
agent().on("authRequired", () => {
notifications.showInformationStartAuth({
onAuthStart: () => {
fsmService.send("authStart");
},
onAuthEnd: () => {
fsmService.send("authEnd");
},
});
});
agent().on("newIssue", (event) => {
if (event.issue.name === "slowCompletionResponseTime") {
notifications.showInformationWhenSlowCompletionResponseTime();
} else if (event.issue.name === "highCompletionTimeoutRate") {
notifications.showInformationWhenHighCompletionTimeoutRate();
}
});
item.show();
return item;
};