feat(vscode): auto-refresh read-only agent view (#2832)

This commit is contained in:
Hunter Bown
2026-06-05 22:21:06 -07:00
committed by GitHub
parent e5974aa850
commit 50b773f1de
10 changed files with 197 additions and 59 deletions
+5 -3
View File
@@ -72,9 +72,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
points. The extension now renders those restore points read-only in its Agent
View, and thread summaries include read-only workspace and branch metadata so
the VS Code Agent View can show when a thread or agent lane is on another
branch. This answers the VS Code GUI lane without exposing chat webviews,
inline edits, or retry/undo/restore runtime mutation endpoints yet (#461,
#462, #480, #1217, #2341, #1584, #2327, #2580, #2808). Thanks @AiurArtanis
branch. Agent View and restore-point data now auto-refresh on a configurable
read-only interval so branch/workspace changes become visible without a
manual refresh. This answers the VS Code GUI lane without exposing chat
webviews, inline edits, or retry/undo/restore runtime mutation endpoints yet
(#461, #462, #480, #1217, #2341, #1584, #2327, #2580, #2808). Thanks @AiurArtanis
for the Agent View prompt, @lbcheng888 for the earlier scaffold, @gaord for
the GUI runtime API direction, @douglarek, @caeserchen, and @nightt5879 for
the branch visibility trail, and @BigBenLabs, @lzx1545642258, @yangdaowan,
+5 -3
View File
@@ -72,9 +72,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
points. The extension now renders those restore points read-only in its Agent
View, and thread summaries include read-only workspace and branch metadata so
the VS Code Agent View can show when a thread or agent lane is on another
branch. This answers the VS Code GUI lane without exposing chat webviews,
inline edits, or retry/undo/restore runtime mutation endpoints yet (#461,
#462, #480, #1217, #2341, #1584, #2327, #2580, #2808). Thanks @AiurArtanis
branch. Agent View and restore-point data now auto-refresh on a configurable
read-only interval so branch/workspace changes become visible without a
manual refresh. This answers the VS Code GUI lane without exposing chat
webviews, inline edits, or retry/undo/restore runtime mutation endpoints yet
(#461, #462, #480, #1217, #2341, #1584, #2327, #2580, #2808). Thanks @AiurArtanis
for the Agent View prompt, @lbcheng888 for the earlier scaffold, @gaord for
the GUI runtime API direction, @douglarek, @caeserchen, and @nightt5879 for
the branch visibility trail, and @BigBenLabs, @lzx1545642258, @yangdaowan,
+5 -1
View File
@@ -11,6 +11,8 @@ This first slice is intentionally small:
- show a read-only Agent View with recent runtime thread summaries from
`/v1/threads/summary`
- show recent read-only restore points from `/v1/snapshots`
- refresh the read-only Agent View automatically so branch/workspace metadata
catches up while agents are working
It does not expose the full chat webview, VS Code Agent View chat/editor
integration, inline edit application, marketplace publish workflow, or
@@ -26,7 +28,9 @@ code --install-extension codewhale-vscode-0.8.53.vsix
```
Configure `codewhale.commandPath`, `codewhale.runtimeHost`,
`codewhale.runtimePort`, and `codewhale.runtimeToken` from VS Code settings.
`codewhale.runtimePort`, `codewhale.runtimeToken`, and
`codewhale.agentViewRefreshIntervalSeconds` from VS Code settings.
Set the refresh interval to `0` to disable automatic read-only refreshes.
Keep the runtime on `127.0.0.1` unless you deliberately front it with trusted
local networking controls.
+63 -17
View File
@@ -42,6 +42,8 @@ function activate(context) {
const output = vscode.window.createOutputChannel("CodeWhale");
const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
const statusView = new status_1.RuntimeStatusView();
let autoRefreshTimer;
let autoRefreshInFlight = false;
status.command = "codewhale.checkRuntime";
context.subscriptions.push(output, status);
context.subscriptions.push(vscode.window.registerWebviewViewProvider(status_1.RuntimeStatusView.viewType, statusView));
@@ -62,23 +64,11 @@ function activate(context) {
status.tooltip = tooltip;
status.show();
};
updateStatus("$(terminal) CodeWhale", "Check CodeWhale runtime");
context.subscriptions.push(vscode.commands.registerCommand("codewhale.openTerminal", () => {
const checkAndRefreshRuntime = async (showSpinner, logResult) => {
const config = (0, runtime_1.readRuntimeConfig)();
(0, runtime_1.openCodeWhaleTerminal)(config);
output.appendLine(`Opened CodeWhale terminal using ${config.commandPath}.`);
}));
context.subscriptions.push(vscode.commands.registerCommand("codewhale.startRuntime", () => {
const config = (0, runtime_1.readRuntimeConfig)();
(0, runtime_1.startRuntimeTerminal)(config);
const baseUrl = (0, runtime_1.runtimeBaseUrl)(config);
updateStatus("$(sync~spin) CodeWhale", `Runtime terminal started for ${baseUrl}`);
output.appendLine(`Started CodeWhale runtime terminal at ${baseUrl}.`);
void vscode.window.showInformationMessage(`CodeWhale runtime starting at ${baseUrl}`);
}));
context.subscriptions.push(vscode.commands.registerCommand("codewhale.checkRuntime", async () => {
const config = (0, runtime_1.readRuntimeConfig)();
updateStatus("$(sync~spin) CodeWhale", "Checking CodeWhale runtime...");
if (showSpinner) {
updateStatus("$(sync~spin) CodeWhale", "Checking CodeWhale runtime...");
}
const state = await (0, runtime_1.checkRuntime)(config);
statusView.update(state);
switch (state.kind) {
@@ -107,8 +97,64 @@ function activate(context) {
statusView.updateSnapshots([], "Connect to the runtime to load restore points.");
break;
}
output.appendLine(`${new Date().toISOString()} ${state.kind}: ${state.detail}`);
if (logResult) {
output.appendLine(`${new Date().toISOString()} ${state.kind}: ${state.detail}`);
}
return state;
};
const runAutoRefresh = async () => {
if (autoRefreshInFlight) {
return;
}
autoRefreshInFlight = true;
try {
await checkAndRefreshRuntime(false, false);
}
finally {
autoRefreshInFlight = false;
}
};
const scheduleAutoRefresh = () => {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
autoRefreshTimer = undefined;
}
const intervalSeconds = (0, runtime_1.readRuntimeConfig)().agentViewRefreshIntervalSeconds;
if (intervalSeconds === 0) {
output.appendLine("Agent View auto-refresh is disabled.");
return;
}
autoRefreshTimer = setInterval(() => {
void runAutoRefresh();
}, intervalSeconds * 1000);
output.appendLine(`Agent View auto-refresh scheduled every ${intervalSeconds}s.`);
};
updateStatus("$(terminal) CodeWhale", "Check CodeWhale runtime");
scheduleAutoRefresh();
context.subscriptions.push(new vscode.Disposable(() => {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
}
}), vscode.workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration("codewhale.agentViewRefreshIntervalSeconds")) {
scheduleAutoRefresh();
}
}));
context.subscriptions.push(vscode.commands.registerCommand("codewhale.openTerminal", () => {
const config = (0, runtime_1.readRuntimeConfig)();
(0, runtime_1.openCodeWhaleTerminal)(config);
output.appendLine(`Opened CodeWhale terminal using ${config.commandPath}.`);
}));
context.subscriptions.push(vscode.commands.registerCommand("codewhale.startRuntime", () => {
const config = (0, runtime_1.readRuntimeConfig)();
(0, runtime_1.startRuntimeTerminal)(config);
const baseUrl = (0, runtime_1.runtimeBaseUrl)(config);
updateStatus("$(sync~spin) CodeWhale", `Runtime terminal started for ${baseUrl}`);
output.appendLine(`Started CodeWhale runtime terminal at ${baseUrl}.`);
void vscode.window.showInformationMessage(`CodeWhale runtime starting at ${baseUrl}`);
}));
context.subscriptions.push(vscode.commands.registerCommand("codewhale.checkRuntime", async () => {
return await checkAndRefreshRuntime(true, true);
}));
context.subscriptions.push(vscode.commands.registerCommand("codewhale.refreshAgentView", async () => {
try {
File diff suppressed because one or more lines are too long
+8
View File
@@ -48,11 +48,13 @@ function readRuntimeConfig() {
const host = config.get("runtimeHost", "127.0.0.1").trim() || "127.0.0.1";
const port = config.get("runtimePort", 7878);
const token = config.get("runtimeToken", "").trim();
const interval = config.get("agentViewRefreshIntervalSeconds", 15);
return {
commandPath,
host,
port,
token: token.length > 0 ? token : undefined,
agentViewRefreshIntervalSeconds: clampRefreshInterval(interval),
};
}
function runtimeBaseUrl(config) {
@@ -229,6 +231,12 @@ function readString(value) {
function readNumber(value) {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function clampRefreshInterval(value) {
if (!Number.isFinite(value)) {
return 15;
}
return Math.max(0, Math.min(300, Math.floor(value)));
}
function shellQuote(value) {
if (/^[A-Za-z0-9_./:=+-]+$/.test(value)) {
return value;
File diff suppressed because one or more lines are too long
+7
View File
@@ -83,6 +83,13 @@
"type": "string",
"default": "",
"description": "Optional bearer token for authenticated runtime endpoints."
},
"codewhale.agentViewRefreshIntervalSeconds": {
"type": "number",
"default": 15,
"minimum": 0,
"maximum": 300,
"description": "Seconds between read-only Agent View refreshes. Set to 0 to disable automatic refresh."
}
}
},
+92 -33
View File
@@ -7,6 +7,7 @@ import {
readRuntimeConfig,
runtimeBaseUrl,
startRuntimeTerminal,
type RuntimeState,
} from "./runtime";
import { RuntimeStatusView } from "./status";
@@ -14,6 +15,8 @@ export function activate(context: vscode.ExtensionContext): void {
const output = vscode.window.createOutputChannel("CodeWhale");
const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
const statusView = new RuntimeStatusView();
let autoRefreshTimer: ReturnType<typeof setInterval> | undefined;
let autoRefreshInFlight = false;
status.command = "codewhale.checkRuntime";
context.subscriptions.push(output, status);
@@ -41,7 +44,95 @@ export function activate(context: vscode.ExtensionContext): void {
status.show();
};
const checkAndRefreshRuntime = async (
showSpinner: boolean,
logResult: boolean,
): Promise<RuntimeState> => {
const config = readRuntimeConfig();
if (showSpinner) {
updateStatus("$(sync~spin) CodeWhale", "Checking CodeWhale runtime...");
}
const state = await checkRuntime(config);
statusView.update(state);
switch (state.kind) {
case "connected":
updateStatus("$(check) CodeWhale", state.detail);
try {
await refreshAgentView();
await refreshSnapshots();
} catch (error: unknown) {
const detail = error instanceof Error ? error.message : String(error);
statusView.updateThreads([], "Runtime thread summaries unavailable.");
statusView.updateSnapshots([], detail);
output.appendLine(`Runtime Agent View details unavailable: ${detail}`);
}
break;
case "auth-required":
updateStatus("$(lock) CodeWhale", state.detail);
statusView.updateThreads([], "Runtime token is required before threads can load.");
statusView.updateSnapshots([], "Runtime token is required before restore points can load.");
break;
case "offline":
case "error":
updateStatus("$(warning) CodeWhale", state.detail);
statusView.updateThreads([], "Connect to the runtime to load recent threads.");
statusView.updateSnapshots([], "Connect to the runtime to load restore points.");
break;
}
if (logResult) {
output.appendLine(`${new Date().toISOString()} ${state.kind}: ${state.detail}`);
}
return state;
};
const runAutoRefresh = async (): Promise<void> => {
if (autoRefreshInFlight) {
return;
}
autoRefreshInFlight = true;
try {
await checkAndRefreshRuntime(false, false);
} finally {
autoRefreshInFlight = false;
}
};
const scheduleAutoRefresh = (): void => {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
autoRefreshTimer = undefined;
}
const intervalSeconds = readRuntimeConfig().agentViewRefreshIntervalSeconds;
if (intervalSeconds === 0) {
output.appendLine("Agent View auto-refresh is disabled.");
return;
}
autoRefreshTimer = setInterval(() => {
void runAutoRefresh();
}, intervalSeconds * 1000);
output.appendLine(`Agent View auto-refresh scheduled every ${intervalSeconds}s.`);
};
updateStatus("$(terminal) CodeWhale", "Check CodeWhale runtime");
scheduleAutoRefresh();
context.subscriptions.push(
new vscode.Disposable(() => {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
}
}),
vscode.workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration("codewhale.agentViewRefreshIntervalSeconds")) {
scheduleAutoRefresh();
}
}),
);
context.subscriptions.push(
vscode.commands.registerCommand("codewhale.openTerminal", () => {
@@ -64,39 +155,7 @@ export function activate(context: vscode.ExtensionContext): void {
context.subscriptions.push(
vscode.commands.registerCommand("codewhale.checkRuntime", async () => {
const config = readRuntimeConfig();
updateStatus("$(sync~spin) CodeWhale", "Checking CodeWhale runtime...");
const state = await checkRuntime(config);
statusView.update(state);
switch (state.kind) {
case "connected":
updateStatus("$(check) CodeWhale", state.detail);
try {
await refreshAgentView();
await refreshSnapshots();
} catch (error: unknown) {
const detail = error instanceof Error ? error.message : String(error);
statusView.updateThreads([], "Runtime thread summaries unavailable.");
statusView.updateSnapshots([], detail);
output.appendLine(`Runtime Agent View details unavailable: ${detail}`);
}
break;
case "auth-required":
updateStatus("$(lock) CodeWhale", state.detail);
statusView.updateThreads([], "Runtime token is required before threads can load.");
statusView.updateSnapshots([], "Runtime token is required before restore points can load.");
break;
case "offline":
case "error":
updateStatus("$(warning) CodeWhale", state.detail);
statusView.updateThreads([], "Connect to the runtime to load recent threads.");
statusView.updateSnapshots([], "Connect to the runtime to load restore points.");
break;
}
output.appendLine(`${new Date().toISOString()} ${state.kind}: ${state.detail}`);
return state;
return await checkAndRefreshRuntime(true, true);
}),
);
+10
View File
@@ -34,6 +34,7 @@ export interface RuntimeConfig {
host: string;
port: number;
token?: string;
agentViewRefreshIntervalSeconds: number;
}
export function readRuntimeConfig(): RuntimeConfig {
@@ -42,11 +43,13 @@ export function readRuntimeConfig(): RuntimeConfig {
const host = config.get<string>("runtimeHost", "127.0.0.1").trim() || "127.0.0.1";
const port = config.get<number>("runtimePort", 7878);
const token = config.get<string>("runtimeToken", "").trim();
const interval = config.get<number>("agentViewRefreshIntervalSeconds", 15);
return {
commandPath,
host,
port,
token: token.length > 0 ? token : undefined,
agentViewRefreshIntervalSeconds: clampRefreshInterval(interval),
};
}
@@ -262,6 +265,13 @@ function readNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function clampRefreshInterval(value: number): number {
if (!Number.isFinite(value)) {
return 15;
}
return Math.max(0, Math.min(300, Math.floor(value)));
}
function shellQuote(value: string): string {
if (/^[A-Za-z0-9_./:=+-]+$/.test(value)) {
return value;