Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions packages/extension/src/chat_bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as os from "node:os";
import * as fs from "node:fs";
import { opencodeDataDir, opencodeConfigDir } from "./opencode_xdg";
import { findForkedOpencodeBinary } from "./opencode_binary";
import type { ExplorerIconTheme } from "./explorer_icon_theme";
import { HARMONIQS_MODEL_ID, HARMONIQS_PROVIDER_ID, testConnection, writeOnboardingConfig } from "./onboarding_panel";
import {
readSkillProviders,
Expand Down Expand Up @@ -102,6 +103,9 @@ export interface BridgeIo {
* "reset" = expand selected + collapse others, "expand" = expand selected
* only, "none" = highlight only. */
onProjectSelected?: (path: string, mode?: "none" | "expand" | "reset") => void;
previewVisibleChildren?: (root: string, relativeDirectory: string) => Promise<Array<{ name: string; kind: "file" | "directory"; absolute: string; relative: string }>>;
/** Returns the currently-selected Explorer file icon theme as opaque assets. */
explorerIconTheme?: () => ExplorerIconTheme;
}

const isAmicode = (msg: unknown): msg is { source: "amicode"; kind: string; tab?: string } =>
Expand Down Expand Up @@ -133,6 +137,39 @@ export function extractReportBugModel(
export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean {
if (!isAmicode(msg)) return false;

// The sandboxed app requests its Preview-tab icon data explicitly. The host
// owns the only asset reader and returns opaque IDs plus allowlisted bytes.
if (msg.kind === "explorer-icon-theme-request") {
if (io.explorerIconTheme) {
io.postToWebview({
source: "amicode",
kind: "explorer-icon-theme",
theme: io.explorerIconTheme(),
...(typeof msg.tab === "string" ? { tab: msg.tab } : {}),
});
}
return true;
}

if (msg.kind === "preview-visible-children-request") {
const requestId = (msg as { requestId?: unknown }).requestId;
const root = (msg as { root?: unknown }).root;
const relativeDirectory = (msg as { relativeDirectory?: unknown }).relativeDirectory;
const tab = typeof msg.tab === "string" ? { tab: msg.tab } : {};
if (typeof requestId !== "string" || requestId.length === 0 || requestId.length > 200) return true;
if (typeof root !== "string" || root.length === 0 || root.length > 4096) return true;
if (typeof relativeDirectory !== "string" || relativeDirectory.length > 4096) return true;
if (!io.previewVisibleChildren) {
io.postToWebview({ source: "amicode", kind: "preview-visible-children-result", requestId, error: "Preview navigation is unavailable", ...tab });
return true;
}
void io.previewVisibleChildren(root, relativeDirectory).then(
(entries) => io.postToWebview({ source: "amicode", kind: "preview-visible-children-result", requestId, entries, ...tab }),
() => io.postToWebview({ source: "amicode", kind: "preview-visible-children-result", requestId, error: "Could not load files", ...tab }),
);
return true;
}

// target=_blank/window.open are dead inside the framed app — open https
// links via the editor (system browser). https-only; scheme is
// case-insensitive (RFC 3986).
Expand Down
41 changes: 33 additions & 8 deletions packages/extension/src/chat_panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { handleAmicodeBridgeMessage } from "./chat_bridge";
import { registerInspectorPoster } from "./inspector_bridge";
import { getBugReport } from "./bug_report";
import { FileWatcherBridge } from "./file_watcher_bridge";
import { resolveExplorerIconTheme } from "./explorer_icon_theme";

// ============================================================================
// ChatPanel — a WebviewPanel that iframes opencode's SolidJS chat at
Expand Down Expand Up @@ -50,6 +51,7 @@ export class ChatPanel {
* "reset" = expand selected + collapse others, "expand" = expand selected
* only, "none" = highlight only. */
private static onProjectSelectedCallback?: (path: string | null, mode?: "none" | "expand" | "reset") => void;
private static previewVisibleChildrenCallback?: (root: string, relativeDirectory: string) => Promise<Array<{ name: string; kind: "file" | "directory"; absolute: string; relative: string }>>;
/** The `amicode_bug_report=1` boot-param gate (amicode#250 AC5): set from the
* staged skill set after every session prep; the composer button renders
* only when the report-a-bug skill is there to answer it. */
Expand Down Expand Up @@ -81,6 +83,10 @@ export class ChatPanel {
ChatPanel.onProjectSelectedCallback = cb;
}

static onPreviewVisibleChildren(cb: ((root: string, relativeDirectory: string) => Promise<Array<{ name: string; kind: "file" | "directory"; absolute: string; relative: string }>>) | undefined): void {
ChatPanel.previewVisibleChildrenCallback = cb;
}

private constructor(
private readonly panel: vscode.WebviewPanel,
private readonly tabTitle: string,
Expand All @@ -104,15 +110,24 @@ export class ChatPanel {
// #351: register this panel as an inspector poster — RunsManager / device
// poll fan out run/device envelopes to every live chat webview.
this.disposables.push(registerInspectorPoster((msg) => void this.panel.webview.postMessage(msg)));
// Live theme bridge: editor theme changes flow extension → outer relay →
// iframe → the app's setColorScheme (boot theme rides ?colorScheme=).
// Live theme bridge: editor theme changes update both the app color scheme
// and icon-theme light variants. The app also requests this after every boot.
vscode.window.onDidChangeActiveColorTheme(
(t) =>
(t) => {
void this.panel.webview.postMessage({
source: "amicode",
kind: "theme",
colorScheme: themeKindToScheme(t.kind),
}),
});
this.postExplorerIconTheme();
},
null,
this.disposables,
);
vscode.workspace.onDidChangeConfiguration(
(event) => {
if (event.affectsConfiguration("workbench.iconTheme")) this.postExplorerIconTheme();
},
null,
this.disposables,
);
Expand Down Expand Up @@ -158,6 +173,8 @@ export class ChatPanel {
onProjectSelected: ChatPanel.onProjectSelectedCallback
? (p, mode) => { this.lastProjectPath = p; ChatPanel.onProjectSelectedCallback!(p, mode); }
: undefined,
previewVisibleChildren: ChatPanel.previewVisibleChildrenCallback,
explorerIconTheme: resolveExplorerIconTheme,
});
if (!handled) console.log("[amicode/chat] webview msg:", msg);
},
Expand All @@ -179,6 +196,14 @@ export class ChatPanel {
);
}

private postExplorerIconTheme(): void {
void this.panel.webview.postMessage({
source: "amicode",
kind: "explorer-icon-theme",
theme: resolveExplorerIconTheme(),
});
}

/** `authToken` is the per-boot server credential (#163) as the app's
* `?auth_token=` bootstrap value — base64("opencode:<password>"), from
* serverAuthToken(). The app adopts it for its authenticated-fetch path and
Expand Down Expand Up @@ -457,7 +482,7 @@ export class ChatPanel {
vscode.postMessage({ source: "amicode", kind: "clipboard-image-read", nonce: d.nonce });
return;
}
if (d && d.source === "amicode" && (d.kind === "command" || d.kind === "clipboard-request" || d.kind === "clipboard-write" || d.kind === "open-external" || d.kind === "open-file" || d.kind === "save-file" || d.kind === "set-default-model" || d.kind === "bug-filed" || d.kind === "bug-report-closed" || d.kind === "bug-report-poke" || d.kind === "dev-tools-update" || d.kind === "dev-tools-rebuild" || d.kind === "dev-tools-build-vsix" || d.kind === "data-storage-query" || d.kind === "data-storage-update" || d.kind === "redo-onboarding" || d.kind === "connect-harmoniqs-provider" || d.kind === "device:refresh" || d.kind === "connections-credential" || d.kind === "connections-disconnect" || d.kind === "connections-revalidate" || d.kind === "connections-auth" || d.kind === "connections-choose-project" || d.kind === "connections-add-custom" || d.kind === "connections-remove" || d.kind === "skill-providers-query" || d.kind === "skill-providers-add" || d.kind === "skill-providers-remove" || d.kind === "skill-providers-rename" || d.kind === "skill-providers-autodiscover" || d.kind === "skill-providers-pick-directory" || d.kind === "add-workspace-project" || d.kind === "project-selected" || d.kind === "app-ready" || d.kind === "watch-files")) {
if (d && d.source === "amicode" && (d.kind === "command" || d.kind === "clipboard-request" || d.kind === "clipboard-write" || d.kind === "open-external" || d.kind === "open-file" || d.kind === "save-file" || d.kind === "set-default-model" || d.kind === "bug-filed" || d.kind === "bug-report-closed" || d.kind === "bug-report-poke" || d.kind === "dev-tools-update" || d.kind === "dev-tools-rebuild" || d.kind === "dev-tools-build-vsix" || d.kind === "data-storage-query" || d.kind === "data-storage-update" || d.kind === "redo-onboarding" || d.kind === "connect-harmoniqs-provider" || d.kind === "device:refresh" || d.kind === "connections-credential" || d.kind === "connections-disconnect" || d.kind === "connections-revalidate" || d.kind === "connections-auth" || d.kind === "connections-choose-project" || d.kind === "connections-add-custom" || d.kind === "connections-remove" || d.kind === "skill-providers-query" || d.kind === "skill-providers-add" || d.kind === "skill-providers-remove" || d.kind === "skill-providers-rename" || d.kind === "skill-providers-autodiscover" || d.kind === "skill-providers-pick-directory" || d.kind === "add-workspace-project" || d.kind === "project-selected" || d.kind === "app-ready" || d.kind === "watch-files" || d.kind === "preview-visible-children-request" || d.kind === "explorer-icon-theme-request")) {
vscode.postMessage(d);
}
return;
Expand All @@ -467,7 +492,7 @@ export class ChatPanel {
// our own envelopes, pinned to the opencode origin. #351 adds
// run:*/device:* envelopes for the Work Column inspector tabs.
// #934: preview-file — sidebar/chat file routing to the Preview companion tab.
if (d && d.source === "amicode" && (d.kind === "theme" || d.kind === "clipboard" || d.kind === "navigate" || d.kind === "open-compute-connect" || d.kind === "open-bug-report" || d.kind === "close-bug-report" || d.kind === "dev-tools-status" || d.kind === "connect-harmoniqs-provider-result" || d.kind === "dev-tools-rebuild-status" || d.kind === "dev-tools-build-vsix-status" || d.kind === "data-storage-defaults" || d.kind === "data-storage-status" || d.kind === "connections-credential-result" || d.kind === "connections-disconnect-result" || d.kind === "connections-revalidate-result" || d.kind === "connections-auth-result" || d.kind === "connections-choose-project-result" || d.kind === "connections-add-custom-result" || d.kind === "connections-remove-result" || d.kind === "skill-providers-data" || d.kind === "skill-providers-discovered" || (typeof d.kind === "string" && (d.kind.indexOf("run:") === 0 || d.kind.indexOf("device:") === 0)) || d.kind === "clipboard-image" || d.kind === "workspace-projects" || d.kind === "file-op-notify" || d.kind === "fs-diff-invalidate" || d.kind === "agent-cycle" || d.kind === "preview-file")) {
if (d && d.source === "amicode" && (d.kind === "theme" || d.kind === "clipboard" || d.kind === "navigate" || d.kind === "open-compute-connect" || d.kind === "open-bug-report" || d.kind === "close-bug-report" || d.kind === "dev-tools-status" || d.kind === "connect-harmoniqs-provider-result" || d.kind === "dev-tools-rebuild-status" || d.kind === "dev-tools-build-vsix-status" || d.kind === "data-storage-defaults" || d.kind === "data-storage-status" || d.kind === "connections-credential-result" || d.kind === "connections-disconnect-result" || d.kind === "connections-revalidate-result" || d.kind === "connections-auth-result" || d.kind === "connections-choose-project-result" || d.kind === "connections-add-custom-result" || d.kind === "connections-remove-result" || d.kind === "skill-providers-data" || d.kind === "skill-providers-discovered" || (typeof d.kind === "string" && (d.kind.indexOf("run:") === 0 || d.kind.indexOf("device:") === 0)) || d.kind === "clipboard-image" || d.kind === "workspace-projects" || d.kind === "file-op-notify" || d.kind === "fs-diff-invalidate" || d.kind === "agent-cycle" || d.kind === "preview-file" || d.kind === "preview-visible-children-result" || d.kind === "explorer-icon-theme")) {
var f = document.querySelector("iframe");
if (f && f.contentWindow) f.contentWindow.postMessage(d, ${origin});
}
Expand Down Expand Up @@ -619,12 +644,12 @@ export class ChatPanel {
vscode.postMessage({ source: "amicode", kind: "clipboard-image-read", nonce: d.nonce });
return;
}
if (d && d.source === "amicode" && (d.kind === "command" || d.kind === "clipboard-request" || d.kind === "clipboard-write" || d.kind === "open-external" || d.kind === "open-file" || d.kind === "save-file" || d.kind === "set-default-model" || d.kind === "bug-filed" || d.kind === "bug-report-closed" || d.kind === "bug-report-poke" || d.kind === "dev-tools-update" || d.kind === "dev-tools-rebuild" || d.kind === "dev-tools-build-vsix" || d.kind === "data-storage-query" || d.kind === "data-storage-update" || d.kind === "redo-onboarding" || d.kind === "connect-harmoniqs-provider" || d.kind === "device:refresh" || d.kind === "connections-credential" || d.kind === "connections-disconnect" || d.kind === "connections-revalidate" || d.kind === "connections-auth" || d.kind === "connections-choose-project" || d.kind === "connections-add-custom" || d.kind === "connections-remove" || d.kind === "skill-providers-query" || d.kind === "skill-providers-add" || d.kind === "skill-providers-remove" || d.kind === "skill-providers-rename" || d.kind === "skill-providers-autodiscover" || d.kind === "skill-providers-pick-directory" || d.kind === "add-workspace-project" || d.kind === "project-selected" || d.kind === "app-ready" || d.kind === "watch-files")) {
if (d && d.source === "amicode" && (d.kind === "command" || d.kind === "clipboard-request" || d.kind === "clipboard-write" || d.kind === "open-external" || d.kind === "open-file" || d.kind === "save-file" || d.kind === "set-default-model" || d.kind === "bug-filed" || d.kind === "bug-report-closed" || d.kind === "bug-report-poke" || d.kind === "dev-tools-update" || d.kind === "dev-tools-rebuild" || d.kind === "dev-tools-build-vsix" || d.kind === "data-storage-query" || d.kind === "data-storage-update" || d.kind === "redo-onboarding" || d.kind === "connect-harmoniqs-provider" || d.kind === "device:refresh" || d.kind === "connections-credential" || d.kind === "connections-disconnect" || d.kind === "connections-revalidate" || d.kind === "connections-auth" || d.kind === "connections-choose-project" || d.kind === "connections-add-custom" || d.kind === "connections-remove" || d.kind === "skill-providers-query" || d.kind === "skill-providers-add" || d.kind === "skill-providers-remove" || d.kind === "skill-providers-rename" || d.kind === "skill-providers-autodiscover" || d.kind === "skill-providers-pick-directory" || d.kind === "add-workspace-project" || d.kind === "project-selected" || d.kind === "app-ready" || d.kind === "watch-files" || d.kind === "preview-visible-children-request" || d.kind === "explorer-icon-theme-request")) {
vscode.postMessage(d);
}
return;
}
if (d && d.source === "amicode" && (d.kind === "theme" || d.kind === "clipboard" || d.kind === "navigate" || d.kind === "open-compute-connect" || d.kind === "open-bug-report" || d.kind === "close-bug-report" || d.kind === "dev-tools-status" || d.kind === "connect-harmoniqs-provider-result" || d.kind === "dev-tools-rebuild-status" || d.kind === "dev-tools-build-vsix-status" || d.kind === "data-storage-defaults" || d.kind === "data-storage-status" || d.kind === "connections-credential-result" || d.kind === "connections-disconnect-result" || d.kind === "connections-revalidate-result" || d.kind === "connections-auth-result" || d.kind === "connections-choose-project-result" || d.kind === "connections-add-custom-result" || d.kind === "connections-remove-result" || d.kind === "skill-providers-data" || d.kind === "skill-providers-discovered" || (typeof d.kind === "string" && (d.kind.indexOf("run:") === 0 || d.kind.indexOf("device:") === 0)) || d.kind === "clipboard-image" || d.kind === "workspace-projects" || d.kind === "file-op-notify" || d.kind === "fs-diff-invalidate" || d.kind === "agent-cycle" || d.kind === "preview-file")) {
if (d && d.source === "amicode" && (d.kind === "theme" || d.kind === "clipboard" || d.kind === "navigate" || d.kind === "open-compute-connect" || d.kind === "open-bug-report" || d.kind === "close-bug-report" || d.kind === "dev-tools-status" || d.kind === "connect-harmoniqs-provider-result" || d.kind === "dev-tools-rebuild-status" || d.kind === "dev-tools-build-vsix-status" || d.kind === "data-storage-defaults" || d.kind === "data-storage-status" || d.kind === "connections-credential-result" || d.kind === "connections-disconnect-result" || d.kind === "connections-revalidate-result" || d.kind === "connections-auth-result" || d.kind === "connections-choose-project-result" || d.kind === "connections-add-custom-result" || d.kind === "connections-remove-result" || d.kind === "skill-providers-data" || d.kind === "skill-providers-discovered" || (typeof d.kind === "string" && (d.kind.indexOf("run:") === 0 || d.kind.indexOf("device:") === 0)) || d.kind === "clipboard-image" || d.kind === "workspace-projects" || d.kind === "file-op-notify" || d.kind === "fs-diff-invalidate" || d.kind === "agent-cycle" || d.kind === "preview-file" || d.kind === "preview-visible-children-result" || d.kind === "explorer-icon-theme")) {
var f = document.querySelector("iframe");
if (f && f.contentWindow) f.contentWindow.postMessage(d, origin);
}
Expand Down
Loading
Loading