chore: remove the VSCode integration
autofix.ci / autofix (pull_request) Failing after 18m46s
CI / Lint (pull_request) Successful in 18m21s
CI / Build Test (stage-web) (pull_request) Failing after 12m34s
CI / Build Test (ui-loading-screens) (pull_request) Failing after 13m24s
CI / Build Test (ui-transitions) (pull_request) Successful in 28m30s
CI / Unit Test (pull_request) Failing after 17m51s
CI / Type Check (pull_request) Successful in 26m58s
CI / Check Provenance (pull_request) Failing after 1m6s
PR Triage Dispatch / Dispatch PR Triage (pull_request_target) Successful in 23s

The fork does not use the VSCode extension. The CI install also failed,
because the extension ran `tsx` from a `prepare` script and never
declared `tsx` as a dependency.

Remove both `integrations/vscode` packages, the release-vsix workflow,
the workspace glob, and the debug launch config. The host plugin was a
stub with no dependents.
This commit is contained in:
2026-09-14 20:37:32 +07:00
parent d1ce4962aa
commit d03b574fd1
24 changed files with 0 additions and 1011 deletions
-42
View File
@@ -1,42 +0,0 @@
name: Release VSCE & OVSX
on:
push:
tags:
- '@proj-airi/airi-plugin-vscode@v*'
- 'v*'
permissions:
contents: read
jobs:
release-vsix:
name: Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 26.7.0
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.2
- name: Configure npm registry
uses: actions/setup-node@v6
with:
# registry-url required. Learn more at
# https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
registry-url: 'https://registry.npmjs.org'
- run: bun install --frozen-lockfile
- run: bun run build:packages
- run: bun run --filter @proj-airi/airi-plugin-vscode build
- run: bun run --filter vscode-airi build
- run: bun run publish
working-directory: ./integrations/vscode/vscode-airi
env:
VSCE_TOKEN: ${{ secrets.VSCE_TOKEN }}
OVSX_TOKEN: ${{ secrets.OVSX_TOKEN }}
-11
View File
@@ -17,17 +17,6 @@
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "Debug VSCode Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/integrations/vscode/airi-plugin-vscode"
],
"outFiles": [
"${workspaceFolder}/integrations/vscode/airi-plugin-vscode/dist/**/*.cjs"
]
}
]
}
-34
View File
@@ -408,28 +408,6 @@
"zod": "catalog:",
},
},
"integrations/vscode/airi-plugin-vscode": {
"name": "@proj-airi/airi-plugin-vscode",
"version": "0.12.0-beta.5",
"devDependencies": {
"@proj-airi/plugin-sdk": "workspace:*",
},
},
"integrations/vscode/vscode-airi": {
"name": "vscode-airi",
"version": "0.12.0-beta.5",
"devDependencies": {
"@guiiai/logg": "catalog:",
"@moeru/std": "catalog:",
"@proj-airi/server-sdk": "workspace:*",
"@types/vscode": "catalog:",
"es-toolkit": "catalog:",
"injeca": "catalog:",
"nanoid": "catalog:",
"tinyexec": "catalog:",
"vscode-ext-gen": "catalog:",
},
},
"packages/audio": {
"name": "@proj-airi/audio",
"dependencies": {
@@ -3065,8 +3043,6 @@
"@proj-airi/airi-plugin-homeassistant": ["@proj-airi/airi-plugin-homeassistant@workspace:plugins/airi-plugin-homeassistant"],
"@proj-airi/airi-plugin-vscode": ["@proj-airi/airi-plugin-vscode@workspace:integrations/vscode/airi-plugin-vscode"],
"@proj-airi/airi-plugin-web-extension": ["@proj-airi/airi-plugin-web-extension@workspace:plugins/airi-plugin-web-extension"],
"@proj-airi/audio": ["@proj-airi/audio@workspace:packages/audio"],
@@ -3605,8 +3581,6 @@
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@types/vscode": ["@types/vscode@1.134.0", "", {}, "sha512-NDEu0hg4sF7+vvFsADsktqUJ6f80LHSZvVK2Ovo1XiQ0/VHck1O3zst+ZZyVA/uvz6vo6LcuoqU2q48YMqOwWw=="],
"@types/w3c-web-hid": ["@types/w3c-web-hid@1.0.7", "", {}, "sha512-/y97wBH7fYB5vKoDIn11O1ZDMNFLCAVqZ9af0OWDN7VSO2ClErEN2HlGbBuPgHBTUmZOVx1og2ZHX1U2gEJM4Q=="],
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
@@ -4939,8 +4913,6 @@
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"injeca": ["injeca@0.2.0", "", { "peerDependencies": { "@guiiai/logg": ">= 1", "error-stack-parser": "^2.1.4", "nanoid": ">=5" }, "optionalPeers": ["@guiiai/logg"] }, "sha512-uV++UZJQMx/wGzIFQzPB1/+3Xc3QQxZ0zvFZFyNlcSpbNqcn/8X7vGvRM2K9Nrj1qMsodQhAhCHP4GCMhXrDfQ=="],
"inspect-function": ["inspect-function@0.3.4", "", { "dependencies": { "inspect-parameters-declaration": "0.0.8", "magicli": "0.0.8", "split-skip": "0.0.1", "stringify-parameters": "0.0.4", "unpack-string": "0.0.2" }, "bin": { "inspect-function": "bin/magicli.js" } }, "sha512-s0RsbJqK/sNZ+U1mykGoTickog3ea1A9Qk4mXniogOBu4PgkkZ56elScO7QC/r8D94lhGmJ2NyDI1ipOA/uq/g=="],
"inspect-parameters-declaration": ["inspect-parameters-declaration@0.0.9", "", { "dependencies": { "magicli": "0.0.5", "split-skip": "0.0.2", "stringify-parameters": "0.0.4", "unpack-string": "0.0.2" }, "bin": { "inspect-parameters-declaration": "./bin/cli.js" } }, "sha512-c3jrKKA1rwwrsjdGMAo2hFWV0vNe3/RKHxpE/OBt41LP3ynOVI1qmgxpZYK5SQu3jtWCyaho8L7AZzCjJ4mEUw=="],
@@ -6367,10 +6339,6 @@
"vitest-browser-vue": ["vitest-browser-vue@2.1.0", "", { "dependencies": { "@vue/test-utils": "2.4.11" }, "peerDependencies": { "vitest": "^4.0.0-0", "vue": "^3.0.0" } }, "sha512-K3H/oxIOY4EjXx2bxwrLsPg4jTMvzpRW6Jb6T8XZRoxUvmDVwGkd8mua130F8GZezE7H5QYoXd/S8hYijw7j5g=="],
"vscode-airi": ["vscode-airi@workspace:integrations/vscode/vscode-airi"],
"vscode-ext-gen": ["vscode-ext-gen@1.6.0", "", { "dependencies": { "cac": "6.7.14", "tinyglobby": "0.2.17" }, "bin": { "vscode-ext-gen": "bin.mjs" } }, "sha512-oYg4QCx56hs1+EOVInhQMeuO4jKOCHuBrG9uppivGac8Cu9crJ7YKZg8bviDO60iX9IHqX7o5oRIV/Y+f96XwA=="],
"vscode-jsonrpc": ["vscode-jsonrpc@9.0.1", "", {}, "sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw=="],
"vscode-languageserver": ["vscode-languageserver@10.1.0", "", { "dependencies": { "vscode-languageserver-protocol": "3.18.2" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-9gEWpXkYGXoqG7pBnE8O8hx/yP7+Aabn4+peQ3KDicQv6qunHSWyLTud3OF0w4S2+HfDD+5HqYKiXQW9HAU6mA=="],
@@ -7239,8 +7207,6 @@
"vitepress/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "10.0.2", "@types/hast": "3.0.5" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
"vscode-ext-gen/cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"vue-i18n/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"vue-macros/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "2.3.5", "acorn": "8.18.0", "picomatch": "4.0.5", "webpack-virtual-modules": "0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
@@ -1,3 +0,0 @@
# @proj-airi/airi-plugin-vscode
Host-side plugin that bridges VS Code context and exposes UI contributions in Moeka.
@@ -1,33 +0,0 @@
{
"name": "@proj-airi/airi-plugin-vscode",
"type": "module",
"version": "0.12.0-beta.5",
"private": true,
"description": "Moeka host plugin that bridges VS Code context and UI",
"repository": {
"type": "git",
"url": "https://github.com/moeru-ai/airi.git",
"directory": "integrations/vscode/airi-plugin-vscode"
},
"exports": {
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
},
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"files": [
"README.md",
"dist",
"package.json"
],
"scripts": {
"dev": "bun run build",
"build": "tsdown",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@proj-airi/plugin-sdk": "workspace:*"
}
}
@@ -1,7 +0,0 @@
export function activate() {
// TODO: bind to vscode-airi bridge over control plane
}
export function deactivate() {
// TODO: clean up subscriptions
}
@@ -1,7 +0,0 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["src"]
}
@@ -1,7 +0,0 @@
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['src/index.ts'],
dts: true,
format: 'esm',
})
@@ -1,8 +0,0 @@
.github/**
.vscode/**
.vscode-test/**
scripts/**
src/**
test/**
tsconfig.json
tsdown.config.ts
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024-PRESENT Neko Ayaka
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -1,3 +0,0 @@
# Moeka VSCode Plugin
> Official VSCode extension for Moeka, streaming your current working at stuff back to Moeka.
@@ -1,81 +0,0 @@
{
"publisher": "proj-airi",
"name": "vscode-airi",
"displayName": "Moeka VSCode adapter",
"type": "module",
"version": "0.12.0-beta.5",
"private": true,
"description": "Adapter that connects VSCode into Project Moeka",
"repository": {
"type": "git",
"url": "https://github.com/moeru-ai/airi.git",
"directory": "integrations/vscode/vscode-airi"
},
"categories": [
"Other"
],
"preview": true,
"main": "./dist/extension.cjs",
"icon": "res/logo.jpg",
"engines": {
"vscode": "^1.116.0"
},
"activationEvents": [
"onStartupFinished"
],
"contributes": {
"commands": [
{
"command": "airi-vscode.enable",
"title": "Moeka: Enable"
},
{
"command": "airi-vscode.disable",
"title": "Moeka: Disable"
},
{
"command": "airi-vscode.status",
"title": "Moeka: Show Status"
}
],
"configuration": {
"title": "Moeka",
"properties": {
"airi-vscode.enabled": {
"type": "boolean",
"default": true,
"description": "Enable or disable the Moeka extension"
},
"airi-vscode.contextLines": {
"type": "number",
"default": 5,
"description": "Number of context lines to send (before and after current line)"
},
"airi-vscode.sendInterval": {
"type": "number",
"default": 3000,
"description": "Interval in milliseconds to send updates (0 for real-time)"
}
}
}
},
"scripts": {
"prepare": "bun run update",
"update": "tsx ./scripts/vscode-ext-gen.ts",
"dev": "tsdown --watch",
"build": "tsdown",
"publish": "tsx ./scripts/publish.ts",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@guiiai/logg": "catalog:",
"@moeru/std": "catalog:",
"@proj-airi/server-sdk": "workspace:*",
"@types/vscode": "catalog:",
"es-toolkit": "catalog:",
"injeca": "catalog:",
"nanoid": "catalog:",
"tinyexec": "catalog:",
"vscode-ext-gen": "catalog:"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

@@ -1,9 +0,0 @@
// https://github.com/unocss/unocss/blob/dba521e377887ed2b3b38dc86f36d4f292230ac8/packages-integrations/vscode/scripts/dev.ts
import { packageJSONForVSCode } from './shared'
async function run() {
await packageJSONForVSCode('airi-vscode')
}
run()
@@ -1,66 +0,0 @@
// https://github.com/unocss/unocss/blob/dba521e377887ed2b3b38dc86f36d4f292230ac8/packages-integrations/vscode/scripts/publish.ts
import process from 'node:process'
import { copyFile, readFile, rm, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { exec } from 'tinyexec'
import { packageJSONForVSCode } from './shared'
const dir = typeof __dirname === 'string' ? __dirname : dirname(fileURLToPath(import.meta.url))
const root = dirname(dir)
async function publish() {
const { restore, isPreview } = await packageJSONForVSCode('airi-vscode')
const pkgPath = join(root, 'package.json')
const rawJSON = await readFile(pkgPath, 'utf-8')
const pkg = JSON.parse(rawJSON)
if (isPreview)
pkg.preview = true
else
delete pkg.preview
await writeFile(pkgPath, JSON.stringify(pkg, null, 2), 'utf-8')
await rm(join(root, 'LICENSE'), { force: true }).catch(() => {})
await copyFile(join(root, '..', '..', '..', 'LICENSE'), join(root, 'LICENSE'))
try {
{
// eslint-disable-next-line no-console
console.log('\nPublish to VSCE...\n')
const vsceArgs = ['@vscode/vsce', 'publish', '--no-dependencies', '-p', process.env.VSCE_TOKEN!]
if (isPreview)
vsceArgs.push('--pre-release')
const execPublish = exec('pnpx', vsceArgs, { nodeOptions: { cwd: root } })
for await (const line of execPublish) {
// eslint-disable-next-line no-console
console.log(line)
}
}
{
// eslint-disable-next-line no-console
console.log('\nPublish to OVSE...\n')
const ovseArgs = ['ovsx', 'publish', '--no-dependencies', '-p', process.env.OVSX_TOKEN!]
if (isPreview)
ovseArgs.push('--pre-release')
const execPublish = exec('pnpx', ovseArgs, { nodeOptions: { cwd: root } })
for await (const line of execPublish) {
// eslint-disable-next-line no-console
console.log(line)
}
}
}
finally {
await restore()
}
}
publish()
@@ -1,94 +0,0 @@
// https://github.com/unocss/unocss/blob/dba521e377887ed2b3b38dc86f36d4f292230ac8/packages-integrations/vscode/scripts/dev.ts
import { readFile, writeFile } from 'node:fs/promises'
export async function packageJSONForVSCode(name: string) {
const json = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf-8'))
const originalName = json.name
const originalVersion = json.version
if (json.name !== name) {
json.name = name
await writeFile(new URL('../package.json', import.meta.url), `${JSON.stringify(json, null, 2)}\n`, 'utf-8')
}
const numericVersion = encodeNumericVersion(originalVersion)
if (json.version !== numericVersion.version) {
json.version = numericVersion.version
await writeFile(new URL('../package.json', import.meta.url), `${JSON.stringify(json, null, 2)}\n`, 'utf-8')
}
return {
json,
originalName,
originalVersion,
name,
version: numericVersion.version,
isPreview: numericVersion.preview,
restore: async () => {
json.name = originalName
json.version = originalVersion
await writeFile(new URL('../package.json', import.meta.url), `${JSON.stringify(json, null, 2)}\n`, 'utf-8')
},
}
}
// NOTICE: VSCE rejects prerelease identifiers, so we encode stage+sequence into a numeric-only patch bucket:
// encodedPatch = patch*10000 + stageBucket + sequence.
// stageBucket: alpha=1000, beta=2000, rc=3000, stable=9000.
// Examples:
// 0.8.0-alpha.6 -> 0.8.(0*10000+1000+6)=0.8.1006 (preview=true)
// 0.8.0-beta.1 -> 0.8.2001 (preview=true)
// 0.8.0 -> 0.8.(0*10000+9000)=0.8.9000 (preview=false)
// This keeps ordering: alpha < beta < rc < stable. Unknown prerelease tags default to alpha.
export function encodeNumericVersion(version: string) {
const match = version.match(/^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(-(?<pre>[0-9A-Z.-]+))?$/i)
if (!match || !match.groups)
throw new Error(`Invalid semver: ${version}`)
const major = Number.parseInt(match.groups.major, 10)
const minor = Number.parseInt(match.groups.minor, 10)
const patch = Number.parseInt(match.groups.patch, 10)
const prerelease = match.groups.pre
const multiplier = 10_000
const stageBuckets = {
alpha: 1_000,
beta: 2_000,
rc: 3_000,
stable: 9_000,
} as const
const { stage, sequence } = parsePrerelease(prerelease)
const maxSequence = (multiplier - 1) - stageBuckets[stage]
if (sequence > maxSequence) {
throw new Error(`Prerelease sequence overflow for ${stage}: ${sequence} exceeds limit ${maxSequence}`)
}
if (sequence < 0) {
throw new Error(`Prerelease sequence must be non-negative: ${sequence}`)
}
const encodedPatch = (patch * multiplier) + (stageBuckets[stage] ?? stageBuckets.alpha) + sequence
const encoded = `${major}.${minor}.${encodedPatch}`
return {
version: encoded,
preview: stage !== 'stable',
}
}
function parsePrerelease(prerelease?: string) {
if (!prerelease) {
return { stage: 'stable' as const, sequence: 0 }
}
const [stageRaw, sequenceRaw] = prerelease.split('.')
const stage = stageRaw === 'beta' || stageRaw === 'rc' || stageRaw === 'alpha' ? stageRaw : 'alpha'
const sequenceParsed = Number.parseInt(sequenceRaw ?? '', 10)
const sequence = Number.isFinite(sequenceParsed) ? sequenceParsed : 0
return { stage, sequence }
}
@@ -1,26 +0,0 @@
import { mkdir, rm, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { generate } from 'vscode-ext-gen'
import { packageJSONForVSCode } from './shared'
async function run() {
const { restore, json } = await packageJSONForVSCode('airi-vscode')
const generated = await generate(json, { extensionScope: 'airi-vscode' })
const url = fileURLToPath(new URL('.', import.meta.url))
const dir = dirname(url)
try {
await rm(join(dir, 'src', 'generated'), { force: true })
}
catch {
}
await mkdir(join(dir, 'src', 'generated'), { recursive: true })
await writeFile(join(dir, 'src', 'generated', 'meta.ts'), generated.dts, 'utf-8')
await restore()
}
run()
@@ -1,61 +0,0 @@
import type { WebSocketEventOptionalSource } from '@proj-airi/server-sdk'
import type { Events } from './types'
import { useLogger } from '@guiiai/logg'
import { ContextUpdateStrategy, Client as ServerClient } from '@proj-airi/server-sdk'
import { nanoid } from 'nanoid'
export class Client {
private client: ServerClient<Events> | null = null
async connect(): Promise<boolean> {
try {
this.client = new ServerClient<Events>({ name: 'proj-airi:plugin-vscode' })
await this.client.connect()
useLogger().log('Moeka connected to Server Channel')
return true
}
catch (error) {
useLogger().errorWithError('Failed to connect to Moeka Server Channel:', error)
return false
}
}
disconnect(): void {
if (this.client) {
this.client.close()
this.client = null
useLogger().log('Moeka disconnected')
}
}
private async send(event: WebSocketEventOptionalSource<Events>): Promise<void> {
if (!this.client) {
useLogger().warn('Cannot send event: not connected to Moeka Server Channel')
return
}
try {
await this.client.connect()
this.client.send(event)
}
catch (error) {
useLogger().errorWithError('Failed to send event to Moeka:', error)
}
}
async replaceContext(context: string): Promise<void> {
const id = nanoid()
this.send({ type: 'context:update', data: { strategy: ContextUpdateStrategy.ReplaceSelf, text: context, id, contextId: id } })
}
async appendContext(context: string): Promise<void> {
const id = nanoid()
this.send({ type: 'context:update', data: { strategy: ContextUpdateStrategy.AppendSelf, text: context, id, contextId: id } })
}
isConnected(): boolean {
return !!this.client
}
}
@@ -1,133 +0,0 @@
import type { CodingContext } from './types'
import { useLogger } from '@guiiai/logg'
import * as vscode from 'vscode'
/**
* Collector for coding context in VSCode
*/
export class ContextCollector {
constructor(
private readonly contextLines: number = 5,
) {}
/**
* Collect context from the current active editor
*/
async collect(editor: vscode.TextEditor): Promise<CodingContext | null> {
try {
const document = editor.document
const position = editor.selection.active
// File information
const file = {
path: document.uri.fsPath,
languageId: document.languageId,
fileName: document.fileName,
workspaceFolder: this.getWorkspaceFolder(document.uri),
}
// Cursor position
const cursor = {
line: position.line,
character: position.character,
}
// Selected text
const selection = editor.selection.isEmpty
? undefined
: {
text: document.getText(editor.selection),
start: {
line: editor.selection.start.line,
character: editor.selection.start.character,
},
end: {
line: editor.selection.end.line,
character: editor.selection.end.character,
},
}
// Current line
const currentLine = {
lineNumber: position.line,
text: document.lineAt(position.line).text,
}
// Context (N lines before and after)
const context = this.getContext(document, position.line)
// Git information (simplified, can be extended later)
const git = await this.getGitInfo(document.uri)
return {
file,
cursor,
selection,
currentLine,
context,
git,
timestamp: Date.now(),
}
}
catch (error) {
useLogger().errorWithError('Failed to collect context:', error)
return null
}
}
/**
* Get context before and after the current line
*/
private getContext(document: vscode.TextDocument, currentLine: number) {
const before: string[] = []
const after: string[] = []
// Get preceding lines
const startLine = Math.max(0, currentLine - this.contextLines)
for (let i = startLine; i < currentLine; i++) {
before.push(document.lineAt(i).text)
}
// Get following lines
const endLine = Math.min(document.lineCount - 1, currentLine + this.contextLines)
for (let i = currentLine + 1; i <= endLine; i++) {
after.push(document.lineAt(i).text)
}
return { before, after }
}
/**
* Get workspace folder path
*/
private getWorkspaceFolder(uri: vscode.Uri): string | undefined {
const folder = vscode.workspace.getWorkspaceFolder(uri)
return folder?.uri.fsPath
}
/**
* Get Git information (simplified)
*/
private async getGitInfo(uri: vscode.Uri): Promise<{ branch: string, isDirty: boolean } | undefined> {
try {
const gitExtension = vscode.extensions.getExtension('vscode.git')?.exports
if (!gitExtension)
return undefined
const git = gitExtension.getAPI(1)
const repo = git.getRepository(uri)
if (!repo)
return undefined
return {
branch: repo.state.HEAD?.name ?? 'unknown',
isDirty: repo.state.workingTreeChanges.length > 0,
}
}
catch {
return undefined
}
}
}
@@ -1,242 +0,0 @@
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, lifecycle } from 'injeca'
import { commands, window, workspace } from 'vscode'
import { Client } from './airi'
import { ContextCollector } from './context-collector'
interface IntervalHandle {
clearInterval: () => void
setInterval: (fn: () => void) => NodeJS.Timeout
}
/**
* Activate the plugin
*/
export async function activate(context: vscode.ExtensionContext) {
initLogger(LoggerLevel.Debug, LoggerFormat.Pretty)
useLogger().log('Moeka is activating...')
// Get the configuration
const config = workspace.getConfiguration('airi-vscode')
const isEnabled = config.get<boolean>('enabled', true)
const contextLines = config.get<number>('contextLines', 5)
const sendInterval = config.get<number>('sendInterval', 3000)
// Initialize
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, eventListeners, lifecycle, controlLoopInterval },
build: ({ dependsOn }) => setup({ ...dependsOn, isEnabled, sendInterval }),
})
injeca.invoke({
dependsOn: { extension },
callback: noop,
})
await injeca.start()
}
async function setup(params: {
client: Client
vscodeContext: vscode.ExtensionContext
contextCollector: ContextCollector
eventListeners: vscode.Disposable[]
lifecycle: Lifecycle
controlLoopInterval: IntervalHandle
isEnabled: boolean
sendInterval: number
}) {
// Connect to Airi Channel Server
if (params.isEnabled) {
const connected = await params.client.connect()
if (connected) {
window.showInformationMessage('Moeka Server Channel connected!')
}
else {
window.showWarningMessage('Moeka Server Channel connection failed!')
}
}
// Register commands
params.vscodeContext.subscriptions.push(
commands.registerCommand('airi-vscode.enable', async () => {
params.isEnabled = true
await params.client.connect()
await registerListeners({ ...params })
window.showInformationMessage('Moeka enabled!')
}),
commands.registerCommand('airi-vscode.disable', () => {
params.isEnabled = false
unregisterListeners({ eventListeners: params.eventListeners, controlLoopInterval: params.controlLoopInterval })
params.client.disconnect()
window.showInformationMessage('Moeka disabled!')
}),
commands.registerCommand('airi-vscode.status', () => {
const status = params.isEnabled && params.client ? 'Connected' : 'Disconnected'
window.showInformationMessage(`Moeka Server Channel status: ${status}.`)
}),
)
// Register event listeners if enabled
if (params.isEnabled) {
await registerListeners({ ...params })
}
useLogger().log('Moeka activated successfully')
}
/**
* Register event listeners for file save and editor switch
*/
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
params.eventListeners.push(
workspace.onDidSaveTextDocument(async (document) => {
const editor = window.activeTextEditor
if (editor && editor.document === document) {
const ctx = await params.contextCollector.collect(editor)
if (!ctx)
return
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
params.eventListeners.push(
window.onDidChangeActiveTextEditor(async (editor) => {
if (!editor) {
return
}
const ctx = await params.contextCollector.collect(editor)
if (!ctx) {
return
}
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({ ...params })
}
}
/**
* Unregister all event listeners
*/
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
lifecycle: Lifecycle
client: Client
controlLoopInterval: IntervalHandle
isEnabled: boolean
sendInterval: number
}) {
stopMonitoring({ controlLoopInterval: params.controlLoopInterval })
params.controlLoopInterval.setInterval(async () => {
if (!params.isEnabled)
return
const editor = window.activeTextEditor
if (!editor)
return
const ctx = await params.contextCollector.collect(editor)
if (!ctx)
return
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(params: { controlLoopInterval: IntervalHandle }) {
params.controlLoopInterval.clearInterval()
}
/**
* Deactivate the plugin
*/
export async function deactivate() {
const { client } = await injeca.resolve({ client: { key: 'proj-airi:client' } as unknown as ProvidedBy<Client> })
const { eventListeners } = await injeca.resolve({ eventListeners: { key: 'self:event-listeners' } as unknown as ProvidedBy<vscode.Disposable[]> })
const { controlLoopInterval } = await injeca.resolve({ controlLoopInterval: { key: 'self:control-loop:interval:send' } as unknown as ProvidedBy<IntervalHandle> })
unregisterListeners({ eventListeners, controlLoopInterval })
client?.disconnect()
useLogger().log('Moeka deactivated!')
}
@@ -1,86 +0,0 @@
/**
* Coding context information
*/
export interface CodingContext {
/**
* 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
*
* {
* "line": 5,
* "character": 35
* }
*/
cursor: {
line: number
character: number
}
/** Selected text */
selection?: {
text: string
start: { line: number, character: number }
end: { line: number, character: number }
}
/** Current line */
currentLine: {
lineNumber: number
text: string
}
/**
* 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\": \"Moeka Team\",",
* " \"email\": \"airi@moeru.ai\",",
* " \"url\": \"https://github.com/moeru-ai\""
* ]
* }
*/
context: {
before: string[]
after: string[]
}
/** Git information */
git?: {
branch: string
isDirty: boolean
}
/**
* Timestamp
*
* @example 1768584314898
*/
timestamp: number
}
/**
* Event types sent to Moeka
*/
export interface Events {
type: 'coding:context' | 'coding:save' | 'coding:switch-file'
data: CodingContext
}
@@ -1,22 +0,0 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"target": "ESNEXT",
"lib": ["ESNEXT"],
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"types": [
"node"
],
"strict": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
@@ -1,14 +0,0 @@
import { defineConfig } from 'tsdown'
export default defineConfig([
{
entry: ['./src/extension.ts'],
format: 'cjs',
platform: 'node',
external: ['vscode'],
sourcemap: true,
clean: true,
dts: false,
inlineOnly: false,
},
])
-1
View File
@@ -90,7 +90,6 @@
"packages/*",
"plugins/*",
"integrations/*",
"integrations/vscode/*",
"services/*",
"examples/*",
"docs",