From c3bc3d12b5869e5f48146515f67e8a68302d56bc Mon Sep 17 00:00:00 2001 From: Zhiming Ma Date: Fri, 16 Aug 2024 15:01:52 +0800 Subject: [PATCH] refactor(agent): deprecate `tabby/agent/*` methods and intro `tabby/config/*` and `tabby/stastus/*`. (#2895) --- clients/tabby-agent/src/TabbyAgent.ts | 14 +- clients/tabby-agent/src/dataStore.ts | 2 + .../tabby-agent/src/lsp/CommandProvider.ts | 21 +- clients/tabby-agent/src/lsp/ConfigProvider.ts | 38 ++ clients/tabby-agent/src/lsp/Server.ts | 101 +---- clients/tabby-agent/src/lsp/StatusProvider.ts | 370 ++++++++++++++++++ clients/tabby-agent/src/lsp/feature.ts | 9 + clients/tabby-agent/src/lsp/protocol.ts | 172 +++++++- 8 files changed, 631 insertions(+), 96 deletions(-) create mode 100644 clients/tabby-agent/src/lsp/ConfigProvider.ts create mode 100644 clients/tabby-agent/src/lsp/StatusProvider.ts create mode 100644 clients/tabby-agent/src/lsp/feature.ts diff --git a/clients/tabby-agent/src/TabbyAgent.ts b/clients/tabby-agent/src/TabbyAgent.ts index 32f7ae2135a1..a7b750779c03 100644 --- a/clients/tabby-agent/src/TabbyAgent.ts +++ b/clients/tabby-agent/src/TabbyAgent.ts @@ -246,7 +246,7 @@ export class TabbyAgent extends EventEmitter implements Agent { return abortSignalFromAnyOf([AbortSignal.timeout(timeout), options?.signal]); } - private async healthCheck(options?: { signal?: AbortSignal; method?: "GET" | "POST" }): Promise { + public async healthCheck(options?: { signal?: AbortSignal; method?: "GET" | "POST" }): Promise { const requestId = uuid(); const requestPath = "/v1/health"; const requestDescription = `${options?.method || "GET"} ${this.config.server.endpoint + requestPath}`; @@ -258,12 +258,14 @@ export class TabbyAgent extends EventEmitter implements Agent { throw new Error("http client not initialized"); } this.logger.debug(`Health check request: ${requestDescription}. [${requestId}]`); + this.emit("connectingStateUpdated", true); let response; if (options?.method === "POST") { response = await this.api.POST(requestPath, requestOptions); } else { response = await this.api.GET(requestPath, requestOptions); } + this.emit("connectingStateUpdated", false); this.logger.debug(`Health check response status: ${response.response.status}. [${requestId}]`); if (response.error || !response.response.ok) { throw new HttpError(response.response); @@ -285,6 +287,7 @@ export class TabbyAgent extends EventEmitter implements Agent { } this.changeStatus("ready"); } catch (error) { + this.emit("connectingStateUpdated", false); this.serverHealthState = undefined; if (error instanceof HttpError && error.status == 405 && options?.method !== "POST") { return await this.healthCheck({ method: "POST" }); @@ -600,6 +603,7 @@ export class TabbyAgent extends EventEmitter implements Agent { solution = new CompletionSolution(context); // Fetch the completion this.logger.info(`Fetching completion...`); + this.emit("fetchingStateUpdated", true); try { const response = await this.fetchCompletion( context.language, @@ -616,6 +620,7 @@ export class TabbyAgent extends EventEmitter implements Agent { solution = undefined; } } + this.emit("fetchingStateUpdated", false); } else { // No cached solution, or cached solution is not completed // TriggerKind is Manual @@ -623,6 +628,7 @@ export class TabbyAgent extends EventEmitter implements Agent { solution = cachedSolution?.withContext(context) ?? new CompletionSolution(context); this.logger.info(`Fetching more completions...`); + this.emit("fetchingStateUpdated", true); try { let tries = 0; @@ -652,6 +658,7 @@ export class TabbyAgent extends EventEmitter implements Agent { solution = undefined; } } + this.emit("fetchingStateUpdated", false); } // Postprocess solution if (solution) { @@ -1032,4 +1039,9 @@ export class TabbyAgent extends EventEmitter implements Agent { : ""; return `${envInfo} ${tabby} ${ide} ${tabbyPlugin}`.trim(); } + + // FIXME(@icycodes): extract data store + public getDataStore(): DataStore | undefined { + return this.dataStore; + } } diff --git a/clients/tabby-agent/src/dataStore.ts b/clients/tabby-agent/src/dataStore.ts index cd1c06f0a7c9..be0d3ca44b62 100644 --- a/clients/tabby-agent/src/dataStore.ts +++ b/clients/tabby-agent/src/dataStore.ts @@ -6,11 +6,13 @@ import deepEqual from "deep-equal"; import chokidar from "chokidar"; import { isBrowser } from "./env"; import type { PartialAgentConfig } from "./AgentConfig"; +import type { StatusIssuesName } from "./lsp/protocol"; export type StoredData = { anonymousId: string; auth: { [endpoint: string]: { jwt: string } }; serverConfig: { [endpoint: string]: PartialAgentConfig }; + statusIgnoredIssues: StatusIssuesName[]; }; export interface DataStore { diff --git a/clients/tabby-agent/src/lsp/CommandProvider.ts b/clients/tabby-agent/src/lsp/CommandProvider.ts index 8b2f21c64f08..49f9d77b15e2 100644 --- a/clients/tabby-agent/src/lsp/CommandProvider.ts +++ b/clients/tabby-agent/src/lsp/CommandProvider.ts @@ -1,11 +1,18 @@ import { Connection, ExecuteCommandParams } from "vscode-languageserver"; -import { ServerCapabilities, ChatEditResolveParams } from "./protocol"; +import { + ServerCapabilities, + StatusShowHelpMessageRequest, + ChatEditResolveRequest, + ChatEditResolveParams, +} from "./protocol"; import { ChatEditProvider } from "./ChatEditProvider"; +import { StatusProvider } from "./StatusProvider"; export class CommandProvider { constructor( private readonly connection: Connection, private readonly chatEditProvider: ChatEditProvider, + private readonly statusProvider: StatusProvider, ) { this.connection.onExecuteCommand(async (params) => { return this.executeCommand(params); @@ -14,15 +21,17 @@ export class CommandProvider { fillServerCapabilities(capabilities: ServerCapabilities): void { capabilities.executeCommandProvider = { - commands: ["tabby/chat/edit/resolve"], + commands: [StatusShowHelpMessageRequest.method, ChatEditResolveRequest.method], }; } async executeCommand(params: ExecuteCommandParams): Promise { - if (params.command === "tabby/chat/edit/resolve") { - const resolveParams = params.arguments?.[0] as ChatEditResolveParams; - if (resolveParams) { - await this.chatEditProvider.resolveEdit(resolveParams); + if (params.command === StatusShowHelpMessageRequest.method) { + await this.statusProvider.showStatusHelpMessage(this.connection); + } else if (params.command === ChatEditResolveRequest.method) { + const commandParams = params.arguments?.[0] as ChatEditResolveParams; + if (commandParams) { + await this.chatEditProvider.resolveEdit(commandParams); } } } diff --git a/clients/tabby-agent/src/lsp/ConfigProvider.ts b/clients/tabby-agent/src/lsp/ConfigProvider.ts new file mode 100644 index 000000000000..1a30fc5ccbd5 --- /dev/null +++ b/clients/tabby-agent/src/lsp/ConfigProvider.ts @@ -0,0 +1,38 @@ +import { EventEmitter } from "events"; +import { Connection } from "vscode-languageserver"; +import { ClientCapabilities, ServerCapabilities, Config, ConfigRequest, ConfigDidChangeNotification } from "./protocol"; +import type { Feature } from "./feature"; +import { TabbyAgent } from "../TabbyAgent"; + +export class ConfigProvider extends EventEmitter implements Feature { + constructor(private readonly agent: TabbyAgent) { + super(); + this.agent.on("configUpdated", async () => { + this.update(); + }); + } + + private async update() { + const status = await this.getConfig(); + this.emit("updated", status); + } + + setup(connection: Connection, clientCapabilities: ClientCapabilities): ServerCapabilities { + connection.onRequest(ConfigRequest.type, async () => { + return this.getConfig(); + }); + if (clientCapabilities.tabby?.configDidChangeListener) { + this.on("updated", (config: Config) => { + connection.sendNotification(ConfigDidChangeNotification.type, config); + }); + } + return {}; + } + + async getConfig(): Promise { + const agentConfig = this.agent.getConfig(); + return { + server: agentConfig.server, + }; + } +} diff --git a/clients/tabby-agent/src/lsp/Server.ts b/clients/tabby-agent/src/lsp/Server.ts index 081469c618fa..6b221640efce 100644 --- a/clients/tabby-agent/src/lsp/Server.ts +++ b/clients/tabby-agent/src/lsp/Server.ts @@ -78,7 +78,6 @@ import { TextDocument } from "vscode-languageserver-textdocument"; import deepEqual from "deep-equal"; import { deepmerge } from "deepmerge-ts"; import type { - AgentIssue, ConfigUpdatedEvent, StatusChangedEvent, IssuesUpdatedEvent, @@ -96,6 +95,8 @@ import { RecentlyChangedCodeSearch } from "../codeSearch/RecentlyChangedCodeSear import { isPositionInRange, intersectionRange } from "../utils/range"; import { extractNonReservedWordList } from "../utils/string"; import { splitLines, isBlank } from "../utils"; +import { ConfigProvider } from "./ConfigProvider"; +import { StatusProvider } from "./StatusProvider"; import { ChatEditProvider } from "./ChatEditProvider"; import { CodeLensProvider } from "./CodeLensProvider"; import { CommandProvider } from "./CommandProvider"; @@ -109,7 +110,12 @@ export class Server { private readonly documents = new TextDocuments(TextDocument); private readonly notebooks = new NotebookDocuments(this.documents); + private recentlyChangedCodeSearch: RecentlyChangedCodeSearch | undefined = undefined; + + private config: ConfigProvider; + private status: StatusProvider; + private chatEditProvider: ChatEditProvider; private codeLensProvider: CodeLensProvider | undefined = undefined; private commandProvider: CommandProvider; @@ -169,8 +175,12 @@ export class Server { this.connection.onNotification(TelemetryEventNotification.type, async (param) => { return this.event(param); }); + + this.config = new ConfigProvider(this.agent); + this.status = new StatusProvider(this.agent); + // Command - this.commandProvider = new CommandProvider(this.connection, this.chatEditProvider); + this.commandProvider = new CommandProvider(this.connection, this.chatEditProvider, this.status); } listen() { @@ -227,6 +237,9 @@ export class Server { } this.commandProvider.fillServerCapabilities(serverCapabilities); + this.config.setup(this.connection, clientCapabilities); + this.status.setup(this.connection, clientCapabilities); + await this.agent.initialize({ config: this.createInitConfig(clientProvidedConfig), clientProperties: this.createInitClientProperties(clientInfo, clientProvidedConfig), @@ -415,7 +428,7 @@ export class Server { } return { name: detail.name, - helpMessage: this.buildHelpMessage(detail, params.helpMessageFormat), + helpMessage: this.status.buildHelpMessage(detail, params.helpMessageFormat), }; } @@ -1062,86 +1075,4 @@ export class Server { }), }; } - - private buildHelpMessage(issueDetail: AgentIssue, format?: "plaintext" | "markdown" | "html"): string | undefined { - const outputFormat = format ?? "plaintext"; - - // "connectionFailed" - if (issueDetail.name == "connectionFailed") { - if (outputFormat == "html") { - return issueDetail.message?.replace(/\n/g, "
"); - } else { - return issueDetail.message; - } - } - - // "slowCompletionResponseTime" or "highCompletionTimeoutRate" - let statsMessage = ""; - if (issueDetail.name == "slowCompletionResponseTime") { - const stats = issueDetail.completionResponseStats; - if (stats && stats["responses"] && stats["averageResponseTime"]) { - statsMessage = `The average response time of recent ${stats["responses"]} completion requests is ${Number( - stats["averageResponseTime"], - ).toFixed(0)}ms.

`; - } - } - - if (issueDetail.name == "highCompletionTimeoutRate") { - const stats = issueDetail.completionResponseStats; - if (stats && stats["total"] && stats["timeouts"]) { - statsMessage = `${stats["timeouts"]} of ${stats["total"]} completion requests timed out.

`; - } - } - - let helpMessageForRunningLargeModelOnCPU = ""; - const serverHealthState = this.agent.getServerHealthState(); - if (serverHealthState?.device === "cpu" && serverHealthState?.model?.match(/[0-9.]+B$/)) { - 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 recommend models in the online documentation.
"; - } - let commonHelpMessage = ""; - if (helpMessageForRunningLargeModelOnCPU.length == 0) { - commonHelpMessage += `
  • The running model ${ - serverHealthState?.model ?? "" - } may be performing poorly due to its large parameter size. `; - commonHelpMessage += - "Please consider trying smaller models. You can find a list of recommend models in the online documentation.
  • "; - } - const host = new URL(this.serverInfo?.config.endpoint ?? "http://localhost:8080").host; - if (!(host.startsWith("localhost") || host.startsWith("127.0.0.1") || host.startsWith("0.0.0.0"))) { - commonHelpMessage += "
  • A poor network connection. Please check your network and proxy settings.
  • "; - commonHelpMessage += "
  • Server overload. Please contact your Tabby server administrator for assistance.
  • "; - } - let helpMessage = ""; - if (helpMessageForRunningLargeModelOnCPU.length > 0) { - helpMessage += helpMessageForRunningLargeModelOnCPU + "
    "; - if (commonHelpMessage.length > 0) { - helpMessage += "Other possible causes of this issue:
      " + commonHelpMessage + "
    "; - } - } else { - // commonHelpMessage should not be empty here - helpMessage += "Possible causes of this issue:
      " + commonHelpMessage + "
    "; - } - - if (outputFormat == "html") { - return statsMessage + helpMessage; - } - if (outputFormat == "markdown") { - return (statsMessage + helpMessage) - .replace(//g, " \n") - .replace(/(.*?)<\/i>/g, "*$1*") - .replace(/]*?\s+)?href=["']([^"']+)["'][^>]*>([^<]+)<\/a>/g, "[$2]($1)") - .replace(/]*>(.*?)<\/ul>/g, "$1") - .replace(/]*>(.*?)<\/li>/g, "- $1 \n"); - } else { - return (statsMessage + helpMessage) - .replace(//g, " \n") - .replace(/(.*?)<\/i>/g, "$1") - .replace(/]*>(.*?)<\/a>/g, "$1") - .replace(/]*>(.*?)<\/ul>/g, "$1") - .replace(/]*>(.*?)<\/li>/g, "- $1 \n"); - } - } } diff --git a/clients/tabby-agent/src/lsp/StatusProvider.ts b/clients/tabby-agent/src/lsp/StatusProvider.ts new file mode 100644 index 000000000000..eee8829b6bc0 --- /dev/null +++ b/clients/tabby-agent/src/lsp/StatusProvider.ts @@ -0,0 +1,370 @@ +import { EventEmitter } from "events"; +import { Connection, ShowMessageRequest, ShowMessageRequestParams, MessageType } from "vscode-languageserver"; +import { + ClientCapabilities, + ServerCapabilities, + StatusInfo, + StatusRequest, + StatusRequestParams, + StatusDidChangeNotification, + StatusShowHelpMessageRequest, + StatusIgnoredIssuesEditRequest, + StatusIgnoredIssuesEditParams, + StatusIssuesName, + InlineCompletionTriggerMode, +} from "./protocol"; +import type { Feature } from "./feature"; +import { getLogger } from "../logger"; +import type { AgentIssue } from "../Agent"; +import { TabbyAgent } from "../TabbyAgent"; +import "../ArrayExt"; + +export class StatusProvider extends EventEmitter implements Feature { + private readonly logger = getLogger("StatusProvider"); + // FIXME(@icycodes): extract http client status + private isConnecting: boolean = false; + private isFetching: boolean = false; + private clientInlineCompletionTriggerMode?: InlineCompletionTriggerMode; + + constructor(private readonly agent: TabbyAgent) { + super(); + this.agent.on("connectingStateUpdated", async (isConnecting: boolean) => { + this.isConnecting = isConnecting; + this.update(); + }); + this.agent.on("fetchingStateUpdated", async (isFetching: boolean) => { + this.isFetching = isFetching; + this.update(); + }); + this.agent.on("statusChanged", async () => { + this.update(); + }); + this.agent.on("issuesUpdated", async () => { + this.update(); + }); + } + + private async update() { + const status = await this.getStatus({}); + this.emit("updated", status); + } + + // FIXME(@icycodes): move to listen to config + setClientInlineCompletionTriggerMode(triggerMode: InlineCompletionTriggerMode): void { + this.clientInlineCompletionTriggerMode = triggerMode; + } + + setup(connection: Connection, clientCapabilities: ClientCapabilities): ServerCapabilities { + connection.onRequest(StatusRequest.type, async (params) => { + return this.getStatus(params); + }); + connection.onRequest(StatusShowHelpMessageRequest.type, async () => { + return this.showStatusHelpMessage(connection); + }); + connection.onRequest(StatusIgnoredIssuesEditRequest.type, async (params) => { + return this.editStatusIgnoredIssues(params); + }); + if (clientCapabilities.tabby?.statusDidChangeListener) { + this.on("updated", (status: StatusInfo) => { + connection.sendNotification(StatusDidChangeNotification.type, status); + }); + } + return {}; + } + + async getStatus(params: StatusRequestParams): Promise { + if (params.recheckConnection) { + await this.agent.healthCheck(); + } + const statusInfo = this.buildStatusInfo(); + this.fillToolTip(statusInfo); + return statusInfo; + } + + async showStatusHelpMessage(connection: Connection): Promise { + let params: ShowMessageRequestParams; + let issue: StatusIssuesName | undefined = undefined; + + const detail = this.agent.getIssueDetail({ index: 0 }); + if (detail?.name === "connectionFailed") { + params = { + type: MessageType.Error, + message: "Connect to Server Failed.\n" + this.buildHelpMessage(detail, "plaintext") ?? "", + actions: [{ title: "OK" }], + }; + } else if (detail?.name === "highCompletionTimeoutRate" || detail?.name === "slowCompletionResponseTime") { + params = { + type: MessageType.Info, + message: this.buildHelpMessage(detail, "plaintext") ?? "", + actions: [{ title: "OK" }, { title: "Never Show Again" }], + }; + issue = "completionResponseSlow"; + } else { + return false; + } + const helpMessage = this.buildHelpMessage(detail, "plaintext"); + if (!helpMessage) { + return false; + } + const result = await connection.sendRequest(ShowMessageRequest.type, params); + switch (result?.title) { + case "Never Show Again": + if (issue) { + await this.editStatusIgnoredIssues({ operation: "add", issues: [issue] }); + await this.update(); + } + break; + case "OK": + break; + default: + break; + } + return true; + } + + async editStatusIgnoredIssues(params: StatusIgnoredIssuesEditParams): Promise { + const issues = Array.isArray(params.issues) ? params.issues : [params.issues]; + const dataStore = this.agent.getDataStore(); + switch (params.operation) { + case "add": + if (dataStore) { + const current = dataStore.data.statusIgnoredIssues ?? []; + dataStore.data.statusIgnoredIssues = current.concat(issues).distinct(); + this.logger.debug( + "Adding ignored issues: [" + + current.join(",") + + "] -> [" + + dataStore.data.statusIgnoredIssues.join(",") + + "].", + ); + await dataStore.save(); + return true; + } + break; + case "remove": + if (dataStore) { + const current = dataStore.data.statusIgnoredIssues ?? []; + dataStore.data.statusIgnoredIssues = current.filter((item) => !issues.includes(item)); + this.logger.debug( + "Removing ignored issues: [" + + current.join(",") + + "] -> [" + + dataStore.data.statusIgnoredIssues.join(",") + + "].", + ); + + await dataStore.save(); + return true; + } + break; + case "removeAll": + if (dataStore) { + dataStore.data.statusIgnoredIssues = []; + this.logger.debug("Removing all ignored issues."); + await dataStore.save(); + return true; + } + break; + default: + break; + } + return false; + } + + buildHelpMessage(issueDetail: AgentIssue, format?: "plaintext" | "markdown" | "html"): string | undefined { + const outputFormat = format ?? "plaintext"; + + // "connectionFailed" + if (issueDetail.name == "connectionFailed") { + if (outputFormat == "html") { + return issueDetail.message?.replace(/\n/g, "
    "); + } else { + return issueDetail.message; + } + } + + // "slowCompletionResponseTime" or "highCompletionTimeoutRate" + let statsMessage = ""; + if (issueDetail.name == "slowCompletionResponseTime") { + const stats = issueDetail.completionResponseStats; + if (stats && stats["responses"] && stats["averageResponseTime"]) { + statsMessage = `The average response time of recent ${stats["responses"]} completion requests is ${Number( + stats["averageResponseTime"], + ).toFixed(0)}ms.

    `; + } + } + + if (issueDetail.name == "highCompletionTimeoutRate") { + const stats = issueDetail.completionResponseStats; + if (stats && stats["total"] && stats["timeouts"]) { + statsMessage = `${stats["timeouts"]} of ${stats["total"]} completion requests timed out.

    `; + } + } + + let helpMessageForRunningLargeModelOnCPU = ""; + const serverHealthState = this.agent.getServerHealthState(); + if (serverHealthState?.device === "cpu" && serverHealthState?.model?.match(/[0-9.]+B$/)) { + 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 recommend models in the online documentation.
    "; + } + let commonHelpMessage = ""; + if (helpMessageForRunningLargeModelOnCPU.length == 0) { + commonHelpMessage += `
  • The running model ${ + serverHealthState?.model ?? "" + } may be performing poorly due to its large parameter size. `; + commonHelpMessage += + "Please consider trying smaller models. You can find a list of recommend models in the online documentation.
  • "; + } + const host = new URL(this.agent.getConfig().server.endpoint ?? "http://localhost:8080").host; + if (!(host.startsWith("localhost") || host.startsWith("127.0.0.1") || host.startsWith("0.0.0.0"))) { + commonHelpMessage += "
  • A poor network connection. Please check your network and proxy settings.
  • "; + commonHelpMessage += "
  • Server overload. Please contact your Tabby server administrator for assistance.
  • "; + } + let helpMessage = ""; + if (helpMessageForRunningLargeModelOnCPU.length > 0) { + helpMessage += helpMessageForRunningLargeModelOnCPU + "
    "; + if (commonHelpMessage.length > 0) { + helpMessage += "Other possible causes of this issue:
      " + commonHelpMessage + "
    "; + } + } else { + // commonHelpMessage should not be empty here + helpMessage += "Possible causes of this issue:
      " + commonHelpMessage + "
    "; + } + + if (outputFormat == "html") { + return statsMessage + helpMessage; + } + if (outputFormat == "markdown") { + return (statsMessage + helpMessage) + .replace(//g, " \n") + .replace(/(.*?)<\/i>/g, "*$1*") + .replace(/]*?\s+)?href=["']([^"']+)["'][^>]*>([^<]+)<\/a>/g, "[$2]($1)") + .replace(/]*>(.*?)<\/ul>/g, "$1") + .replace(/]*>(.*?)<\/li>/g, "- $1 \n"); + } else { + return (statsMessage + helpMessage) + .replace(//g, " \n") + .replace(/(.*?)<\/i>/g, "$1") + .replace(/]*>(.*?)<\/a>/g, "$1") + .replace(/]*>(.*?)<\/ul>/g, "$1") + .replace(/]*>(.*?)<\/li>/g, "- $1 \n"); + } + } + + private buildStatusInfo(): StatusInfo { + const agentStatus = this.agent.getStatus(); + switch (agentStatus) { + case "notInitialized": + return { + status: this.isConnecting ? "connecting" : "notInitialized", + }; + case "finalized": + return { + status: "finalized", + }; + case "unauthorized": + return { + status: this.isConnecting ? "connecting" : "unauthorized", + }; + case "disconnected": + return { + status: this.isConnecting ? "connecting" : "disconnected", + command: { + title: "Detail", + command: "tabby/status/showHelpMessage", + arguments: [{}], + }, + }; + case "ready": { + const serverHealth = this.agent.getServerHealthState(); + let currentIssue: StatusIssuesName | null = null; + const agentIssue = this.agent.getIssues(); + if ( + (agentIssue.length > 0 && agentIssue[0] === "highCompletionTimeoutRate") || + agentIssue[0] === "slowCompletionResponseTime" + ) { + currentIssue = "completionResponseSlow"; + } + const dataStore = this.agent.getDataStore(); + const ignored = dataStore?.data.statusIgnoredIssues ?? []; + if (currentIssue && !ignored.includes(currentIssue)) { + return { + status: "completionResponseSlow", + serverHealth: serverHealth ?? undefined, + command: { + title: "Detail", + command: "tabby/status/showHelpMessage", + arguments: [{}], + }, + }; + } + if (this.isFetching) { + return { + status: "fetching", + serverHealth: serverHealth ?? undefined, + }; + } + switch (this.clientInlineCompletionTriggerMode) { + case "auto": + return { + status: "readyForAutoTrigger", + serverHealth: serverHealth ?? undefined, + }; + case "manual": + return { + status: "readyForManualTrigger", + serverHealth: serverHealth ?? undefined, + }; + default: + return { + status: "ready", + serverHealth: serverHealth ?? undefined, + }; + } + } + default: + return { + status: "notInitialized", + }; + } + } + + private fillToolTip(statusInfo: StatusInfo) { + switch (statusInfo.status) { + case "notInitialized": + statusInfo.tooltip = "Tabby: Initializing"; + break; + case "finalized": + statusInfo.tooltip = "Tabby"; + break; + case "connecting": + statusInfo.tooltip = "Tabby: Connecting to Server..."; + break; + case "unauthorized": + statusInfo.tooltip = "Tabby: Authorization Required"; + break; + case "disconnected": + statusInfo.tooltip = "Tabby: Connect to Server Failed"; + break; + case "ready": + statusInfo.tooltip = "Tabby: Code Completion Enabled"; + break; + case "readyForAutoTrigger": + statusInfo.tooltip = "Tabby: Automatic Code Completion Enabled"; + break; + case "readyForManualTrigger": + statusInfo.tooltip = "Tabby: Manual Code Completion Enabled"; + break; + case "fetching": + statusInfo.tooltip = "Tabby: Generating Completions..."; + break; + case "completionResponseSlow": + statusInfo.tooltip = "Tabby: Slow Completion Response Detected"; + break; + default: + break; + } + } +} diff --git a/clients/tabby-agent/src/lsp/feature.ts b/clients/tabby-agent/src/lsp/feature.ts new file mode 100644 index 000000000000..01565d05dc68 --- /dev/null +++ b/clients/tabby-agent/src/lsp/feature.ts @@ -0,0 +1,9 @@ +import type { Connection } from "vscode-languageserver"; +import type { ClientCapabilities, ServerCapabilities } from "./protocol"; + +export interface Feature { + setup( + connection: Connection, + clientCapabilities: ClientCapabilities, + ): ServerCapabilities | Promise; +} diff --git a/clients/tabby-agent/src/lsp/protocol.ts b/clients/tabby-agent/src/lsp/protocol.ts index 76b7645d5288..c312b0d209f8 100644 --- a/clients/tabby-agent/src/lsp/protocol.ts +++ b/clients/tabby-agent/src/lsp/protocol.ts @@ -6,6 +6,7 @@ import { ProtocolNotificationType, RegistrationType, MessageDirection, + LSPAny, URI, Range, Location, @@ -95,6 +96,7 @@ export type ClientCapabilities = LspClientCapabilities & { }; tabby?: { /** + * @deprecated Use configListener and statusListener instead. * The client supports: * - `tabby/agent/didUpdateServerInfo` * - `tabby/agent/didChangeStatus` @@ -102,6 +104,18 @@ export type ClientCapabilities = LspClientCapabilities & { * This capability indicates that client support receiving agent notifications. */ agent?: boolean; + /** + * The client supports: + * - `tabby/config/didChange` + * This capability indicates that client support receiving notifications for configuration changes. + */ + configDidChangeListener?: boolean; + /** + * The client supports: + * - `tabby/status/didChange` + * This capability indicates that client support receiving notifications for status sync to display a status bar. + */ + statusDidChangeListener?: boolean; /** * The client supports: * - `tabby/workspaceFileSystem/readFile` @@ -156,6 +170,14 @@ export type ServerCapabilities = LspServerCapabilities & { }; }; +export namespace TextDocumentCompletionFeatureRegistration { + export const type = new RegistrationType("textDocument/completion"); +} + +export namespace TextDocumentInlineCompletionFeatureRegistration { + export const type = new RegistrationType("textDocument/inlineCompletion"); +} + export namespace ChatFeatureRegistration { export const type = new RegistrationType("tabby/chat"); } @@ -164,13 +186,13 @@ export namespace ChatFeatureRegistration { * Extends LSP method Configuration Request(↪️) * * - method: `workspace/configuration` - * - params: any - * - result: {@link ClientProvidedConfig}[] (the array contains only one config) + * - params: any, not used + * - result: [{@link ClientProvidedConfig}] (the array should contains only one ClientProvidedConfig item) */ export namespace ConfigurationRequest { export const method = LspConfigurationRequest.method; export const messageDirection = LspConfigurationRequest.messageDirection; - export const type = new ProtocolRequestType(method); + export const type = new ProtocolRequestType(method); } /** @@ -189,7 +211,7 @@ export type ClientProvidedConfig = { * Sending this config to the server is for telemetry purposes. */ inlineCompletion?: { - triggerMode?: "auto" | "manual"; + triggerMode?: InlineCompletionTriggerMode; }; /** * Keybindings should be implemented on the client side. @@ -204,6 +226,8 @@ export type ClientProvidedConfig = { }; }; +export type InlineCompletionTriggerMode = "auto" | "manual"; + /** * Extends LSP method DidChangeConfiguration Notification(➡️) * - method: `workspace/didChangeConfiguration` @@ -509,6 +533,7 @@ export type EventParams = { }; /** + * @deprecated See {@link StatusDidChangeNotification} {@link ConfigDidChangeNotification} * [Tabby] DidUpdateServerInfo Notification(⬅️) * * This method is sent from the server to the client to notify the current Tabby server info has changed. @@ -536,6 +561,7 @@ export type ServerInfo = { }; /** + * @deprecated See {@link StatusRequest} {@link ConfigRequest} * [Tabby] Server Info Request(↩️) * * This method is sent from the client to the server to check the current Tabby server info. @@ -550,6 +576,7 @@ export namespace AgentServerInfoRequest { } /** + * @deprecated See {@link StatusDidChangeNotification} * [Tabby] DidChangeStatus Notification(⬅️) * * This method is sent from the server to the client to notify the client about the status of the server. @@ -570,6 +597,7 @@ export type DidChangeStatusParams = { export type Status = "notInitialized" | "ready" | "disconnected" | "unauthorized" | "finalized"; /** + * @deprecated See {@link StatusRequest} * [Tabby] Status Request(↩️) * * This method is sent from the client to the server to check the current status of the server. @@ -584,6 +612,7 @@ export namespace AgentStatusRequest { } /** + * @deprecated See {@link StatusDidChangeNotification} * [Tabby] DidUpdateIssue Notification(⬅️) * * This method is sent from the server to the client to notify the client about the current issues. @@ -606,6 +635,7 @@ export type IssueList = { export type IssueName = "slowCompletionResponseTime" | "highCompletionTimeoutRate" | "connectionFailed"; /** + * @deprecated See {@link StatusRequest} * [Tabby] Issues Request(↩️) * * This method is sent from the client to the server to check if there is any issue. @@ -620,6 +650,7 @@ export namespace AgentIssuesRequest { } /** + * @deprecated See {@link StatusShowHelpMessageRequest} * [Tabby] Issue Detail Request(↩️) * * This method is sent from the client to the server to check the detail of an issue. @@ -643,6 +674,139 @@ export type IssueDetailResult = { helpMessage?: string; }; +/** + * [Tabby] Config Request(↩️) + * + * This method is sent from the client to the server to get the current configuration. + * - method: `tabby/config` + * - params: any, not used + * - result: {@link Config} + */ +export namespace ConfigRequest { + export const method = "tabby/config"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolRequestType(method); +} + +export type Config = { + server: { + endpoint: string; + token: string; + requestHeaders: Record; + }; +}; + +/** + * [Tabby] Config DidChange Notification(⬅️) + * + * This method is sent from the server to the client to notify the client of the configuration changes. + * - method: `tabby/config/didChange` + * - params: {@link Config} + * - result: void + */ +export namespace ConfigDidChangeNotification { + export const method = "tabby/config/didChange"; + export const messageDirection = MessageDirection.serverToClient; + export const type = new ProtocolNotificationType(method); +} + +/** + * [Tabby] Status Request(↩️) + * + * This method is sent from the client to the server to check the current status of the server. + * - method: `tabby/status` + * - params: {@link StatusRequestParams} + * - result: {@link StatusInfo} + */ +export namespace StatusRequest { + export const method = "tabby/status"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolRequestType(method); +} + +export type StatusRequestParams = { + /** + * Forces a recheck of the connection to the Tabby server, and waiting for result. + */ + recheckConnection?: boolean; +}; + +/** + * [Tabby] StatusInfo is used to display the status bar in the editor. + */ +export type StatusInfo = { + status: + | "notInitialized" + | "finalized" + | "connecting" + | "unauthorized" + | "disconnected" + | "ready" + | "readyForAutoTrigger" + | "readyForManualTrigger" + | "fetching" + | "completionResponseSlow"; + tooltip?: string; + serverHealth?: Record; + command?: StatusShowHelpMessageCommand | LspCommand; +}; + +/** + * [Tabby] Status DidChange Notification(⬅️) + * + * This method is sent from the server to the client to notify the client of the status changes. + * - method: `tabby/status/didChange` + * - params: {@link StatusInfo} + * - result: void + */ +export namespace StatusDidChangeNotification { + export const method = "tabby/status/didChange"; + export const messageDirection = MessageDirection.serverToClient; + export const type = new ProtocolNotificationType(method); +} + +/** + * [Tabby] Status Show Help Message Request(↩️) + * + * This method is sent from the client to the server to request to show the help message for the current status. + * The server will callback client to show request using ShowMessageRequest (`window/showMessageRequest`). + * - method: `tabby/status/showHelpMessage` + * - params: any, not used + * - result: boolean + */ +export namespace StatusShowHelpMessageRequest { + export const method = "tabby/status/showHelpMessage"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolRequestType(method); +} + +export type StatusShowHelpMessageCommand = LspCommand & { + title: string; + command: "tabby/status/showHelpMessage"; + arguments: [LSPAny]; +}; + +/** + * [Tabby] Status Ignored Issues Edit Request(↩️) + * + * This method is sent from the client to the server to add or remove the issues that marked as ignored. + * - method: `tabby/status/ignoredIssues/edit` + * - params: {@link StatusIgnoredIssuesEditParams} + * - result: boolean + */ +export namespace StatusIgnoredIssuesEditRequest { + export const method = "tabby/status/ignoredIssues/edit"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolRequestType(method); +} + +export type StatusIssuesName = "completionResponseSlow"; + +export type StatusIgnoredIssuesEditParams = { + operation: "add" | "remove" | "removeAll"; + issues: StatusIssuesName | StatusIssuesName[]; +}; + /** * [Tabby] Read File Request(↪️) *