@@ -754,6 +797,21 @@ export default function Library() {
onClick={closePreview}
/>
+ {showChatToggle && (
+
diff --git a/apps/mesh/src/web/layouts/main-panel-tabs/tab-id.test.ts b/apps/mesh/src/web/layouts/main-panel-tabs/tab-id.test.ts
index 46288df90b..106bce9867 100644
--- a/apps/mesh/src/web/layouts/main-panel-tabs/tab-id.test.ts
+++ b/apps/mesh/src/web/layouts/main-panel-tabs/tab-id.test.ts
@@ -79,8 +79,8 @@ describe("deck tab id", () => {
expect(parseDeckTabId("deck:%E0%A4%A")).toBeNull();
});
- test("deck tabs are per-thread", () => {
- expect(isPerThreadTab(formatDeckTabId("decks/a.html"))).toBe(true);
+ test("deck tabs are NOT per-thread (org home volume survives task switches)", () => {
+ expect(isPerThreadTab(formatDeckTabId("decks/a.html"))).toBe(false);
});
});
diff --git a/apps/mesh/src/web/layouts/main-panel-tabs/tab-id.ts b/apps/mesh/src/web/layouts/main-panel-tabs/tab-id.ts
index 2379aaf752..fc4b554c5c 100644
--- a/apps/mesh/src/web/layouts/main-panel-tabs/tab-id.ts
+++ b/apps/mesh/src/web/layouts/main-panel-tabs/tab-id.ts
@@ -135,14 +135,16 @@ const FIXED_SYSTEM_TAB_SET = new Set(FIXED_SYSTEM_TABS);
* - "app::" (expanded tool / pinned view)
* - "automation:" (ephemeral automation detail)
* - "file:" (ephemeral thread-output file preview)
- * - "deck:" (ephemeral HTML-artifact preview/editor)
+ *
+ * Note: "deck:" is intentionally NOT per-thread — deck files live
+ * in the org home volume and outlive any thread, so the preview should persist
+ * when the user starts a new chat to continue working on the same page.
*/
export function isPerThreadTab(tabId: string): boolean {
return (
tabId.startsWith("app:") ||
tabId.startsWith("automation:") ||
- tabId.startsWith("file:") ||
- tabId.startsWith("deck:")
+ tabId.startsWith("file:")
);
}
diff --git a/packages/harness/src/decopilot/built-in-tools/open.ts b/packages/harness/src/decopilot/built-in-tools/open.ts
new file mode 100644
index 0000000000..39fb0ff14b
--- /dev/null
+++ b/packages/harness/src/decopilot/built-in-tools/open.ts
@@ -0,0 +1,41 @@
+/**
+ * open — tells the Studio UI to open a file in the preview panel.
+ *
+ * The tool itself has no server-side side-effects: it emits a transient
+ * `data-open-preview` stream part that the React client picks up in onChunk
+ * to navigate the preview panel, then returns immediately.
+ */
+
+import { tool, type UIMessageStreamWriter } from "ai";
+import { z } from "zod";
+
+export const OpenInputSchema = z.object({
+ filepath: z
+ .string()
+ .describe(
+ "Home-volume-relative path of the HTML file to open in the preview panel " +
+ "(e.g. `pages/landing.html` or `decks/q3-launch.html`).",
+ ),
+});
+
+export function createOpenTool(writer: UIMessageStreamWriter) {
+ return tool({
+ description:
+ "Open an HTML file in the Studio preview panel so the user can see it. " +
+ "Call this after creating or editing a page or deck. " +
+ "Pass the home-volume-relative path, e.g. `pages/landing.html`.",
+ inputSchema: OpenInputSchema,
+ execute: async ({ filepath }) => {
+ // Strip any leading slash so "pages/landing.html" and
+ // "/pages/landing.html" both produce the same deck tab id.
+ const normalizedPath = filepath.replace(/^\/+/, "");
+ // Emitted without an `id` so the part is NOT persisted in the message —
+ // this is a one-shot navigation signal, not durable message state.
+ writer.write({
+ type: "data-open-preview",
+ data: { filepath: normalizedPath },
+ });
+ return { success: true };
+ },
+ });
+}