diff --git a/tools/vscode/CHANGELOG.md b/tools/vscode/CHANGELOG.md
index 1aa6a93f5..30706ae2f 100644
--- a/tools/vscode/CHANGELOG.md
+++ b/tools/vscode/CHANGELOG.md
@@ -1,5 +1,10 @@
# Changelog
+## 0.3.34
+
+- Show Inspect version in status bar
+- Release with internal changes to support future log viewing features.
+
## 0.3.33
- Fix bug that prevented run, debug buttons from appearing for tasks whose function declarations spanned more than single line.
diff --git a/tools/vscode/assets/icon/eval.svg b/tools/vscode/assets/icon/eval.svg
new file mode 100644
index 000000000..7e3ef40fb
--- /dev/null
+++ b/tools/vscode/assets/icon/eval.svg
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/tools/vscode/package.json b/tools/vscode/package.json
index 046afffc0..7a23a7edd 100644
--- a/tools/vscode/package.json
+++ b/tools/vscode/package.json
@@ -7,7 +7,7 @@
"author": {
"name": "UK AI Safety Institute"
},
- "version": "0.3.33",
+ "version": "0.3.34",
"license": "MIT",
"homepage": "https://inspect.ai-safety-institute.org.uk/",
"repository": {
@@ -32,12 +32,44 @@
"onWebviewPanel:inspect.logview",
"onWebviewPanel:inspect_ai.task-configuration",
"onWebviewPanel:inspect_ai.env-configuration-view",
+ "onLanguage:eval",
+ "workspaceContains:**/*.eval",
"onLanguage:python",
- "workspaceContains:*.py",
- "workspaceContains:*.ipynb"
+ "workspaceContains:**/*.py",
+ "workspaceContains:**/*.ipynb"
],
"main": "./dist/extension.js",
"contributes": {
+ "languages": [
+ {
+ "id": "eval-log",
+ "aliases": [
+ "Eval Log"
+ ],
+ "extensions": [
+ ".eval"
+ ],
+ "icon": {
+ "light": "./assets/icon/eval.svg",
+ "dark": "./assets/icon/eval.svg"
+ }
+ }
+ ],
+ "customEditors": [
+ {
+ "viewType": "inspect-ai.log-editor",
+ "displayName": "Inspect Log Viewer",
+ "selector": [
+ {
+ "filenamePattern": "*.eval"
+ },
+ {
+ "filenamePattern": "{[0-9][0-9][0-9][0-9]}-{[0-9][0-9]}-{[0-9][0-9]}T{[0-9][0-9]}[:-]{[0-9][0-9]}[:-]{[0-9][0-9]}*{[A-Za-z0-9]{21}}*.json"
+ }
+ ],
+ "priority": "default"
+ }
+ ],
"commands": [
{
"command": "inspect.showLogview",
@@ -163,7 +195,8 @@
"inspect_ai.openLogView": {
"type": "boolean",
"default": true,
- "description": "Open the Inspect log view when evaluations in the workspace complete."
+ "description": "Open the Inspect Log View when evaluations in the workspace complete.",
+ "order": 1
},
"inspect_ai.logViewType": {
"type": "string",
@@ -175,7 +208,14 @@
"markdownEnumDescriptions": [
"Use `inspect view` to display evaluation log files.",
"Use a text editor to display evaluation log files."
- ]
+ ],
+ "order": 2
+ },
+ "inspect_ai.jsonLogView": {
+ "type": "boolean",
+ "default": true,
+ "markdownDescription": "Use Inspect Log View as the default viewer for JSON log files.",
+ "order": 3
},
"inspect_ai.taskListView": {
"type": "string",
@@ -184,12 +224,14 @@
"tree",
"list"
],
- "description": "Display task outline as a tree or list."
+ "description": "Display task outline as a tree or list.",
+ "order": 4
},
"inspect_ai.debugSingleSample": {
"type": "boolean",
"default": true,
- "description": "Limit evaluation to one sample when debugging."
+ "description": "Limit evaluation to one sample when debugging.",
+ "order": 5
}
}
},
@@ -265,7 +307,7 @@
{
"command": "inspect.openInInspectView",
"group": "navigation@100",
- "when": "inspect_ai.enableOpenInView && resourceFilename =~ /^.*\\.json$/"
+ "when": "inspect_ai.enableOpenInView && resourceFilename =~ /^.*\\.(json|eval)$/"
}
],
"view/item/context": [
@@ -340,6 +382,7 @@
"test": "vscode-test"
},
"devDependencies": {
+ "@types/async-lock": "^1.4.2",
"@types/lodash": "^4.17.0",
"@types/mocha": "^10.0.6",
"@types/node": "18.x",
@@ -362,7 +405,8 @@
"@microsoft/fast-components": "^2.30.6",
"@microsoft/fast-element": "^1.13.0",
"@vscode/webview-ui-toolkit": "^1.4.0",
+ "async-lock": "^1.4.1",
"lodash": "^4.17.21",
"semver": "^7.6.0"
}
-}
+}
\ No newline at end of file
diff --git a/tools/vscode/src/components/webview.ts b/tools/vscode/src/components/webview.ts
index 7e2c195a0..319374a97 100644
--- a/tools/vscode/src/components/webview.ts
+++ b/tools/vscode/src/components/webview.ts
@@ -5,6 +5,7 @@ import {
ExtensionContext,
window,
commands,
+ WebviewPanel,
} from "vscode";
import { Disposable } from "../core/dispose";
@@ -13,6 +14,7 @@ import { ExtensionHost, HostWebviewPanel } from "../hooks";
import { isNotebook } from "./notebook";
import { FocusManager } from "./focus";
import { log } from "../core/log";
+import { InspectViewServer } from "../providers/inspect/inspect-view-server";
export interface ShowOptions {
readonly preserveFocus?: boolean;
@@ -21,32 +23,38 @@ export interface ShowOptions {
export class InspectWebviewManager, S> {
constructor(
- protected readonly context: ExtensionContext,
+ protected readonly context_: ExtensionContext,
+ private readonly server_: InspectViewServer,
private readonly viewType_: string,
private readonly title_: string,
private readonly localResourceRoots: Uri[],
private webviewType_: new (
context: ExtensionContext,
+ server: InspectViewServer,
state: S,
webviewPanel: HostWebviewPanel
) => T,
private host_: ExtensionHost
) {
- this.extensionUri_ = context.extensionUri;
+ this.extensionUri_ = context_.extensionUri;
- context.subscriptions.push(
+ context_.subscriptions.push(
window.registerWebviewPanelSerializer(this.viewType_, {
- deserializeWebviewPanel: (panel) => {
- //this.restoreWebview(panel as HostWebviewPanel, state);
- setTimeout(() => {
- panel.dispose();
- }, 200);
+ deserializeWebviewPanel: (panel: WebviewPanel, state?: S) => {
+ state = state || this.getWorkspaceState();
+ if (state) {
+ this.restoreWebview(panel as HostWebviewPanel, state);
+ } else {
+ setTimeout(() => {
+ panel.dispose();
+ }, 200);
+ }
return Promise.resolve();
},
})
);
- this.focusManager_ = new FocusManager(context);
+ this.focusManager_ = new FocusManager(context_);
}
private focusManager_: FocusManager;
@@ -62,7 +70,7 @@ export class InspectWebviewManager, S> {
if (this.activeView_) {
this.activeView_.show(state, options);
} else {
- const view = this.createWebview(this.context, state, options);
+ const view = this.createWebview(this.context_, state, options);
this.registerWebviewListeners(view);
this.activeView_ = view;
}
@@ -97,6 +105,11 @@ export class InspectWebviewManager, S> {
protected onViewStateChanged() { }
+ protected getWorkspaceState(): S | undefined {
+ return undefined;
+ }
+
+
private resolveOnShow() {
if (this.onShow_) {
this.onShow_();
@@ -147,7 +160,7 @@ export class InspectWebviewManager, S> {
private restoreWebview(panel: HostWebviewPanel, state: S): void {
- const view = new this.webviewType_(this.context, state, panel);
+ const view = new this.webviewType_(this.context_, this.server_, state, panel);
this.registerWebviewListeners(view);
this.activeView_ = view;
}
@@ -172,7 +185,7 @@ export class InspectWebviewManager, S> {
}
);
- const inspectWebView = new this.webviewType_(context, state, previewPanel);
+ const inspectWebView = new this.webviewType_(context, this.server_, state, previewPanel);
return inspectWebView;
}
@@ -217,7 +230,8 @@ export abstract class InspectWebview extends Disposable {
public readonly onDispose = this._onDidDispose.event;
public constructor(
- private readonly context: ExtensionContext,
+ private readonly _context: ExtensionContext,
+ private readonly _server: InspectViewServer,
state: T,
webviewPanel: HostWebviewPanel
) {
@@ -229,8 +243,6 @@ export abstract class InspectWebview extends Disposable {
this.dispose();
})
);
-
- this.show(state);
}
public override dispose() {
@@ -254,7 +266,7 @@ export abstract class InspectWebview extends Disposable {
protected abstract getHtml(state: T): string;
protected getExtensionVersion(): string {
- return (this.context.extension.packageJSON as Record)
+ return (this._context.extension.packageJSON as Record)
.version as string;
}
@@ -331,7 +343,7 @@ export abstract class InspectWebview extends Disposable {
protected extensionResourceUrl(parts: string[]): Uri {
return this._webviewPanel.webview.asWebviewUri(
- Uri.joinPath(this.context.extensionUri, ...parts)
+ Uri.joinPath(this._context.extensionUri, ...parts)
);
}
diff --git a/tools/vscode/src/core/jsonrpc.ts b/tools/vscode/src/core/jsonrpc.ts
index 3eb30616b..4efd175c6 100644
--- a/tools/vscode/src/core/jsonrpc.ts
+++ b/tools/vscode/src/core/jsonrpc.ts
@@ -1,6 +1,8 @@
// constants for json-rpc methods
export const kMethodEvalLogs = "eval_logs";
export const kMethodEvalLog = "eval_log";
+export const kMethodEvalLogSize = "eval_log_size";
+export const kMethodEvalLogBytes = "eval_log_bytes";
export const kMethodEvalLogHeaders = "eval_log_headers";
diff --git a/tools/vscode/src/core/process.ts b/tools/vscode/src/core/process.ts
index 0759a9fc8..fdb132f6e 100644
--- a/tools/vscode/src/core/process.ts
+++ b/tools/vscode/src/core/process.ts
@@ -1,4 +1,4 @@
-import { SpawnSyncOptionsWithStringEncoding, spawn, spawnSync } from "child_process";
+import { SpawnOptions, SpawnSyncOptionsWithStringEncoding, spawn, spawnSync } from "child_process";
import { AbsolutePath } from "./path";
@@ -35,10 +35,10 @@ export function runProcess(
export function spawnProcess(
cmd: string,
args: string[],
- cwd: AbsolutePath,
+ options: SpawnOptions,
io?: {
- stdout?: (data: Buffer | string) => void;
- stderr?: (data: Buffer | string) => void;
+ stdout?: (data: string) => void;
+ stderr?: (data: string) => void;
},
lifecycle?: {
onError?: (error: Error) => void;
@@ -46,16 +46,14 @@ export function spawnProcess(
}
) {
// Process options
- const options = {
- cwd: cwd.path,
- detached: true,
- };
+ options = { detached: true, ...options };
// Start the actual process
const process = spawn(cmd, args, options);
// Capture stdout
if (process.stdout) {
+ process.stdout.setEncoding("utf-8");
if (io?.stdout) {
process.stdout.on("data", io.stdout);
}
@@ -65,6 +63,7 @@ export function spawnProcess(
// Capture stderr
if (process.stderr) {
+ process.stderr.setEncoding("utf-8");
if (io?.stderr) {
process.stderr.on("data", io.stderr);
}
diff --git a/tools/vscode/src/core/python/exec.ts b/tools/vscode/src/core/python/exec.ts
index 8a9fcd945..9bd2a3f75 100644
--- a/tools/vscode/src/core/python/exec.ts
+++ b/tools/vscode/src/core/python/exec.ts
@@ -77,8 +77,8 @@ export function spawnPython(
args: string[],
cwd: AbsolutePath,
io?: {
- stdout?: (data: Buffer | string) => void;
- stderr?: (data: Buffer | string) => void;
+ stdout?: (data: string) => void;
+ stderr?: (data: string) => void;
},
lifecycle?: {
onError?: (error: Error) => void;
@@ -89,7 +89,7 @@ export function spawnPython(
if (execCommand) {
const cmd = execCommand[0];
args = [...execCommand.slice(1), ...args];
- return spawnProcess(cmd, args, cwd, io, lifecycle);
+ return spawnProcess(cmd, args, { cwd: cwd.path }, io, lifecycle);
} else {
throw new Error("No active Python interpreter available.");
}
diff --git a/tools/vscode/src/extension.ts b/tools/vscode/src/extension.ts
index 6b3a841a9..68ce3dab1 100644
--- a/tools/vscode/src/extension.ts
+++ b/tools/vscode/src/extension.ts
@@ -5,7 +5,7 @@ import { activateCodeLens } from "./providers/codelens/codelens-provider";
import { activateLogview } from "./providers/logview/logview";
import { LogViewFileWatcher } from "./providers/logview/logview-file-watcher";
import { logviewTerminalLinkProvider } from "./providers/logview/logview-link-provider";
-import { InspectLogviewManager } from "./providers/logview/logview-manager";
+import { InspectViewManager } from "./providers/logview/logview-view";
import { InspectSettingsManager } from "./providers/settings/inspect-settings";
import { initializeGlobalSettings } from "./providers/settings/user-settings";
import { activateEvalManager } from "./providers/inspect/inspect-eval";
@@ -24,6 +24,7 @@ import { activateInspectManager } from "./providers/inspect/inspect-manager";
import { checkActiveWorkspaceFolder } from "./core/workspace";
import { inspectBinPath, inspectVersionDescriptor } from "./inspect/props";
import { extensionHost } from "./hooks";
+import { activateStatusBar } from "./providers/statusbar";
const kInspectMinimumVersion = "0.3.8";
@@ -129,6 +130,9 @@ export async function activate(context: ExtensionContext) {
// Activate Code Lens
activateCodeLens(context);
+ // Activate Status Bar
+ activateStatusBar(context, inspectManager);
+
// Activate commands
[
...logViewCommands,
@@ -153,7 +157,7 @@ export function deactivate() {
let logFileWatcher: LogViewFileWatcher | undefined;
const startLogWatcher = (
- logviewWebviewManager: InspectLogviewManager,
+ logviewWebviewManager: InspectViewManager,
workspaceStateManager: WorkspaceStateManager,
settingsMgr: InspectSettingsManager
) => {
diff --git a/tools/vscode/src/inspect/props.ts b/tools/vscode/src/inspect/props.ts
index 73f33e84d..db2153372 100644
--- a/tools/vscode/src/inspect/props.ts
+++ b/tools/vscode/src/inspect/props.ts
@@ -13,6 +13,7 @@ import { existsSync } from "fs";
const kPythonPackageName = "inspect_ai";
export interface VersionDescriptor {
+ raw: string;
version: SemVer,
isDeveloperBuild: boolean
}
@@ -96,6 +97,7 @@ export function inspectVersionDescriptor(): VersionDescriptor | null {
if (parsedVersion) {
const isDeveloperVersion = version.version.indexOf('.dev') > -1;
const inspectVersion = {
+ raw: version.version,
version: parsedVersion,
isDeveloperBuild: isDeveloperVersion
};
diff --git a/tools/vscode/src/inspect/version.ts b/tools/vscode/src/inspect/version.ts
index ff8855f5a..d4f506062 100644
--- a/tools/vscode/src/inspect/version.ts
+++ b/tools/vscode/src/inspect/version.ts
@@ -4,11 +4,18 @@ export function withMinimumInspectVersion(version: string, hasVersion: () => voi
export function withMinimumInspectVersion(version: string, hasVersion: () => T, doesntHaveVersion: () => T): T;
export function withMinimumInspectVersion(version: string, hasVersion: () => T, doesntHaveVersion: () => T): T | void {
- const descriptor = inspectVersionDescriptor();
- if (descriptor && (descriptor.version.compare(version) >= 0 || descriptor.isDeveloperBuild)) {
+ if (hasMinimumInspectVersion(version)) {
return hasVersion();
} else {
return doesntHaveVersion();
}
}
+export function hasMinimumInspectVersion(version: string, strictDevCheck = false): boolean {
+ const descriptor = inspectVersionDescriptor();
+ if (descriptor && (descriptor.version.compare(version) >= 0 || (!strictDevCheck && descriptor.isDeveloperBuild))) {
+ return true;
+ } else {
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/tools/vscode/src/providers/activity-bar/activity-bar-provider.ts b/tools/vscode/src/providers/activity-bar/activity-bar-provider.ts
index 7bc31cbbe..47c5422a7 100644
--- a/tools/vscode/src/providers/activity-bar/activity-bar-provider.ts
+++ b/tools/vscode/src/providers/activity-bar/activity-bar-provider.ts
@@ -9,12 +9,12 @@ import { WorkspaceStateManager } from "../workspace/workspace-state-provider";
import { TaskConfigurationProvider } from "./task-config-provider";
import { InspectManager } from "../inspect/inspect-manager";
import { DebugConfigTaskCommand, RunConfigTaskCommand } from "./task-config-commands";
-import { InspectLogviewManager } from "../logview/logview-manager";
+import { InspectViewManager } from "../logview/logview-view";
export async function activateActivityBar(
inspectManager: InspectManager,
inspectEvalMgr: InspectEvalManager,
- inspectLogviewManager: InspectLogviewManager,
+ inspectLogviewManager: InspectViewManager,
activeTaskManager: ActiveTaskManager,
workspaceTaskMgr: WorkspaceTaskManager,
workspaceStateMgr: WorkspaceStateManager,
diff --git a/tools/vscode/src/providers/activity-bar/task-outline-commands.ts b/tools/vscode/src/providers/activity-bar/task-outline-commands.ts
index 941c223c7..e4cda5b37 100644
--- a/tools/vscode/src/providers/activity-bar/task-outline-commands.ts
+++ b/tools/vscode/src/providers/activity-bar/task-outline-commands.ts
@@ -31,7 +31,7 @@ import {
taskRangeForNotebook,
} from "../../components/notebook";
import { scheduleReturnFocus } from "../../components/focus";
-import { InspectLogviewManager } from "../logview/logview-manager";
+import { InspectViewManager } from "../logview/logview-view";
import { ActiveTaskManager } from "../active-task/active-task-provider";
export class ShowTaskTree implements Command {
@@ -93,7 +93,7 @@ export class DebugSelectedEvalCommand implements Command {
export class EditSelectedTaskCommand implements Command {
constructor(
private readonly tree_: TreeView,
- private inspectLogviewManager_: InspectLogviewManager,
+ private inspectLogviewManager_: InspectViewManager,
private activeTaskManager_: ActiveTaskManager
) { }
async execute() {
diff --git a/tools/vscode/src/providers/activity-bar/task-outline-provider.ts b/tools/vscode/src/providers/activity-bar/task-outline-provider.ts
index 306220ff4..91e153b6e 100644
--- a/tools/vscode/src/providers/activity-bar/task-outline-provider.ts
+++ b/tools/vscode/src/providers/activity-bar/task-outline-provider.ts
@@ -38,7 +38,7 @@ import {
import { throttle } from "lodash";
import { inspectVersion } from "../../inspect";
import { InspectManager } from "../inspect/inspect-manager";
-import { InspectLogviewManager } from "../logview/logview-manager";
+import { InspectViewManager } from "../logview/logview-view";
import { DocumentTaskInfo } from "../../components/task";
// Activation function for the task outline
@@ -48,7 +48,7 @@ export async function activateTaskOutline(
workspaceTaskMgr: WorkspaceTaskManager,
activeTaskManager: ActiveTaskManager,
inspectManager: InspectManager,
- inspectLogviewManager: InspectLogviewManager
+ inspectLogviewManager: InspectViewManager
): Promise<[Command[], Disposable]> {
// Command when item is clicked
const treeDataProvider = new TaskOutLineTreeDataProvider(workspaceTaskMgr, {
diff --git a/tools/vscode/src/providers/inspect/inspect-constants.ts b/tools/vscode/src/providers/inspect/inspect-constants.ts
index b6f3818af..d094b8a0f 100644
--- a/tools/vscode/src/providers/inspect/inspect-constants.ts
+++ b/tools/vscode/src/providers/inspect/inspect-constants.ts
@@ -14,3 +14,4 @@ export const kLogLevelEnv = "INSPECT_EVAL_MODEL";
export const kInspectChangeEvalSignalVersion = "0.3.10";
export const kInspectOpenInspectViewVersion = "0.3.16";
export const kInspectMaxLogFileSizeVersion = "0.3.26";
+export const kInspectEvalLogFormatVersion = "0.3.42";
diff --git a/tools/vscode/src/providers/inspect/inspect-view-server.ts b/tools/vscode/src/providers/inspect/inspect-view-server.ts
new file mode 100644
index 000000000..d62aca3b6
--- /dev/null
+++ b/tools/vscode/src/providers/inspect/inspect-view-server.ts
@@ -0,0 +1,281 @@
+import { ChildProcess, SpawnOptions } from "child_process";
+import { randomUUID } from "crypto";
+import * as os from "os";
+import AsyncLock from "async-lock";
+
+import { Disposable, ExtensionContext, OutputChannel, Uri, window } from "vscode";
+
+import { findOpenPort } from "../../core/port";
+import { hasMinimumInspectVersion, withMinimumInspectVersion } from "../../inspect/version";
+import { kInspectEvalLogFormatVersion, kInspectOpenInspectViewVersion } from "./inspect-constants";
+import { inspectEvalLog, inspectEvalLogHeaders, inspectEvalLogs } from "../../inspect/logs";
+import { activeWorkspacePath } from "../../core/path";
+import { inspectBinPath } from "../../inspect/props";
+import { shQuote } from "../../core/string";
+import { spawnProcess } from "../../core/process";
+import { InspectManager } from "./inspect-manager";
+
+
+export class InspectViewServer implements Disposable {
+ constructor(context: ExtensionContext, inspectManager: InspectManager) {
+ // create output channel for debugging
+ this.outputChannel_ = window.createOutputChannel("Inspect View");
+
+ // shutdown server when inspect version changes (then we'll launch
+ // a new instance w/ the correct version)
+ context.subscriptions.push(
+ inspectManager.onInspectChanged(() => {
+ this.shutdown();
+ })
+ );
+ }
+
+ public async evalLogs(log_dir: Uri): Promise {
+ if (this.haveInspectEvalLogFormat()) {
+ return this.api_json(`/api/logs?log_dir=${encodeURIComponent(log_dir.toString())}`);
+ } else {
+ return evalLogs(log_dir);
+ }
+ }
+
+ public async evalLogsSolo(log_file: Uri): Promise {
+ if (this.haveInspectEvalLogFormat()) {
+ await this.ensureRunning();
+ }
+ return JSON.stringify({ log_dir: "", files: [{ name: log_file.toString(true) }] });
+ }
+
+ public async evalLog(
+ file: string,
+ headerOnly: boolean | number
+ ): Promise {
+ if (this.haveInspectEvalLogFormat()) {
+ return await this.api_json(`/api/logs/${encodeURIComponent(file)}?header-only=${headerOnly}`);
+ } else {
+ return evalLog(file, headerOnly);
+ }
+ }
+
+
+ public async evalLogSize(
+ file: string
+ ): Promise {
+
+ if (this.haveInspectEvalLogFormat()) {
+ return Number(await this.api_json(`/api/log-size/${encodeURIComponent(file)}`));
+ } else {
+ throw new Error("evalLogSize not implemented");
+ }
+ }
+
+ public async evalLogBytes(
+ file: string,
+ start: number,
+ end: number
+ ): Promise {
+ if (this.haveInspectEvalLogFormat()) {
+ return this.api_bytes(`/api/log-bytes/${encodeURIComponent(file)}?start=${start}&end=${end}`);
+ } else {
+ throw new Error("evalLogBytes not implemented");
+ }
+ }
+
+ public async evalLogHeaders(files: string[]): Promise {
+
+ if (this.haveInspectEvalLogFormat()) {
+ const params = new URLSearchParams();
+ for (const file of files) {
+ params.append("file", file);
+ }
+ return this.api_json(`/api/log-headers?${params.toString()}`);
+ } else {
+ return evalLogHeaders(files);
+ }
+
+ }
+
+ private async ensureRunning(): Promise {
+
+ // only do this if we have a new enough version of inspect
+ if (!this.haveInspectEvalLogFormat()) {
+ return;
+ }
+
+ await this.serverStartupLock_.acquire("server-startup", async () => {
+ if (this.serverProcess_ === undefined || this.serverProcess_.exitCode !== null) {
+
+ // find port and establish auth token
+ this.serverProcess_ = undefined;
+ this.serverPort_ = await findOpenPort(7676);
+ this.serverAuthToken_ = randomUUID();
+
+ // launch server and wait to resolve/return until it produces output
+ return new Promise((resolve, reject) => {
+
+ // find inspect
+ const inspect = inspectBinPath();
+ if (!inspect) {
+ throw new Error("inspect view: inspect installation not found");
+ }
+
+ // launch process
+ const options: SpawnOptions = {
+ cwd: activeWorkspacePath().path,
+ env: {
+ "COLUMNS": "150",
+ "INSPECT_VIEW_AUTHORIZATION_TOKEN": this.serverAuthToken_,
+ },
+ shell: os.platform() === "win32"
+ };
+
+ // forward output to channel and resolve promise
+ let resolved = false;
+ const onOutput = (output: string) => {
+ this.outputChannel_.append(output);
+ if (!resolved) {
+ if (output.includes("Running on ")) {
+ resolved = true;
+ resolve(undefined);
+ }
+ }
+ };
+
+ // run server
+ const quote = os.platform() === "win32" ? shQuote : (arg: string) => arg;
+ const args = [
+ "view", "start",
+ "--port", String(this.serverPort_),
+ "--log-level", "info", "--no-ansi"
+ ];
+ this.serverProcess_ = spawnProcess(quote(inspect.path), args.map(quote), options, {
+ stdout: onOutput,
+ stderr: onOutput,
+ }, {
+ onClose: (code: number) => {
+ this.outputChannel_.appendLine(`Inspect View exited with code ${code} (pid=${this.serverProcess_?.pid})`);
+ },
+ onError: (error: Error) => {
+ this.outputChannel_.appendLine(`Error starting Inspect View ${error.message}`);
+ reject(error);
+ },
+ });
+ this.outputChannel_.appendLine(`Starting Inspect View on port ${this.serverPort_} (pid=${this.serverProcess_?.pid})`);
+ });
+
+ }
+ });
+ }
+
+
+ private haveInspectEvalLogFormat() {
+ return hasMinimumInspectVersion(kInspectEvalLogFormatVersion, true);
+ }
+
+ private async api_json(path: string): Promise {
+ return await this.api(path, false) as string;
+ }
+
+ private async api_bytes(path: string): Promise {
+ return await this.api(path, true) as Uint8Array;
+ }
+
+ private async api(path: string, binary: boolean = false): Promise {
+
+ // ensure the server is started and ready
+ await this.ensureRunning();
+
+ // build headers
+ const headers = {
+ Authorization: this.serverAuthToken_,
+ Accept: binary ? "application/octet-stream" : "application/json",
+ Pragma: "no-cache",
+ Expires: "0",
+ ["Cache-Control"]: "no-cache",
+ };
+
+ // make request
+ const response = await fetch(`http://localhost:${this.serverPort_}${path}`, { method: "GET", headers });
+ if (response.ok) {
+ if (binary) {
+ const buffer = await response.arrayBuffer();
+ return new Uint8Array(buffer);
+ } else {
+ return await response.text();
+ }
+ } else if (response.status !== 200) {
+ const message = (await response.text()) || response.statusText;
+ const error = new Error(`Error: ${response.status}: ${message})`);
+ throw error;
+ } else {
+ throw new Error(`${response.status} - ${response.statusText} `);
+ }
+ }
+
+ private shutdown() {
+ this.serverProcess_?.kill();
+ this.serverProcess_ = undefined;
+ this.serverPort_ = undefined;
+ this.serverAuthToken_ = "";
+ }
+
+ dispose() {
+ this.shutdown();
+ this.outputChannel_.dispose();
+ }
+
+ private outputChannel_: OutputChannel;
+ private serverStartupLock_ = new AsyncLock();
+ private serverProcess_?: ChildProcess = undefined;
+ private serverPort_?: number = undefined;
+ private serverAuthToken_: string = "";
+
+}
+
+
+
+// The eval commands below need to be coordinated in terms of their working directory
+// The evalLogs() call will return log files with relative paths to the working dir (if possible)
+// and subsequent calls like evalLog() need to be able to deal with these relative paths
+// by using the same working directory.
+//
+// So, we always use the workspace root as the working directory and will resolve
+// paths that way. Note that paths can be S3 urls, for example, in which case the paths
+// will be absolute (so cwd doesn't really matter so much in this case).
+function evalLogs(log_dir: Uri): Promise {
+ // Return both the log_dir and the logs
+
+ const response = withMinimumInspectVersion(
+ kInspectOpenInspectViewVersion,
+ () => {
+ const logs = inspectEvalLogs(activeWorkspacePath(), log_dir);
+ const logsJson = logs ? (JSON.parse(logs) as unknown) : [];
+ return JSON.stringify({ log_dir: log_dir.toString(), files: logsJson });
+ },
+ () => {
+ // Return the original log content
+ return inspectEvalLogs(activeWorkspacePath());
+ }
+ );
+ return Promise.resolve(response);
+
+}
+
+function evalLog(
+ file: string,
+ headerOnly: boolean | number
+): Promise {
+ // Old clients pass a boolean value which we need to resolve
+ // into the max number of MB the log can be before samples are excluded
+ // and it becomes header_only
+ if (typeof headerOnly === "boolean") {
+ headerOnly = headerOnly ? 0 : Number.MAX_SAFE_INTEGER;
+ }
+
+ return Promise.resolve(
+ inspectEvalLog(activeWorkspacePath(), file, headerOnly)
+ );
+}
+
+function evalLogHeaders(files: string[]) {
+ return Promise.resolve(inspectEvalLogHeaders(activeWorkspacePath(), files));
+}
diff --git a/tools/vscode/src/providers/logview/commands.ts b/tools/vscode/src/providers/logview/commands.ts
index d11359113..ae9200434 100644
--- a/tools/vscode/src/providers/logview/commands.ts
+++ b/tools/vscode/src/providers/logview/commands.ts
@@ -1,24 +1,20 @@
import { Command } from "../../core/command";
-import { InspectLogviewManager } from "./logview-manager";
+import { InspectViewManager } from "./logview-view";
import { showError } from "../../components/error";
import { MessageItem, Uri, commands, window } from "vscode";
import { withMinimumInspectVersion } from "../../inspect/version";
import { kInspectOpenInspectViewVersion } from "../inspect/inspect-constants";
import { inspectLogInfo } from "../../inspect/logs";
-
-export interface LogviewState {
- log_file?: Uri;
- log_dir: Uri;
- background_refresh?: boolean;
-}
+import { LogviewState } from "./logview-state";
export interface LogviewOptions {
state?: LogviewState;
activate?: boolean;
}
+
export async function logviewCommands(
- manager: InspectLogviewManager,
+ manager: InspectViewManager,
): Promise {
// Check whether the open in inspect view command should be enabled
@@ -35,7 +31,7 @@ export async function logviewCommands(
}
class ShowLogviewCommand implements Command {
- constructor(private readonly manager_: InspectLogviewManager) { }
+ constructor(private readonly manager_: InspectViewManager) { }
async execute(): Promise {
// ensure logview is visible
try {
@@ -54,7 +50,7 @@ class ShowLogviewCommand implements Command {
class OpenInInspectViewCommand implements Command {
- constructor(private readonly manager_: InspectLogviewManager) { }
+ constructor(private readonly manager_: InspectViewManager) { }
async execute(fileUri: Uri): Promise {
// ensure logview is visible
try {
diff --git a/tools/vscode/src/providers/logview/logview-editor.ts b/tools/vscode/src/providers/logview/logview-editor.ts
new file mode 100644
index 000000000..a5971c3ce
--- /dev/null
+++ b/tools/vscode/src/providers/logview/logview-editor.ts
@@ -0,0 +1,122 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import * as vscode from 'vscode';
+import { Uri, window } from 'vscode';
+import { inspectViewPath } from '../../inspect/props';
+import { LogviewPanel } from './logview-panel';
+import { InspectViewServer } from '../inspect/inspect-view-server';
+import { HostWebviewPanel } from '../../hooks';
+import { InspectSettingsManager } from "../settings/inspect-settings";
+import { log } from '../../core/log';
+import { hasMinimumInspectVersion } from '../../inspect/version';
+import { kInspectEvalLogFormatVersion } from '../inspect/inspect-constants';
+
+
+class InspectLogReadonlyEditor implements vscode.CustomReadonlyEditorProvider {
+
+ static register(
+ context: vscode.ExtensionContext,
+ settings: InspectSettingsManager,
+ server: InspectViewServer
+ ): vscode.Disposable {
+ const provider = new InspectLogReadonlyEditor(context, settings, server);
+ const providerRegistration = vscode.window.registerCustomEditorProvider(
+ InspectLogReadonlyEditor.viewType,
+ provider,
+ {
+ webviewOptions: {
+ retainContextWhenHidden: false
+ },
+ supportsMultipleEditorsPerDocument: true
+ }
+ );
+ return providerRegistration;
+ }
+
+ private static readonly viewType = 'inspect-ai.log-editor';
+
+ constructor(
+ private readonly context_: vscode.ExtensionContext,
+ private readonly settings_: InspectSettingsManager,
+ private readonly server_: InspectViewServer
+ ) { }
+
+ // eslint-disable-next-line @typescript-eslint/require-await
+ async openCustomDocument(
+ uri: vscode.Uri,
+ _openContext: vscode.CustomDocumentOpenContext,
+ _token: vscode.CancellationToken
+ ): Promise {
+ return { uri, dispose: () => { } };
+ }
+
+ // eslint-disable-next-line @typescript-eslint/require-await
+ async resolveCustomEditor(
+ document: vscode.CustomDocument,
+ webviewPanel: vscode.WebviewPanel,
+ _token: vscode.CancellationToken
+ ): Promise {
+
+ // check if we should use the log viewer (pref + never show files > 100mb)
+ let useLogViewer = this.settings_.getSettings().jsonLogView;
+
+ // Check for a required version
+ useLogViewer = hasMinimumInspectVersion(kInspectEvalLogFormatVersion, true);
+
+ if (useLogViewer) {
+ const docUri = document.uri.toString();
+ if (docUri.endsWith(".json")) {
+ const fileSize = await this.server_.evalLogSize(docUri);
+ if (fileSize > (1024 * 1000 * 100)) {
+ log.info(`JSON log file ${document.uri.path} is to large for Inspect View, opening in text editor.`);
+ useLogViewer = false;
+ }
+ }
+ }
+
+ if (useLogViewer) {
+ // local resource roots
+ const localResourceRoots: Uri[] = [];
+ const viewDir = inspectViewPath();
+ if (viewDir) {
+ localResourceRoots.push(Uri.file(viewDir.path));
+ }
+ Uri.joinPath(this.context_.extensionUri, "assets", "www");
+
+ // set webview options
+ webviewPanel.webview.options = {
+ enableScripts: true,
+ enableForms: true,
+ localResourceRoots
+ };
+
+ // editor panel implementation
+ this.logviewPanel_ = new LogviewPanel(
+ webviewPanel as HostWebviewPanel,
+ this.context_,
+ this.server_,
+ "file",
+ document.uri
+ );
+
+ // set html
+ webviewPanel.webview.html = this.logviewPanel_.getHtml(document.uri);
+ } else {
+ const viewColumn = webviewPanel.viewColumn;
+ await vscode.commands.executeCommand('vscode.openWith', document.uri, 'default', viewColumn);
+ }
+ }
+
+ dispose() {
+ this.logviewPanel_?.dispose();
+ }
+
+ private logviewPanel_?: LogviewPanel;
+
+}
+
+export function activateLogviewEditor(
+ context: vscode.ExtensionContext,
+ settings: InspectSettingsManager,
+ server: InspectViewServer) {
+ context.subscriptions.push(InspectLogReadonlyEditor.register(context, settings, server));
+}
\ No newline at end of file
diff --git a/tools/vscode/src/providers/logview/logview-file-watcher.ts b/tools/vscode/src/providers/logview/logview-file-watcher.ts
index b7025fe6d..a51d81b54 100644
--- a/tools/vscode/src/providers/logview/logview-file-watcher.ts
+++ b/tools/vscode/src/providers/logview/logview-file-watcher.ts
@@ -1,5 +1,5 @@
import { Uri, Disposable } from "vscode";
-import { InspectLogviewManager } from "./logview-manager";
+import { InspectViewManager } from "./logview-view";
import { showError } from "../../components/error";
import { inspectLastEvalPath } from "../../inspect/props";
@@ -13,7 +13,7 @@ import { InspectSettingsManager } from "../settings/inspect-settings";
export class LogViewFileWatcher implements Disposable {
constructor(
- private readonly logviewManager_: InspectLogviewManager,
+ private readonly logviewManager_: InspectViewManager,
private readonly workspaceStateManager_: WorkspaceStateManager,
private readonly settingsMgr_: InspectSettingsManager
) {
diff --git a/tools/vscode/src/providers/logview/logview-link-provider.ts b/tools/vscode/src/providers/logview/logview-link-provider.ts
index 9a66d7455..c05f1ebda 100644
--- a/tools/vscode/src/providers/logview/logview-link-provider.ts
+++ b/tools/vscode/src/providers/logview/logview-link-provider.ts
@@ -1,6 +1,6 @@
import { MessageItem, Uri, window, workspace } from "vscode";
-import { InspectLogviewManager } from "./logview-manager";
+import { InspectViewManager } from "./logview-view";
import { workspacePath } from "../../core/path";
import { showError } from "../../components/error";
import { TerminalLink, TerminalLinkContext } from "vscode";
@@ -13,7 +13,7 @@ interface LogViewTerminalLink extends TerminalLink {
data: string;
}
-export const logviewTerminalLinkProvider = (manager: InspectLogviewManager) => {
+export const logviewTerminalLinkProvider = (manager: InspectViewManager) => {
return {
provideTerminalLinks: (
context: TerminalLinkContext,
diff --git a/tools/vscode/src/providers/logview/logview-manager.ts b/tools/vscode/src/providers/logview/logview-manager.ts
deleted file mode 100644
index e4c46b897..000000000
--- a/tools/vscode/src/providers/logview/logview-manager.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { Uri, ViewColumn, window, workspace } from "vscode";
-import { InspectLogviewWebviewManager } from "./logview-webview";
-import { InspectSettingsManager } from "../settings/inspect-settings";
-import { WorkspaceEnvManager } from "../workspace/workspace-env-provider";
-import { activeWorkspaceFolder } from "../../core/workspace";
-import { workspacePath } from "../../core/path";
-import { kInspectEnvValues } from "../inspect/inspect-constants";
-import { join } from "path";
-
-export class InspectLogviewManager {
- constructor(
- private readonly webViewManager_: InspectLogviewWebviewManager,
- private readonly settingsMgr_: InspectSettingsManager,
- private readonly envMgr_: WorkspaceEnvManager
- ) { }
-
- public async showLogFile(logFile: Uri, activation?: "open" | "activate") {
- const settings = this.settingsMgr_.getSettings();
- if (settings.logViewType === "text" && logFile.scheme === "file") {
- await workspace.openTextDocument(logFile).then(async (doc) => {
- await window.showTextDocument(doc, {
- preserveFocus: true,
- viewColumn: ViewColumn.Two,
- });
- });
- } else {
- await this.webViewManager_.showLogFile(logFile, activation);
- }
- }
-
- public async showInspectView() {
- // See if there is a log dir
- const envVals = this.envMgr_.getValues();
- const env_log = envVals[kInspectEnvValues.logDir];
-
- // If there is a log dir, try to parse and use it
- let log_uri;
- try {
- log_uri = Uri.parse(env_log, true);
- } catch {
- // This isn't a uri, bud
- const logDir = env_log ? workspacePath(env_log).path : join(workspacePath().path, "logs");
- log_uri = Uri.file(logDir);
- }
-
- // Show the log view for the log dir (or the workspace)
- const log_dir = log_uri || activeWorkspaceFolder().uri;
- await this.webViewManager_.showLogview({ log_dir }, "activate");
- }
-
- public viewColumn() {
- return this.webViewManager_.viewColumn();
- }
-}
diff --git a/tools/vscode/src/providers/logview/logview-panel.ts b/tools/vscode/src/providers/logview/logview-panel.ts
new file mode 100644
index 000000000..c1b9e9ae0
--- /dev/null
+++ b/tools/vscode/src/providers/logview/logview-panel.ts
@@ -0,0 +1,250 @@
+import vscode from "vscode";
+import { env, ExtensionContext, MessageItem, Uri, window } from "vscode";
+import { getNonce } from "../../core/nonce";
+import { HostWebviewPanel } from "../../hooks";
+import { inspectViewPath } from "../../inspect/props";
+import { readFileSync } from "fs";
+import { Disposable } from "../../core/dispose";
+import { jsonRpcPostMessageServer, JsonRpcPostMessageTarget, JsonRpcServerMethod, kMethodEvalLog, kMethodEvalLogBytes, kMethodEvalLogHeaders, kMethodEvalLogs, kMethodEvalLogSize } from "../../core/jsonrpc";
+import { InspectViewServer } from "../inspect/inspect-view-server";
+import { workspacePath } from "../../core/path";
+
+
+
+export class LogviewPanel extends Disposable {
+ constructor(
+ private panel_: HostWebviewPanel,
+ private context_: ExtensionContext,
+ server: InspectViewServer,
+ type: "file" | "dir",
+ uri: Uri,
+ ) {
+ super();
+
+ // serve eval log api to webview
+ this._rpcDisconnect = webviewPanelJsonRpcServer(panel_, {
+ [kMethodEvalLogs]: async () => type === "dir"
+ ? server.evalLogs(uri)
+ : server.evalLogsSolo(uri),
+ [kMethodEvalLog]: (params: unknown[]) => server.evalLog(params[0] as string, params[1] as number | boolean),
+ [kMethodEvalLogSize]: (params: unknown[]) => server.evalLogSize(params[0] as string),
+ [kMethodEvalLogBytes]: (params: unknown[]) => server.evalLogBytes(params[0] as string, params[1] as number, params[2] as number),
+ [kMethodEvalLogHeaders]: (params: unknown[]) => server.evalLogHeaders(params[0] as string[])
+ });
+
+ // serve post message api to webview
+ this._pmUnsubcribe = panel_.webview.onDidReceiveMessage(
+ async (e: { type: string; url: string;[key: string]: unknown }) => {
+ switch (e.type) {
+ case "openExternal":
+ try {
+ const url = Uri.parse(e.url);
+ await env.openExternal(url);
+ } catch {
+ // Noop
+ }
+ break;
+ case "openWorkspaceFile":
+ {
+ if (e.url) {
+ const file = workspacePath(e.url);
+ try {
+ await window.showTextDocument(Uri.file(file.path));
+ } catch (err) {
+ if (
+ err instanceof Error &&
+ err.name === "CodeExpectedError"
+ ) {
+ const close: MessageItem = { title: "Close" };
+ await window.showInformationMessage(
+ "This file is too large to be opened by the viewer.",
+ close
+ );
+ } else {
+ throw err;
+ }
+ }
+ }
+ }
+ break;
+ }
+ }
+ );
+ }
+
+ public dispose() {
+ this._rpcDisconnect();
+ this._pmUnsubcribe.dispose();
+ }
+
+ public getHtml(log_file?: Uri): string {
+ // read the index.html from the log view directory
+ const viewDir = inspectViewPath();
+ if (viewDir) {
+ // get nonce
+ const nonce = getNonce();
+
+ // file uri for view dir
+ const viewDirUri = Uri.file(viewDir.path);
+
+ // get base html
+ let indexHtml = readFileSync(viewDir.child("index.html").path, "utf-8");
+
+ // Determine whether this is the old unbundled version of the html or the new
+ // bundled version
+ const isUnbundled = indexHtml.match(/"\.(\/App\.mjs)"/g);
+
+ // Add a stylesheet to further customize the view appearance
+ const overrideCssPath = this.extensionResourceUrl([
+ "assets",
+ "www",
+ "view",
+ "view-overrides.css",
+ ]);
+ const overrideCssHtml = isUnbundled
+ ? ``
+ : "";
+
+ // If there is a log file selected in state, embed the startup message
+ // within the view itself. This will allow the log to be set immediately
+ // which avoids timing issues when first opening the view (e.g. the updateState
+ // message being sent before the view itself is configured to receive messages)
+ const stateMsg = {
+ type: "updateState",
+ url: log_file?.toString(),
+ };
+ const stateScript =
+ log_file
+ ? ``
+ : "";
+
+ // decorate the html tag
+ indexHtml = indexHtml.replace("\n",
+ `
+
+
+ ${stateScript}
+ ${overrideCssHtml}
+
+ `
+ );
+
+ // function to resolve resource uri
+ const resourceUri = (path: string) =>
+ this.panel_.webview
+ .asWebviewUri(Uri.joinPath(viewDirUri, path))
+ .toString();
+
+ // nonces for scripts
+ indexHtml = indexHtml.replace(
+ /`
- : "";
-
- // decorate the html tag
- indexHtml = indexHtml.replace("\n",
- `
-
-
- ${stateScript}
- ${overrideCssHtml}
-
- `
- );
-
- // function to resolve resource uri
- const resourceUri = (path: string) =>
- this._webviewPanel.webview
- .asWebviewUri(Uri.joinPath(viewDirUri, path))
- .toString();
-
- // nonces for scripts
- indexHtml = indexHtml.replace(
- /