feat(vscode): add notification when failed to connect to server. (#808)

release-fix-intellij-update-support-version-range
Zhiming Ma 2023-11-17 12:30:04 +08:00 committed by GitHub
parent d0c9b56467
commit adb4bcd13f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 386 additions and 144 deletions

View File

@ -2,14 +2,11 @@
"openapi": "3.0.3", "openapi": "3.0.3",
"info": { "info": {
"title": "Tabby Server", "title": "Tabby Server",
"description": "\n[![tabby stars](https://img.shields.io/github/stars/TabbyML/tabby?style=social)](https://github.com/TabbyML/tabby)\n\nOpenAPI documentation for [tabby](https://github.com/TabbyML/tabby), a self-hosted AI coding assistant.", "description": "\n[![tabby stars](https://img.shields.io/github/stars/TabbyML/tabby)](https://github.com/TabbyML/tabby)\n[![Join Slack](https://shields.io/badge/Join-Tabby%20Slack-red?logo=slack)](https://join.slack.com/t/tabbycommunity/shared_invite/zt-1xeiddizp-bciR2RtFTaJ37RBxr8VxpA)\n\nInstall following IDE / Editor extensions to get started with [Tabby](https://github.com/TabbyML/tabby).\n* [VSCode Extension](https://github.com/TabbyML/tabby/tree/main/clients/vscode) Install from the [marketplace](https://marketplace.visualstudio.com/items?itemName=TabbyML.vscode-tabby), or [open-vsx.org](https://open-vsx.org/extension/TabbyML/vscode-tabby)\n* [VIM Extension](https://github.com/TabbyML/tabby/tree/main/clients/vim)\n* [IntelliJ Platform Plugin](https://github.com/TabbyML/tabby/tree/main/clients/intellij) Install from the [marketplace](https://plugins.jetbrains.com/plugin/22379-tabby)\n",
"license": { "name": "Apache 2.0", "url": "https://github.com/TabbyML/tabby/blob/main/LICENSE" }, "license": { "name": "Apache 2.0", "url": "https://github.com/TabbyML/tabby/blob/main/LICENSE" },
"version": "0.1.0" "version": "0.5.5"
}, },
"servers": [ "servers": [{ "url": "/", "description": "Server" }],
{ "url": "https://playground.app.tabbyml.com", "description": "Playground server" },
{ "url": "http://localhost:8080", "description": "Local server" }
],
"paths": { "paths": {
"/v1/completions": { "/v1/completions": {
"post": { "post": {
@ -40,7 +37,7 @@
} }
}, },
"/v1/health": { "/v1/health": {
"post": { "get": {
"tags": ["v1"], "tags": ["v1"],
"operationId": "health", "operationId": "health",
"responses": { "responses": {
@ -50,6 +47,34 @@
} }
} }
} }
},
"/v1beta/search": {
"get": {
"tags": ["v1beta"],
"operationId": "search",
"parameters": [
{ "name": "q", "in": "query", "required": true, "schema": { "type": "string", "default": "get" } },
{
"name": "limit",
"in": "query",
"required": false,
"schema": { "type": "integer", "default": 20, "nullable": true, "minimum": 0.0 }
},
{
"name": "offset",
"in": "query",
"required": false,
"schema": { "type": "integer", "default": 0, "nullable": true, "minimum": 0.0 }
}
],
"responses": {
"200": {
"description": "Success",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/SearchResponse" } } }
},
"501": { "description": "When code search is not enabled, the endpoint will returns 501 Not Implemented" }
}
}
} }
}, },
"components": { "components": {
@ -65,7 +90,6 @@
"CompletionRequest": { "CompletionRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
"prompt": { "type": "string", "example": "def fib(n):", "nullable": true },
"language": { "language": {
"type": "string", "type": "string",
"description": "Language identifier, full list is maintained at\nhttps://code.visualstudio.com/docs/languages/identifiers", "description": "Language identifier, full list is maintained at\nhttps://code.visualstudio.com/docs/languages/identifiers",
@ -73,7 +97,12 @@
"nullable": true "nullable": true
}, },
"segments": { "allOf": [{ "$ref": "#/components/schemas/Segments" }], "nullable": true }, "segments": { "allOf": [{ "$ref": "#/components/schemas/Segments" }], "nullable": true },
"user": { "type": "string", "nullable": true } "user": {
"type": "string",
"description": "A unique identifier representing your end-user, which can help Tabby to monitor & generating\nreports.",
"nullable": true
},
"debug_options": { "allOf": [{ "$ref": "#/components/schemas/DebugOptions" }], "nullable": true }
}, },
"example": { "example": {
"language": "python", "language": "python",
@ -85,16 +114,41 @@
"required": ["id", "choices"], "required": ["id", "choices"],
"properties": { "properties": {
"id": { "type": "string" }, "id": { "type": "string" },
"choices": { "type": "array", "items": { "$ref": "#/components/schemas/Choice" } } "choices": { "type": "array", "items": { "$ref": "#/components/schemas/Choice" } },
"debug_data": { "allOf": [{ "$ref": "#/components/schemas/DebugData" }], "nullable": true }
},
"example": { "choices": [{ "index": 0, "text": "string" }], "id": "string" }
},
"DebugData": {
"type": "object",
"properties": {
"snippets": { "type": "array", "items": { "$ref": "#/components/schemas/Snippet" }, "nullable": true },
"prompt": { "type": "string", "nullable": true }
}
},
"DebugOptions": {
"type": "object",
"properties": {
"raw_prompt": {
"type": "string",
"description": "When `raw_prompt` is specified, it will be passed directly to the inference engine for completion. `segments` field in `CompletionRequest` will be ignored.\n\nThis is useful for certain requests that aim to test the tabby's e2e quality.",
"nullable": true
},
"return_snippets": { "type": "boolean", "description": "When true, returns `snippets` in `debug_data`." },
"return_prompt": { "type": "boolean", "description": "When true, returns `prompt` in `debug_data`." },
"disable_retrieval_augmented_code_completion": {
"type": "boolean",
"description": "When true, disable retrieval augmented code completion."
}
} }
}, },
"HealthState": { "HealthState": {
"type": "object", "type": "object",
"required": ["model", "device", "compute_type", "arch", "cpu_info", "cpu_count", "cuda_devices", "version"], "required": ["model", "device", "arch", "cpu_info", "cpu_count", "cuda_devices", "version"],
"properties": { "properties": {
"model": { "type": "string" }, "model": { "type": "string" },
"chat_model": { "type": "string", "nullable": true },
"device": { "type": "string" }, "device": { "type": "string" },
"compute_type": { "type": "string" },
"arch": { "type": "string" }, "arch": { "type": "string" },
"cpu_info": { "type": "string" }, "cpu_info": { "type": "string" },
"cpu_count": { "type": "integer", "minimum": 0.0 }, "cpu_count": { "type": "integer", "minimum": 0.0 },
@ -102,6 +156,27 @@
"version": { "$ref": "#/components/schemas/Version" } "version": { "$ref": "#/components/schemas/Version" }
} }
}, },
"Hit": {
"type": "object",
"required": ["score", "doc", "id"],
"properties": {
"score": { "type": "number", "format": "float" },
"doc": { "$ref": "#/components/schemas/HitDocument" },
"id": { "type": "integer", "format": "int32", "minimum": 0.0 }
}
},
"HitDocument": {
"type": "object",
"required": ["body", "filepath", "git_url", "kind", "language", "name"],
"properties": {
"body": { "type": "string" },
"filepath": { "type": "string" },
"git_url": { "type": "string" },
"kind": { "type": "string" },
"language": { "type": "string" },
"name": { "type": "string" }
}
},
"LogEventRequest": { "LogEventRequest": {
"type": "object", "type": "object",
"required": ["type", "completion_id", "choice_index"], "required": ["type", "completion_id", "choice_index"],
@ -111,6 +186,14 @@
"choice_index": { "type": "integer", "format": "int32", "minimum": 0.0 } "choice_index": { "type": "integer", "format": "int32", "minimum": 0.0 }
} }
}, },
"SearchResponse": {
"type": "object",
"required": ["num_hits", "hits"],
"properties": {
"num_hits": { "type": "integer", "minimum": 0.0 },
"hits": { "type": "array", "items": { "$ref": "#/components/schemas/Hit" } }
}
},
"Segments": { "Segments": {
"type": "object", "type": "object",
"required": ["prefix"], "required": ["prefix"],
@ -123,6 +206,15 @@
} }
} }
}, },
"Snippet": {
"type": "object",
"required": ["filepath", "body", "score"],
"properties": {
"filepath": { "type": "string" },
"body": { "type": "string" },
"score": { "type": "number", "format": "float" }
}
},
"Version": { "Version": {
"type": "object", "type": "object",
"required": ["build_date", "build_timestamp", "git_sha", "git_describe"], "required": ["build_date", "build_timestamp", "git_sha", "git_describe"],

View File

@ -30,7 +30,11 @@ export type HighCompletionTimeoutRateIssue = {
name: "highCompletionTimeoutRate"; name: "highCompletionTimeoutRate";
completionResponseStats: Record<string, number>; completionResponseStats: Record<string, number>;
}; };
export type AgentIssue = SlowCompletionResponseTimeIssue | HighCompletionTimeoutRateIssue; export type ConnectionFailedIssue = {
name: "connectionFailed";
message: string;
};
export type AgentIssue = SlowCompletionResponseTimeIssue | HighCompletionTimeoutRateIssue | ConnectionFailedIssue;
/** /**
* Represents the status of the agent. * Represents the status of the agent.
@ -95,14 +99,14 @@ export interface AgentFunction {
/** /**
* @returns the current issues if any exists * @returns the current issues if any exists
*/ */
getIssues(detail?: boolean): AgentIssue["name"][]; getIssues(): AgentIssue["name"][];
/** /**
* Get the detail of an issue by index or name. * Get the detail of an issue by index or name.
* @param options if `index` is provided, `name` will be ignored * @param options if `index` is provided, `name` will be ignored
* @returns the issue detail if exists, otherwise null * @returns the issue detail if exists, otherwise null
*/ */
getIssueDetail(options: { index?: number; name?: AgentIssue["name"] }): AgentIssue | null; getIssueDetail<T extends AgentIssue>(options: { index?: number; name?: T["name"] }): T | null;
/** /**
* @returns server info returned from latest server health check, returns null if not available * @returns server info returned from latest server health check, returns null if not available

View File

@ -81,7 +81,7 @@ export class AnonymousUsageLogger {
if (this.disabled) { if (this.disabled) {
return; return;
} }
if (unique && this.emittedUniqueEvent.indexOf(event) >= 0) { if (unique && this.emittedUniqueEvent.includes(event)) {
return; return;
} }
if (unique) { if (unique) {

View File

@ -169,7 +169,7 @@ export class Auth extends EventEmitter {
clearInterval(timer); clearInterval(timer);
resolve(true); resolve(true);
} catch (error) { } catch (error) {
if (error instanceof HttpError && [400, 401, 403, 405].indexOf(error.status) !== -1) { if (error instanceof HttpError && [400, 401, 403, 405].includes(error.status)) {
this.logger.debug({ error }, "Expected error when polling jwt"); this.logger.debug({ error }, "Expected error when polling jwt");
} else { } else {
// unknown error but still keep polling // unknown error but still keep polling
@ -205,7 +205,7 @@ export class Auth extends EventEmitter {
payload: decodeJwt(refreshedJwt.data.jwt), payload: decodeJwt(refreshedJwt.data.jwt),
}; };
} catch (error) { } catch (error) {
if (error instanceof HttpError && [400, 401, 403, 405].indexOf(error.status) !== -1) { if (error instanceof HttpError && [400, 401, 403, 405].includes(error.status)) {
this.logger.debug({ error }, "Error when refreshing jwt"); this.logger.debug({ error }, "Error when refreshing jwt");
} else { } else {
// unknown error, retry a few times // unknown error, retry a few times

View File

@ -4,8 +4,9 @@ import deepEqual from "deep-equal";
import { deepmerge } from "deepmerge-ts"; import { deepmerge } from "deepmerge-ts";
import { getProperty, setProperty, deleteProperty } from "dot-prop"; import { getProperty, setProperty, deleteProperty } from "dot-prop";
import createClient from "openapi-fetch"; import createClient from "openapi-fetch";
import { paths as TabbyApi } from "./types/tabbyApi"; import type { ParseAs } from "openapi-fetch";
import { isBlank, abortSignalFromAnyOf, HttpError, isTimeoutError, isCanceledError } from "./utils"; import type { paths as TabbyApi } from "./types/tabbyApi";
import { isBlank, abortSignalFromAnyOf, HttpError, isTimeoutError, isCanceledError, errorToString } from "./utils";
import type { import type {
Agent, Agent,
AgentStatus, AgentStatus,
@ -47,6 +48,7 @@ export class TabbyAgent extends EventEmitter implements Agent {
private status: AgentStatus = "notInitialized"; private status: AgentStatus = "notInitialized";
private issues: AgentIssue["name"][] = []; private issues: AgentIssue["name"][] = [];
private serverHealthState: ServerHealthState | null = null; private serverHealthState: ServerHealthState | null = null;
private connectionErrorMessage: string | null = null;
private api: ReturnType<typeof createClient<TabbyApi>>; private api: ReturnType<typeof createClient<TabbyApi>>;
private auth: Auth; private auth: Auth;
private dataStore: DataStore | null = null; private dataStore: DataStore | null = null;
@ -88,6 +90,7 @@ export class TabbyAgent extends EventEmitter implements Agent {
this.config = deepmerge(defaultAgentConfig, this.userConfig, this.clientConfig); this.config = deepmerge(defaultAgentConfig, this.userConfig, this.clientConfig);
allLoggers.forEach((logger) => (logger.level = this.config.logs.level)); allLoggers.forEach((logger) => (logger.level = this.config.logs.level));
this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable; this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable;
if (isBlank(this.config.server.token) && this.config.server.requestHeaders["Authorization"] === undefined) { if (isBlank(this.config.server.token) && this.config.server.requestHeaders["Authorization"] === undefined) {
if (this.config.server.endpoint !== this.auth?.endpoint) { if (this.config.server.endpoint !== this.auth?.endpoint) {
this.auth = await Auth.create({ endpoint: this.config.server.endpoint, dataStore: this.dataStore }); this.auth = await Auth.create({ endpoint: this.config.server.endpoint, dataStore: this.dataStore });
@ -104,6 +107,8 @@ export class TabbyAgent extends EventEmitter implements Agent {
this.completionProviderStats.resetWindowed(); this.completionProviderStats.resetWindowed();
this.popIssue("slowCompletionResponseTime"); this.popIssue("slowCompletionResponseTime");
this.popIssue("highCompletionTimeoutRate"); this.popIssue("highCompletionTimeoutRate");
this.popIssue("connectionFailed");
this.connectionErrorMessage = null;
} }
await this.setupApi(); await this.setupApi();
@ -167,11 +172,16 @@ export class TabbyAgent extends EventEmitter implements Agent {
name: "slowCompletionResponseTime", name: "slowCompletionResponseTime",
completionResponseStats: this.completionProviderStats.windowed().stats, completionResponseStats: this.completionProviderStats.windowed().stats,
}; };
case "connectionFailed":
return {
name: "connectionFailed",
message: this.connectionErrorMessage,
};
} }
} }
private pushIssue(issue: AgentIssue["name"]) { private pushIssue(issue: AgentIssue["name"]) {
if (this.issues.indexOf(issue) === -1) { if (!this.issues.includes(issue)) {
this.issues.push(issue); this.issues.push(issue);
this.logger.debug({ issue }, "Issues Pushed"); this.logger.debug({ issue }, "Issues Pushed");
this.emitIssueUpdated(); this.emitIssueUpdated();
@ -206,65 +216,61 @@ export class TabbyAgent extends EventEmitter implements Agent {
} }
} }
private async post<T extends Parameters<typeof this.api.POST>[0]>( private createAbortSignal(options?: { signal?: AbortSignal; timeout?: number }): AbortSignal {
path: T, const timeout = Math.min(0x7fffffff, options?.timeout || this.config.server.requestTimeout);
requestOptions: Parameters<typeof this.api.POST<T>>[1], return abortSignalFromAnyOf([AbortSignal.timeout(timeout), options?.signal]);
abortOptions?: { signal?: AbortSignal; timeout?: number },
): Promise<Awaited<ReturnType<typeof this.api.POST<T>>>["data"]> {
const requestId = uuid();
this.logger.debug({ requestId, path, requestOptions, abortOptions }, "API request");
try {
const timeout = Math.min(0x7fffffff, abortOptions?.timeout || this.config.server.requestTimeout);
const signal = abortSignalFromAnyOf([AbortSignal.timeout(timeout), abortOptions?.signal]);
const response = await this.api.POST(path, { ...requestOptions, signal });
if (response.error) {
throw new HttpError(response.response);
}
this.logger.debug({ requestId, path, response: response.data }, "API response");
this.changeStatus("ready");
return response.data;
} catch (error) {
if (isTimeoutError(error)) {
this.logger.debug({ requestId, path, error }, "API request timeout");
} else if (isCanceledError(error)) {
this.logger.debug({ requestId, path, error }, "API request canceled");
} else if (
error instanceof HttpError &&
[401, 403, 405].indexOf(error.status) !== -1 &&
new URL(this.config.server.endpoint).hostname.endsWith("app.tabbyml.com") &&
isBlank(this.config.server.token) &&
this.config.server.requestHeaders["Authorization"] === undefined
) {
this.logger.debug({ requestId, path, error }, "API unauthorized");
this.changeStatus("unauthorized");
} else if (error instanceof HttpError) {
this.logger.error({ requestId, path, error }, "API error");
this.changeStatus("disconnected");
} else {
this.logger.error({ requestId, path, error }, "API request failed with unknown error");
this.changeStatus("disconnected");
}
throw error;
}
} }
private async healthCheck(options?: AbortSignalOption): Promise<any> { private async healthCheck(options?: AbortSignalOption): Promise<any> {
const requestId = uuid();
const requestPath = "/v1/health";
const requestUrl = this.config.server.endpoint + requestPath;
const requestOptions = {
signal: this.createAbortSignal(options),
};
try { try {
const healthState = await this.post("/v1/health", {}, options); this.logger.debug({ requestId, requestOptions, url: requestUrl }, "Health check request");
const response = await this.api.GET(requestPath, requestOptions);
if (response.error) {
throw new HttpError(response.response);
}
this.logger.debug({ requestId, response }, "Health check response");
this.changeStatus("ready");
this.popIssue("connectionFailed");
this.connectionErrorMessage = null;
const healthState = response.data;
if ( if (
typeof healthState === "object" && typeof healthState === "object" &&
healthState["model"] !== undefined && healthState["model"] !== undefined &&
healthState["device"] !== undefined healthState["device"] !== undefined
) { ) {
this.serverHealthState = healthState; this.serverHealthState = healthState;
if (this.status === "ready") {
this.anonymousUsageLogger.uniqueEvent("AgentConnected", healthState); this.anonymousUsageLogger.uniqueEvent("AgentConnected", healthState);
} }
} } catch (error) {
} catch (_) {
if (this.status === "ready" || this.status === "notInitialized") {
this.changeStatus("disconnected");
this.serverHealthState = null; this.serverHealthState = null;
if (
error instanceof HttpError &&
[401, 403, 405].includes(error.status) &&
new URL(this.config.server.endpoint).hostname.endsWith("app.tabbyml.com") &&
isBlank(this.config.server.token) &&
this.config.server.requestHeaders["Authorization"] === undefined
) {
this.logger.debug({ requestId, error }, "Health check error: unauthorized");
this.changeStatus("unauthorized");
} else {
if (isTimeoutError(error)) {
this.logger.debug({ requestId, error }, "Health check error: timeout");
this.connectionErrorMessage = `GET ${requestUrl}: Timed out.`;
} else if (isCanceledError(error)) {
this.logger.debug({ requestId, error }, "Health check error: canceled");
this.connectionErrorMessage = `GET ${requestUrl}: Canceled.`;
} else {
this.logger.error({ requestId, error }, "Health check error: unknown error");
this.connectionErrorMessage = `GET ${requestUrl}: Request failed: \n${errorToString(error)}`;
}
this.pushIssue("connectionFailed");
this.changeStatus("disconnected");
} }
} }
} }
@ -380,11 +386,11 @@ export class TabbyAgent extends EventEmitter implements Agent {
return this.issues; return this.issues;
} }
public getIssueDetail(options: { index?: number; name?: AgentIssue["name"] }): AgentIssue | null { public getIssueDetail<T extends AgentIssue>(options: { index?: number; name?: T["name"] }): T | null {
if (options.index !== undefined) { if (options.index !== undefined && options.index < this.issues.length) {
return this.issueFromName(this.issues[options.index]); return this.issueFromName(this.issues[options.index]) as T;
} else if (options.name !== undefined && this.issues.indexOf(options.name) !== -1) { } else if (options.name !== undefined && this.issues.includes(options.name)) {
return this.issueFromName(options.name); return this.issueFromName(options.name) as T;
} else { } else {
return null; return null;
} }
@ -480,27 +486,36 @@ export class TabbyAgent extends EventEmitter implements Agent {
); );
// Send http request // Send http request
const requestId = uuid();
stats.requestSent = true; stats.requestSent = true;
requestStartedAt = performance.now(); requestStartedAt = performance.now();
try { try {
const response = await this.post( const requestPath = "/v1/completions";
"/v1/completions", const requestOptions = {
{
body: { body: {
language: request.language, language: request.language,
segments, segments,
user: this.auth?.user, user: this.auth?.user,
}, },
}, signal: this.createAbortSignal({
{
signal, signal,
timeout: this.config.completion.timeout, timeout: this.config.completion.timeout,
}, }),
};
this.logger.debug(
{ requestId, requestOptions, url: this.config.server.endpoint + requestPath },
"Completion request",
); );
const response = await this.api.POST(requestPath, requestOptions);
if (response.error) {
throw new HttpError(response.response);
}
this.logger.debug({ requestId, response }, "Completion response");
const responseData = response.data;
stats.requestLatency = performance.now() - requestStartedAt; stats.requestLatency = performance.now() - requestStartedAt;
completionResponse = { completionResponse = {
id: response.id, id: responseData.id,
choices: response.choices.map((choice) => { choices: responseData.choices.map((choice) => {
return { return {
index: choice.index, index: choice.index,
text: choice.text, text: choice.text,
@ -513,12 +528,17 @@ export class TabbyAgent extends EventEmitter implements Agent {
}; };
} catch (error) { } catch (error) {
if (isCanceledError(error)) { if (isCanceledError(error)) {
this.logger.debug({ requestId, error }, "Completion request canceled");
stats.requestCanceled = true; stats.requestCanceled = true;
stats.requestLatency = performance.now() - requestStartedAt; stats.requestLatency = performance.now() - requestStartedAt;
} } else if (isTimeoutError(error)) {
if (isTimeoutError(error)) { this.logger.debug({ requestId, error }, "Completion request timeout");
stats.requestTimeout = true; stats.requestTimeout = true;
stats.requestLatency = NaN; stats.requestLatency = NaN;
} else {
this.logger.error({ requestId, error }, "Completion request failed with unknown error");
// schedule a health check
this.healthCheck();
} }
// rethrow error // rethrow error
throw error; throw error;
@ -586,19 +606,35 @@ export class TabbyAgent extends EventEmitter implements Agent {
throw new Error("Agent is not initialized"); throw new Error("Agent is not initialized");
} }
this.completionProviderStats.addEvent(request.type); this.completionProviderStats.addEvent(request.type);
await this.post( const requestId = uuid();
"/v1/events", try {
{ const requestPath = "/v1/events";
const requestOptions = {
body: request, body: request,
params: { params: {
query: { query: {
select_kind: request.select_kind, select_kind: request.select_kind,
}, },
}, },
parseAs: "text", signal: this.createAbortSignal(options),
}, parseAs: "text" as ParseAs,
options, };
); this.logger.debug({ requestId, requestOptions, url: this.config.server.endpoint + requestPath }, "Event request");
const response = await this.api.POST(requestPath, requestOptions);
if (response.error) {
throw new HttpError(response.response);
}
this.logger.debug({ requestId, response }, "Event response");
return true; return true;
} catch (error) {
if (isTimeoutError(error)) {
this.logger.debug({ requestId, error }, "Event request timeout");
} else if (isCanceledError(error)) {
this.logger.debug({ requestId, error }, "Event request canceled");
} else {
this.logger.error({ requestId, error }, "Event request failed with unknown error");
}
return false;
}
} }
} }

View File

@ -12,6 +12,7 @@ export {
IssuesUpdatedEvent, IssuesUpdatedEvent,
SlowCompletionResponseTimeIssue, SlowCompletionResponseTimeIssue,
HighCompletionTimeoutRateIssue, HighCompletionTimeoutRateIssue,
ConnectionFailedIssue,
ClientProperties, ClientProperties,
AgentInitOptions, AgentInitOptions,
ServerHealthState, ServerHealthState,

View File

@ -11,7 +11,7 @@ export async function calculateReplaceRangeBySyntax(
context: CompletionContext, context: CompletionContext,
): Promise<CompletionResponse> { ): Promise<CompletionResponse> {
const { position, prefix, suffix, prefixLines, suffixLines, language } = context; const { position, prefix, suffix, prefixLines, suffixLines, language } = context;
if (supportedLanguages.indexOf(language) < 0) { if (!supportedLanguages.includes(language)) {
return response; return response;
} }
const languageConfig = languagesConfigs[language]; const languageConfig = languagesConfigs[language];

View File

@ -15,7 +15,7 @@ export function limitScope(
return limitScopeByIndentation(context, config["indentation"])(input); return limitScopeByIndentation(context, config["indentation"])(input);
} }
: (input) => { : (input) => {
if (config.experimentalSyntax && supportedLanguages.indexOf(context.language) >= 0) { if (config.experimentalSyntax && supportedLanguages.includes(context.language)) {
return limitScopeBySyntax(context)(input); return limitScopeBySyntax(context)(input);
} else { } else {
return limitScopeByIndentation(context, config["indentation"])(input); return limitScopeByIndentation(context, config["indentation"])(input);

View File

@ -42,7 +42,7 @@ function findScope(node: TreeSitterParser.SyntaxNode, typeList: string[][]): Tre
for (const types of typeList) { for (const types of typeList) {
let scope = node; let scope = node;
while (scope) { while (scope) {
if (types.indexOf(scope.type) >= 0) { if (types.includes(scope.type)) {
return scope; return scope;
} }
scope = scope.parent; scope = scope.parent;
@ -54,7 +54,7 @@ function findScope(node: TreeSitterParser.SyntaxNode, typeList: string[][]): Tre
export function limitScopeBySyntax(context: CompletionContext): PostprocessFilter { export function limitScopeBySyntax(context: CompletionContext): PostprocessFilter {
return async (input) => { return async (input) => {
const { position, text, language, prefix, suffix } = context; const { position, text, language, prefix, suffix } = context;
if (supportedLanguages.indexOf(language) < 0) { if (!supportedLanguages.includes(language)) {
return input; return input;
} }
const languageConfig = languagesConfigs[language]; const languageConfig = languagesConfigs[language];

View File

@ -11,7 +11,10 @@ export interface paths {
post: operations["event"]; post: operations["event"];
}; };
"/v1/health": { "/v1/health": {
post: operations["health"]; get: operations["health"];
};
"/v1beta/search": {
get: operations["search"];
}; };
} }
@ -34,8 +37,6 @@ export interface components {
* } * }
*/ */
CompletionRequest: { CompletionRequest: {
/** @example def fib(n): */
prompt?: string | null;
/** /**
* @description Language identifier, full list is maintained at * @description Language identifier, full list is maintained at
* https://code.visualstudio.com/docs/languages/identifiers * https://code.visualstudio.com/docs/languages/identifiers
@ -43,22 +44,72 @@ export interface components {
*/ */
language?: string | null; language?: string | null;
segments?: components["schemas"]["Segments"] | null; segments?: components["schemas"]["Segments"] | null;
/**
* @description A unique identifier representing your end-user, which can help Tabby to monitor & generating
* reports.
*/
user?: string | null; user?: string | null;
debug_options?: components["schemas"]["DebugOptions"] | null;
}; };
/**
* @example {
* "choices": [
* {
* "index": 0,
* "text": "string"
* }
* ],
* "id": "string"
* }
*/
CompletionResponse: { CompletionResponse: {
id: string; id: string;
choices: components["schemas"]["Choice"][]; choices: components["schemas"]["Choice"][];
debug_data?: components["schemas"]["DebugData"] | null;
};
DebugData: {
snippets?: components["schemas"]["Snippet"][] | null;
prompt?: string | null;
};
DebugOptions: {
/**
* @description When `raw_prompt` is specified, it will be passed directly to the inference engine for completion. `segments` field in `CompletionRequest` will be ignored.
*
* This is useful for certain requests that aim to test the tabby's e2e quality.
*/
raw_prompt?: string | null;
/** @description When true, returns `snippets` in `debug_data`. */
return_snippets?: boolean;
/** @description When true, returns `prompt` in `debug_data`. */
return_prompt?: boolean;
/** @description When true, disable retrieval augmented code completion. */
disable_retrieval_augmented_code_completion?: boolean;
}; };
HealthState: { HealthState: {
model: string; model: string;
chat_model?: string | null;
device: string; device: string;
compute_type: string;
arch: string; arch: string;
cpu_info: string; cpu_info: string;
cpu_count: number; cpu_count: number;
cuda_devices: string[]; cuda_devices: string[];
version: components["schemas"]["Version"]; version: components["schemas"]["Version"];
}; };
Hit: {
/** Format: float */
score: number;
doc: components["schemas"]["HitDocument"];
/** Format: int32 */
id: number;
};
HitDocument: {
body: string;
filepath: string;
git_url: string;
kind: string;
language: string;
name: string;
};
LogEventRequest: { LogEventRequest: {
/** /**
* @description Event type, should be `view` or `select`. * @description Event type, should be `view` or `select`.
@ -69,12 +120,22 @@ export interface components {
/** Format: int32 */ /** Format: int32 */
choice_index: number; choice_index: number;
}; };
SearchResponse: {
num_hits: number;
hits: components["schemas"]["Hit"][];
};
Segments: { Segments: {
/** @description Content that appears before the cursor in the editor window. */ /** @description Content that appears before the cursor in the editor window. */
prefix: string; prefix: string;
/** @description Content that appears after the cursor in the editor window. */ /** @description Content that appears after the cursor in the editor window. */
suffix?: string | null; suffix?: string | null;
}; };
Snippet: {
filepath: string;
body: string;
/** Format: float */
score: number;
};
Version: { Version: {
build_date: string; build_date: string;
build_timestamp: string; build_timestamp: string;
@ -114,6 +175,11 @@ export interface operations {
}; };
}; };
event: { event: {
parameters: {
query: {
select_kind?: string | null;
};
};
requestBody: { requestBody: {
content: { content: {
"application/json": components["schemas"]["LogEventRequest"]; "application/json": components["schemas"]["LogEventRequest"];
@ -140,4 +206,25 @@ export interface operations {
}; };
}; };
}; };
search: {
parameters: {
query: {
q: string;
limit?: number | null;
offset?: number | null;
};
};
responses: {
/** @description Success */
200: {
content: {
"application/json": components["schemas"]["SearchResponse"];
};
};
/** @description When code search is not enabled, the endpoint will returns 501 Not Implemented */
501: {
content: never;
};
};
};
} }

