diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index 79d78f8fa8f3..7e2838de3dda 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 { + const clientApi = await createThreadFromInsideIframe({ expose: { init: api.init, executeCommand: api.executeCommand, @@ -402,67 +404,17 @@ export function createServer(api: ServerApi): ClientApi { 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) { ... } + // TODO: put this part into createThread{} 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 @@ -470,8 +422,6 @@ export function createServer(api: ServerApi): ClientApi { 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 diff --git a/clients/tabby-chat-panel/src/react.ts b/clients/tabby-chat-panel/src/react.ts index e6a57edeb295..651d410d90cd 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,23 @@ function useServer(api: ServerApi) { let isCreated = false useEffect(() => { - const isInIframe = window.self !== window.top - if (isInIframe && !isCreated) { - isCreated = true - setServer(createServer(api)) + // TODO: solving .then proxy issue + 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 + // 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:', server) + } } + // 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..aa5e6682967d 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,80 @@ 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:", 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:", 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:", + event.data + ); listen(event.data); }, { signal } @@ -92,4 +124,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..d04b2e9b7eea 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"); 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,6 +400,35 @@ 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; + } } } @@ -345,7 +441,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 +528,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 +544,13 @@ function createCallable( {}, { get(_target, property) { + 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..7172c9b0638b 100644 --- a/clients/vscode/src/chat/createClient.ts +++ b/clients/vscode/src/chat/createClient.ts @@ -1,33 +1,45 @@ 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, - onApplyInEditorV2: api.onApplyInEditorV2, + // onApplyInEditorV2: api.onApplyInEditorV2, onLoaded: api.onLoaded, onCopy: api.onCopy, onKeyboardEvent: api.onKeyboardEvent, @@ -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..29a3f13a5564 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') @@ -268,6 +267,17 @@ export default function ChatPage() { ) }) } + // eslint-disable-next-line no-console + server.supports['onApplyInEditorV2'].then(res => console.log(res)) + if ('onApplyInEditor' in server.supports) { + // eslint-disable-next-line no-console + console.log('onApplyInEditor is supported') + } else { + // eslint-disable-next-line no-console + console.log('onApplyInEditor is not supported') + } + // eslint-disable-next-line no-console + console.log('hahaha') checkCapabilities().then(() => { setIsServerLoaded(true)