From 43f2344d10edcdd57790589637ac9ba4cf41f0df Mon Sep 17 00:00:00 2001 From: Pi on Code Agent <313040514+pi-on-code-agent[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 06:39:28 +0000 Subject: [PATCH 1/3] feat: add new session without split option --- package.json | 5 +++ src/extension.ts | 7 +++- src/test/new-session-without-split.test.ts | 46 ++++++++++++++++++++++ src/webview-panel.ts | 4 +- 4 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 src/test/new-session-without-split.test.ts diff --git a/package.json b/package.json index 97b51c9..def4d4a 100644 --- a/package.json +++ b/package.json @@ -327,6 +327,11 @@ "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.newSessionWithoutSplit": { + "type": "boolean", + "default": false, + "description": "Open new Pi sessions in the active editor group without splitting the editor area (default keeps the historical right-side split)." } } }, diff --git a/src/extension.ts b/src/extension.ts index a41668b..c2c1780 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1962,7 +1962,12 @@ function addSession(context: vscode.ExtensionContext, cwd = getWorkspaceCwd()): void saveOpenSessionPaths(); }; setActiveSession(sw); - void sw.webviewPanel.show(); + const withoutSplit = vscode.workspace + .getConfiguration("pi-on-code") + .get("newSessionWithoutSplit", false); + // New sessions may open in the active editor group without splitting the + // editor area; otherwise the historical split-to-column-Two default stands. + void sw.webviewPanel.show(withoutSplit ? vscode.ViewColumn.Active : undefined); void initSessionInBackground(context, sw, { fresh: true }); } diff --git a/src/test/new-session-without-split.test.ts b/src/test/new-session-without-split.test.ts new file mode 100644 index 0000000..940c04e --- /dev/null +++ b/src/test/new-session-without-split.test.ts @@ -0,0 +1,46 @@ +import * as assert from "node:assert"; +import { readFileSync } from "node:fs"; + +const extension = readFileSync( + new URL("../../src/extension.ts", import.meta.url), + "utf8", +); +const panel = readFileSync( + new URL("../../src/webview-panel.ts", import.meta.url), + "utf8", +); + +suite("New session without split", () => { + test("publishes a boolean configuration defaulting to false", () => { + const manifest = JSON.parse( + readFileSync(new URL("../../package.json", import.meta.url), "utf8"), + ) as { + contributes: { + configuration: { + properties: Record; + }; + }; + }; + const property = manifest.contributes.configuration.properties["pi-on-code.newSessionWithoutSplit"]; + assert.ok(property, "pi-on-code.newSessionWithoutSplit property is missing"); + assert.strictEqual(property.type, "boolean"); + assert.strictEqual(property.default, false); + }); + + test("new sessions open in the active group when enabled", () => { + assert.match( + extension, + /getConfiguration\("pi-on-code"\)\s*\n?\s*\.get\("newSessionWithoutSplit", false\)/, + ); + assert.match( + extension, + /sw\.webviewPanel\.show\(withoutSplit \? vscode\.ViewColumn\.Active : undefined\)/, + ); + }); + + test("keeps the historical split default when disabled", () => { + assert.match(panel, /async show\(column\?: vscode\.ViewColumn\)/); + assert.match(panel, /column \?\? vscode\.ViewColumn\.Two/); + assert.match(extension, /show\(withoutSplit \? vscode\.ViewColumn\.Active : undefined\)/); + }); +}); 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, From 4cb408ef75229e61f9ff4a8f5abacc38e97c8b41 Mon Sep 17 00:00:00 2001 From: auchan Date: Sun, 6 Sep 2026 15:54:27 +0800 Subject: [PATCH 2/3] feat: open chats in active group via chatPanelLocation setting --- package.json | 13 +++-- src/chat-panel-location.ts | 17 +++++++ src/extension.ts | 21 +++++---- src/test/chat-panel-location.test.ts | 55 ++++++++++++++++++++++ src/test/new-session-without-split.test.ts | 46 ------------------ 5 files changed, 94 insertions(+), 58 deletions(-) create mode 100644 src/chat-panel-location.ts create mode 100644 src/test/chat-panel-location.test.ts delete mode 100644 src/test/new-session-without-split.test.ts diff --git a/package.json b/package.json index def4d4a..405eb48 100644 --- a/package.json +++ b/package.json @@ -328,10 +328,15 @@ "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.newSessionWithoutSplit": { - "type": "boolean", - "default": false, - "description": "Open new Pi sessions in the active editor group without splitting the editor area (default keeps the historical right-side split)." + "pi-on-code.chatPanelLocation": { + "type": "string", + "default": "splitPanel", + "enum": ["panel", "splitPanel"], + "enumDescriptions": [ + "Open Pi chats as tabs in the active editor group without splitting the editor area.", + "Open Pi chats in a new editor group split to the right of the active group (historical default)." + ], + "description": "Where new, 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 c2c1780..ca38bf0 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()); 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()); setSessionResultUnread(sw, false); sessionTreeProvider?.refresh(); void initSessionInBackground(context, sw, { openPath: resolved }); @@ -1946,6 +1947,15 @@ async function doUpdatePackage(source: string): Promise { // ── Add a new session window ────────────────────────── +/** Column for newly opened chats: active group in panel mode, otherwise the + * historical right-side split (ViewColumn.Two is the panel default). */ +function chatShowColumn(): vscode.ViewColumn | undefined { + const location = parseChatPanelLocation( + vscode.workspace.getConfiguration("pi-on-code").get("chatPanelLocation"), + ); + 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,12 +1972,7 @@ function addSession(context: vscode.ExtensionContext, cwd = getWorkspaceCwd()): void saveOpenSessionPaths(); }; setActiveSession(sw); - const withoutSplit = vscode.workspace - .getConfiguration("pi-on-code") - .get("newSessionWithoutSplit", false); - // New sessions may open in the active editor group without splitting the - // editor area; otherwise the historical split-to-column-Two default stands. - void sw.webviewPanel.show(withoutSplit ? vscode.ViewColumn.Active : undefined); + void sw.webviewPanel.show(chatShowColumn()); 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..63d5252 --- /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 a matching configuration property", () => { + const manifest = JSON.parse( + readFileSync(new URL("../../package.json", import.meta.url), "utf8"), + ) as { + contributes: { + configuration: { + properties: Record; + }; + }; + }; + const property = manifest.contributes.configuration.properties["pi-on-code.chatPanelLocation"]; + assert.ok(property, "pi-on-code.chatPanelLocation property is missing"); + assert.strictEqual(property.default, "splitPanel"); + assert.deepStrictEqual(property.enum, ["panel", "splitPanel"]); + assert.strictEqual(property.enumDescriptions?.length, 2); + }); + + test("routes new, resumed, and forked chats through the shared column helper", () => { + const extension = readFileSync( + new URL("../../src/extension.ts", import.meta.url), + "utf8", + ); + assert.match(extension, /function chatShowColumn\(\)/); + assert.match(extension, /parseChatPanelLocation\(/); + assert.match(extension, /chatPanelLocation/); + assert.match(extension, /sw\.webviewPanel\.show\(chatShowColumn\(\)\)/); + assert.match(extension, /newSw\.webviewPanel\.show\(chatShowColumn\(\)\)/); + // Resume path also follows the setting. + assert.match(extension, /await sw\.webviewPanel\.show\(chatShowColumn\(\)\)/); + }); +}); diff --git a/src/test/new-session-without-split.test.ts b/src/test/new-session-without-split.test.ts deleted file mode 100644 index 940c04e..0000000 --- a/src/test/new-session-without-split.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as assert from "node:assert"; -import { readFileSync } from "node:fs"; - -const extension = readFileSync( - new URL("../../src/extension.ts", import.meta.url), - "utf8", -); -const panel = readFileSync( - new URL("../../src/webview-panel.ts", import.meta.url), - "utf8", -); - -suite("New session without split", () => { - test("publishes a boolean configuration defaulting to false", () => { - const manifest = JSON.parse( - readFileSync(new URL("../../package.json", import.meta.url), "utf8"), - ) as { - contributes: { - configuration: { - properties: Record; - }; - }; - }; - const property = manifest.contributes.configuration.properties["pi-on-code.newSessionWithoutSplit"]; - assert.ok(property, "pi-on-code.newSessionWithoutSplit property is missing"); - assert.strictEqual(property.type, "boolean"); - assert.strictEqual(property.default, false); - }); - - test("new sessions open in the active group when enabled", () => { - assert.match( - extension, - /getConfiguration\("pi-on-code"\)\s*\n?\s*\.get\("newSessionWithoutSplit", false\)/, - ); - assert.match( - extension, - /sw\.webviewPanel\.show\(withoutSplit \? vscode\.ViewColumn\.Active : undefined\)/, - ); - }); - - test("keeps the historical split default when disabled", () => { - assert.match(panel, /async show\(column\?: vscode\.ViewColumn\)/); - assert.match(panel, /column \?\? vscode\.ViewColumn\.Two/); - assert.match(extension, /show\(withoutSplit \? vscode\.ViewColumn\.Active : undefined\)/); - }); -}); From cd3c7e470d74efa7d3ee247a08f3b9abd94f78b9 Mon Sep 17 00:00:00 2001 From: auchan Date: Sun, 6 Sep 2026 15:55:54 +0800 Subject: [PATCH 3/3] feat: separate new-session panel location (default panel) --- package.json | 16 +++++++++++++--- src/extension.ts | 13 ++++++------- src/test/chat-panel-location.test.ts | 28 ++++++++++++++-------------- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 405eb48..6cccc1c 100644 --- a/package.json +++ b/package.json @@ -328,15 +328,25 @@ "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 Pi chats as tabs in the active editor group without splitting the editor area.", - "Open Pi chats in a new editor group split to the right of the active group (historical default)." + "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 new, resumed, and forked Pi chats open in the editor." + "description": "Where resumed and forked Pi chats open in the editor." } } }, diff --git a/src/extension.ts b/src/extension.ts index ca38bf0..d4b5900 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1128,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(chatShowColumn()); + void newSw.webviewPanel.show(chatShowColumn("chatPanelLocation")); sessionTreeProvider?.refresh(); await initSessionInBackground(context, newSw, { openPath: forkedPath }); @@ -1374,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(chatShowColumn()); + await sw.webviewPanel.show(chatShowColumn("chatPanelLocation")); setSessionResultUnread(sw, false); sessionTreeProvider?.refresh(); void initSessionInBackground(context, sw, { openPath: resolved }); @@ -1947,11 +1947,10 @@ async function doUpdatePackage(source: string): Promise { // ── Add a new session window ────────────────────────── -/** Column for newly opened chats: active group in panel mode, otherwise the - * historical right-side split (ViewColumn.Two is the panel default). */ -function chatShowColumn(): vscode.ViewColumn | undefined { +/** 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("chatPanelLocation"), + vscode.workspace.getConfiguration("pi-on-code").get(key), ); return location === "panel" ? vscode.ViewColumn.Active : undefined; } @@ -1972,7 +1971,7 @@ function addSession(context: vscode.ExtensionContext, cwd = getWorkspaceCwd()): void saveOpenSessionPaths(); }; setActiveSession(sw); - void sw.webviewPanel.show(chatShowColumn()); + 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 index 63d5252..dcf0dc5 100644 --- a/src/test/chat-panel-location.test.ts +++ b/src/test/chat-panel-location.test.ts @@ -18,7 +18,7 @@ suite("Chat panel location", () => { assert.strictEqual(parseChatPanelLocation("splitPanel"), "splitPanel"); }); - test("publishes a matching configuration property", () => { + test("publishes new (panel) and resume/fork (splitPanel) properties", () => { const manifest = JSON.parse( readFileSync(new URL("../../package.json", import.meta.url), "utf8"), ) as { @@ -32,24 +32,24 @@ suite("Chat panel location", () => { }; }; }; - const property = manifest.contributes.configuration.properties["pi-on-code.chatPanelLocation"]; - assert.ok(property, "pi-on-code.chatPanelLocation property is missing"); - assert.strictEqual(property.default, "splitPanel"); - assert.deepStrictEqual(property.enum, ["panel", "splitPanel"]); - assert.strictEqual(property.enumDescriptions?.length, 2); + 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, resumed, and forked chats through the shared column helper", () => { + 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\(\)/); - assert.match(extension, /parseChatPanelLocation\(/); - assert.match(extension, /chatPanelLocation/); - assert.match(extension, /sw\.webviewPanel\.show\(chatShowColumn\(\)\)/); - assert.match(extension, /newSw\.webviewPanel\.show\(chatShowColumn\(\)\)/); - // Resume path also follows the setting. - assert.match(extension, /await sw\.webviewPanel\.show\(chatShowColumn\(\)\)/); + 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"\)\)/); }); });