diff --git a/package.json b/package.json index 97b51c9..6cccc1c 100644 --- a/package.json +++ b/package.json @@ -327,6 +327,26 @@ "type": "boolean", "default": false, "description": "Automatically reveal a new Pi chat tab when the extension activates. Previously open Pi session tabs are always restored." + }, + "pi-on-code.newChatPanelLocation": { + "type": "string", + "default": "panel", + "enum": ["panel", "splitPanel"], + "enumDescriptions": [ + "Open brand-new Pi chats as tabs in the active editor group without splitting the editor area.", + "Open brand-new Pi chats in a new editor group split to the right of the active group." + ], + "description": "Where brand-new Pi chats open in the editor." + }, + "pi-on-code.chatPanelLocation": { + "type": "string", + "default": "splitPanel", + "enum": ["panel", "splitPanel"], + "enumDescriptions": [ + "Open resumed/forked Pi chats as tabs in the active editor group without splitting the editor area.", + "Open resumed/forked Pi chats in a new editor group split to the right of the active group (historical default)." + ], + "description": "Where resumed and forked Pi chats open in the editor." } } }, diff --git a/src/chat-panel-location.ts b/src/chat-panel-location.ts new file mode 100644 index 0000000..a72a148 --- /dev/null +++ b/src/chat-panel-location.ts @@ -0,0 +1,17 @@ +/** + * Where a newly opened Pi chat panel is placed inside the VS Code window. + * + * - `"panel"` — the chat opens as a tab in the active editor group; the editor + * area is never split for it. + * - `"splitPanel"` — the chat opens in a new editor group split to the right + * of the active group (historical default behavior). + */ +export type ChatPanelLocation = "panel" | "splitPanel"; + +export const DEFAULT_CHAT_PANEL_LOCATION: ChatPanelLocation = "splitPanel"; + +export function parseChatPanelLocation(raw: unknown): ChatPanelLocation { + return raw === "panel" || raw === "splitPanel" + ? raw + : DEFAULT_CHAT_PANEL_LOCATION; +} diff --git a/src/extension.ts b/src/extension.ts index a41668b..d4b5900 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,6 +15,7 @@ import { } from "./session-sidebar-provider.js"; import { initLogger, piLog, piWarn } from "./logger.js"; import { getWorkspaceCwd, getWorkspaceFolders, getWorkspaceRoot, getWorkspaceUri } from "./workspace-context.js"; +import { parseChatPanelLocation } from "./chat-panel-location.js"; import { registerPhase3Commands } from "./phase3-commands.js"; import { registerPhase4Commands } from "./phase4-commands.js"; import type { SessionSummary } from "./types.js"; @@ -1127,7 +1128,7 @@ export async function activate(context: vscode.ExtensionContext): Promise ): Promise { const newSw = createSessionWindow(context, { path: forkedPath }, false, cwd); setActiveSession(newSw); - void newSw.webviewPanel.show(); + void newSw.webviewPanel.show(chatShowColumn("chatPanelLocation")); sessionTreeProvider?.refresh(); await initSessionInBackground(context, newSw, { openPath: forkedPath }); @@ -1373,7 +1374,7 @@ export async function activate(context: vscode.ExtensionContext): Promise title: summary?.name ?? summary?.firstMessage, }, false, summary?.cwd ?? getWorkspaceCwd()); setActiveSession(sw); - await sw.webviewPanel.show(); + await sw.webviewPanel.show(chatShowColumn("chatPanelLocation")); setSessionResultUnread(sw, false); sessionTreeProvider?.refresh(); void initSessionInBackground(context, sw, { openPath: resolved }); @@ -1946,6 +1947,14 @@ async function doUpdatePackage(source: string): Promise { // ── Add a new session window ────────────────────────── +/** Column for a chat opened under the given location setting key. */ +function chatShowColumn(key: string): vscode.ViewColumn | undefined { + const location = parseChatPanelLocation( + vscode.workspace.getConfiguration("pi-on-code").get(key), + ); + return location === "panel" ? vscode.ViewColumn.Active : undefined; +} + function addSession(context: vscode.ExtensionContext, cwd = getWorkspaceCwd()): void { const existingDraft = findReusableDraft(sessions.filter((session) => session.cwd === cwd)); if (existingDraft) { @@ -1962,7 +1971,7 @@ function addSession(context: vscode.ExtensionContext, cwd = getWorkspaceCwd()): void saveOpenSessionPaths(); }; setActiveSession(sw); - void sw.webviewPanel.show(); + void sw.webviewPanel.show(chatShowColumn("newChatPanelLocation")); void initSessionInBackground(context, sw, { fresh: true }); } diff --git a/src/test/chat-panel-location.test.ts b/src/test/chat-panel-location.test.ts new file mode 100644 index 0000000..dcf0dc5 --- /dev/null +++ b/src/test/chat-panel-location.test.ts @@ -0,0 +1,55 @@ +import * as assert from "node:assert"; +import { readFileSync } from "node:fs"; +import { + DEFAULT_CHAT_PANEL_LOCATION, + parseChatPanelLocation, +} from "../chat-panel-location.js"; + +suite("Chat panel location", () => { + test("defaults to splitPanel for missing or invalid values", () => { + assert.strictEqual(DEFAULT_CHAT_PANEL_LOCATION, "splitPanel"); + assert.strictEqual(parseChatPanelLocation(undefined), "splitPanel"); + assert.strictEqual(parseChatPanelLocation("sidebar-left"), "splitPanel"); + assert.strictEqual(parseChatPanelLocation(42), "splitPanel"); + }); + + test("accepts the two documented locations", () => { + assert.strictEqual(parseChatPanelLocation("panel"), "panel"); + assert.strictEqual(parseChatPanelLocation("splitPanel"), "splitPanel"); + }); + + test("publishes new (panel) and resume/fork (splitPanel) properties", () => { + const manifest = JSON.parse( + readFileSync(new URL("../../package.json", import.meta.url), "utf8"), + ) as { + contributes: { + configuration: { + properties: Record; + }; + }; + }; + const newProp = manifest.contributes.configuration.properties["pi-on-code.newChatPanelLocation"]; + const resumeProp = manifest.contributes.configuration.properties["pi-on-code.chatPanelLocation"]; + assert.ok(newProp, "pi-on-code.newChatPanelLocation property is missing"); + assert.strictEqual(newProp.default, "panel"); + assert.deepStrictEqual(newProp.enum, ["panel", "splitPanel"]); + assert.ok(resumeProp, "pi-on-code.chatPanelLocation property is missing"); + assert.strictEqual(resumeProp.default, "splitPanel"); + assert.deepStrictEqual(resumeProp.enum, ["panel", "splitPanel"]); + }); + + test("routes new chats through newChatPanelLocation and resumes/forks through chatPanelLocation", () => { + const extension = readFileSync( + new URL("../../src/extension.ts", import.meta.url), + "utf8", + ); + assert.match(extension, /function chatShowColumn\(key: string\)/); + assert.match(extension, /sw\.webviewPanel\.show\(chatShowColumn\("newChatPanelLocation"\)\)/); + assert.match(extension, /newSw\.webviewPanel\.show\(chatShowColumn\("chatPanelLocation"\)\)/); + assert.match(extension, /await sw\.webviewPanel\.show\(chatShowColumn\("chatPanelLocation"\)\)/); + }); +}); diff --git a/src/webview-panel.ts b/src/webview-panel.ts index ea6d534..3a2fc85 100644 --- a/src/webview-panel.ts +++ b/src/webview-panel.ts @@ -98,7 +98,7 @@ export class PiWebviewPanel { private _onActivateCb: (() => void) | null = null; get isActive(): boolean { return this.panel?.active === true; } - async show(): Promise { + async show(column?: vscode.ViewColumn): Promise { if (this.panel) { this.panel.reveal(); // Retained webviews can keep the compositor surface from the size they @@ -115,7 +115,7 @@ export class PiWebviewPanel { this.panel = vscode.window.createWebviewPanel( "pi-chat-" + randomId, "Pi on Code", - vscode.ViewColumn.Two, + column ?? vscode.ViewColumn.Two, { enableScripts: true, retainContextWhenHidden: true,