View File

@ -102,10 +102,18 @@ export class HttpError extends Error {
export function isTimeoutError(error: any) { export function isTimeoutError(error: any) {
return ( return (
(error instanceof Error && error.name === "TimeoutError") || (error instanceof Error && error.name === "TimeoutError") ||
(error instanceof HttpError && [408, 499].indexOf(error.status) !== -1) (error instanceof HttpError && [408, 499].includes(error.status))
); );
} }
export function isCanceledError(error: any) { export function isCanceledError(error: any) {
return error instanceof Error && error.name === "AbortError"; return error instanceof Error && error.name === "AbortError";
} }
export function errorToString(error: any) {
let message = error.message || error.toString();
if (error.cause) {
message += "\nCaused by: " + errorToString(error.cause);
}
return message;
}

View File

@ -1,18 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/naming-convention": "warn",
"@typescript-eslint/semi": "warn",
"curly": "warn",
"eqeqeq": "warn",
"no-throw-literal": "warn",
"semi": "off"
},
"ignorePatterns": ["out", "dist", "**/*.d.ts"]
}

View File

@ -1,5 +1,6 @@
import { StatusBarAlignment, ThemeColor, window } from "vscode"; import { StatusBarAlignment, ThemeColor, window } from "vscode";
import { createMachine, interpret } from "@xstate/fsm"; import { createMachine, interpret } from "@xstate/fsm";
import type { StatusChangedEvent, AuthRequiredEvent, IssuesUpdatedEvent } from "tabby-agent";
import { agent } from "./agent"; import { agent } from "./agent";
import { notifications } from "./notifications"; import { notifications } from "./notifications";
import { TabbyCompletionProvider } from "./TabbyCompletionProvider"; import { TabbyCompletionProvider } from "./TabbyCompletionProvider";
@ -137,12 +138,12 @@ export class TabbyStatusBarItem {
this.completionProvider.on("loadingStatusUpdated", () => { this.completionProvider.on("loadingStatusUpdated", () => {
this.fsmService.send(agent().getStatus()); this.fsmService.send(agent().getStatus());
}); });
agent().on("statusChanged", (event) => { agent().on("statusChanged", (event: StatusChangedEvent) => {
console.debug("Tabby agent statusChanged", { event }); console.debug("Tabby agent statusChanged", { event });
this.fsmService.send(event.status); this.fsmService.send(event.status);
}); });
agent().on("authRequired", (event) => { agent().on("authRequired", (event: AuthRequiredEvent) => {
console.debug("Tabby agent authRequired", { event }); console.debug("Tabby agent authRequired", { event });
notifications.showInformationStartAuth({ notifications.showInformationStartAuth({
onAuthStart: () => { onAuthStart: () => {
@ -154,16 +155,17 @@ export class TabbyStatusBarItem {
}); });
}); });
agent().on("issuesUpdated", (event) => { agent().on("issuesUpdated", (event: IssuesUpdatedEvent) => {
console.debug("Tabby agent issuesUpdated", { event }); console.debug("Tabby agent issuesUpdated", { event });
this.fsmService.send(agent().getStatus()); this.fsmService.send(agent().getStatus());
if (event.issues.length > 0 && !this.completionResponseWarningShown) { if (event.issues.includes("connectionFailed")) {
notifications.showInformationWhenDisconnected();
} else if (!this.completionResponseWarningShown && event.issues.includes("highCompletionTimeoutRate")) {
this.completionResponseWarningShown = true; this.completionResponseWarningShown = true;
if (event.issues[0] === "slowCompletionResponseTime") {
notifications.showInformationWhenSlowCompletionResponseTime();
} else if (event.issues[0] === "highCompletionTimeoutRate") {
notifications.showInformationWhenHighCompletionTimeoutRate(); notifications.showInformationWhenHighCompletionTimeoutRate();
} } else if (!this.completionResponseWarningShown && event.issues.includes("slowCompletionResponseTime")) {
this.completionResponseWarningShown = true;
notifications.showInformationWhenSlowCompletionResponseTime();
} }
}); });
} }
@ -281,14 +283,16 @@ export class TabbyStatusBarItem {
this.item.color = colorWarning; this.item.color = colorWarning;
this.item.backgroundColor = backgroundColorWarning; this.item.backgroundColor = backgroundColorWarning;
this.item.text = `${iconIssueExist} ${label}`; this.item.text = `${iconIssueExist} ${label}`;
const issue = agent().getIssueDetail({ index: 0 }); const issue =
agent().getIssueDetail({ name: "highCompletionTimeoutRate" }) ??
agent().getIssueDetail({ name: "slowCompletionResponseTime" });
switch (issue?.name) { switch (issue?.name) {
case "slowCompletionResponseTime":
this.item.tooltip = "Completion requests appear to take too much time.";
break;
case "highCompletionTimeoutRate": case "highCompletionTimeoutRate":
this.item.tooltip = "Most completion requests timed out."; this.item.tooltip = "Most completion requests timed out.";
break; break;
case "slowCompletionResponseTime":
this.item.tooltip = "Completion requests appear to take too much time.";
break;
default: default:
this.item.tooltip = ""; this.item.tooltip = "";
break; break;
@ -299,12 +303,12 @@ export class TabbyStatusBarItem {
arguments: [ arguments: [
() => { () => {
switch (issue?.name) { switch (issue?.name) {
case "slowCompletionResponseTime":
notifications.showInformationWhenSlowCompletionResponseTime();
break;
case "highCompletionTimeoutRate": case "highCompletionTimeoutRate":
notifications.showInformationWhenHighCompletionTimeoutRate(); notifications.showInformationWhenHighCompletionTimeoutRate();
break; break;
case "slowCompletionResponseTime":
notifications.showInformationWhenSlowCompletionResponseTime();
break;
} }
}, },
], ],

View File

@ -1,4 +1,9 @@
import { commands, window, workspace, env, ConfigurationTarget, Uri } from "vscode"; import { commands, window, workspace, env, ConfigurationTarget, Uri } from "vscode";
import type {
HighCompletionTimeoutRateIssue,
SlowCompletionResponseTimeIssue,
ConnectionFailedIssue,
} from "tabby-agent";
import { agent } from "./agent"; import { agent } from "./agent";
function showInformationWhenInitializing() { function showInformationWhenInitializing() {
@ -84,9 +89,18 @@ function showInformationWhenInlineSuggestDisabled() {
}); });
} }
function showInformationWhenDisconnected() { function showInformationWhenDisconnected(modal: boolean = false) {
if (modal) {
const message = agent().getIssueDetail<ConnectionFailedIssue>({ name: "connectionFailed" })?.message;
window window
.showInformationMessage("Cannot connect to Tabby Server. Please check settings.", "Settings") .showWarningMessage(
`Cannot connect to Tabby Server.`,
{
modal: true,
detail: message,
},
"Settings",
)
.then((selection) => { .then((selection) => {
switch (selection) { switch (selection) {
case "Settings": case "Settings":
@ -94,6 +108,18 @@ function showInformationWhenDisconnected() {
break; break;
} }
}); });
} else {
window.showWarningMessage(`Cannot connect to Tabby Server.`, "Detail", "Settings").then((selection) => {
switch (selection) {
case "Detail":
showInformationWhenDisconnected(true);
break;
case "Settings":
commands.executeCommand("tabby.openSettings");
break;
}
});
}
} }
function showInformationStartAuth(callbacks?: { onAuthStart?: () => void; onAuthEnd?: () => void }) { function showInformationStartAuth(callbacks?: { onAuthStart?: () => void; onAuthEnd?: () => void }) {
@ -171,7 +197,8 @@ function getHelpMessageForCompletionResponseTimeIssue() {
function showInformationWhenSlowCompletionResponseTime(modal: boolean = false) { function showInformationWhenSlowCompletionResponseTime(modal: boolean = false) {
if (modal) { if (modal) {
const stats = agent().getIssueDetail({ name: "slowCompletionResponseTime" })?.completionResponseStats; const stats = agent().getIssueDetail<SlowCompletionResponseTimeIssue>({ name: "slowCompletionResponseTime" })
?.completionResponseStats;
let statsMessage = ""; let statsMessage = "";
if (stats && stats["responses"] && stats["averageResponseTime"]) { if (stats && stats["responses"] && stats["averageResponseTime"]) {
statsMessage = `The average response time of recent ${stats["responses"]} completion requests is ${Number( statsMessage = `The average response time of recent ${stats["responses"]} completion requests is ${Number(
@ -212,7 +239,8 @@ function showInformationWhenSlowCompletionResponseTime(modal: boolean = false) {
function showInformationWhenHighCompletionTimeoutRate(modal: boolean = false) { function showInformationWhenHighCompletionTimeoutRate(modal: boolean = false) {
if (modal) { if (modal) {
const stats = agent().getIssueDetail({ name: "highCompletionTimeoutRate" })?.completionResponseStats; const stats = agent().getIssueDetail<HighCompletionTimeoutRateIssue>({ name: "highCompletionTimeoutRate" })
?.completionResponseStats;
let statsMessage = ""; let statsMessage = "";
if (stats && stats["total"] && stats["timeouts"]) { if (stats && stats["total"] && stats["timeouts"]) {
statsMessage = `${stats["timeouts"]} of ${stats["total"]} completion requests timed out.\n\n`; statsMessage = `${stats["timeouts"]} of ${stats["total"]} completion requests timed out.\n\n`;