feat(vscode): add notification when failed to connect to server. (#808)
parent
d0c9b56467
commit
adb4bcd13f
|
|
@ -2,14 +2,11 @@
|
||||||
"openapi": "3.0.3",
|
"openapi": "3.0.3",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "Tabby Server",
|
"title": "Tabby Server",
|
||||||
"description": "\n[](https://github.com/TabbyML/tabby)\n\nOpenAPI documentation for [tabby](https://github.com/TabbyML/tabby), a self-hosted AI coding assistant.",
|
"description": "\n[](https://github.com/TabbyML/tabby)\n[](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"],
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 (_) {
|
} catch (error) {
|
||||||
if (this.status === "ready" || this.status === "notInitialized") {
|
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");
|
this.changeStatus("disconnected");
|
||||||
this.serverHealthState = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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");
|
||||||
return true;
|
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;
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export {
|
||||||
IssuesUpdatedEvent,
|
IssuesUpdatedEvent,
|
||||||
SlowCompletionResponseTimeIssue,
|
SlowCompletionResponseTimeIssue,
|
||||||
HighCompletionTimeoutRateIssue,
|
HighCompletionTimeoutRateIssue,
|
||||||
|
ConnectionFailedIssue,
|
||||||
ClientProperties,
|
ClientProperties,
|
||||||
AgentInitOptions,
|
AgentInitOptions,
|
||||||
ServerHealthState,
|
ServerHealthState,
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
|
||||||
}
|
|
||||||
|
|
@ -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.showInformationWhenHighCompletionTimeoutRate();
|
||||||
notifications.showInformationWhenSlowCompletionResponseTime();
|
} else if (!this.completionResponseWarningShown && event.issues.includes("slowCompletionResponseTime")) {
|
||||||
} else if (event.issues[0] === "highCompletionTimeoutRate") {
|
this.completionResponseWarningShown = true;
|
||||||
notifications.showInformationWhenHighCompletionTimeoutRate();
|
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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,16 +89,37 @@ function showInformationWhenInlineSuggestDisabled() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showInformationWhenDisconnected() {
|
function showInformationWhenDisconnected(modal: boolean = false) {
|
||||||
window
|
if (modal) {
|
||||||
.showInformationMessage("Cannot connect to Tabby Server. Please check settings.", "Settings")
|
const message = agent().getIssueDetail<ConnectionFailedIssue>({ name: "connectionFailed" })?.message;
|
||||||
.then((selection) => {
|
window
|
||||||
|
.showWarningMessage(
|
||||||
|
`Cannot connect to Tabby Server.`,
|
||||||
|
{
|
||||||
|
modal: true,
|
||||||
|
detail: message,
|
||||||
|
},
|
||||||
|
"Settings",
|
||||||
|
)
|
||||||
|
.then((selection) => {
|
||||||
|
switch (selection) {
|
||||||
|
case "Settings":
|
||||||
|
commands.executeCommand("tabby.openSettings");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.showWarningMessage(`Cannot connect to Tabby Server.`, "Detail", "Settings").then((selection) => {
|
||||||
switch (selection) {
|
switch (selection) {
|
||||||
|
case "Detail":
|
||||||
|
showInformationWhenDisconnected(true);
|
||||||
|
break;
|
||||||
case "Settings":
|
case "Settings":
|
||||||
commands.executeCommand("tabby.openSettings");
|
commands.executeCommand("tabby.openSettings");
|
||||||
break;
|
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`;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue