返回 CodeWhale
extension.ts
根目录 / extensions / vscode / src / extension.ts
1 import * as vscode from "vscode";
2 import { checkConnection, listSnapshots, type ApiConfig, type ConnectionInfo } from "./api";
3 import { ChatView } from "./chat";
4 import {
5 openCodeWhaleTerminal,
6 readRuntimeConfig,
7 runtimeBaseUrl,
8 startRuntimeTerminal,
9 type RuntimeState,
10 } from "./runtime";
11 import { promptForToken, resolveToken } from "./secrets";
12 import { RuntimeStatusView } from "./status";
13
14 export function activate(context: vscode.ExtensionContext): void {
15 const output = vscode.window.createOutputChannel("CodeWhale");
16 const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
17 const statusView = new RuntimeStatusView();
18 const apiConfig = async (): Promise<ApiConfig> => {
19 const config = readRuntimeConfig();
20 // SecretStorage is the only token source; `resolveToken` treats the
21 // deprecated `codewhale.runtimeToken` setting as a migration source and
22 // ignores a workspace-scoped one outright.
23 const token = await resolveToken(context);
24 return { baseUrl: runtimeBaseUrl(config), token };
25 };
26 const chatView = new ChatView(context, apiConfig, output);
27 let autoRefreshTimer: ReturnType<typeof setInterval> | undefined;
28 let autoRefreshInFlight = false;
29 let lastConnectionKind: ConnectionInfo["kind"] | undefined;
30
31 status.command = "codewhale.checkRuntime";
32 context.subscriptions.push(output, status);
33 // The chat lives in the secondary (right) sidebar on hosts that support it,
34 // and falls back to the activity bar on older ones. The manifest gates both
35 // containers on `codewhale.noSecondarySidebar`, so this key MUST be set at
36 // activation — an unset key is falsy, which would hide the activity-bar
37 // container AND leave the secondary panel unserved. Threshold matches the
38 // shipping Codex extension, which gates at runtime rather than via engines.
39 const [vsMajor = 0, vsMinor = 0] = vscode.version
40 .split(".")
41 .map((part) => Number.parseInt(part, 10) || 0);
42 const supportsSecondarySidebar = vsMajor > 1 || (vsMajor === 1 && vsMinor >= 106);
43 void vscode.commands.executeCommand(
44 "setContext",
45 "codewhale.noSecondarySidebar",
46 !supportsSecondarySidebar,
47 );
48
49 // One ChatView instance serves both ids; only the gated one ever resolves.
50 context.subscriptions.push(
51 vscode.window.registerWebviewViewProvider(ChatView.viewType, chatView),
52 vscode.window.registerWebviewViewProvider(ChatView.secondaryViewType, chatView),
53 vscode.window.registerWebviewViewProvider(RuntimeStatusView.viewType, statusView),
54 );
55
56 const updateStatus = (text: string, tooltip: string): void => {
57 status.text = text;
58 status.tooltip = tooltip;
59 status.show();
60 };
61
62 const checkAndRefreshRuntime = async (
63 showSpinner: boolean,
64 logResult: boolean,
65 ): Promise<RuntimeState> => {
66 const config = readRuntimeConfig();
67 const baseUrl = runtimeBaseUrl(config);
68 if (showSpinner) {
69 updateStatus("$(sync~spin) CodeWhale", "Checking CodeWhale runtime...");
70 }
71
72 let connection: ConnectionInfo;
73 try {
74 connection = await checkConnection(await apiConfig());
75 } catch (error: unknown) {
76 const detail = error instanceof Error ? error.message : String(error);
77 connection = { kind: "error", detail };
78 }
79 const state: RuntimeState = { ...connection, baseUrl };
80
81 statusView.update(state);
82 chatView.setConnection(connection);
83
84 const becameConnected =
85 connection.kind === "connected" && lastConnectionKind !== "connected";
86 lastConnectionKind = connection.kind;
87
88 switch (connection.kind) {
89 case "connected":
90 updateStatus("$(check) CodeWhale", state.detail);
91 await chatView.refreshThreads();
92 if (becameConnected) {
93 await chatView.resyncAfterConnection();
94 }
95 break;
96 case "auth-required":
97 updateStatus("$(lock) CodeWhale", state.detail);
98 statusView.updateThreads([], "Runtime token is required before threads can load.");
99 statusView.updateSnapshots([], "Runtime token is required before restore points can load.");
100 break;
101 case "offline":
102 case "error":
103 updateStatus("$(warning) CodeWhale", state.detail);
104 statusView.updateThreads([], "Connect to the runtime to load recent threads.");
105 statusView.updateSnapshots([], "Connect to the runtime to load restore points.");
106 break;
107 }
108
109 if (logResult) {
110 output.appendLine(`${new Date().toISOString()} ${state.kind}: ${state.detail}`);
111 }
112 return state;
113 };
114
115 const runAutoRefresh = async (): Promise<void> => {
116 if (autoRefreshInFlight) {
117 return;
118 }
119 autoRefreshInFlight = true;
120 try {
121 await checkAndRefreshRuntime(false, false);
122 } finally {
123 autoRefreshInFlight = false;
124 }
125 };
126
127 const scheduleAutoRefresh = (): void => {
128 if (autoRefreshTimer) {
129 clearInterval(autoRefreshTimer);
130 autoRefreshTimer = undefined;
131 }
132 const intervalSeconds = readRuntimeConfig().agentViewRefreshIntervalSeconds;
133 if (intervalSeconds === 0) {
134 output.appendLine("Auto-refresh is disabled.");
135 return;
136 }
137 autoRefreshTimer = setInterval(() => {
138 void runAutoRefresh();
139 }, intervalSeconds * 1000);
140 output.appendLine(`Runtime auto-refresh scheduled every ${intervalSeconds}s.`);
141 };
142
143 updateStatus("$(terminal) CodeWhale", "Check CodeWhale runtime");
144 scheduleAutoRefresh();
145 context.subscriptions.push(
146 new vscode.Disposable(() => {
147 if (autoRefreshTimer) {
148 clearInterval(autoRefreshTimer);
149 }
150 }),
151 vscode.workspace.onDidChangeConfiguration((event) => {
152 if (
153 event.affectsConfiguration("codewhale.agentViewRefreshIntervalSeconds") ||
154 event.affectsConfiguration("codewhale.runtimeHost") ||
155 event.affectsConfiguration("codewhale.runtimePort") ||
156 event.affectsConfiguration("codewhale.runtimeToken")
157 ) {
158 lastConnectionKind = undefined;
159 scheduleAutoRefresh();
160 void checkAndRefreshRuntime(false, true);
161 }
162 }),
163 );
164
165 context.subscriptions.push(
166 vscode.commands.registerCommand("codewhale.openTerminal", () => {
167 openCodeWhaleTerminal(readRuntimeConfig());
168 output.appendLine(`Opened CodeWhale terminal using ${readRuntimeConfig().commandPath}.`);
169 }),
170 vscode.commands.registerCommand("codewhale.startRuntime", async () => {
171 const config = readRuntimeConfig();
172 const baseUrl = runtimeBaseUrl(config);
173 const token = await resolveToken(context);
174
175 // Anything that answers on this address is already bound to the port; a
176 // second `serve` would only fail noisily, so report instead of starting.
177 let bound: ConnectionInfo | undefined;
178 try {
179 bound = await checkConnection({ baseUrl, token });
180 } catch {
181 bound = undefined;
182 }
183 if (bound && bound.kind !== "offline") {
184 const detail = `A runtime is already listening at ${baseUrl}: ${bound.detail}`;
185 output.appendLine(detail);
186 void vscode.window.showInformationMessage(detail);
187 await checkAndRefreshRuntime(false, false);
188 return;
189 }
190
191 startRuntimeTerminal(config, token);
192 updateStatus("$(sync~spin) CodeWhale", `Runtime terminal started for ${baseUrl}`);
193 output.appendLine(`Started CodeWhale runtime terminal at ${baseUrl}.`);
194 void vscode.window.showInformationMessage(`CodeWhale runtime starting at ${baseUrl}`);
195 }),
196 vscode.commands.registerCommand("codewhale.checkRuntime", async () => {
197 return await checkAndRefreshRuntime(true, true);
198 }),
199 vscode.commands.registerCommand("codewhale.refreshAgentView", async () => {
200 await chatView.refreshThreads();
201 }),
202 vscode.commands.registerCommand("codewhale.refreshSnapshots", async () => {
203 try {
204 const snapshots = await listSnapshots(await apiConfig());
205 statusView.updateSnapshots(snapshots, "Showing recent restore points.");
206 } catch (error: unknown) {
207 const detail = error instanceof Error ? error.message : String(error);
208 statusView.updateSnapshots([], detail);
209 output.appendLine(`Runtime restore points unavailable: ${detail}`);
210 void vscode.window.showWarningMessage(detail);
211 }
212 }),
213 vscode.commands.registerCommand("codewhale.openRuntimeDocs", () => {
214 void vscode.env.openExternal(
215 vscode.Uri.parse("https://github.com/Hmbown/CodeWhale/blob/main/docs/RUNTIME_API.md"),
216 );
217 }),
218 vscode.commands.registerCommand("codewhale.ask", async () => {
219 await chatView.askWithSelection();
220 }),
221 vscode.commands.registerCommand("codewhale.newChat", async () => {
222 await chatView.reveal();
223 await chatView.newThread();
224 }),
225 vscode.commands.registerCommand("codewhale.setRuntimeToken", async () => {
226 const token = await promptForToken(context);
227 if (token !== undefined) {
228 await checkAndRefreshRuntime(true, true);
229 }
230 }),
231 );
232
233 void vscode.commands.executeCommand("codewhale.checkRuntime");
234 }
235
236 export function deactivate(): void {
237 // No background process is owned by the extension; runtime starts in a user-visible terminal.
238 }
239
239 lines TYPESCRIPT