feat: chat capture (@bridge participant, onDidChangeTextDocument), !stop command, chat snapshot scanner
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user