From 65467d83ae85818d31463be2693378174a6e1480 Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Mon, 20 Jan 2025 02:27:01 -0600 Subject: [PATCH 01/19] to: adding a draft approach --- clients/tabby-chat-panel/src/index.ts | 88 ++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index 7818732df8e4..8e74f7a80eef 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import { createThreadFromIframe, createThreadFromInsideIframe } from 'tabby-threads' import { version } from '../package.json' @@ -352,6 +353,9 @@ export interface ClientApiMethods { readFileContent?: (info: FileRange) => Promise } +interface SupportProxy { + [key: string]: Promise +} export interface ClientApi extends ClientApiMethods { /** * Checks if the client supports this capability. @@ -359,6 +363,8 @@ export interface ClientApi extends ClientApiMethods { * Note: This method should not be used to ensure compatibility across different chat panel SDK versions. */ hasCapability: (method: keyof ClientApiMethods) => Promise + + supports: SupportProxy } export function createClient(target: HTMLIFrameElement, api: ClientApiMethods): ServerApi { @@ -384,7 +390,7 @@ export function createClient(target: HTMLIFrameElement, api: ClientApiMethods): } export function createServer(api: ServerApi): ClientApi { - return createThreadFromInsideIframe({ + const clientApi = createThreadFromInsideIframe({ expose: { init: api.init, executeCommand: api.executeCommand, @@ -394,5 +400,85 @@ export function createServer(api: ServerApi): ClientApi { updateTheme: api.updateTheme, updateActiveSelection: api.updateActiveSelection, }, + }) as unknown as ClientApi + + console.log('clientApi', Object.keys(clientApi)) + + const supportCache = new Map() + let cacheInitialized = false + + const initializeCache = async () => { + if (cacheInitialized) + return + console.log('Initializing cache...') + try { + // TODO: remove this workaround after the server is updated + const methods = [ + 'refresh', + 'onApplyInEditor', + 'onApplyInEditorV2', + 'onLoaded', + 'onCopy', + 'onKeyboardEvent', + 'lookupSymbol', + 'openInEditor', + 'openExternal', + 'readWorkspaceGitRepositories', + 'getActiveEditorSelection', + ] + + await Promise.all( + methods.map(async (method) => { + try { + console.log('Checking method:', method) + const supported = await clientApi.hasCapability(method as keyof ClientApiMethods) + supportCache.set(method, supported) + console.log(`Method ${method} supported:`, supported) + } + catch (e) { + console.log('Error checking method:', method, e) + supportCache.set(method, false) + } + }), + ) + cacheInitialized = true + console.log('Cache initialized:', supportCache) + } + catch (e) { + console.error('Failed to initialize cache:', e) + } + } + + initializeCache() + + return new Proxy(clientApi, { + get(target, property, receiver) { + // Approach 1: use supports keyword to check if the method is supported + // support get and has method for supports + // get method for 'supports' operator e.g. server.supports['refresh'].then(setSupportRefresh) + // has for 'in' operator if('refresh' in server.supports) { ... } + if (property === 'supports') { + return new Proxy({}, { + get: async (_target, capability: string) => { + const cleanCapability = capability.replace('?', '') + await initializeCache() + return supportCache.has(cleanCapability) + ? supportCache.get(cleanCapability) + : false + }, + has: (_target, capability: string) => { + // FIXME: bug here, always return false when server just load, need to fix + const cleanCapability = capability.replace('?', '') + if (!cacheInitialized) + return false + return supportCache.has(cleanCapability) + ? supportCache.get(cleanCapability) ?? false + : false + }, + }) + } + // Approach 2: use ClientApiMethods getter to check if the method is supported + return Reflect.get(target, property, receiver) + }, }) } From f7fa804984eb346a7adefcd61f12790950fce0dd Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Mon, 20 Jan 2025 02:46:17 -0600 Subject: [PATCH 02/19] refactor(api): improve type definitions for ClientApiMethods and SupportProxy --- clients/tabby-chat-panel/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index 8e74f7a80eef..79d78f8fa8f3 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -352,9 +352,10 @@ export interface ClientApiMethods { */ readFileContent?: (info: FileRange) => Promise } +type ClientApiMethod = keyof ClientApiMethods -interface SupportProxy { - [key: string]: Promise +type SupportProxy = { + [K in ClientApiMethod]: Promise } export interface ClientApi extends ClientApiMethods { /** From 4b83730aa697e2a7b24b41968c92a5c7d0bc0356 Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Tue, 21 Jan 2025 01:26:07 -0600 Subject: [PATCH 03/19] refactor(chat): convert client creation to async and enhance logging --- clients/tabby-chat-panel/src/index.ts | 90 +------------- clients/tabby-chat-panel/src/react.ts | 35 ++++-- .../source/targets/iframe/iframe.ts | 65 +++++++++- .../source/targets/iframe/nested.ts | 89 ++++++++++---- .../tabby-threads/source/targets/target.ts | 112 +++++++++++++++++- clients/vscode/src/chat/createClient.ts | 24 +++- clients/vscode/src/chat/webview.ts | 11 +- ee/tabby-ui/app/chat/page.tsx | 1 - 8 files changed, 291 insertions(+), 136 deletions(-) diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index 79d78f8fa8f3..ebfe0f827771 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -368,8 +368,9 @@ export interface ClientApi extends ClientApiMethods { supports: SupportProxy } -export function createClient(target: HTMLIFrameElement, api: ClientApiMethods): ServerApi { - return createThreadFromIframe(target, { +// TODO: change this to async +export async function createClient(target: HTMLIFrameElement, api: ClientApiMethods): Promise { + const thread = await createThreadFromIframe(target, { expose: { refresh: api.refresh, onApplyInEditor: api.onApplyInEditor, @@ -388,10 +389,11 @@ export function createClient(target: HTMLIFrameElement, api: ClientApiMethods): readFileContent: api.readFileContent, }, }) + return thread as unknown as ServerApi } -export function createServer(api: ServerApi): ClientApi { - const clientApi = createThreadFromInsideIframe({ +export async function createServer(api: ServerApi): Promise { + return await createThreadFromInsideIframe({ expose: { init: api.init, executeCommand: api.executeCommand, @@ -401,85 +403,5 @@ export function createServer(api: ServerApi): ClientApi { updateTheme: api.updateTheme, updateActiveSelection: api.updateActiveSelection, }, - }) as unknown as ClientApi - - console.log('clientApi', Object.keys(clientApi)) - - const supportCache = new Map() - let cacheInitialized = false - - const initializeCache = async () => { - if (cacheInitialized) - return - console.log('Initializing cache...') - try { - // TODO: remove this workaround after the server is updated - const methods = [ - 'refresh', - 'onApplyInEditor', - 'onApplyInEditorV2', - 'onLoaded', - 'onCopy', - 'onKeyboardEvent', - 'lookupSymbol', - 'openInEditor', - 'openExternal', - 'readWorkspaceGitRepositories', - 'getActiveEditorSelection', - ] - - await Promise.all( - methods.map(async (method) => { - try { - console.log('Checking method:', method) - const supported = await clientApi.hasCapability(method as keyof ClientApiMethods) - supportCache.set(method, supported) - console.log(`Method ${method} supported:`, supported) - } - catch (e) { - console.log('Error checking method:', method, e) - supportCache.set(method, false) - } - }), - ) - cacheInitialized = true - console.log('Cache initialized:', supportCache) - } - catch (e) { - console.error('Failed to initialize cache:', e) - } - } - - initializeCache() - - return new Proxy(clientApi, { - get(target, property, receiver) { - // Approach 1: use supports keyword to check if the method is supported - // support get and has method for supports - // get method for 'supports' operator e.g. server.supports['refresh'].then(setSupportRefresh) - // has for 'in' operator if('refresh' in server.supports) { ... } - if (property === 'supports') { - return new Proxy({}, { - get: async (_target, capability: string) => { - const cleanCapability = capability.replace('?', '') - await initializeCache() - return supportCache.has(cleanCapability) - ? supportCache.get(cleanCapability) - : false - }, - has: (_target, capability: string) => { - // FIXME: bug here, always return false when server just load, need to fix - const cleanCapability = capability.replace('?', '') - if (!cacheInitialized) - return false - return supportCache.has(cleanCapability) - ? supportCache.get(cleanCapability) ?? false - : false - }, - }) - } - // Approach 2: use ClientApiMethods getter to check if the method is supported - return Reflect.get(target, property, receiver) - }, }) } diff --git a/clients/tabby-chat-panel/src/react.ts b/clients/tabby-chat-panel/src/react.ts index e6a57edeb295..692db1e5874c 100644 --- a/clients/tabby-chat-panel/src/react.ts +++ b/clients/tabby-chat-panel/src/react.ts @@ -9,10 +9,13 @@ function useClient(iframeRef: RefObject, api: ClientApiMethod let isCreated = false useEffect(() => { - if (iframeRef.current && !isCreated) { - isCreated = true - setClient(createClient(iframeRef.current!, api)) + const init = async () => { + if (iframeRef.current && !isCreated) { + isCreated = true + setClient(await createClient(iframeRef.current!, api)) + } } + init() }, [iframeRef.current]) return client @@ -23,11 +26,29 @@ function useServer(api: ServerApi) { let isCreated = false useEffect(() => { - const isInIframe = window.self !== window.top - if (isInIframe && !isCreated) { - isCreated = true - setServer(createServer(api)) + const init = async () => { + const isInIframe = window.self !== window.top + // eslint-disable-next-line no-console + console.log('[useServer] isInIframe:', isInIframe) + if (isInIframe && !isCreated) { + isCreated = true + try { + // eslint-disable-next-line no-console + console.log('[useServer] Creating server...') + setServer(await createServer(api)) + // eslint-disable-next-line no-console + console.log('[useServer] Server created successfully') + } + catch (error) { + // eslint-disable-next-line no-console + console.error('[useServer] Failed to create server:', error) + isCreated = false + } + } } + // eslint-disable-next-line no-console + console.log('[useServer] Starting initialization...') + init() }, []) return server diff --git a/clients/tabby-threads/source/targets/iframe/iframe.ts b/clients/tabby-threads/source/targets/iframe/iframe.ts index b7c510aa4a34..1dae607b23a8 100644 --- a/clients/tabby-threads/source/targets/iframe/iframe.ts +++ b/clients/tabby-threads/source/targets/iframe/iframe.ts @@ -16,7 +16,7 @@ import { CHECK_MESSAGE, RESPONSE_MESSAGE } from "./shared"; * const thread = createThreadFromInsideIframe(iframe); * await thread.sendMessage('Hello world!'); */ -export function createThreadFromIframe< +export async function createThreadFromIframe< Self = Record, Target = Record, >( @@ -35,8 +35,13 @@ export function createThreadFromIframe< } = {} ) { let connected = false; + console.log( + "[createThreadFromIframe] Starting connection process with iframe:", + iframe + ); const sendMessage: ThreadTarget["send"] = function send(message, transfer) { + console.log("[createThreadFromIframe] Sending message:", message); iframe.contentWindow?.postMessage(message, targetOrigin, transfer); }; @@ -45,12 +50,22 @@ export function createThreadFromIframe< ? new NestedAbortController(options.signal) : new AbortController(); + console.log("[createThreadFromIframe] Setting up message listener"); window.addEventListener( "message", (event) => { - if (event.source !== iframe.contentWindow) return; + if (event.source !== iframe.contentWindow) { + console.log( + "[createThreadFromIframe] Ignoring message from unknown source" + ); + return; + } + console.log("[createThreadFromIframe] Received message:", event.data); if (event.data === RESPONSE_MESSAGE) { + console.log( + "[createThreadFromIframe] Received RESPONSE_MESSAGE, connection established" + ); connected = true; abort.abort(); resolve(); @@ -62,31 +77,66 @@ export function createThreadFromIframe< abort.signal.addEventListener( "abort", () => { + console.log("[createThreadFromIframe] Abort signal received"); resolve(); }, { once: true } ); + console.log("[createThreadFromIframe] Sending CHECK_MESSAGE"); sendMessage(CHECK_MESSAGE); }); - return createThread( + console.log("[createThreadFromIframe] Waiting for connection..."); + await connectedPromise; + console.log( + "[createThreadFromIframe] Connection established, creating thread" + ); + + const thread = await createThread( { send(message, transfer) { if (!connected) { + console.log( + "[createThreadFromIframe] Message queued until connection:", + message + ); return connectedPromise.then(() => { - if (connected) return sendMessage(message, transfer); + if (connected) { + console.log( + "[createThreadFromIframe] Sending queued message:", + message + ); + return sendMessage(message, transfer); + } + console.log( + "[createThreadFromIframe] Connection lost, message dropped:", + message + ); }); } return sendMessage(message, transfer); }, listen(listen, { signal }) { + console.log("[createThreadFromIframe] Setting up message listener"); self.addEventListener( "message", (event) => { - if (event.source !== iframe.contentWindow) return; - if (event.data === RESPONSE_MESSAGE) return; + if (event.source !== iframe.contentWindow) { + console.log( + "[createThreadFromIframe] Ignoring message from unknown source" + ); + return; + } + if (event.data === RESPONSE_MESSAGE) { + console.log("[createThreadFromIframe] Ignoring RESPONSE_MESSAGE"); + return; + } + console.log( + "[createThreadFromIframe] Received message:", + event.data + ); listen(event.data); }, { signal } @@ -95,4 +145,7 @@ export function createThreadFromIframe< }, options ); + + console.log("[createThreadFromIframe] Thread created successfully"); + return thread; } diff --git a/clients/tabby-threads/source/targets/iframe/nested.ts b/clients/tabby-threads/source/targets/iframe/nested.ts index 9890e543ca19..45cd39b79057 100644 --- a/clients/tabby-threads/source/targets/iframe/nested.ts +++ b/clients/tabby-threads/source/targets/iframe/nested.ts @@ -15,7 +15,7 @@ import { CHECK_MESSAGE, RESPONSE_MESSAGE } from "./shared"; * const thread = createThreadFromInsideIframe(); * await thread.sendMessage('Hello world!'); */ -export function createThreadFromInsideIframe< +export async function createThreadFromInsideIframe< Self = Record, Target = Record, >({ @@ -42,48 +42,86 @@ export function createThreadFromInsideIframe< ? new NestedAbortController(options.signal) : new AbortController(); - const ready = () => { - const respond = () => parent.postMessage(RESPONSE_MESSAGE, targetOrigin); + console.log("[createThreadFromInsideIframe] Starting connection process"); + + const connectionPromise = new Promise((resolve) => { + let isConnected = false; + + const respond = () => { + if (!isConnected) { + console.log("[createThreadFromInsideIframe] Sending RESPONSE_MESSAGE"); + isConnected = true; + parent.postMessage(RESPONSE_MESSAGE, targetOrigin); + resolve(); + } + }; - // Handles wrappers that want to connect after the page has already loaded self.addEventListener( "message", ({ data }) => { - if (data === CHECK_MESSAGE) respond(); + console.log( + "[createThreadFromInsideIframe] Received message:", + JSON.stringify(data) + ); + if (data === CHECK_MESSAGE) { + console.log("[createThreadFromInsideIframe] Received CHECK_MESSAGE"); + respond(); + } }, { signal: options.signal } ); - respond(); - }; + if (document.readyState === "complete") { + console.log( + "[createThreadFromInsideIframe] Document already complete, responding" + ); + respond(); + } else { + console.log( + "[createThreadFromInsideIframe] Waiting for document to complete" + ); + document.addEventListener( + "readystatechange", + () => { + if (document.readyState === "complete") { + console.log( + "[createThreadFromInsideIframe] Document completed, responding" + ); + respond(); + abort.abort(); + } + }, + { signal: abort.signal } + ); + } + }); - // Listening to `readyState` in iframe, though the child iframe could probably - // send a `postMessage` that it is ready to receive messages sooner than that. - if (document.readyState === "complete") { - ready(); - } else { - document.addEventListener( - "readystatechange", - () => { - if (document.readyState === "complete") { - ready(); - abort.abort(); - } - }, - { signal: abort.signal } - ); - } + await connectionPromise; + console.log( + "[createThreadFromInsideIframe] Connection established, creating thread" + ); - return createThread( + const thread = await createThread( { send(message, transfer) { + console.log( + "[createThreadFromInsideIframe] Sending message:", + JSON.stringify(message) + ); return parent.postMessage(message, targetOrigin, transfer); }, listen(listen, { signal }) { + console.log( + "[createThreadFromInsideIframe] Setting up message listener" + ); self.addEventListener( "message", (event) => { if (event.data === CHECK_MESSAGE) return; + console.log( + "[createThreadFromInsideIframe] Received message:", + JSON.stringify(event.data) + ); listen(event.data); }, { signal } @@ -92,4 +130,7 @@ export function createThreadFromInsideIframe< }, options ); + + console.log("[createThreadFromInsideIframe] Thread created successfully"); + return thread; } diff --git a/clients/tabby-threads/source/targets/target.ts b/clients/tabby-threads/source/targets/target.ts index b4b4504edad8..3ec12c44df21 100644 --- a/clients/tabby-threads/source/targets/target.ts +++ b/clients/tabby-threads/source/targets/target.ts @@ -69,6 +69,7 @@ const RELEASE = 3; const FUNCTION_APPLY = 5; const FUNCTION_RESULT = 6; const CHECK_CAPABILITY = 7; +const EXPOSE_LIST = 8; interface MessageMap { [CALL]: [string, string | number, any]; @@ -78,6 +79,8 @@ interface MessageMap { [FUNCTION_APPLY]: [string, string, any]; [FUNCTION_RESULT]: [string, Error?, any?]; [CHECK_CAPABILITY]: [string, string]; + [EXPOSE_LIST]: [string, string[]]; // Request to exchange methods: [callId, our_methods] + // The other side will respond with their methods via RESULT } type MessageData = { @@ -88,7 +91,7 @@ type MessageData = { * Creates a thread from any object that conforms to the `ThreadTarget` * interface. */ -export function createThread< +export async function createThread< Self = Record, Target = Record, >( @@ -100,7 +103,14 @@ export function createThread< uuid = defaultUuid, encoder = createBasicEncoder(), }: ThreadOptions = {} -): Thread { +): Promise> { + console.log("[createThread] Initializing with options:", { + hasExpose: !!expose, + hasCallable: !!callable, + hasSignal: !!signal, + exposeMethods: expose ? Object.keys(expose) : [], + }); + let terminated = false; const activeApi = new Map(); const functionsToId = new Map(); @@ -121,6 +131,32 @@ export function createThread< ) => void >(); + console.log("[createThread] Starting expose list exchange"); + + // Send our expose list to the other side + const ourMethods = Array.from(activeApi.keys()).map(String); + console.log("[createThread] Our expose list:", ourMethods); + + const id = uuid(); + console.log("[createThread] Setting up expose list resolver"); + + // This will be called when we receive the RESULT with other side's methods + callIdsToResolver.set(id, (_, __, value) => { + const theirMethods = encoder.decode(value, encoderApi) as string[]; + console.log( + "[createThread] Got RESULT with other side's methods:", + theirMethods + ); + // Store their methods for future use if needed + console.log("[createThread] Expose list exchange completed"); + }); + + // Send EXPOSE_LIST with our methods + console.log("[createThread] Sending EXPOSE_LIST with our methods"); + send(EXPOSE_LIST, [id, ourMethods]); + + // Create proxy without waiting for response + console.log("[createThread] Creating proxy without waiting for response"); const call = createCallable>(handlerForCall, callable); const encoderApi: ThreadEncoderApi = { @@ -220,28 +256,40 @@ export function createThread< target.listen(listener, { signal }); - return call; + return Promise.resolve(call); function send( type: Type, args: MessageMap[Type], transferables?: Transferable[] ) { - if (terminated) return; + if (terminated) { + console.log("[createThread] Not sending message - thread terminated"); + return; + } + console.log("[createThread] Sending message:", { + type, + args, + transferables, + }); target.send([type, args], transferables); } async function listener(rawData: unknown) { + console.log("[createThread] Received raw data:", rawData); + const isThreadMessageData = Array.isArray(rawData) && typeof rawData[0] === "number" && (rawData[1] == null || Array.isArray(rawData[1])); if (!isThreadMessageData) { + console.log("[createThread] Invalid message format, ignoring:,", rawData); return; } const data = rawData as MessageData; + console.log("[createThread] Processing message type:", data[0]); switch (data[0]) { case TERMINATE: { @@ -275,6 +323,25 @@ export function createThread< break; } case RESULT: { + const [id, error, value] = data[1]; + console.log("[createThread] Received RESULT message:", { + id, + error, + value, + }); + + // If this is a response to our EXPOSE_LIST + const resolver = callIdsToResolver.get(id); + if (resolver) { + console.log("[createThread] Found resolver for RESULT"); + if (error) { + console.log("[createThread] Error in RESULT:", error); + } else { + const methods = encoder.decode(value, encoderApi); + console.log("[createThread] Decoded methods from RESULT:", methods); + } + } + resolveCall(...data[1]); break; } @@ -333,10 +400,40 @@ export function createThread< send(RESULT, [id, undefined, encoder.encode(hasMethod, encoderApi)[0]]); break; } + case EXPOSE_LIST: { + const [id, theirMethods] = data[1]; + console.log( + "[createThread] Received EXPOSE_LIST with their methods:", + theirMethods + ); + + // Store their methods for future use + const theirMethodsList = theirMethods as string[]; + console.log("[createThread] Stored their methods:", theirMethodsList); + + // Send back our methods as RESULT + const ourMethods = Array.from(activeApi.keys()).map(String); + console.log( + "[createThread] Sending RESULT with our methods:", + ourMethods + ); + + send(RESULT, [ + id, + undefined, + encoder.encode(ourMethods, encoderApi)[0], + ]); + + console.log( + "[createThread] Expose list exchange completed for this side" + ); + break; + } } } function handlerForCall(property: string | number | symbol) { + if (property === "then") return undefined; return (...args: any[]) => { try { if (terminated) { @@ -345,7 +442,7 @@ export function createThread< if (typeof property !== "string" && typeof property !== "number") { throw new Error( - `Can’t call a symbol method on a thread: ${property.toString()}` + `Can't call a symbol method on a thread: ${property.toString()}` ); } @@ -432,6 +529,7 @@ function createCallable( ) => AnyFunction | undefined, callable?: (keyof T)[] ): T { + console.log("[createCallable] Creating callable with methods:", callable); let call: any; if (callable == null) { @@ -447,10 +545,14 @@ function createCallable( {}, { get(_target, property) { + if (property === "then") return undefined; + console.log("[createCallable] Accessing property:", property); if (cache.has(property)) { + console.log("[createCallable] Using cached handler for:", property); return cache.get(property); } + console.log("[createCallable] Creating new handler for:", property); const handler = handlerForCall(property); cache.set(property, handler); return handler; diff --git a/clients/vscode/src/chat/createClient.ts b/clients/vscode/src/chat/createClient.ts index a962a5637f45..a2553aad3034 100644 --- a/clients/vscode/src/chat/createClient.ts +++ b/clients/vscode/src/chat/createClient.ts @@ -1,29 +1,41 @@ import type { Webview } from "vscode"; import type { ServerApi, ClientApiMethods } from "tabby-chat-panel"; import { createThread, type ThreadOptions } from "tabby-threads"; +import { getLogger } from "../logger"; -function createThreadFromWebview, Target = Record>( +async function createThreadFromWebview, Target = Record>( webview: Webview, options?: ThreadOptions, ) { - return createThread( + getLogger().info("Creating thread from webview"); + const thread = await createThread( { send(message) { + getLogger().debug("Sending message to chat panel", message); webview.postMessage({ action: "postMessageToChatPanel", message }); }, listen(listener, { signal }) { - const { dispose } = webview.onDidReceiveMessage(listener); + getLogger().debug("Setting up message listener"); + const { dispose } = webview.onDidReceiveMessage((msg) => { + getLogger().debug("Received message from chat panel", msg); + listener(msg); + }); signal?.addEventListener("abort", () => { + getLogger().debug("Disposing message listener"); dispose(); }); }, }, options, ); + getLogger().info("Thread created"); + return thread; } -export function createClient(webview: Webview, api: ClientApiMethods): ServerApi { - return createThreadFromWebview(webview, { +export async function createClient(webview: Webview, api: ClientApiMethods): Promise { + const logger = getLogger(); + logger.info("Creating client with exposed methods:", Object.keys(api)); + const thread = await createThreadFromWebview(webview, { expose: { refresh: api.refresh, onApplyInEditor: api.onApplyInEditor, @@ -42,4 +54,6 @@ export function createClient(webview: Webview, api: ClientApiMethods): ServerApi readFileContent: api.readFileContent, }, }); + getLogger().info("Client created"); + return thread as unknown as ServerApi; } diff --git a/clients/vscode/src/chat/webview.ts b/clients/vscode/src/chat/webview.ts index 1e453b139ca1..dd75b878d7d8 100644 --- a/clients/vscode/src/chat/webview.ts +++ b/clients/vscode/src/chat/webview.ts @@ -96,8 +96,11 @@ export class ChatWebview { }; this.webview = webview; - this.client = this.createChatPanelApiClient(); - + this.logger.info("Initializing chat panel webview."); + this.createChatPanelApiClient().then((client) => { + this.client = client; + }); + this.logger.info("Chat panel webview initialized."); const statusListener = () => { this.checkStatusAndLoadContent(); }; @@ -206,12 +209,12 @@ export class ChatWebview { } } - private createChatPanelApiClient(): ServerApi | undefined { + private async createChatPanelApiClient(): Promise { const webview = this.webview; if (!webview) { return undefined; } - return createClient(webview, { + return await createClient(webview, { refresh: async () => { commands.executeCommand("tabby.reconnectToServer"); return; diff --git a/ee/tabby-ui/app/chat/page.tsx b/ee/tabby-ui/app/chat/page.tsx index 4f9e010270e8..e75c9eb2d881 100644 --- a/ee/tabby-ui/app/chat/page.tsx +++ b/ee/tabby-ui/app/chat/page.tsx @@ -243,7 +243,6 @@ export default function ChatPage() { server?.onLoaded({ apiVersion: TABBY_CHAT_PANEL_API_VERSION }) - const checkCapabilities = async () => { server ?.hasCapability('onApplyInEditorV2') From 9d8fbbe81bf8ec177a42fd441777bc6e13239363 Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Tue, 21 Jan 2025 02:28:18 -0600 Subject: [PATCH 04/19] refactor(iframe): enhance method exchange and logging in thread creation --- .../source/targets/iframe/iframe.ts | 8 +- .../source/targets/iframe/nested.ts | 11 ++ .../tabby-threads/source/targets/target.ts | 104 ++++++++++++++---- clients/tabby-threads/source/types.ts | 26 ++++- clients/vscode/src/chat/createClient.ts | 1 + 5 files changed, 120 insertions(+), 30 deletions(-) diff --git a/clients/tabby-threads/source/targets/iframe/iframe.ts b/clients/tabby-threads/source/targets/iframe/iframe.ts index 1dae607b23a8..6e0181e1396b 100644 --- a/clients/tabby-threads/source/targets/iframe/iframe.ts +++ b/clients/tabby-threads/source/targets/iframe/iframe.ts @@ -98,7 +98,7 @@ export async function createThreadFromIframe< send(message, transfer) { if (!connected) { console.log( - "[createThreadFromIframe] Message queued until connection:", + "[createThreadFromIframe] Queuing message until connected:", message ); return connectedPromise.then(() => { @@ -115,7 +115,6 @@ export async function createThreadFromIframe< ); }); } - return sendMessage(message, transfer); }, listen(listen, { signal }) { @@ -147,5 +146,10 @@ export async function createThreadFromIframe< ); console.log("[createThreadFromIframe] Thread created successfully"); + + // After connection is established and thread is created, exchange methods + console.log("[createThreadFromIframe] Connection ready, exchanging methods"); + thread.exchangeMethods(); + return thread; } diff --git a/clients/tabby-threads/source/targets/iframe/nested.ts b/clients/tabby-threads/source/targets/iframe/nested.ts index 45cd39b79057..881931a5c0ac 100644 --- a/clients/tabby-threads/source/targets/iframe/nested.ts +++ b/clients/tabby-threads/source/targets/iframe/nested.ts @@ -132,5 +132,16 @@ export async function createThreadFromInsideIframe< ); console.log("[createThreadFromInsideIframe] Thread created successfully"); + + // After connection is established and thread is created, request methods from outside + console.log( + "[createThreadFromInsideIframe] Connection ready, requesting methods from outside" + ); + const methods = await thread.requestMethods(); + console.log( + "[createThreadFromInsideIframe] Received methods from outside:", + methods + ); + return thread; } diff --git a/clients/tabby-threads/source/targets/target.ts b/clients/tabby-threads/source/targets/target.ts index 3ec12c44df21..fc5ece87149e 100644 --- a/clients/tabby-threads/source/targets/target.ts +++ b/clients/tabby-threads/source/targets/target.ts @@ -117,6 +117,9 @@ export async function createThread< const idsToFunction = new Map(); const idsToProxy = new Map(); + // Cache for the other side's methods + let theirMethodsCache: string[] | null = null; + if (expose) { for (const key of Object.keys(expose)) { const value = expose[key as keyof typeof expose]; @@ -131,33 +134,73 @@ export async function createThread< ) => void >(); - console.log("[createThread] Starting expose list exchange"); + // Create functions for method exchange + const exchangeMethods = () => { + console.log("[createThread] Starting expose list exchange"); + const ourMethods = Array.from(activeApi.keys()).map(String); + console.log("[createThread] Our expose list:", ourMethods); + + const id = uuid(); + console.log("[createThread] Setting up expose list resolver"); + + // This will be called when we receive the RESULT with other side's methods + callIdsToResolver.set(id, (_, __, value) => { + const theirMethods = encoder.decode(value, encoderApi) as string[]; + console.log( + "[createThread] Got RESULT with other side's methods:", + theirMethods + ); + // Cache their methods + theirMethodsCache = theirMethods; + console.log("[createThread] Expose list exchange completed"); + }); - // Send our expose list to the other side - const ourMethods = Array.from(activeApi.keys()).map(String); - console.log("[createThread] Our expose list:", ourMethods); + // Send EXPOSE_LIST with our methods + console.log("[createThread] Sending EXPOSE_LIST with our methods"); + send(EXPOSE_LIST, [id, ourMethods]); + }; - const id = uuid(); - console.log("[createThread] Setting up expose list resolver"); + // Create a function to request methods from the other side + const requestMethods = async () => { + // If we have cached methods and connection is still active, return them + if (theirMethodsCache !== null && !terminated) { + console.log( + "[createThread] Returning cached methods:", + theirMethodsCache + ); + return theirMethodsCache; + } - // This will be called when we receive the RESULT with other side's methods - callIdsToResolver.set(id, (_, __, value) => { - const theirMethods = encoder.decode(value, encoderApi) as string[]; - console.log( - "[createThread] Got RESULT with other side's methods:", - theirMethods - ); - // Store their methods for future use if needed - console.log("[createThread] Expose list exchange completed"); - }); + console.log("[createThread] Requesting methods from other side"); + const id = uuid(); - // Send EXPOSE_LIST with our methods - console.log("[createThread] Sending EXPOSE_LIST with our methods"); - send(EXPOSE_LIST, [id, ourMethods]); + // Create a promise that will resolve with the other side's methods + const methodsPromise = new Promise((resolve) => { + callIdsToResolver.set(id, (_, __, value) => { + const theirMethods = encoder.decode(value, encoderApi) as string[]; + console.log( + "[createThread] Got RESULT with other side's methods:", + theirMethods + ); + // Cache the methods for future use + theirMethodsCache = theirMethods; + resolve(theirMethods); + }); + }); + + // Send EXPOSE_LIST with empty methods array to request other side's methods + console.log("[createThread] Sending method request"); + send(EXPOSE_LIST, [id, []]); + + return methodsPromise; + }; - // Create proxy without waiting for response + // Create proxy for method calls console.log("[createThread] Creating proxy without waiting for response"); - const call = createCallable>(handlerForCall, callable); + const call = createCallable>(handlerForCall, callable, { + exchangeMethods, + requestMethods, + }); const encoderApi: ThreadEncoderApi = { functions: { @@ -243,6 +286,7 @@ export async function createThread< functionsToId.clear(); idsToFunction.clear(); idsToProxy.clear(); + theirMethodsCache = null; // Clear the cache when connection is terminated }; signal?.addEventListener( @@ -256,7 +300,7 @@ export async function createThread< target.listen(listener, { signal }); - return Promise.resolve(call); + return call; function send( type: Type, @@ -278,6 +322,7 @@ export async function createThread< async function listener(rawData: unknown) { console.log("[createThread] Received raw data:", rawData); + // FIXME: don't ignore anything, just for testing now const isThreadMessageData = Array.isArray(rawData) && typeof rawData[0] === "number" && @@ -433,7 +478,6 @@ export async function createThread< } function handlerForCall(property: string | number | symbol) { - if (property === "then") return undefined; return (...args: any[]) => { try { if (terminated) { @@ -527,7 +571,11 @@ function createCallable( handlerForCall: ( property: string | number | symbol ) => AnyFunction | undefined, - callable?: (keyof T)[] + callable?: (keyof T)[], + methods?: { + exchangeMethods: () => void; + requestMethods: () => Promise; + } ): T { console.log("[createCallable] Creating callable with methods:", callable); let call: any; @@ -546,6 +594,14 @@ function createCallable( { get(_target, property) { if (property === "then") return undefined; + if (property === "exchangeMethods") { + console.log("[createCallable] Accessing exchangeMethods"); + return methods?.exchangeMethods; + } + if (property === "requestMethods") { + console.log("[createCallable] Accessing requestMethods"); + return methods?.requestMethods; + } console.log("[createCallable] Accessing property:", property); if (cache.has(property)) { console.log("[createCallable] Using cached handler for:", property); diff --git a/clients/tabby-threads/source/types.ts b/clients/tabby-threads/source/types.ts index 61e4b1803aa8..e9310c6d7d8f 100644 --- a/clients/tabby-threads/source/types.ts +++ b/clients/tabby-threads/source/types.ts @@ -3,7 +3,7 @@ import type { RETAIN_METHOD, ENCODE_METHOD, RETAINED_BY, -} from './constants.ts'; +} from "./constants.ts"; /** * A thread represents a target JavaScript environment that exposes a set @@ -18,6 +18,21 @@ export type Thread = { ? Target[K] : never : never; +} & { + /** + * Exchange method lists between threads after connection is established. + * This should be called only after the connection is ready to ensure + * proper method exchange. + */ + exchangeMethods(): void; + + /** + * Request methods from the other side and wait for response. + * Returns a promise that resolves with the list of available methods. + * This is useful when you want to get the methods list from the other side + * without sending your own methods. + */ + requestMethods(): Promise; }; /** @@ -40,7 +55,10 @@ export interface ThreadTarget { * and handle its content. This method may be passed an `AbortSignal` to abort the * listening process. */ - listen(listener: (value: any) => void, options: {signal?: AbortSignal}): void; + listen( + listener: (value: any) => void, + options: { signal?: AbortSignal } + ): void; } /** @@ -82,7 +100,7 @@ export interface ThreadEncoder { decode( value: unknown, api: ThreadEncoderApi, - retainedBy?: Iterable, + retainedBy?: Iterable ): unknown; } @@ -112,7 +130,7 @@ export interface ThreadEncoderApi { * An object that provides a custom process to encode its value. */ export interface ThreadEncodable { - [ENCODE_METHOD](api: {encode(value: any): unknown}): any; + [ENCODE_METHOD](api: { encode(value: any): unknown }): any; } export type AnyFunction = Function; diff --git a/clients/vscode/src/chat/createClient.ts b/clients/vscode/src/chat/createClient.ts index a2553aad3034..0596155433bd 100644 --- a/clients/vscode/src/chat/createClient.ts +++ b/clients/vscode/src/chat/createClient.ts @@ -28,6 +28,7 @@ async function createThreadFromWebview, Target = Re }, options, ); + setTimeout(() => thread.exchangeMethods(), 3000); getLogger().info("Thread created"); return thread; } From 02e80a627864084a5c1d723a273a35e5845d0b18 Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Tue, 21 Jan 2025 03:00:07 -0600 Subject: [PATCH 05/19] refactor(api): update SupportProxy type to use boolean and enhance createCallable with state management --- clients/tabby-chat-panel/src/index.ts | 2 +- .../tabby-threads/source/targets/target.ts | 76 +++++++++++++++---- ee/tabby-ui/app/chat/page.tsx | 9 ++- 3 files changed, 70 insertions(+), 17 deletions(-) diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index ebfe0f827771..4be8dc602fe1 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -355,7 +355,7 @@ export interface ClientApiMethods { type ClientApiMethod = keyof ClientApiMethods type SupportProxy = { - [K in ClientApiMethod]: Promise + [K in ClientApiMethod]: boolean } export interface ClientApi extends ClientApiMethods { /** diff --git a/clients/tabby-threads/source/targets/target.ts b/clients/tabby-threads/source/targets/target.ts index fc5ece87149e..9de8775d0e61 100644 --- a/clients/tabby-threads/source/targets/target.ts +++ b/clients/tabby-threads/source/targets/target.ts @@ -4,15 +4,11 @@ import type { ThreadEncoder, ThreadEncoderApi, AnyFunction, -} from "../types.ts"; - -import { - RELEASE_METHOD, - RETAINED_BY, - RETAIN_METHOD, - StackFrame, - isMemoryManageable, -} from "../memory"; +} from "../types"; + +import { RELEASE_METHOD, RETAINED_BY, RETAIN_METHOD } from "../constants"; + +import { StackFrame, isMemoryManageable } from "../memory"; import { createBasicEncoder } from "../encoding/basic"; export type { ThreadTarget }; @@ -79,8 +75,7 @@ interface MessageMap { [FUNCTION_APPLY]: [string, string, any]; [FUNCTION_RESULT]: [string, Error?, any?]; [CHECK_CAPABILITY]: [string, string]; - [EXPOSE_LIST]: [string, string[]]; // Request to exchange methods: [callId, our_methods] - // The other side will respond with their methods via RESULT + [EXPOSE_LIST]: [string, string[]]; } type MessageData = { @@ -197,10 +192,18 @@ export async function createThread< // Create proxy for method calls console.log("[createThread] Creating proxy without waiting for response"); - const call = createCallable>(handlerForCall, callable, { - exchangeMethods, - requestMethods, - }); + const call = createCallable>( + handlerForCall, + callable, + { + exchangeMethods, + requestMethods, + }, + { + getMethodsCache: () => theirMethodsCache, + isTerminated: () => terminated, + } + ); const encoderApi: ThreadEncoderApi = { functions: { @@ -575,6 +578,10 @@ function createCallable( methods?: { exchangeMethods: () => void; requestMethods: () => Promise; + }, + state?: { + getMethodsCache: () => string[] | null; + isTerminated: () => boolean; } ): T { console.log("[createCallable] Creating callable with methods:", callable); @@ -602,6 +609,21 @@ function createCallable( console.log("[createCallable] Accessing requestMethods"); return methods?.requestMethods; } + if (property === "supports") { + return new Proxy( + {}, + { + get(_target, method: string) { + if (!state) return false; + const cache = state.getMethodsCache(); + if (cache !== null && !state.isTerminated()) { + return cache.includes(method); + } + return false; + }, + } + ); + } console.log("[createCallable] Accessing property:", property); if (cache.has(property)) { console.log("[createCallable] Using cached handler for:", property); @@ -613,6 +635,30 @@ function createCallable( cache.set(property, handler); return handler; }, + has(_target, property) { + console.log("[createCallable] Checking has property:", property); + if ( + property === "then" || + property === "exchangeMethods" || + property === "requestMethods" + ) { + return true; + } + if (!state) { + console.log("[createCallable] No state available, returning false"); + return false; + } + const cache = state.getMethodsCache(); + if (cache !== null && !state.isTerminated()) { + console.log( + "[createCallable] Checking cache for method:", + property + ); + return cache.includes(String(property)); + } + console.log("[createCallable] No cache available, returning false"); + return false; + }, } ); } else { diff --git a/ee/tabby-ui/app/chat/page.tsx b/ee/tabby-ui/app/chat/page.tsx index e75c9eb2d881..70e59d7f76b8 100644 --- a/ee/tabby-ui/app/chat/page.tsx +++ b/ee/tabby-ui/app/chat/page.tsx @@ -243,6 +243,14 @@ export default function ChatPage() { server?.onLoaded({ apiVersion: TABBY_CHAT_PANEL_API_VERSION }) + + if ('refresh' in server) { + // eslint-disable-next-line no-console + console.log('refresh in server') + } + // eslint-disable-next-line no-console + console.log('support v2?:', server?.supports['onApplyInEditorV2']) + const checkCapabilities = async () => { server ?.hasCapability('onApplyInEditorV2') @@ -267,7 +275,6 @@ export default function ChatPage() { ) }) } - checkCapabilities().then(() => { setIsServerLoaded(true) }) From 9e2fc101c2e4c261da6589cff63d43f16a8dd85e Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Wed, 22 Jan 2025 14:20:43 -0600 Subject: [PATCH 06/19] refactor(api): update exchangeMethods to return a Promise and enhance ClientApi with supports method --- clients/tabby-chat-panel/src/index.ts | 9 +- clients/tabby-chat-panel/src/react.ts | 35 ++---- .../source/targets/iframe/iframe.ts | 62 +---------- .../source/targets/iframe/nested.ts | 43 +------ .../tabby-threads/source/targets/target.ts | 105 +++--------------- clients/tabby-threads/source/types.ts | 2 +- clients/vscode/src/chat/createClient.ts | 11 -- 7 files changed, 38 insertions(+), 229 deletions(-) diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index 4be8dc602fe1..1e9b76bac7df 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -352,8 +352,12 @@ export interface ClientApiMethods { */ readFileContent?: (info: FileRange) => Promise } + type ClientApiMethod = keyof ClientApiMethods +/** + * Provide a convenient way to check if the client supports a specific method. + */ type SupportProxy = { [K in ClientApiMethod]: boolean } @@ -365,6 +369,9 @@ export interface ClientApi extends ClientApiMethods { */ hasCapability: (method: keyof ClientApiMethods) => Promise + /** + * The convenient accessor to check if the client supports a specific method from {@link ClientApiMethods}. + */ supports: SupportProxy } @@ -389,7 +396,7 @@ export async function createClient(target: HTMLIFrameElement, api: ClientApiMeth readFileContent: api.readFileContent, }, }) - return thread as unknown as ServerApi + return thread as unknown as Promise } export async function createServer(api: ServerApi): Promise { diff --git a/clients/tabby-chat-panel/src/react.ts b/clients/tabby-chat-panel/src/react.ts index 692db1e5874c..9b938e1116e7 100644 --- a/clients/tabby-chat-panel/src/react.ts +++ b/clients/tabby-chat-panel/src/react.ts @@ -9,13 +9,10 @@ function useClient(iframeRef: RefObject, api: ClientApiMethod let isCreated = false useEffect(() => { - const init = async () => { - if (iframeRef.current && !isCreated) { - isCreated = true - setClient(await createClient(iframeRef.current!, api)) - } + if (iframeRef.current && !isCreated) { + isCreated = true + createClient(iframeRef.current!, api).then(setClient) } - init() }, [iframeRef.current]) return client @@ -26,29 +23,11 @@ function useServer(api: ServerApi) { let isCreated = false useEffect(() => { - const init = async () => { - const isInIframe = window.self !== window.top - // eslint-disable-next-line no-console - console.log('[useServer] isInIframe:', isInIframe) - if (isInIframe && !isCreated) { - isCreated = true - try { - // eslint-disable-next-line no-console - console.log('[useServer] Creating server...') - setServer(await createServer(api)) - // eslint-disable-next-line no-console - console.log('[useServer] Server created successfully') - } - catch (error) { - // eslint-disable-next-line no-console - console.error('[useServer] Failed to create server:', error) - isCreated = false - } - } + const isInIframe = window.self !== window.top + if (isInIframe && !isCreated) { + isCreated = true + createServer(api).then(setServer) } - // eslint-disable-next-line no-console - console.log('[useServer] Starting initialization...') - init() }, []) return server diff --git a/clients/tabby-threads/source/targets/iframe/iframe.ts b/clients/tabby-threads/source/targets/iframe/iframe.ts index 6e0181e1396b..335e9d8253de 100644 --- a/clients/tabby-threads/source/targets/iframe/iframe.ts +++ b/clients/tabby-threads/source/targets/iframe/iframe.ts @@ -35,13 +35,8 @@ export async function createThreadFromIframe< } = {} ) { let connected = false; - console.log( - "[createThreadFromIframe] Starting connection process with iframe:", - iframe - ); const sendMessage: ThreadTarget["send"] = function send(message, transfer) { - console.log("[createThreadFromIframe] Sending message:", message); iframe.contentWindow?.postMessage(message, targetOrigin, transfer); }; @@ -50,22 +45,12 @@ export async function createThreadFromIframe< ? new NestedAbortController(options.signal) : new AbortController(); - console.log("[createThreadFromIframe] Setting up message listener"); window.addEventListener( "message", (event) => { - if (event.source !== iframe.contentWindow) { - console.log( - "[createThreadFromIframe] Ignoring message from unknown source" - ); - return; - } + if (event.source !== iframe.contentWindow) return; - console.log("[createThreadFromIframe] Received message:", event.data); if (event.data === RESPONSE_MESSAGE) { - console.log( - "[createThreadFromIframe] Received RESPONSE_MESSAGE, connection established" - ); connected = true; abort.abort(); resolve(); @@ -77,65 +62,32 @@ export async function createThreadFromIframe< abort.signal.addEventListener( "abort", () => { - console.log("[createThreadFromIframe] Abort signal received"); resolve(); }, { once: true } ); - console.log("[createThreadFromIframe] Sending CHECK_MESSAGE"); sendMessage(CHECK_MESSAGE); }); - console.log("[createThreadFromIframe] Waiting for connection..."); await connectedPromise; - console.log( - "[createThreadFromIframe] Connection established, creating thread" - ); const thread = await createThread( { send(message, transfer) { if (!connected) { - console.log( - "[createThreadFromIframe] Queuing message until connected:", - message - ); return connectedPromise.then(() => { - if (connected) { - console.log( - "[createThreadFromIframe] Sending queued message:", - message - ); - return sendMessage(message, transfer); - } - console.log( - "[createThreadFromIframe] Connection lost, message dropped:", - message - ); + if (connected) return sendMessage(message, transfer); }); } return sendMessage(message, transfer); }, listen(listen, { signal }) { - console.log("[createThreadFromIframe] Setting up message listener"); self.addEventListener( "message", (event) => { - if (event.source !== iframe.contentWindow) { - console.log( - "[createThreadFromIframe] Ignoring message from unknown source" - ); - return; - } - if (event.data === RESPONSE_MESSAGE) { - console.log("[createThreadFromIframe] Ignoring RESPONSE_MESSAGE"); - return; - } - console.log( - "[createThreadFromIframe] Received message:", - event.data - ); + if (event.source !== iframe.contentWindow) return; + if (event.data === RESPONSE_MESSAGE) return; listen(event.data); }, { signal } @@ -145,11 +97,9 @@ export async function createThreadFromIframe< options ); - console.log("[createThreadFromIframe] Thread created successfully"); - + // FIXME(Sma1lboy): We don't have this need for now. // After connection is established and thread is created, exchange methods - console.log("[createThreadFromIframe] Connection ready, exchanging methods"); - thread.exchangeMethods(); + await thread.requestMethods(); return thread; } diff --git a/clients/tabby-threads/source/targets/iframe/nested.ts b/clients/tabby-threads/source/targets/iframe/nested.ts index 881931a5c0ac..6ffda55e230c 100644 --- a/clients/tabby-threads/source/targets/iframe/nested.ts +++ b/clients/tabby-threads/source/targets/iframe/nested.ts @@ -42,14 +42,11 @@ export async function createThreadFromInsideIframe< ? new NestedAbortController(options.signal) : new AbortController(); - console.log("[createThreadFromInsideIframe] Starting connection process"); - const connectionPromise = new Promise((resolve) => { let isConnected = false; const respond = () => { if (!isConnected) { - console.log("[createThreadFromInsideIframe] Sending RESPONSE_MESSAGE"); isConnected = true; parent.postMessage(RESPONSE_MESSAGE, targetOrigin); resolve(); @@ -59,12 +56,7 @@ export async function createThreadFromInsideIframe< self.addEventListener( "message", ({ data }) => { - console.log( - "[createThreadFromInsideIframe] Received message:", - JSON.stringify(data) - ); if (data === CHECK_MESSAGE) { - console.log("[createThreadFromInsideIframe] Received CHECK_MESSAGE"); respond(); } }, @@ -72,21 +64,12 @@ export async function createThreadFromInsideIframe< ); if (document.readyState === "complete") { - console.log( - "[createThreadFromInsideIframe] Document already complete, responding" - ); respond(); } else { - console.log( - "[createThreadFromInsideIframe] Waiting for document to complete" - ); document.addEventListener( "readystatechange", () => { if (document.readyState === "complete") { - console.log( - "[createThreadFromInsideIframe] Document completed, responding" - ); respond(); abort.abort(); } @@ -97,31 +80,17 @@ export async function createThreadFromInsideIframe< }); await connectionPromise; - console.log( - "[createThreadFromInsideIframe] Connection established, creating thread" - ); const thread = await createThread( { send(message, transfer) { - console.log( - "[createThreadFromInsideIframe] Sending message:", - JSON.stringify(message) - ); return parent.postMessage(message, targetOrigin, transfer); }, listen(listen, { signal }) { - console.log( - "[createThreadFromInsideIframe] Setting up message listener" - ); self.addEventListener( "message", (event) => { if (event.data === CHECK_MESSAGE) return; - console.log( - "[createThreadFromInsideIframe] Received message:", - JSON.stringify(event.data) - ); listen(event.data); }, { signal } @@ -131,17 +100,7 @@ export async function createThreadFromInsideIframe< options ); - console.log("[createThreadFromInsideIframe] Thread created successfully"); - - // After connection is established and thread is created, request methods from outside - console.log( - "[createThreadFromInsideIframe] Connection ready, requesting methods from outside" - ); - const methods = await thread.requestMethods(); - console.log( - "[createThreadFromInsideIframe] Received methods from outside:", - methods - ); + await thread.requestMethods(); return thread; } diff --git a/clients/tabby-threads/source/targets/target.ts b/clients/tabby-threads/source/targets/target.ts index 9de8775d0e61..bda6547462f4 100644 --- a/clients/tabby-threads/source/targets/target.ts +++ b/clients/tabby-threads/source/targets/target.ts @@ -99,13 +99,6 @@ export async function createThread< encoder = createBasicEncoder(), }: ThreadOptions = {} ): Promise> { - console.log("[createThread] Initializing with options:", { - hasExpose: !!expose, - hasCallable: !!callable, - hasSignal: !!signal, - exposeMethods: expose ? Object.keys(expose) : [], - }); - let terminated = false; const activeApi = new Map(); const functionsToId = new Map(); @@ -130,53 +123,41 @@ export async function createThread< >(); // Create functions for method exchange - const exchangeMethods = () => { - console.log("[createThread] Starting expose list exchange"); + const exchangeMethods = async () => { const ourMethods = Array.from(activeApi.keys()).map(String); - console.log("[createThread] Our expose list:", ourMethods); const id = uuid(); - console.log("[createThread] Setting up expose list resolver"); - - // This will be called when we receive the RESULT with other side's methods - callIdsToResolver.set(id, (_, __, value) => { - const theirMethods = encoder.decode(value, encoderApi) as string[]; - console.log( - "[createThread] Got RESULT with other side's methods:", - theirMethods - ); - // Cache their methods - theirMethodsCache = theirMethods; - console.log("[createThread] Expose list exchange completed"); + + // Create a promise that will resolve when we receive their methods + const methodsPromise = new Promise((resolve) => { + callIdsToResolver.set(id, (_, __, value) => { + const theirMethods = encoder.decode(value, encoderApi) as string[]; + // Cache their methods + theirMethodsCache = theirMethods; + resolve(); + }); }); // Send EXPOSE_LIST with our methods - console.log("[createThread] Sending EXPOSE_LIST with our methods"); send(EXPOSE_LIST, [id, ourMethods]); + + // Wait for their methods to be received + return methodsPromise; }; // Create a function to request methods from the other side const requestMethods = async () => { // If we have cached methods and connection is still active, return them if (theirMethodsCache !== null && !terminated) { - console.log( - "[createThread] Returning cached methods:", - theirMethodsCache - ); return theirMethodsCache; } - console.log("[createThread] Requesting methods from other side"); const id = uuid(); // Create a promise that will resolve with the other side's methods const methodsPromise = new Promise((resolve) => { callIdsToResolver.set(id, (_, __, value) => { const theirMethods = encoder.decode(value, encoderApi) as string[]; - console.log( - "[createThread] Got RESULT with other side's methods:", - theirMethods - ); // Cache the methods for future use theirMethodsCache = theirMethods; resolve(theirMethods); @@ -184,14 +165,12 @@ export async function createThread< }); // Send EXPOSE_LIST with empty methods array to request other side's methods - console.log("[createThread] Sending method request"); send(EXPOSE_LIST, [id, []]); return methodsPromise; }; // Create proxy for method calls - console.log("[createThread] Creating proxy without waiting for response"); const call = createCallable>( handlerForCall, callable, @@ -311,20 +290,12 @@ export async function createThread< transferables?: Transferable[] ) { if (terminated) { - console.log("[createThread] Not sending message - thread terminated"); return; } - console.log("[createThread] Sending message:", { - type, - args, - transferables, - }); target.send([type, args], transferables); } async function listener(rawData: unknown) { - console.log("[createThread] Received raw data:", rawData); - // FIXME: don't ignore anything, just for testing now const isThreadMessageData = Array.isArray(rawData) && @@ -332,12 +303,10 @@ export async function createThread< (rawData[1] == null || Array.isArray(rawData[1])); if (!isThreadMessageData) { - console.log("[createThread] Invalid message format, ignoring:,", rawData); return; } const data = rawData as MessageData; - console.log("[createThread] Processing message type:", data[0]); switch (data[0]) { case TERMINATE: { @@ -371,25 +340,6 @@ export async function createThread< break; } case RESULT: { - const [id, error, value] = data[1]; - console.log("[createThread] Received RESULT message:", { - id, - error, - value, - }); - - // If this is a response to our EXPOSE_LIST - const resolver = callIdsToResolver.get(id); - if (resolver) { - console.log("[createThread] Found resolver for RESULT"); - if (error) { - console.log("[createThread] Error in RESULT:", error); - } else { - const methods = encoder.decode(value, encoderApi); - console.log("[createThread] Decoded methods from RESULT:", methods); - } - } - resolveCall(...data[1]); break; } @@ -450,31 +400,19 @@ export async function createThread< } case EXPOSE_LIST: { const [id, theirMethods] = data[1]; - console.log( - "[createThread] Received EXPOSE_LIST with their methods:", - theirMethods - ); // Store their methods for future use const theirMethodsList = theirMethods as string[]; - console.log("[createThread] Stored their methods:", theirMethodsList); - + // Save their methods in cache + theirMethodsCache = theirMethodsList; // Send back our methods as RESULT const ourMethods = Array.from(activeApi.keys()).map(String); - console.log( - "[createThread] Sending RESULT with our methods:", - ourMethods - ); send(RESULT, [ id, undefined, encoder.encode(ourMethods, encoderApi)[0], ]); - - console.log( - "[createThread] Expose list exchange completed for this side" - ); break; } } @@ -584,7 +522,6 @@ function createCallable( isTerminated: () => boolean; } ): T { - console.log("[createCallable] Creating callable with methods:", callable); let call: any; if (callable == null) { @@ -602,11 +539,9 @@ function createCallable( get(_target, property) { if (property === "then") return undefined; if (property === "exchangeMethods") { - console.log("[createCallable] Accessing exchangeMethods"); return methods?.exchangeMethods; } if (property === "requestMethods") { - console.log("[createCallable] Accessing requestMethods"); return methods?.requestMethods; } if (property === "supports") { @@ -624,19 +559,15 @@ function createCallable( } ); } - console.log("[createCallable] Accessing property:", property); if (cache.has(property)) { - console.log("[createCallable] Using cached handler for:", property); return cache.get(property); } - console.log("[createCallable] Creating new handler for:", property); const handler = handlerForCall(property); cache.set(property, handler); return handler; }, has(_target, property) { - console.log("[createCallable] Checking has property:", property); if ( property === "then" || property === "exchangeMethods" || @@ -645,18 +576,12 @@ function createCallable( return true; } if (!state) { - console.log("[createCallable] No state available, returning false"); return false; } const cache = state.getMethodsCache(); if (cache !== null && !state.isTerminated()) { - console.log( - "[createCallable] Checking cache for method:", - property - ); return cache.includes(String(property)); } - console.log("[createCallable] No cache available, returning false"); return false; }, } diff --git a/clients/tabby-threads/source/types.ts b/clients/tabby-threads/source/types.ts index e9310c6d7d8f..4690c56150b0 100644 --- a/clients/tabby-threads/source/types.ts +++ b/clients/tabby-threads/source/types.ts @@ -24,7 +24,7 @@ export type Thread = { * This should be called only after the connection is ready to ensure * proper method exchange. */ - exchangeMethods(): void; + exchangeMethods(): Promise; /** * Request methods from the other side and wait for response. diff --git a/clients/vscode/src/chat/createClient.ts b/clients/vscode/src/chat/createClient.ts index 0596155433bd..27ed309063f6 100644 --- a/clients/vscode/src/chat/createClient.ts +++ b/clients/vscode/src/chat/createClient.ts @@ -1,41 +1,31 @@ import type { Webview } from "vscode"; import type { ServerApi, ClientApiMethods } from "tabby-chat-panel"; import { createThread, type ThreadOptions } from "tabby-threads"; -import { getLogger } from "../logger"; async function createThreadFromWebview, Target = Record>( webview: Webview, options?: ThreadOptions, ) { - getLogger().info("Creating thread from webview"); const thread = await createThread( { send(message) { - getLogger().debug("Sending message to chat panel", message); webview.postMessage({ action: "postMessageToChatPanel", message }); }, listen(listener, { signal }) { - getLogger().debug("Setting up message listener"); const { dispose } = webview.onDidReceiveMessage((msg) => { - getLogger().debug("Received message from chat panel", msg); listener(msg); }); signal?.addEventListener("abort", () => { - getLogger().debug("Disposing message listener"); dispose(); }); }, }, options, ); - setTimeout(() => thread.exchangeMethods(), 3000); - getLogger().info("Thread created"); return thread; } export async function createClient(webview: Webview, api: ClientApiMethods): Promise { - const logger = getLogger(); - logger.info("Creating client with exposed methods:", Object.keys(api)); const thread = await createThreadFromWebview(webview, { expose: { refresh: api.refresh, @@ -55,6 +45,5 @@ export async function createClient(webview: Webview, api: ClientApiMethods): Pro readFileContent: api.readFileContent, }, }); - getLogger().info("Client created"); return thread as unknown as ServerApi; } From 1024b74db95bdc8d408e7703af45f9cad5670309 Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Wed, 22 Jan 2025 14:21:42 -0600 Subject: [PATCH 07/19] refactor(chat-panel): remove unused eslint-disable for console statements --- clients/tabby-chat-panel/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index 1e9b76bac7df..b8eb83bb0d4a 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import { createThreadFromIframe, createThreadFromInsideIframe } from 'tabby-threads' import { version } from '../package.json' From 986119f6b21b7b41438161a10f0ca4064e107d59 Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Wed, 22 Jan 2025 14:26:19 -0600 Subject: [PATCH 08/19] refactor(client): simplify createClient function by returning thread directly --- clients/tabby-chat-panel/src/index.ts | 4 +--- clients/vscode/src/chat/createClient.ts | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index b8eb83bb0d4a..e7182b8eef5a 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -374,9 +374,8 @@ export interface ClientApi extends ClientApiMethods { supports: SupportProxy } -// TODO: change this to async export async function createClient(target: HTMLIFrameElement, api: ClientApiMethods): Promise { - const thread = await createThreadFromIframe(target, { + return await createThreadFromIframe(target, { expose: { refresh: api.refresh, onApplyInEditor: api.onApplyInEditor, @@ -395,7 +394,6 @@ export async function createClient(target: HTMLIFrameElement, api: ClientApiMeth readFileContent: api.readFileContent, }, }) - return thread as unknown as Promise } export async function createServer(api: ServerApi): Promise { diff --git a/clients/vscode/src/chat/createClient.ts b/clients/vscode/src/chat/createClient.ts index 27ed309063f6..bfc489400b67 100644 --- a/clients/vscode/src/chat/createClient.ts +++ b/clients/vscode/src/chat/createClient.ts @@ -26,7 +26,7 @@ async function createThreadFromWebview, Target = Re } export async function createClient(webview: Webview, api: ClientApiMethods): Promise { - const thread = await createThreadFromWebview(webview, { + return await createThreadFromWebview(webview, { expose: { refresh: api.refresh, onApplyInEditor: api.onApplyInEditor, @@ -45,5 +45,4 @@ export async function createClient(webview: Webview, api: ClientApiMethods): Pro readFileContent: api.readFileContent, }, }); - return thread as unknown as ServerApi; } From 524913d40557242257b30d47a5f8943227e9c3db Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Wed, 22 Jan 2025 14:31:26 -0600 Subject: [PATCH 09/19] refactor(iframe): streamline message event listener by removing unnecessary conditional block refactor(chat-page): simplify capability checks by consolidating server support evaluations refactor(chat-webview): remove redundant logging during initialization --- .../source/targets/iframe/nested.ts | 4 +- clients/vscode/src/chat/webview.ts | 2 - ee/tabby-ui/app/chat/page.tsx | 48 ++++++------------- 3 files changed, 16 insertions(+), 38 deletions(-) diff --git a/clients/tabby-threads/source/targets/iframe/nested.ts b/clients/tabby-threads/source/targets/iframe/nested.ts index 6ffda55e230c..f603ebcd93b2 100644 --- a/clients/tabby-threads/source/targets/iframe/nested.ts +++ b/clients/tabby-threads/source/targets/iframe/nested.ts @@ -56,9 +56,7 @@ export async function createThreadFromInsideIframe< self.addEventListener( "message", ({ data }) => { - if (data === CHECK_MESSAGE) { - respond(); - } + if (data === CHECK_MESSAGE) respond(); }, { signal: options.signal } ); diff --git a/clients/vscode/src/chat/webview.ts b/clients/vscode/src/chat/webview.ts index dd75b878d7d8..8262122aa3c6 100644 --- a/clients/vscode/src/chat/webview.ts +++ b/clients/vscode/src/chat/webview.ts @@ -96,11 +96,9 @@ export class ChatWebview { }; this.webview = webview; - this.logger.info("Initializing chat panel webview."); this.createChatPanelApiClient().then((client) => { this.client = client; }); - this.logger.info("Chat panel webview initialized."); const statusListener = () => { this.checkStatusAndLoadContent(); }; diff --git a/ee/tabby-ui/app/chat/page.tsx b/ee/tabby-ui/app/chat/page.tsx index 70e59d7f76b8..9c10fa72d201 100644 --- a/ee/tabby-ui/app/chat/page.tsx +++ b/ee/tabby-ui/app/chat/page.tsx @@ -28,6 +28,8 @@ import { MemoizedReactMarkdown } from '@/components/markdown' import './page.css' +import { set } from 'date-fns' + import { saveFetcherOptions } from '@/lib/tabby/token-management' import { PromptFormRef } from '@/components/chat/form-editor/types' @@ -244,40 +246,20 @@ export default function ChatPage() { apiVersion: TABBY_CHAT_PANEL_API_VERSION }) - if ('refresh' in server) { - // eslint-disable-next-line no-console - console.log('refresh in server') - } - // eslint-disable-next-line no-console - console.log('support v2?:', server?.supports['onApplyInEditorV2']) - - const checkCapabilities = async () => { - server - ?.hasCapability('onApplyInEditorV2') - .then(setSupportsOnApplyInEditorV2) - server?.hasCapability('lookupSymbol').then(setSupportsOnLookupSymbol) - server - ?.hasCapability('readWorkspaceGitRepositories') - .then(setSupportsReadWorkspaceGitRepoInfo) - server - ?.hasCapability('listFileInWorkspace') - .then(setSupportProvideFileAtInfo) - server - ?.hasCapability('readFileContent') - .then(setSupportsReadFileContent) - - Promise.all([ - server?.hasCapability('fetchSessionState'), - server?.hasCapability('storeSessionState') - ]).then(results => { - setSupportsStoreAndFetchSessionState( - results.every(result => !!result) - ) - }) + setSupportProvideFileAtInfo(!!server?.supports['listFileInWorkspace']) + setSupportsReadFileContent(!!server?.supports['readFileContent']) + setSupportsOnApplyInEditorV2(!!server?.supports['onApplyInEditorV2']) + setSupportsOnLookupSymbol(!!server?.supports['lookupSymbol']) + setSupportsReadWorkspaceGitRepoInfo( + !!server?.supports['readWorkspaceGitRepositories'] + ) + if ( + !!server?.supports['fetchSessionState'] && + !!server?.supports['storeSessionState'] + ) { + setSupportsStoreAndFetchSessionState(true) } - checkCapabilities().then(() => { - setIsServerLoaded(true) - }) + setIsServerLoaded(true) } }, [server]) From 77f14b6c26bfe5db8322a1263180d4b289013beb Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 20:37:03 +0000 Subject: [PATCH 10/19] [autofix.ci] apply automated fixes --- ee/tabby-ui/app/chat/page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ee/tabby-ui/app/chat/page.tsx b/ee/tabby-ui/app/chat/page.tsx index 9c10fa72d201..c1c408a859f9 100644 --- a/ee/tabby-ui/app/chat/page.tsx +++ b/ee/tabby-ui/app/chat/page.tsx @@ -28,8 +28,6 @@ import { MemoizedReactMarkdown } from '@/components/markdown' import './page.css' -import { set } from 'date-fns' - import { saveFetcherOptions } from '@/lib/tabby/token-management' import { PromptFormRef } from '@/components/chat/form-editor/types' From 2daa34d6ddcc5d892701e2f8f0a1f15893cf9b5e Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Wed, 22 Jan 2025 14:40:45 -0600 Subject: [PATCH 11/19] refactor(target): remove exchangeMethods function and related references --- .../tabby-threads/source/targets/target.ts | 35 +------------------ clients/tabby-threads/source/types.ts | 7 ---- 2 files changed, 1 insertion(+), 41 deletions(-) diff --git a/clients/tabby-threads/source/targets/target.ts b/clients/tabby-threads/source/targets/target.ts index bda6547462f4..feb7b7289d6b 100644 --- a/clients/tabby-threads/source/targets/target.ts +++ b/clients/tabby-threads/source/targets/target.ts @@ -121,30 +121,6 @@ export async function createThread< ...args: MessageMap[typeof FUNCTION_RESULT] | MessageMap[typeof RESULT] ) => void >(); - - // Create functions for method exchange - const exchangeMethods = async () => { - const ourMethods = Array.from(activeApi.keys()).map(String); - - const id = uuid(); - - // Create a promise that will resolve when we receive their methods - const methodsPromise = new Promise((resolve) => { - callIdsToResolver.set(id, (_, __, value) => { - const theirMethods = encoder.decode(value, encoderApi) as string[]; - // Cache their methods - theirMethodsCache = theirMethods; - resolve(); - }); - }); - - // Send EXPOSE_LIST with our methods - send(EXPOSE_LIST, [id, ourMethods]); - - // Wait for their methods to be received - return methodsPromise; - }; - // Create a function to request methods from the other side const requestMethods = async () => { // If we have cached methods and connection is still active, return them @@ -175,7 +151,6 @@ export async function createThread< handlerForCall, callable, { - exchangeMethods, requestMethods, }, { @@ -514,7 +489,6 @@ function createCallable( ) => AnyFunction | undefined, callable?: (keyof T)[], methods?: { - exchangeMethods: () => void; requestMethods: () => Promise; }, state?: { @@ -538,9 +512,6 @@ function createCallable( { get(_target, property) { if (property === "then") return undefined; - if (property === "exchangeMethods") { - return methods?.exchangeMethods; - } if (property === "requestMethods") { return methods?.requestMethods; } @@ -568,11 +539,7 @@ function createCallable( return handler; }, has(_target, property) { - if ( - property === "then" || - property === "exchangeMethods" || - property === "requestMethods" - ) { + if (property === "then" || property === "requestMethods") { return true; } if (!state) { diff --git a/clients/tabby-threads/source/types.ts b/clients/tabby-threads/source/types.ts index 4690c56150b0..9bf738f3b0c6 100644 --- a/clients/tabby-threads/source/types.ts +++ b/clients/tabby-threads/source/types.ts @@ -19,13 +19,6 @@ export type Thread = { : never : never; } & { - /** - * Exchange method lists between threads after connection is established. - * This should be called only after the connection is ready to ensure - * proper method exchange. - */ - exchangeMethods(): Promise; - /** * Request methods from the other side and wait for response. * Returns a promise that resolves with the list of available methods. From 5312275c8f33f058af63887c8c55e5547903fa83 Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Thu, 23 Jan 2025 18:48:34 -0600 Subject: [PATCH 12/19] refactor(client): simplify createClient and related functions by removing unnecessary async/await --- clients/tabby-chat-panel/src/index.ts | 4 +- clients/tabby-chat-panel/src/react.ts | 2 +- .../source/targets/iframe/iframe.ts | 8 +- .../source/targets/iframe/nested.ts | 6 +- .../tabby-threads/source/targets/target.ts | 252 ++++++++++-------- clients/vscode/src/chat/createClient.ts | 9 +- clients/vscode/src/chat/webview.ts | 8 +- 7 files changed, 154 insertions(+), 135 deletions(-) diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index e7182b8eef5a..4f372f6161b7 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -374,8 +374,8 @@ export interface ClientApi extends ClientApiMethods { supports: SupportProxy } -export async function createClient(target: HTMLIFrameElement, api: ClientApiMethods): Promise { - return await createThreadFromIframe(target, { +export function createClient(target: HTMLIFrameElement, api: ClientApiMethods): ServerApi { + return createThreadFromIframe(target, { expose: { refresh: api.refresh, onApplyInEditor: api.onApplyInEditor, diff --git a/clients/tabby-chat-panel/src/react.ts b/clients/tabby-chat-panel/src/react.ts index 9b938e1116e7..c028ebeaf11f 100644 --- a/clients/tabby-chat-panel/src/react.ts +++ b/clients/tabby-chat-panel/src/react.ts @@ -11,7 +11,7 @@ function useClient(iframeRef: RefObject, api: ClientApiMethod useEffect(() => { if (iframeRef.current && !isCreated) { isCreated = true - createClient(iframeRef.current!, api).then(setClient) + setClient(createClient(iframeRef.current!, api)) } }, [iframeRef.current]) diff --git a/clients/tabby-threads/source/targets/iframe/iframe.ts b/clients/tabby-threads/source/targets/iframe/iframe.ts index 335e9d8253de..e3cfb62318a6 100644 --- a/clients/tabby-threads/source/targets/iframe/iframe.ts +++ b/clients/tabby-threads/source/targets/iframe/iframe.ts @@ -16,7 +16,7 @@ import { CHECK_MESSAGE, RESPONSE_MESSAGE } from "./shared"; * const thread = createThreadFromInsideIframe(iframe); * await thread.sendMessage('Hello world!'); */ -export async function createThreadFromIframe< +export function createThreadFromIframe< Self = Record, Target = Record, >( @@ -70,9 +70,7 @@ export async function createThreadFromIframe< sendMessage(CHECK_MESSAGE); }); - await connectedPromise; - - const thread = await createThread( + const thread = createThread( { send(message, transfer) { if (!connected) { @@ -99,7 +97,7 @@ export async function createThreadFromIframe< // FIXME(Sma1lboy): We don't have this need for now. // After connection is established and thread is created, exchange methods - await thread.requestMethods(); + // await thread.requestMethods(); return thread; } diff --git a/clients/tabby-threads/source/targets/iframe/nested.ts b/clients/tabby-threads/source/targets/iframe/nested.ts index f603ebcd93b2..4c1a61de298f 100644 --- a/clients/tabby-threads/source/targets/iframe/nested.ts +++ b/clients/tabby-threads/source/targets/iframe/nested.ts @@ -42,7 +42,7 @@ export async function createThreadFromInsideIframe< ? new NestedAbortController(options.signal) : new AbortController(); - const connectionPromise = new Promise((resolve) => { + new Promise((resolve) => { let isConnected = false; const respond = () => { @@ -77,9 +77,7 @@ export async function createThreadFromInsideIframe< } }); - await connectionPromise; - - const thread = await createThread( + const thread = createThread( { send(message, transfer) { return parent.postMessage(message, targetOrigin, transfer); diff --git a/clients/tabby-threads/source/targets/target.ts b/clients/tabby-threads/source/targets/target.ts index feb7b7289d6b..89daae8050f6 100644 --- a/clients/tabby-threads/source/targets/target.ts +++ b/clients/tabby-threads/source/targets/target.ts @@ -10,6 +10,7 @@ import { RELEASE_METHOD, RETAINED_BY, RETAIN_METHOD } from "../constants"; import { StackFrame, isMemoryManageable } from "../memory"; import { createBasicEncoder } from "../encoding/basic"; +import { RESPONSE_MESSAGE } from "./iframe/shared"; export type { ThreadTarget }; @@ -86,7 +87,7 @@ type MessageData = { * Creates a thread from any object that conforms to the `ThreadTarget` * interface. */ -export async function createThread< +export function createThread< Self = Record, Target = Record, >( @@ -98,7 +99,7 @@ export async function createThread< uuid = defaultUuid, encoder = createBasicEncoder(), }: ThreadOptions = {} -): Promise> { +): Thread { let terminated = false; const activeApi = new Map(); const functionsToId = new Map(); @@ -121,30 +122,6 @@ export async function createThread< ...args: MessageMap[typeof FUNCTION_RESULT] | MessageMap[typeof RESULT] ) => void >(); - // Create a function to request methods from the other side - const requestMethods = async () => { - // If we have cached methods and connection is still active, return them - if (theirMethodsCache !== null && !terminated) { - return theirMethodsCache; - } - - const id = uuid(); - - // Create a promise that will resolve with the other side's methods - const methodsPromise = new Promise((resolve) => { - callIdsToResolver.set(id, (_, __, value) => { - const theirMethods = encoder.decode(value, encoderApi) as string[]; - // Cache the methods for future use - theirMethodsCache = theirMethods; - resolve(theirMethods); - }); - }); - - // Send EXPOSE_LIST with empty methods array to request other side's methods - send(EXPOSE_LIST, [id, []]); - - return methodsPromise; - }; // Create proxy for method calls const call = createCallable>( @@ -154,7 +131,6 @@ export async function createThread< requestMethods, }, { - getMethodsCache: () => theirMethodsCache, isTerminated: () => terminated, } ); @@ -270,7 +246,41 @@ export async function createThread< target.send([type, args], transferables); } + // Create a function to request methods from the other side + async function requestMethods() { + // If we have cached methods and connection is still active, return them + if (theirMethodsCache !== null && !terminated) { + return theirMethodsCache; + } + + const id = uuid(); + + // Create a promise that will resolve with the other side's methods + const methodsPromise = new Promise((resolve) => { + callIdsToResolver.set(id, (_, __, value) => { + const theirMethods = encoder.decode(value, encoderApi) as string[]; + // Cache the methods for future use + theirMethodsCache = theirMethods; + resolve(theirMethods); + }); + }); + + // Send EXPOSE_LIST with empty methods array to request other side's methods + send(EXPOSE_LIST, [id, []]); + + return methodsPromise; + } + async function listener(rawData: unknown) { + if (rawData === RESPONSE_MESSAGE) { + console.log("response message received"); + requestMethods() + .then((res) => { + console.log("their methods: ", res); + }) + .catch(() => {}); + return; + } // FIXME: don't ignore anything, just for testing now const isThreadMessageData = Array.isArray(rawData) && @@ -467,6 +477,108 @@ export async function createThread< callIdsToResolver.delete(callId); } } + + function createCallable( + handlerForCall: ( + property: string | number | symbol + ) => AnyFunction | undefined, + callable?: (keyof T)[], + methods?: { + requestMethods: () => Promise; + }, + state?: { + isTerminated: () => boolean; + } + ): T { + let call: any; + + if (callable == null) { + if (typeof Proxy !== "function") { + throw new Error( + `You must pass an array of callable methods in environments without Proxies.` + ); + } + + const cache = new Map< + string | number | symbol, + AnyFunction | undefined + >(); + + call = new Proxy( + {}, + { + get(_target, property) { + if (property === "then") { + console.warn("then not found"); + return undefined; + } + if (property === "requestMethods") { + return methods?.requestMethods; + } + if (property === "supports") { + return new Proxy( + {}, + { + get(_target, method: string) { + if (!state) return false; + const cache = theirMethodsCache; + console.log("cache for supports", cache); + if (cache !== null && !state.isTerminated()) { + return cache.includes(method); + } + return false; + }, + } + ); + } + + if ( + theirMethodsCache && + !theirMethodsCache.includes(String(property)) + ) { + console.log("method not found", property); + return undefined; + } + + if (cache.has(property)) { + return cache.get(property); + } + + const handler = handlerForCall(property); + cache.set(property, handler); + return handler; + }, + has(_target, property) { + if (property === "then" || property === "requestMethods") { + return true; + } + if (!state) { + return false; + } + const cache = theirMethodsCache; + console.log("cache for has", cache); + if (cache !== null && !state.isTerminated()) { + return cache.includes(String(property)); + } + return false; + }, + } + ); + } else { + call = {}; + + for (const method of callable) { + Object.defineProperty(call, method, { + value: handlerForCall(method), + writable: false, + configurable: true, + enumerable: true, + }); + } + } + + return call; + } } class ThreadTerminatedError extends Error { @@ -482,89 +594,3 @@ function defaultUuid() { function uuidSegment() { return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16); } - -function createCallable( - handlerForCall: ( - property: string | number | symbol - ) => AnyFunction | undefined, - callable?: (keyof T)[], - methods?: { - requestMethods: () => Promise; - }, - state?: { - getMethodsCache: () => string[] | null; - isTerminated: () => boolean; - } -): T { - let call: any; - - if (callable == null) { - if (typeof Proxy !== "function") { - throw new Error( - `You must pass an array of callable methods in environments without Proxies.` - ); - } - - const cache = new Map(); - - call = new Proxy( - {}, - { - get(_target, property) { - if (property === "then") return undefined; - if (property === "requestMethods") { - return methods?.requestMethods; - } - if (property === "supports") { - return new Proxy( - {}, - { - get(_target, method: string) { - if (!state) return false; - const cache = state.getMethodsCache(); - if (cache !== null && !state.isTerminated()) { - return cache.includes(method); - } - return false; - }, - } - ); - } - if (cache.has(property)) { - return cache.get(property); - } - - const handler = handlerForCall(property); - cache.set(property, handler); - return handler; - }, - has(_target, property) { - if (property === "then" || property === "requestMethods") { - return true; - } - if (!state) { - return false; - } - const cache = state.getMethodsCache(); - if (cache !== null && !state.isTerminated()) { - return cache.includes(String(property)); - } - return false; - }, - } - ); - } else { - call = {}; - - for (const method of callable) { - Object.defineProperty(call, method, { - value: handlerForCall(method), - writable: false, - configurable: true, - enumerable: true, - }); - } - } - - return call; -} diff --git a/clients/vscode/src/chat/createClient.ts b/clients/vscode/src/chat/createClient.ts index bfc489400b67..b66b94d806fc 100644 --- a/clients/vscode/src/chat/createClient.ts +++ b/clients/vscode/src/chat/createClient.ts @@ -2,11 +2,11 @@ import type { Webview } from "vscode"; import type { ServerApi, ClientApiMethods } from "tabby-chat-panel"; import { createThread, type ThreadOptions } from "tabby-threads"; -async function createThreadFromWebview, Target = Record>( +function createThreadFromWebview, Target = Record>( webview: Webview, options?: ThreadOptions, ) { - const thread = await createThread( + return createThread( { send(message) { webview.postMessage({ action: "postMessageToChatPanel", message }); @@ -22,11 +22,10 @@ async function createThreadFromWebview, Target = Re }, options, ); - return thread; } -export async function createClient(webview: Webview, api: ClientApiMethods): Promise { - return await createThreadFromWebview(webview, { +export function createClient(webview: Webview, api: ClientApiMethods): ServerApi { + return createThreadFromWebview(webview, { expose: { refresh: api.refresh, onApplyInEditor: api.onApplyInEditor, diff --git a/clients/vscode/src/chat/webview.ts b/clients/vscode/src/chat/webview.ts index 8262122aa3c6..0ff3256f52c2 100644 --- a/clients/vscode/src/chat/webview.ts +++ b/clients/vscode/src/chat/webview.ts @@ -96,9 +96,7 @@ export class ChatWebview { }; this.webview = webview; - this.createChatPanelApiClient().then((client) => { - this.client = client; - }); + this.client = this.createChatPanelApiClient(); const statusListener = () => { this.checkStatusAndLoadContent(); }; @@ -207,12 +205,12 @@ export class ChatWebview { } } - private async createChatPanelApiClient(): Promise { + private createChatPanelApiClient(): ServerApi | undefined { const webview = this.webview; if (!webview) { return undefined; } - return await createClient(webview, { + return createClient(webview, { refresh: async () => { commands.executeCommand("tabby.reconnectToServer"); return; From ea16035bb8b2bb47dcdc9ddca3057b76a35a8f22 Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Thu, 23 Jan 2025 19:13:00 -0600 Subject: [PATCH 13/19] refactor(iframe): streamline thread creation and message handling in iframe --- .../source/targets/iframe/iframe.ts | 8 +-- .../source/targets/iframe/nested.ts | 49 +++++++++---------- .../tabby-threads/source/targets/target.ts | 1 + clients/vscode/src/chat/createClient.ts | 4 +- clients/vscode/src/chat/webview.ts | 1 + 5 files changed, 26 insertions(+), 37 deletions(-) diff --git a/clients/tabby-threads/source/targets/iframe/iframe.ts b/clients/tabby-threads/source/targets/iframe/iframe.ts index e3cfb62318a6..35925f3e179b 100644 --- a/clients/tabby-threads/source/targets/iframe/iframe.ts +++ b/clients/tabby-threads/source/targets/iframe/iframe.ts @@ -70,7 +70,7 @@ export function createThreadFromIframe< sendMessage(CHECK_MESSAGE); }); - const thread = createThread( + return createThread( { send(message, transfer) { if (!connected) { @@ -94,10 +94,4 @@ export function createThreadFromIframe< }, options ); - - // FIXME(Sma1lboy): We don't have this need for now. - // After connection is established and thread is created, exchange methods - // await thread.requestMethods(); - - return thread; } diff --git a/clients/tabby-threads/source/targets/iframe/nested.ts b/clients/tabby-threads/source/targets/iframe/nested.ts index 4c1a61de298f..b223d11cea43 100644 --- a/clients/tabby-threads/source/targets/iframe/nested.ts +++ b/clients/tabby-threads/source/targets/iframe/nested.ts @@ -42,17 +42,10 @@ export async function createThreadFromInsideIframe< ? new NestedAbortController(options.signal) : new AbortController(); - new Promise((resolve) => { - let isConnected = false; - - const respond = () => { - if (!isConnected) { - isConnected = true; - parent.postMessage(RESPONSE_MESSAGE, targetOrigin); - resolve(); - } - }; + const ready = () => { + const respond = () => parent.postMessage(RESPONSE_MESSAGE, targetOrigin); + // Handles wrappers that want to connect after the page has already loaded self.addEventListener( "message", ({ data }) => { @@ -61,21 +54,25 @@ export async function createThreadFromInsideIframe< { signal: options.signal } ); - if (document.readyState === "complete") { - respond(); - } else { - document.addEventListener( - "readystatechange", - () => { - if (document.readyState === "complete") { - respond(); - abort.abort(); - } - }, - { signal: abort.signal } - ); - } - }); + respond(); + }; + + // Listening to `readyState` in iframe, though the child iframe could probably + // send a `postMessage` that it is ready to receive messages sooner than that. + if (document.readyState === "complete") { + ready(); + } else { + document.addEventListener( + "readystatechange", + () => { + if (document.readyState === "complete") { + ready(); + abort.abort(); + } + }, + { signal: abort.signal } + ); + } const thread = createThread( { @@ -95,8 +92,6 @@ export async function createThreadFromInsideIframe< }, options ); - await thread.requestMethods(); - return thread; } diff --git a/clients/tabby-threads/source/targets/target.ts b/clients/tabby-threads/source/targets/target.ts index 89daae8050f6..9073bdce9dfb 100644 --- a/clients/tabby-threads/source/targets/target.ts +++ b/clients/tabby-threads/source/targets/target.ts @@ -272,6 +272,7 @@ export function createThread< } async function listener(rawData: unknown) { + // this method receives messages from the other side means the other side is ready if (rawData === RESPONSE_MESSAGE) { console.log("response message received"); requestMethods() diff --git a/clients/vscode/src/chat/createClient.ts b/clients/vscode/src/chat/createClient.ts index b66b94d806fc..a962a5637f45 100644 --- a/clients/vscode/src/chat/createClient.ts +++ b/clients/vscode/src/chat/createClient.ts @@ -12,9 +12,7 @@ function createThreadFromWebview, Target = Record { - listener(msg); - }); + const { dispose } = webview.onDidReceiveMessage(listener); signal?.addEventListener("abort", () => { dispose(); }); diff --git a/clients/vscode/src/chat/webview.ts b/clients/vscode/src/chat/webview.ts index 0ff3256f52c2..1e453b139ca1 100644 --- a/clients/vscode/src/chat/webview.ts +++ b/clients/vscode/src/chat/webview.ts @@ -97,6 +97,7 @@ export class ChatWebview { this.webview = webview; this.client = this.createChatPanelApiClient(); + const statusListener = () => { this.checkStatusAndLoadContent(); }; From d03eeb503b3a7803a684283e04a90d81203c4baa Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Thu, 23 Jan 2025 19:15:50 -0600 Subject: [PATCH 14/19] refactor(iframe): remove unnecessary line break in createThreadFromIframe and simplify listen method signature --- clients/tabby-threads/source/targets/iframe/iframe.ts | 1 + clients/tabby-threads/source/types.ts | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/clients/tabby-threads/source/targets/iframe/iframe.ts b/clients/tabby-threads/source/targets/iframe/iframe.ts index 35925f3e179b..b7c510aa4a34 100644 --- a/clients/tabby-threads/source/targets/iframe/iframe.ts +++ b/clients/tabby-threads/source/targets/iframe/iframe.ts @@ -78,6 +78,7 @@ export function createThreadFromIframe< if (connected) return sendMessage(message, transfer); }); } + return sendMessage(message, transfer); }, listen(listen, { signal }) { diff --git a/clients/tabby-threads/source/types.ts b/clients/tabby-threads/source/types.ts index 9bf738f3b0c6..2990e86a6b12 100644 --- a/clients/tabby-threads/source/types.ts +++ b/clients/tabby-threads/source/types.ts @@ -48,10 +48,7 @@ export interface ThreadTarget { * and handle its content. This method may be passed an `AbortSignal` to abort the * listening process. */ - listen( - listener: (value: any) => void, - options: { signal?: AbortSignal } - ): void; + listen(listener: (value: any) => void, options: {signal?: AbortSignal}): void; } /** From f7412bcd182c3dc00137b26645941065c6da5aac Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Thu, 23 Jan 2025 19:19:16 -0600 Subject: [PATCH 15/19] refactor(chat): remove unnecessary console log for session state support --- ee/tabby-ui/app/chat/page.tsx | 40 +++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/ee/tabby-ui/app/chat/page.tsx b/ee/tabby-ui/app/chat/page.tsx index c1c408a859f9..4f9e010270e8 100644 --- a/ee/tabby-ui/app/chat/page.tsx +++ b/ee/tabby-ui/app/chat/page.tsx @@ -244,20 +244,34 @@ export default function ChatPage() { apiVersion: TABBY_CHAT_PANEL_API_VERSION }) - setSupportProvideFileAtInfo(!!server?.supports['listFileInWorkspace']) - setSupportsReadFileContent(!!server?.supports['readFileContent']) - setSupportsOnApplyInEditorV2(!!server?.supports['onApplyInEditorV2']) - setSupportsOnLookupSymbol(!!server?.supports['lookupSymbol']) - setSupportsReadWorkspaceGitRepoInfo( - !!server?.supports['readWorkspaceGitRepositories'] - ) - if ( - !!server?.supports['fetchSessionState'] && - !!server?.supports['storeSessionState'] - ) { - setSupportsStoreAndFetchSessionState(true) + const checkCapabilities = async () => { + server + ?.hasCapability('onApplyInEditorV2') + .then(setSupportsOnApplyInEditorV2) + server?.hasCapability('lookupSymbol').then(setSupportsOnLookupSymbol) + server + ?.hasCapability('readWorkspaceGitRepositories') + .then(setSupportsReadWorkspaceGitRepoInfo) + server + ?.hasCapability('listFileInWorkspace') + .then(setSupportProvideFileAtInfo) + server + ?.hasCapability('readFileContent') + .then(setSupportsReadFileContent) + + Promise.all([ + server?.hasCapability('fetchSessionState'), + server?.hasCapability('storeSessionState') + ]).then(results => { + setSupportsStoreAndFetchSessionState( + results.every(result => !!result) + ) + }) } - setIsServerLoaded(true) + + checkCapabilities().then(() => { + setIsServerLoaded(true) + }) } }, [server]) From 03f11bfaa1c06d38f08754f929b790c00a48a2cc Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Thu, 23 Jan 2025 19:26:31 -0600 Subject: [PATCH 16/19] refactor(client): remove unused support proxy type and simplify method checks --- clients/tabby-chat-panel/src/index.ts | 13 ---------- .../tabby-threads/source/targets/target.ts | 26 +++---------------- 2 files changed, 4 insertions(+), 35 deletions(-) diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index 4f372f6161b7..6f46bc9a6523 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -352,14 +352,6 @@ export interface ClientApiMethods { readFileContent?: (info: FileRange) => Promise } -type ClientApiMethod = keyof ClientApiMethods - -/** - * Provide a convenient way to check if the client supports a specific method. - */ -type SupportProxy = { - [K in ClientApiMethod]: boolean -} export interface ClientApi extends ClientApiMethods { /** * Checks if the client supports this capability. @@ -367,11 +359,6 @@ export interface ClientApi extends ClientApiMethods { * Note: This method should not be used to ensure compatibility across different chat panel SDK versions. */ hasCapability: (method: keyof ClientApiMethods) => Promise - - /** - * The convenient accessor to check if the client supports a specific method from {@link ClientApiMethods}. - */ - supports: SupportProxy } export function createClient(target: HTMLIFrameElement, api: ClientApiMethods): ServerApi { diff --git a/clients/tabby-threads/source/targets/target.ts b/clients/tabby-threads/source/targets/target.ts index 9073bdce9dfb..a3ffbc278afb 100644 --- a/clients/tabby-threads/source/targets/target.ts +++ b/clients/tabby-threads/source/targets/target.ts @@ -240,9 +240,7 @@ export function createThread< args: MessageMap[Type], transferables?: Transferable[] ) { - if (terminated) { - return; - } + if (terminated) return; target.send([type, args], transferables); } @@ -282,7 +280,7 @@ export function createThread< .catch(() => {}); return; } - // FIXME: don't ignore anything, just for testing now + const isThreadMessageData = Array.isArray(rawData) && typeof rawData[0] === "number" && @@ -510,29 +508,13 @@ export function createThread< { get(_target, property) { if (property === "then") { - console.warn("then not found"); + console.log("target then", property); + console.log("_target", _target); return undefined; } if (property === "requestMethods") { return methods?.requestMethods; } - if (property === "supports") { - return new Proxy( - {}, - { - get(_target, method: string) { - if (!state) return false; - const cache = theirMethodsCache; - console.log("cache for supports", cache); - if (cache !== null && !state.isTerminated()) { - return cache.includes(method); - } - return false; - }, - } - ); - } - if ( theirMethodsCache && !theirMethodsCache.includes(String(property)) From 2d177e77f35dc1a08ea93b46e8758362b8788678 Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Thu, 23 Jan 2025 19:40:05 -0600 Subject: [PATCH 17/19] refactor(target): simplify listener and property handler logic by removing unnecessary console logs and streamlining method checks --- .../tabby-threads/source/targets/target.ts | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/clients/tabby-threads/source/targets/target.ts b/clients/tabby-threads/source/targets/target.ts index a3ffbc278afb..a9b04cf3c5d3 100644 --- a/clients/tabby-threads/source/targets/target.ts +++ b/clients/tabby-threads/source/targets/target.ts @@ -272,12 +272,7 @@ export function createThread< async function listener(rawData: unknown) { // this method receives messages from the other side means the other side is ready if (rawData === RESPONSE_MESSAGE) { - console.log("response message received"); - requestMethods() - .then((res) => { - console.log("their methods: ", res); - }) - .catch(() => {}); + requestMethods().catch(() => {}); return; } @@ -507,29 +502,22 @@ export function createThread< {}, { get(_target, property) { - if (property === "then") { - console.log("target then", property); - console.log("_target", _target); - return undefined; + switch (property) { + // FIXME: remove this, now is hack way + case "then": + return undefined; + case "requestMethods": + return methods?.requestMethods; + case "hasCapability": + default: + if (cache.has(property)) { + return cache.get(property); + } + + const handler = handlerForCall(property); + cache.set(property, handler); + return handler; } - if (property === "requestMethods") { - return methods?.requestMethods; - } - if ( - theirMethodsCache && - !theirMethodsCache.includes(String(property)) - ) { - console.log("method not found", property); - return undefined; - } - - if (cache.has(property)) { - return cache.get(property); - } - - const handler = handlerForCall(property); - cache.set(property, handler); - return handler; }, has(_target, property) { if (property === "then" || property === "requestMethods") { From 17859e1bfcd520cf9ce9e513c8eb9e9572a539c9 Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Thu, 23 Jan 2025 19:46:12 -0600 Subject: [PATCH 18/19] refactor(client): remove unnecessary async keyword from createServer function --- clients/tabby-chat-panel/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index 6f46bc9a6523..d02b0c4db5b6 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -383,8 +383,8 @@ export function createClient(target: HTMLIFrameElement, api: ClientApiMethods): }) } -export async function createServer(api: ServerApi): Promise { - return await createThreadFromInsideIframe({ +export function createServer(api: ServerApi): Promise { + return createThreadFromInsideIframe({ expose: { init: api.init, executeCommand: api.executeCommand, From 911f783d404fb42324ecdc39904ff6ee2fdb469e Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Thu, 23 Jan 2025 21:24:21 -0600 Subject: [PATCH 19/19] refactor(target): enhance property handler logic by adding undefined return for unsupported properties --- clients/tabby-threads/source/targets/target.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/clients/tabby-threads/source/targets/target.ts b/clients/tabby-threads/source/targets/target.ts index a9b04cf3c5d3..62ee392d847f 100644 --- a/clients/tabby-threads/source/targets/target.ts +++ b/clients/tabby-threads/source/targets/target.ts @@ -509,7 +509,11 @@ export function createThread< case "requestMethods": return methods?.requestMethods; case "hasCapability": + return handlerForCall(property); default: + if (!theirMethodsCache?.includes(String(property))) { + return undefined; + } if (cache.has(property)) { return cache.get(property); }