feat: chat capture (@bridge participant, onDidChangeTextDocument), !stop command, chat snapshot scanner

This commit is contained in:
2026-03-07 14:45:44 +09:00
parent d44b4c2f77
commit 35ee916440
3 changed files with 230 additions and 5 deletions

View File

@@ -2,13 +2,14 @@
"name": "gravity-bridge",
"displayName": "Gravity Bridge",
"description": "Antigravity ↔ Discord 브리지 연동 확장",
"version": "0.1.0",
"version": "0.2.0",
"publisher": "variet",
"engines": {
"vscode": "^1.80.0"
"vscode": "^1.109.0"
},
"categories": [
"Other"
"Other",
"Chat"
],
"activationEvents": [
"onStartupFinished"
@@ -19,11 +20,20 @@
"watch": "tsc -watch -p ./"
},
"devDependencies": {
"@types/vscode": "^1.80.0",
"@types/node": "^20.0.0",
"@types/vscode": "^1.109.0",
"typescript": "^5.3.0"
},
"contributes": {
"chatParticipants": [
{
"id": "gravity-bridge.bridge",
"name": "bridge",
"fullName": "Gravity Bridge",
"description": "대화 히스토리를 Discord로 전송 + AI 제어",
"isSticky": false
}
],
"commands": [
{
"command": "gravityBridge.start",
@@ -57,7 +67,7 @@
"gravityBridge.projectName": {
"type": "string",
"default": "",
"description": "프로젝트 이름 (기본: 워크스페이스 폴더명, 예: gravity_control)"
"description": "프로젝트 이름 (기본: git remote 레포명)"
}
}
}

View File

@@ -97,6 +97,26 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand('gravityBridge.reject', () => handleManualAction(false)),
);
// Chat document change listener — captures AI text responses
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument((event) => {
handleChatDocumentChange(event);
})
);
// Register @bridge Chat Participant for history relay
try {
const participant = vscode.chat.createChatParticipant(
'gravity-bridge.bridge',
bridgeChatHandler
);
participant.iconPath = new vscode.ThemeIcon('radio-tower');
context.subscriptions.push(participant);
console.log('Gravity Bridge: @bridge chat participant registered');
} catch (err) {
console.log('Gravity Bridge: chat participant API not available (OK)');
}
// Auto-watch brain/ for new conversations → auto-register
watchBrainForNewSessions();
@@ -220,6 +240,19 @@ async function handleCommand(filePath: string) {
const text = command.text.trim();
console.log(`Gravity Bridge [${projectName}]: command — "${text.substring(0, 50)}"`);
// Special command: !stop — cancel AI work
if (text === '!stop') {
try {
await vscode.commands.executeCommand('workbench.action.chat.stop');
vscode.window.showWarningMessage(`⏹️ [${projectName}] AI 작업 중지됨`);
} catch {
vscode.window.showErrorMessage('AI 중지 명령 실행 실패');
}
command.consumed = true;
fs.writeFileSync(filePath, JSON.stringify(command, null, 2), 'utf-8');
return;
}
// Special command: auto-approve toggle
if (text === '!auto on' || text === '!auto off') {
const enabled = text === '!auto on';
@@ -503,6 +536,134 @@ function watchBrainForNewSessions() {
}
}
/**
* Monitor text document changes for chat panel content.
* VS Code chat documents have special URI schemes (vscode-chat-response, etc.).
* We capture significant changes and relay to Discord.
*/
let lastChatContent = '';
let chatDebounceTimer: NodeJS.Timeout | null = null;
function handleChatDocumentChange(event: vscode.TextDocumentChangeEvent) {
const doc = event.document;
const scheme = doc.uri.scheme;
// Log ALL schemes to discover chat-related ones (debug mode)
if (scheme !== 'file' && scheme !== 'git' && scheme !== 'output' &&
scheme !== 'vscode-userdata' && scheme !== 'untitled') {
console.log(`Gravity Bridge [${projectName}]: doc change scheme="${scheme}" uri="${doc.uri.toString().substring(0, 80)}"`);
}
// Capture chat-related documents
// Known chat schemes: vscode-chat-response, vscode-copilot-chat, etc.
const isChatDoc = scheme.includes('chat') || scheme.includes('copilot') ||
scheme.includes('notebook') || doc.uri.path.includes('chat');
if (!isChatDoc) { return; }
const content = doc.getText();
if (!content || content === lastChatContent) { return; }
// Debounce: wait 2s for content to stabilize (AI streams text)
if (chatDebounceTimer) { clearTimeout(chatDebounceTimer); }
chatDebounceTimer = setTimeout(() => {
const finalContent = doc.getText();
if (finalContent && finalContent !== lastChatContent && finalContent.length > 20) {
lastChatContent = finalContent;
writeChatSnapshot(finalContent);
}
}, 2000);
}
/**
* Write a chat content snapshot to bridge for the bot to relay.
*/
function writeChatSnapshot(content: string) {
const snapshotDir = path.join(bridgePath, 'chat_snapshots');
if (!fs.existsSync(snapshotDir)) {
fs.mkdirSync(snapshotDir, { recursive: true });
}
const id = `chat-${Date.now()}`;
const filePath = path.join(snapshotDir, `${id}.json`);
const data = {
id,
project_name: projectName,
content: content.substring(0, 4000), // Limit size
timestamp: Date.now() / 1000,
};
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
console.log(`Gravity Bridge [${projectName}]: chat snapshot written (${content.length} chars)`);
}
/**
* @bridge Chat Participant handler.
* Reads conversation history and sends to Discord via bridge.
*/
const bridgeChatHandler: vscode.ChatRequestHandler = async (
request: vscode.ChatRequest,
context: vscode.ChatContext,
stream: vscode.ChatResponseStream,
token: vscode.CancellationToken
) => {
const command = request.prompt.trim().toLowerCase();
if (command === 'stop' || command === '중지') {
// Cancel current AI work
try {
await vscode.commands.executeCommand('workbench.action.chat.stop');
stream.markdown('⏹️ AI 작업 중지 요청을 보냈습니다.');
} catch {
stream.markdown('⚠️ 중지 명령을 실행할 수 없습니다.');
}
return;
}
// Collect conversation history
const historyLines: string[] = [];
historyLines.push(`# 대화 히스토리 (${projectName})\n`);
for (const entry of context.history) {
if (entry instanceof vscode.ChatRequestTurn) {
historyLines.push(`## 👤 사용자\n${entry.prompt}\n`);
} else if (entry instanceof vscode.ChatResponseTurn) {
let responseText = '';
for (const part of entry.response) {
if (part instanceof vscode.ChatResponseMarkdownPart) {
responseText += part.value.value;
}
}
if (responseText) {
historyLines.push(`## 🤖 AI\n${responseText}\n`);
}
}
}
if (historyLines.length <= 1) {
stream.markdown('대화 히스토리가 비어있습니다. AI와 대화를 먼저 진행한 후 `@bridge`를 호출하세요.');
return;
}
// Write to bridge for Discord relay
const fullHistory = historyLines.join('\n');
const cmdId = `bridge-history-${Date.now()}`;
const cmdPath = path.join(bridgePath, 'commands', `${cmdId}.json`);
const data = {
id: cmdId,
project_name: projectName,
text: `[HISTORY]\n${fullHistory}`,
timestamp: Date.now() / 1000,
consumed: false,
};
fs.writeFileSync(cmdPath, JSON.stringify(data, null, 2), 'utf-8');
stream.markdown(`✅ 대화 히스토리 (${context.history.length}개 턴)를 Discord에 전송했습니다.`);
console.log(`Gravity Bridge [${projectName}]: sent ${context.history.length} turns to Discord`);
};
export function deactivate() {
stopBridge();
}