chore: bump version to 2.80.27 and update core features

This commit is contained in:
g1nation
2026-05-09 01:16:12 +09:00
parent 5ffb472d22
commit 3220a126fd
41 changed files with 4457 additions and 72 deletions
@@ -0,0 +1,121 @@
import * as vscode from 'vscode';
import { ApprovalQueue, Approval } from './approvalQueue';
/**
* A small webview view that surfaces the currently pending approval, separate
* from the chat. The provider is intentionally thin: state lives in
* ApprovalQueue, this class only renders + relays button clicks.
*
* Mirroring the existing sidebar.html + media/ separation pattern would be
* appropriate once the panel grows, but the current UI is small enough
* (~40 lines of HTML) that an inline template keeps the diff focused.
*/
export class ApprovalPanelProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'g1nation-approval-panel';
private _view?: vscode.WebviewView;
private _subscription?: vscode.Disposable;
constructor(
private readonly _extensionUri: vscode.Uri,
private readonly _queue: ApprovalQueue
) {}
public resolveWebviewView(view: vscode.WebviewView): void {
this._view = view;
view.webview.options = { enableScripts: true, localResourceRoots: [this._extensionUri] };
view.webview.html = this._render(this._queue.current());
view.webview.onDidReceiveMessage((msg: { type: string; id?: string }) => {
if (msg?.type === 'approve') void this._queue.approve(msg.id);
else if (msg?.type === 'reject') void this._queue.reject(msg.id);
else if (msg?.type === 'refresh') view.webview.html = this._render(this._queue.current());
});
this._subscription?.dispose();
this._subscription = this._queue.onChange(() => {
if (this._view) this._view.webview.html = this._render(this._queue.current());
});
view.onDidDispose(() => {
this._subscription?.dispose();
this._subscription = undefined;
this._view = undefined;
});
}
/** Bring the panel into focus; used by the status bar badge. */
public focus(): void {
void vscode.commands.executeCommand(`${ApprovalPanelProvider.viewType}.focus`);
}
private _render(approval: Approval | null): string {
const csp = `default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline';`;
const empty = !approval;
const body = empty ? this._renderEmpty() : this._renderApproval(approval as Approval);
return `<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="${csp}">
<style>
body { font-family: var(--vscode-font-family); font-size: 12px; padding: 12px; color: var(--vscode-foreground); }
.empty { color: var(--vscode-descriptionForeground); padding: 24px 8px; text-align: center; }
.card { border: 1px solid var(--vscode-panel-border); border-radius: 6px; padding: 12px; margin-bottom: 8px; background: var(--vscode-editor-background); }
.title { font-weight: 600; margin-bottom: 4px; }
.summary { color: var(--vscode-descriptionForeground); margin-bottom: 8px; font-size: 11px; }
.files { margin: 8px 0; padding: 6px 8px; background: var(--vscode-textCodeBlock-background); border-radius: 4px; max-height: 160px; overflow-y: auto; }
.files li { font-family: var(--vscode-editor-font-family); font-size: 11px; line-height: 1.6; word-break: break-all; list-style: none; }
.files .badge { display: inline-block; min-width: 56px; padding: 1px 6px; border-radius: 3px; font-size: 10px; margin-right: 6px; text-align: center; background: var(--vscode-badge-background); color: var(--vscode-badge-foreground); }
.actions { display: flex; gap: 6px; margin-top: 10px; }
button { flex: 1; padding: 6px 10px; border: 1px solid var(--vscode-button-border, transparent); border-radius: 4px; cursor: pointer; font-size: 12px; }
button.approve { background: var(--vscode-button-background); color: var(--vscode-button-foreground); }
button.approve:hover { background: var(--vscode-button-hoverBackground); }
button.reject { background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); }
button.reject:hover { background: var(--vscode-button-secondaryHoverBackground); }
</style>
</head>
<body>
${body}
<script>
const vscode = acquireVsCodeApi();
for (const btn of document.querySelectorAll('button[data-action]')) {
btn.addEventListener('click', () => {
vscode.postMessage({ type: btn.dataset.action, id: btn.dataset.id });
});
}
</script>
</body>
</html>`;
}
private _renderEmpty(): string {
return `<div class="empty">대기 중인 승인이 없습니다.</div>`;
}
private _renderApproval(a: Approval): string {
const filesHtml = a.files.length === 0
? '<li style="color: var(--vscode-descriptionForeground);">파일 변경 없음</li>'
: a.files.map(f => `<li><span class="badge">변경</span>${this._escape(f)}</li>`).join('');
const elapsed = Math.max(0, Math.floor((Date.now() - a.createdAt) / 1000));
return `
<div class="card">
<div class="title">${this._escape(a.title)}</div>
<div class="summary">${this._escape(a.summary)} · ${elapsed}초 전</div>
<ul class="files">${filesHtml}</ul>
<div class="actions">
<button class="approve" data-action="approve" data-id="${this._escape(a.id)}">승인</button>
<button class="reject" data-action="reject" data-id="${this._escape(a.id)}">거부</button>
</div>
</div>`;
}
private _escape(s: string): string {
return String(s).replace(/[&<>"']/g, ch => (
ch === '&' ? '&amp;' :
ch === '<' ? '&lt;' :
ch === '>' ? '&gt;' :
ch === '"' ? '&quot;' : '&#39;'
));
}
}
+129
View File
@@ -0,0 +1,129 @@
import * as vscode from 'vscode';
import { logError, logInfo } from '../../utils';
/**
* Pending-approval coordination for ConnectAI.
*
* Why this module exists:
* - The agent already has a transaction-based "dry run" approval flow
* (see agent.ts:2362, transactionManager.commit/rollback). The decision
* UI lives inside the chat as an inline box, which is fine for a single
* prompt but misses the broader problem: the user wants a stable place
* to see "what is the agent waiting for me to approve?", with a status
* bar badge that pulls them in.
*
* - Connect_origin solves this with a queue of pending actions in a
* dashboard. ConnectAI today only ever has a single active transaction,
* so we model the queue as **0..1 current approval** to keep the surface
* small. Extending to N approvals later is a list change inside this
* module — no consumer needs to switch shapes.
*
* Wiring (read-only summary):
* agent.ts (dryRun) ──enqueue──▶ ApprovalQueue ──onChange──▶ ApprovalPanelProvider (webview)
* ──onChange──▶ ApprovalStatusBar (badge)
* webview button ──approve/reject──▶ ApprovalQueue ──invokes callback──▶ agent.approveTransaction()
*/
export type ApprovalKind = 'transaction' | 'file-write' | 'file-create' | 'file-delete' | 'command';
export interface Approval {
id: string;
kind: ApprovalKind;
title: string;
summary: string;
files: string[];
createdAt: number;
}
export interface ApprovalCallbacks {
approve: () => Promise<void> | void;
reject: () => Promise<void> | void;
}
interface ApprovalEntry {
approval: Approval;
callbacks: ApprovalCallbacks;
}
export class ApprovalQueue {
private _current: ApprovalEntry | null = null;
private readonly _emitter = new vscode.EventEmitter<void>();
/** Fires whenever the current approval changes (set / approved / rejected / cleared). */
public readonly onChange = this._emitter.event;
/**
* Replace the currently pending approval, if any. The previous approval is
* silently dropped — its callbacks are NOT invoked. This matches the
* agent's transaction model: the new dry-run pre-empts whatever was waiting.
*/
enqueue(approval: Approval, callbacks: ApprovalCallbacks): void {
if (this._current) {
logInfo('Approval pre-empted by newer pending approval.', {
droppedId: this._current.approval.id,
newId: approval.id,
});
}
this._current = { approval, callbacks };
logInfo('Approval enqueued.', { id: approval.id, kind: approval.kind, fileCount: approval.files.length });
this._emitter.fire();
}
/** Returns the currently pending approval, or null. */
current(): Approval | null {
return this._current?.approval ?? null;
}
/** 0 or 1 with the current model — future-proof for a real queue. */
pendingCount(): number {
return this._current ? 1 : 0;
}
/**
* Approve the pending entry whose id matches. If `id` is omitted, approve
* the current entry. Mismatched ids are ignored to avoid stale-button
* double-fire from the webview.
*/
async approve(id?: string): Promise<void> {
const entry = this._take(id);
if (!entry) return;
try {
await entry.callbacks.approve();
} catch (e: any) {
logError('Approval approve callback threw.', { id: entry.approval.id, error: e?.message ?? String(e) });
}
}
async reject(id?: string): Promise<void> {
const entry = this._take(id);
if (!entry) return;
try {
await entry.callbacks.reject();
} catch (e: any) {
logError('Approval reject callback threw.', { id: entry.approval.id, error: e?.message ?? String(e) });
}
}
/** Clear without firing callbacks (used by host on shutdown). */
clear(): void {
if (!this._current) return;
this._current = null;
this._emitter.fire();
}
dispose(): void {
this._current = null;
this._emitter.dispose();
}
private _take(id?: string): ApprovalEntry | null {
if (!this._current) return null;
if (id !== undefined && id !== this._current.approval.id) {
logInfo('Approval id mismatch — ignoring.', { requested: id, current: this._current.approval.id });
return null;
}
const entry = this._current;
this._current = null;
this._emitter.fire();
return entry;
}
}
@@ -0,0 +1,41 @@
import * as vscode from 'vscode';
import { ApprovalQueue } from './approvalQueue';
/**
* Status-bar badge that pulses while an approval is pending. Clicking it
* focuses the Approval Panel webview view. Hidden when nothing is pending.
*
* Lives separately from `src/core/statusBar.ts` (agent run-status indicator)
* because the two states change for completely different reasons and the
* single-item statusBarManager would lose the agent-status during a long
* dry-run review otherwise.
*/
export class ApprovalStatusBar implements vscode.Disposable {
private readonly _item: vscode.StatusBarItem;
private readonly _sub: vscode.Disposable;
public static readonly focusCommand = 'g1nation.approval.focus';
constructor(private readonly _queue: ApprovalQueue) {
this._item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 95);
this._item.command = ApprovalStatusBar.focusCommand;
this._item.tooltip = 'Astra: 승인 대기 중인 작업이 있습니다. 클릭해서 검토하세요.';
this._sub = this._queue.onChange(() => this._refresh());
this._refresh();
}
private _refresh(): void {
const count = this._queue.pendingCount();
if (count === 0) {
this._item.hide();
return;
}
this._item.text = `$(warning) 승인 대기 ${count}`;
this._item.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
this._item.show();
}
dispose(): void {
this._sub.dispose();
this._item.dispose();
}
}