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",
"info": {
"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" },
"version": "0.1.0"
"version": "0.5.5"
},
"servers": [
{ "url": "https://playground.app.tabbyml.com", "description": "Playground server" },
{ "url": "http://localhost:8080", "description": "Local server" }
],
"servers": [{ "url": "/", "description": "Server" }],
"paths": {
"/v1/completions": {
"post": {
@ -40,7 +37,7 @@
}
},
"/v1/health": {
"post": {
"get": {
"tags": ["v1"],
"operationId": "health",
"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": {
@ -65,7 +90,6 @@
"CompletionRequest": {
"type": "object",
"properties": {
"prompt": { "type": "string", "example": "def fib(n):", "nullable": true },
"language": {
"type": "string",
"description": "Language identifier, full list is maintained at\nhttps://code.visualstudio.com/docs/languages/identifiers",
@ -73,7 +97,12 @@
"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": {
"language": "python",
@ -85,16 +114,41 @@
"required": ["id", "choices"],
"properties": {
"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": {
"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": {
"model": { "type": "string" },
"chat_model": { "type": "string", "nullable": true },
"device": { "type": "string" },
"compute_type": { "type": "string" },
"arch": { "type": "string" },
"cpu_info": { "type": "string" },
"cpu_count": { "type": "integer", "minimum": 0.0 },
@ -102,6 +156,27 @@
"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": {
"type": "object",
"required": ["type", "completion_id", "choice_index"],
@ -111,6 +186,14 @@
"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": {
"type": "object",
"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": {
"type": "object",
"required": ["build_date", "build_timestamp", "git_sha", "git_describe"],

View File

@ -30,7 +30,11 @@ export type HighCompletionTimeoutRateIssue = {
name: "highCompletionTimeoutRate";
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.
@ -95,14 +99,14 @@ export interface AgentFunction {
/**
* @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.
* @param options if `index` is provided, `name` will be ignored
* @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

View File

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

View File

@ -169,7 +169,7 @@ export class Auth extends EventEmitter {
clearInterval(timer);
resolve(true);
} 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");
} else {
// unknown error but still keep polling
@ -205,7 +205,7 @@ export class Auth extends EventEmitter {
payload: decodeJwt(refreshedJwt.data.jwt),
};
} 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");
} else {
// unknown error, retry a few times

View File

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

View File

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

View File

@ -15,7 +15,7 @@ export function limitScope(
return limitScopeByIndentation(context, config["indentation"])(input);
}
: (input) => {
if (config.experimentalSyntax && supportedLanguages.indexOf(context.language) >= 0) {
if (config.experimentalSyntax && supportedLanguages.includes(context.language)) {
return limitScopeBySyntax(context)(input);
} else {
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) {
let scope = node;
while (scope) {
if (types.indexOf(scope.type) >= 0) {
if (types.includes(scope.type)) {
return scope;
}
scope = scope.parent;
@ -54,7 +54,7 @@ function findScope(node: TreeSitterParser.SyntaxNode, typeList: string[][]): Tre
export function limitScopeBySyntax(context: CompletionContext): PostprocessFilter {
return async (input) => {
const { position, text, language, prefix, suffix } = context;
if (supportedLanguages.indexOf(language) < 0) {
if (!supportedLanguages.includes(language)) {
return input;
}
const languageConfig = languagesConfigs[language];

View File

@ -11,7 +11,10 @@ export interface paths {
post: operations["event"];
};
"/v1/health": {
post: operations["health"];
get: operations["health"];
};
"/v1beta/search": {
get: operations["search"];
};
}
@ -34,8 +37,6 @@ export interface components {
* }
*/
CompletionRequest: {
/** @example def fib(n): */
prompt?: string | null;
/**
* @description Language identifier, full list is maintained at
* https://code.visualstudio.com/docs/languages/identifiers
@ -43,22 +44,72 @@ export interface components {
*/
language?: string | 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;
debug_options?: components["schemas"]["DebugOptions"] | null;
};
/**
* @example {
* "choices": [
* {
* "index": 0,
* "text": "string"
* }
* ],
* "id": "string"
* }
*/
CompletionResponse: {
id: string;
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: {
model: string;
chat_model?: string | null;
device: string;
compute_type: string;
arch: string;
cpu_info: string;
cpu_count: number;
cuda_devices: string[];
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: {
/**
* @description Event type, should be `view` or `select`.
@ -69,12 +120,22 @@ export interface components {
/** Format: int32 */
choice_index: number;
};
SearchResponse: {
num_hits: number;
hits: components["schemas"]["Hit"][];
};
Segments: {
/** @description Content that appears before the cursor in the editor window. */
prefix: string;
/** @description Content that appears after the cursor in the editor window. */
suffix?: string | null;
};
Snippet: {
filepath: string;
body: string;
/** Format: float */
score: number;
};
Version: {
build_date: string;
build_timestamp: string;
@ -114,6 +175,11 @@ export interface operations {
};
};
event: {
parameters: {
query: {
select_kind?: string | null;
};
};
requestBody: {
content: {
"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) {
return (
(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) {
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 { createMachine, interpret } from "@xstate/fsm";
import type { StatusChangedEvent, AuthRequiredEvent, IssuesUpdatedEvent } from "tabby-agent";
import { agent } from "./agent";
import { notifications } from "./notifications";
import { TabbyCompletionProvider } from "./TabbyCompletionProvider";
@ -137,12 +138,12 @@ export class TabbyStatusBarItem {
this.completionProvider.on("loadingStatusUpdated", () => {
this.fsmService.send(agent().getStatus());
});
agent().on("statusChanged", (event) => {
agent().on("statusChanged", (event: StatusChangedEvent) => {
console.debug("Tabby agent statusChanged", { event });
this.fsmService.send(event.status);
});
agent().on("authRequired", (event) => {
agent().on("authRequired", (event: AuthRequiredEvent) => {
console.debug("Tabby agent authRequired", { event });
notifications.showInformationStartAuth({
onAuthStart: () => {
@ -154,16 +155,17 @@ export class TabbyStatusBarItem {
});
});
agent().on("issuesUpdated", (event) => {
agent().on("issuesUpdated", (event: IssuesUpdatedEvent) => {
console.debug("Tabby agent issuesUpdated", { event });
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;
if (event.issues[0] === "slowCompletionResponseTime") {
notifications.showInformationWhenSlowCompletionResponseTime();
} else if (event.issues[0] === "highCompletionTimeoutRate") {
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.backgroundColor = backgroundColorWarning;
this.item.text = `${iconIssueExist} ${label}`;
const issue = agent().getIssueDetail({ index: 0 });
const issue =
agent().getIssueDetail({ name: "highCompletionTimeoutRate" }) ??
agent().getIssueDetail({ name: "slowCompletionResponseTime" });
switch (issue?.name) {
case "slowCompletionResponseTime":
this.item.tooltip = "Completion requests appear to take too much time.";
break;
case "highCompletionTimeoutRate":
this.item.tooltip = "Most completion requests timed out.";
break;
case "slowCompletionResponseTime":
this.item.tooltip = "Completion requests appear to take too much time.";
break;
default:
this.item.tooltip = "";
break;
@ -299,12 +303,12 @@ export class TabbyStatusBarItem {
arguments: [
() => {
switch (issue?.name) {
case "slowCompletionResponseTime":
notifications.showInformationWhenSlowCompletionResponseTime();
break;
case "highCompletionTimeoutRate":
notifications.showInformationWhenHighCompletionTimeoutRate();
break;
case "slowCompletionResponseTime":
notifications.showInformationWhenSlowCompletionResponseTime();
break;
}
},
],

View File

@ -1,4 +1,9 @@
import { commands, window, workspace, env, ConfigurationTarget, Uri } from "vscode";
import type {
HighCompletionTimeoutRateIssue,
SlowCompletionResponseTimeIssue,
ConnectionFailedIssue,
} from "tabby-agent";
import { agent } from "./agent";
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
.showInformationMessage("Cannot connect to Tabby Server. Please check settings.", "Settings")
.showWarningMessage(
`Cannot connect to Tabby Server.`,
{
modal: true,
detail: message,
},
"Settings",
)
.then((selection) => {
switch (selection) {
case "Settings":
@ -94,6 +108,18 @@ function showInformationWhenDisconnected() {
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 }) {
@ -171,7 +197,8 @@ function getHelpMessageForCompletionResponseTimeIssue() {
function showInformationWhenSlowCompletionResponseTime(modal: boolean = false) {
if (modal) {
const stats = agent().getIssueDetail({ name: "slowCompletionResponseTime" })?.completionResponseStats;
const stats = agent().getIssueDetail<SlowCompletionResponseTimeIssue>({ name: "slowCompletionResponseTime" })
?.completionResponseStats;
let statsMessage = "";
if (stats && stats["responses"] && stats["averageResponseTime"]) {
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) {
if (modal) {
const stats = agent().getIssueDetail({ name: "highCompletionTimeoutRate" })?.completionResponseStats;
const stats = agent().getIssueDetail<HighCompletionTimeoutRateIssue>({ name: "highCompletionTimeoutRate" })
?.completionResponseStats;
let statsMessage = "";
if (stats && stats["total"] && stats["timeouts"]) {
statsMessage = `${stats["timeouts"]} of ${stats["total"]} completion requests timed out.\n\n`;