From e01e20918ac819fbf73554d030a5f970101d3b7c Mon Sep 17 00:00:00 2001 From: Neko Ayaka Date: Sat, 17 Jan 2026 02:48:55 +0800 Subject: [PATCH] refactor(airi-plugin-vscode): better structure & format --- .vscode/launch.json | 2 +- apps/stage-tamagotchi/package.json | 2 +- plugins/airi-plugin-vscode/src/extension.ts | 117 ++++++++++++++------ plugins/airi-plugin-vscode/src/types.ts | 46 +++++++- pnpm-lock.yaml | 30 +++-- pnpm-workspace.yaml | 2 +- 6 files changed, 149 insertions(+), 50 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 15f7827bb..433b26fda 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -34,7 +34,7 @@ "--extensionDevelopmentPath=${workspaceFolder}/plugins/airi-plugin-vscode" ], "outFiles": [ - "${workspaceFolder}/plugins/airi-plugin-vscode/dist/**/*.js" + "${workspaceFolder}/plugins/airi-plugin-vscode/dist/**/*.cjs" ] } ], diff --git a/apps/stage-tamagotchi/package.json b/apps/stage-tamagotchi/package.json index 0867d182c..f0a302f04 100644 --- a/apps/stage-tamagotchi/package.json +++ b/apps/stage-tamagotchi/package.json @@ -95,7 +95,7 @@ "electron-updater": "^6.6.2", "es-toolkit": "^1.43.0", "h3": "2.0.1-rc.5", - "injeca": "^0.1.5", + "injeca": "catalog:", "jszip": "^3.10.1", "localforage": "^1.10.0", "mediabunny": "^1.27.2", diff --git a/plugins/airi-plugin-vscode/src/extension.ts b/plugins/airi-plugin-vscode/src/extension.ts index 4a3a70c26..26f4887b5 100644 --- a/plugins/airi-plugin-vscode/src/extension.ts +++ b/plugins/airi-plugin-vscode/src/extension.ts @@ -1,16 +1,18 @@ -import type { DependencyMap, ProvidedKey } from 'injeca' +import type { Lifecycle, ProvidedBy } from 'injeca' import type * as vscode from 'vscode' import { initLogger, LoggerFormat, LoggerLevel, useLogger } from '@guiiai/logg' import { noop } from 'es-toolkit' -import { injeca } from 'injeca' +import { injeca, lifecycle } from 'injeca' import { commands, window, workspace } from 'vscode' import { Client } from './airi' import { ContextCollector } from './context-collector' -let updateTimer: NodeJS.Timeout | null = null -let eventListeners: vscode.Disposable[] = [] +interface IntervalHandle { + clearInterval: () => void + setInterval: (fn: () => void) => NodeJS.Timeout +} /** * Activate the plugin @@ -30,9 +32,25 @@ export async function activate(context: vscode.ExtensionContext) { const vscodeContext = injeca.provide('vscode:context', () => context) const client = injeca.provide('proj-airi:client', () => new Client()) const contextCollector = injeca.provide('self:context-collector', () => new ContextCollector(contextLines)) + const eventListeners = injeca.provide('self:event-listeners', () => [] as vscode.Disposable[]) + const controlLoopInterval = injeca.provide('self:control-loop:interval:send', () => { + let intervalTimer: NodeJS.Timeout | null = null + + return { + clearInterval: () => { + if (intervalTimer) { + clearInterval(intervalTimer) + } + }, + setInterval: (fn: () => void) => { + intervalTimer = setInterval(fn, sendInterval) + return intervalTimer + }, + } satisfies IntervalHandle + }) const extension = injeca.provide('extension', { - dependsOn: { client, vscodeContext, contextCollector }, + dependsOn: { client, vscodeContext, contextCollector, eventListeners, lifecycle, controlLoopInterval }, build: ({ dependsOn }) => setup({ ...dependsOn, isEnabled, sendInterval }), }) @@ -48,6 +66,9 @@ async function setup(params: { client: Client vscodeContext: vscode.ExtensionContext contextCollector: ContextCollector + eventListeners: vscode.Disposable[] + lifecycle: Lifecycle + controlLoopInterval: IntervalHandle isEnabled: boolean sendInterval: number }) { @@ -67,13 +88,13 @@ async function setup(params: { commands.registerCommand('airi-vscode.enable', async () => { params.isEnabled = true await params.client.connect() - await registerListeners({ sendInterval: params.sendInterval, contextCollector: params.contextCollector, client: params.client, isEnabled: params.isEnabled }) + await registerListeners({ ...params }) window.showInformationMessage('AIRI enabled!') }), commands.registerCommand('airi-vscode.disable', () => { params.isEnabled = false - unregisterListeners() + unregisterListeners({ eventListeners: params.eventListeners, controlLoopInterval: params.controlLoopInterval }) params.client.disconnect() window.showInformationMessage('AIRI disabled!') }), @@ -86,7 +107,7 @@ async function setup(params: { // Register event listeners if enabled if (params.isEnabled) { - await registerListeners({ sendInterval: params.sendInterval, contextCollector: params.contextCollector, client: params.client, isEnabled: params.isEnabled }) + await registerListeners({ ...params }) } useLogger().log('AIRI activated successfully') @@ -95,11 +116,19 @@ async function setup(params: { /** * Register event listeners for file save and editor switch */ -async function registerListeners(params: { contextCollector: ContextCollector, client: Client, isEnabled: boolean, sendInterval: number }) { - unregisterListeners() +async function registerListeners(params: { + contextCollector: ContextCollector + lifecycle: Lifecycle + eventListeners: vscode.Disposable[] + client: Client + controlLoopInterval: IntervalHandle + isEnabled: boolean + sendInterval: number +}) { + unregisterListeners({ eventListeners: params.eventListeners, controlLoopInterval: params.controlLoopInterval }) // File save event - eventListeners.push( + params.eventListeners.push( workspace.onDidSaveTextDocument(async (document) => { const editor = window.activeTextEditor if (editor && editor.document === document) { @@ -107,13 +136,19 @@ async function registerListeners(params: { contextCollector: ContextCollector, c if (!ctx) return - params.client.replaceContext(JSON.stringify({ type: 'coding:save', data: ctx })) + params.client.replaceContext('' + + `User saved the file: ${ctx.file.fileName} (located at ${ctx.file.path}). Here is the context around the cursor after saving:\n` + + '\n' + + `${ctx.context.before.join('\n')}\n` + + `${ctx.currentLine.text}\n` + + `${ctx.context.after.join('\n')}`, + ) } }), ) // Switch file event - eventListeners.push( + params.eventListeners.push( window.onDidChangeActiveTextEditor(async (editor) => { if (!editor) { return @@ -124,32 +159,45 @@ async function registerListeners(params: { contextCollector: ContextCollector, c return } - params.client.replaceContext(JSON.stringify({ type: 'coding:switch-file', data: ctx })) + params.client.replaceContext('' + + `User switched to file: ${ctx.file.fileName} (located at ${ctx.file.path}). Here is the context around the cursor after switching:\n` + + '\n' + + `${ctx.context.before.join('\n')}\n` + + `${ctx.currentLine.text}\n` + + `${ctx.context.after.join('\n')}`, + ) }), ) // Start periodic monitoring if interval is set if (params.sendInterval > 0) { - startMonitoring({ contextCollector: params.contextCollector, client: params.client, isEnabled: params.isEnabled, interval: params.sendInterval }) + startMonitoring({ ...params }) } } /** * Unregister all event listeners */ -function unregisterListeners() { - eventListeners.forEach(listener => listener.dispose()) - eventListeners = [] - stopMonitoring() +function unregisterListeners(params: { eventListeners: vscode.Disposable[], controlLoopInterval: IntervalHandle }) { + params.eventListeners.forEach(listener => listener.dispose()) + params.eventListeners = [] + stopMonitoring({ controlLoopInterval: params.controlLoopInterval }) } /** * Start monitoring the coding context */ -function startMonitoring(params: { contextCollector: ContextCollector, client: Client, isEnabled: boolean, interval: number }) { - stopMonitoring() +function startMonitoring(params: { + contextCollector: ContextCollector + lifecycle: Lifecycle + client: Client + controlLoopInterval: IntervalHandle + isEnabled: boolean + sendInterval: number +}) { + stopMonitoring({ controlLoopInterval: params.controlLoopInterval }) - updateTimer = setInterval(async () => { + params.controlLoopInterval.setInterval(async () => { if (!params.isEnabled) return @@ -161,27 +209,34 @@ function startMonitoring(params: { contextCollector: ContextCollector, client: C if (!ctx) return - params.client.replaceContext(JSON.stringify({ type: 'coding:context', data: ctx })) - }, params.interval) + params.client.replaceContext('' + + `User opened file is: ${ctx.file.fileName} (located at ${ctx.file.path}), and current cursor is at line ${ctx.cursor.line + 1}, character ${ctx.cursor.character + 1}.\n` + + '\n' + + `Here is the context around the cursor:\n` + + `\n` + + `${ctx.context.before.join('\n')}\n` + + `${ctx.currentLine.text}\n` + + `${ctx.context.after.join('\n')}`, + ) + }) } /** * Stop monitoring */ -function stopMonitoring() { - if (updateTimer) { - clearInterval(updateTimer) - updateTimer = null - } +function stopMonitoring(params: { controlLoopInterval: IntervalHandle }) { + params.controlLoopInterval.clearInterval() } /** * Deactivate the plugin */ export async function deactivate() { - const { client } = await injeca.resolve({ client: { key: 'proj-airi:client' } as ProvidedKey<'proj-airi:client', Client, DependencyMap | undefined> }) + const { client } = await injeca.resolve({ client: { key: 'proj-airi:client' } as unknown as ProvidedBy }) + const { eventListeners } = await injeca.resolve({ eventListeners: { key: 'self:event-listeners' } as unknown as ProvidedBy }) + const { controlLoopInterval } = await injeca.resolve({ controlLoopInterval: { key: 'self:control-loop:interval:send' } as unknown as ProvidedBy }) - unregisterListeners() + unregisterListeners({ eventListeners, controlLoopInterval }) client?.disconnect() useLogger().log('AIRI deactivated!') } diff --git a/plugins/airi-plugin-vscode/src/types.ts b/plugins/airi-plugin-vscode/src/types.ts index 1da2126fe..1c4eb94b3 100644 --- a/plugins/airi-plugin-vscode/src/types.ts +++ b/plugins/airi-plugin-vscode/src/types.ts @@ -2,14 +2,29 @@ * Coding context information */ export interface CodingContext { - /** File information */ + /** + * File information + * + * @example { + * "path": "/home/neko/Git/github.com/moeru-ai/airi/package.json", + * "languageId": "json", + * "fileName": "/home/neko/Git/github.com/moeru-ai/airi/package.json" + * } + */ file: { path: string languageId: string fileName: string workspaceFolder?: string } - /** Cursor position */ + /** + * Cursor position + * + * { + * "line": 5, + * "character": 35 + * } + */ cursor: { line: number character: number @@ -25,7 +40,26 @@ export interface CodingContext { lineNumber: number text: string } - /** Context (previous and next N lines) */ + /** + * Context (previous and next N lines) + * + * @example { + * "before": [ + * "{", + * " \"name\": \"@proj-airi/root\",", + * " \"type\": \"module\",", + * " \"version\": \"0.8.1-beta.12\",", + * " \"private\": true," + * ], + * "after": [ + * " \"description\": \"LLM powered virtual character\",", + * " \"author\": {", + * " \"name\": \"Moeru AI Project AIRI Team\",", + * " \"email\": \"airi@moeru.ai\",", + * " \"url\": \"https://github.com/moeru-ai\"" + * ] + * } + */ context: { before: string[] after: string[] @@ -35,7 +69,11 @@ export interface CodingContext { branch: string isDirty: boolean } - /** Timestamp */ + /** + * Timestamp + * + * @example 1768584314898 + */ timestamp: number } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 486ed6dda..b7ab59508 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -127,8 +127,8 @@ catalogs: specifier: ^6.2.2 version: 6.2.2 injeca: - specifier: ^0.1.5 - version: 0.1.5 + specifier: ^0.1.7 + version: 0.1.7 is-network-error: specifier: ^1.3.0 version: 1.3.0 @@ -428,7 +428,7 @@ importers: version: 4.11.3 injeca: specifier: 'catalog:' - version: 0.1.5(@guiiai/logg@1.2.11)(error-stack-parser@2.1.4)(nanoid@5.1.6) + version: 0.1.7(@guiiai/logg@1.2.11)(error-stack-parser@2.1.4)(nanoid@5.1.6) postgres: specifier: ^3.4.7 version: 3.4.7 @@ -1011,8 +1011,8 @@ importers: specifier: 2.0.1-rc.5 version: 2.0.1-rc.5(crossws@0.4.1(srvx@0.9.8(patch_hash=f0151386fdcbcb6f53833cf8f66926ef2eb31b71a2782d6e0960dec832ef108d))) injeca: - specifier: ^0.1.5 - version: 0.1.5(@guiiai/logg@1.2.11)(error-stack-parser@2.1.4)(nanoid@5.1.6) + specifier: 'catalog:' + version: 0.1.7(@guiiai/logg@1.2.11)(error-stack-parser@2.1.4)(nanoid@5.1.6) jszip: specifier: ^3.10.1 version: 3.10.1 @@ -2969,7 +2969,7 @@ importers: version: 1.43.0 injeca: specifier: 'catalog:' - version: 0.1.5(@guiiai/logg@1.2.11)(error-stack-parser@2.1.4)(nanoid@5.1.6) + version: 0.1.7(@guiiai/logg@1.2.11)(error-stack-parser@2.1.4)(nanoid@5.1.6) nanoid: specifier: 'catalog:' version: 5.1.6 @@ -2991,7 +2991,7 @@ importers: version: 66.5.11 '@wxt-dev/module-vue': specifier: ^1.0.3 - version: 1.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 1.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vue-tsc: specifier: ^3.1.8 version: 3.2.1(typescript@5.9.3) @@ -12137,8 +12137,8 @@ packages: resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - injeca@0.1.5: - resolution: {integrity: sha512-fufRnru65A5mHQ8L7mZpmJ5FHOjoqJl9Cfwbv+rcclcYWzScxcWFNbAyrH5Fkor0qccHFxBp3+yRc8sNC1yZSQ==} + injeca@0.1.7: + resolution: {integrity: sha512-Nh166QCTJbshSNBTWGzdGNDkxMKvHALJfCW70zYJ9AF+4FDQLXRcjGJnaaazW7RZWLQsnWEvK5/sXMnexUjvBg==} peerDependencies: '@guiiai/logg': '>= 1' error-stack-parser: ^2.1.4 @@ -22467,6 +22467,12 @@ snapshots: vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vue: 3.5.25(typescript@5.9.3) + '@vitejs/plugin-vue@6.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.53 + vite: 8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vue: 3.5.25(typescript@5.9.3) + '@vitest/browser-playwright@4.0.16(bufferutil@4.1.0)(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)': dependencies: '@vitest/browser': 4.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16) @@ -23226,9 +23232,9 @@ snapshots: '@types/filesystem': 0.0.36 '@types/har-format': 1.2.16 - '@wxt-dev/module-vue@1.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@wxt-dev/module-vue@1.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@vitejs/plugin-vue': 6.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3)) + '@vitejs/plugin-vue': 6.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3)) wxt: 0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - vite @@ -26657,7 +26663,7 @@ snapshots: ini@4.1.3: {} - injeca@0.1.5(@guiiai/logg@1.2.11)(error-stack-parser@2.1.4)(nanoid@5.1.6): + injeca@0.1.7(@guiiai/logg@1.2.11)(error-stack-parser@2.1.4)(nanoid@5.1.6): dependencies: error-stack-parser: 2.1.4 nanoid: 5.1.6 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7fb6d65a4..ef0e641a4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -64,7 +64,7 @@ catalog: es-toolkit: 1.43.0 hono: 4.11.3 idb-keyval: ^6.2.2 - injeca: ^0.1.5 + injeca: ^0.1.7 is-network-error: ^1.3.0 nano-staged: ^0.9.0 nanoid: 5.1.6