Skip to content

Commit 8c43127

Browse files
feat(plugin): VSCode extension (#717)
Co-authored-by: Neko <neko@ayaka.moe>
1 parent c7cdb47 commit 8c43127

File tree

10 files changed

+917
-424
lines changed

10 files changed

+917
-424
lines changed

.vscode/launch.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@
2525
"presentation": {
2626
"hidden": true
2727
}
28+
},
29+
{
30+
"name": "Debug VSCode Extension",
31+
"type": "extensionHost",
32+
"request": "launch",
33+
"args": [
34+
"--extensionDevelopmentPath=${workspaceFolder}/plugins/airi-plugin-vscode"
35+
],
36+
"outFiles": [
37+
"${workspaceFolder}/plugins/airi-plugin-vscode/dist/**/*.js"
38+
],
39+
"preLaunchTask": "npm: build - plugins/airi-plugin-vscode"
2840
}
2941
],
3042
"compounds": [

packages/server-shared/src/types/websocket/events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export interface WebSocketEvents<C = undefined> {
5959
'input:voice': {
6060
audio: ArrayBuffer
6161
} & Partial<WithInputSource<'browser' | 'discord'>>
62+
'vscode:context': C
6263
}
6364

6465
export type WebSocketEvent<C = undefined> = {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"publisher": "airi",
3+
"name": "@proj-airi/airi-plugin-vscode",
4+
"displayName": "Airi",
5+
"version": "0.1.0",
6+
"description": "VSCode extension that shares your coding context with Airi",
7+
"categories": [
8+
"Other"
9+
],
10+
"main": "./dist/extension.js",
11+
"engines": {
12+
"vscode": "^1.85.0"
13+
},
14+
"activationEvents": [
15+
"onStartupFinished"
16+
],
17+
"contributes": {
18+
"commands": [
19+
{
20+
"command": "airi.enable",
21+
"title": "AIRI: Enable"
22+
},
23+
{
24+
"command": "airi.disable",
25+
"title": "AIRI: Disable"
26+
},
27+
{
28+
"command": "airi.status",
29+
"title": "AIRI: Show Status"
30+
}
31+
],
32+
"configuration": {
33+
"title": "AIRI",
34+
"properties": {
35+
"airi.enabled": {
36+
"type": "boolean",
37+
"default": true,
38+
"description": "Enable Airi companion"
39+
},
40+
"airi.contextLines": {
41+
"type": "number",
42+
"default": 5,
43+
"description": "Number of context lines to send (before and after current line)"
44+
},
45+
"airi.sendInterval": {
46+
"type": "number",
47+
"default": 3000,
48+
"description": "Interval in milliseconds to send updates (0 for real-time)"
49+
}
50+
}
51+
}
52+
},
53+
"scripts": {
54+
"vscode:prepublish": "pnpm run build",
55+
"build": "tsdown",
56+
"dev": "tsdown --watch",
57+
"typecheck": "tsc --noEmit"
58+
},
59+
"dependencies": {
60+
"@guiiai/logg": "catalog:",
61+
"@proj-airi/server-sdk": "workspace:*"
62+
},
63+
"devDependencies": {
64+
"@types/node": "^24.10.0",
65+
"@types/vscode": "^1.85.0",
66+
"tsdown": "^0.15.2",
67+
"typescript": "^5.3.0"
68+
}
69+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { AiriEvent } from './types'
2+
3+
import { useLogger } from '@guiiai/logg'
4+
import { Client } from '@proj-airi/server-sdk'
5+
6+
/**
7+
* Airi Channel Server Client
8+
*/
9+
export class AiriClient {
10+
private client: Client<AiriEvent> | null = null
11+
12+
/**
13+
* Connect to Channel Server
14+
*/
15+
async connect(): Promise<boolean> {
16+
try {
17+
this.client = new Client({ name: 'proj-airi:plugin-vscode' })
18+
19+
useLogger().log('Airi companion connected to Channel Server')
20+
return true
21+
}
22+
catch (error) {
23+
useLogger().errorWithError('Failed to connect to Airi Channel Server:', error)
24+
return false
25+
}
26+
}
27+
28+
/**
29+
* Disconnect from Channel Server
30+
*/
31+
disconnect(): void {
32+
if (this.client) {
33+
this.client.close()
34+
this.client = null
35+
useLogger().log('Airi companion disconnected')
36+
}
37+
}
38+
39+
/**
40+
* Send event to Airi
41+
*/
42+
sendEvent(event: AiriEvent): void {
43+
if (!this.client) {
44+
useLogger().warn('Cannot send event: not connected to Airi Channel Server')
45+
return
46+
}
47+
48+
try {
49+
// Send event to Airi
50+
this.client.send({
51+
type: 'vscode:context',
52+
data: event,
53+
})
54+
55+
useLogger().log(`Sent event to Airi: ${event.type}`, event)
56+
}
57+
catch (error) {
58+
useLogger().errorWithError('Failed to send event to Airi:', error)
59+
}
60+
}
61+
62+
/**
63+
* Is connected to Channel Server
64+
*/
65+
isConnected(): boolean {
66+
return !!this.client
67+
}
68+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import type { CodingContext } from './types'
2+
3+
import { useLogger } from '@guiiai/logg'
4+
5+
import * as vscode from 'vscode'
6+
7+
/**
8+
* Collector for coding context in VSCode
9+
*/
10+
export class ContextCollector {
11+
constructor(
12+
private readonly contextLines: number = 5,
13+
) {}
14+
15+
/**
16+
* Collect context from the current active editor
17+
*/
18+
async collect(editor: vscode.TextEditor): Promise<CodingContext | null> {
19+
try {
20+
const document = editor.document
21+
const position = editor.selection.active
22+
23+
// File information
24+
const file = {
25+
path: document.uri.fsPath,
26+
languageId: document.languageId,
27+
fileName: document.fileName,
28+
workspaceFolder: this.getWorkspaceFolder(document.uri),
29+
}
30+
31+
// Cursor position
32+
const cursor = {
33+
line: position.line,
34+
character: position.character,
35+
}
36+
37+
// Selected text
38+
const selection = editor.selection.isEmpty
39+
? undefined
40+
: {
41+
text: document.getText(editor.selection),
42+
start: {
43+
line: editor.selection.start.line,
44+
character: editor.selection.start.character,
45+
},
46+
end: {
47+
line: editor.selection.end.line,
48+
character: editor.selection.end.character,
49+
},
50+
}
51+
52+
// Current line
53+
const currentLine = {
54+
lineNumber: position.line,
55+
text: document.lineAt(position.line).text,
56+
}
57+
58+
// Context (N lines before and after)
59+
const context = this.getContext(document, position.line)
60+
61+
// Git information (simplified, can be extended later)
62+
const git = await this.getGitInfo(document.uri)
63+
64+
return {
65+
file,
66+
cursor,
67+
selection,
68+
currentLine,
69+
context,
70+
git,
71+
timestamp: Date.now(),
72+
}
73+
}
74+
catch (error) {
75+
useLogger().errorWithError('Failed to collect context:', error)
76+
return null
77+
}
78+
}
79+
80+
/**
81+
* Get context before and after the current line
82+
*/
83+
private getContext(document: vscode.TextDocument, currentLine: number) {
84+
const before: string[] = []
85+
const after: string[] = []
86+
87+
// Get preceding lines
88+
const startLine = Math.max(0, currentLine - this.contextLines)
89+
for (let i = startLine; i < currentLine; i++) {
90+
before.push(document.lineAt(i).text)
91+
}
92+
93+
// Get following lines
94+
const endLine = Math.min(document.lineCount - 1, currentLine + this.contextLines)
95+
for (let i = currentLine + 1; i <= endLine; i++) {
96+
after.push(document.lineAt(i).text)
97+
}
98+
99+
return { before, after }
100+
}
101+
102+
/**
103+
* Get workspace folder path
104+
*/
105+
private getWorkspaceFolder(uri: vscode.Uri): string | undefined {
106+
const folder = vscode.workspace.getWorkspaceFolder(uri)
107+
return folder?.uri.fsPath
108+
}
109+
110+
/**
111+
* Get Git information (simplified)
112+
*/
113+
private async getGitInfo(uri: vscode.Uri): Promise<{ branch: string, isDirty: boolean } | undefined> {
114+
try {
115+
const gitExtension = vscode.extensions.getExtension('vscode.git')?.exports
116+
if (!gitExtension)
117+
return undefined
118+
119+
const git = gitExtension.getAPI(1)
120+
const repo = git.getRepository(uri)
121+
if (!repo)
122+
return undefined
123+
124+
return {
125+
branch: repo.state.HEAD?.name ?? 'unknown',
126+
isDirty: repo.state.workingTreeChanges.length > 0,
127+
}
128+
}
129+
catch {
130+
return undefined
131+
}
132+
}
133+
}

0 commit comments

Comments
 (0)