Skip to content
Closed
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
15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,21 @@
"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.chatPanelLocation": {
"type": "string",
"default": "splitPanel",
"enum": [
"panel",
"splitPanel",
"secondarySideBar"
],
"enumDescriptions": [
"Open new chats as tabs in the active editor group without splitting the editor area.",
"Open new chats in an editor group split to the right of the active group (default).",
"Keep chats in the right-most chat editor group: split once, then stack further chats there (right-hand chat rail)."
],
"description": "Where new Pi chat panels are placed: editor tab (Panel), right split (SplitPanel), or the right chat rail (SecondarySideBar)."
}
}
},
Expand Down
41 changes: 41 additions & 0 deletions src/chat-panel-location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Where a newly created 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).
* - `"secondarySideBar"` — chats live in the right-most chat editor group:
* the first chat splits to the right once and later chats are stacked into
* the same far-right group, approximating a right-hand chat rail without
* multiplying editor columns.
*/
export type ChatPanelLocation = "panel" | "splitPanel" | "secondarySideBar";

export const DEFAULT_CHAT_PANEL_LOCATION: ChatPanelLocation = "splitPanel";

export function parseChatPanelLocation(raw: unknown): ChatPanelLocation {
return raw === "panel" || raw === "splitPanel" || raw === "secondarySideBar"
? raw
: DEFAULT_CHAT_PANEL_LOCATION;
}

export type ChatColumnTarget =
| { kind: "active" }
| { kind: "beside" }
| { kind: "column"; column: number };

/** Choose the editor column a new chat panel should open in. */
export function resolveChatColumnTarget(
location: ChatPanelLocation,
openChatColumns: ReadonlyArray<number>,
): ChatColumnTarget {
if (location === "panel") { return { kind: "active" }; }
if (location === "secondarySideBar") {
if (openChatColumns.length > 0) {
return { kind: "column", column: Math.max(...openChatColumns) };
}
return { kind: "beside" };
}
return { kind: "beside" };
}
18 changes: 17 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import { extractSessionId } from "./session-reference.js";
import { shouldRevealSessionPanel } from "./session-startup.js";
import { findReusableDraft, shouldPromoteDraft } from "./session-draft.js";
import { normalizeSessionRename } from "./session-rename.js";
import {
parseChatPanelLocation,
resolveChatColumnTarget,
} from "./chat-panel-location.js";
import {
emptySessionListPreferences,
isSessionArchived,
Expand Down Expand Up @@ -1604,7 +1608,19 @@ function addSession(context: vscode.ExtensionContext, cwd = getWorkspaceCwd()):
void saveOpenSessionPaths();
};
setActiveSession(sw);
void sw.webviewPanel.show();
const location = parseChatPanelLocation(
vscode.workspace.getConfiguration("pi-on-code").get("chatPanelLocation"),
);
const openChatColumns = sessions
.map((session) => session.webviewPanel.viewColumn)
.filter((column): column is number => typeof column === "number");
const target = resolveChatColumnTarget(location, openChatColumns);
const viewColumn = target.kind === "active"
? vscode.ViewColumn.Active
: target.kind === "beside"
? vscode.ViewColumn.Beside
: target.column;
void sw.webviewPanel.show(viewColumn);
void initSessionInBackground(context, sw, { fresh: true });
}

Expand Down
88 changes: 88 additions & 0 deletions src/test/chat-panel-location.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import * as assert from "node:assert";
import { readFileSync } from "node:fs";
import {
DEFAULT_CHAT_PANEL_LOCATION,
parseChatPanelLocation,
resolveChatColumnTarget,
} 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 three documented locations", () => {
assert.strictEqual(parseChatPanelLocation("panel"), "panel");
assert.strictEqual(parseChatPanelLocation("splitPanel"), "splitPanel");
assert.strictEqual(parseChatPanelLocation("secondarySideBar"), "secondarySideBar");
});

test("panel mode opens in the active editor group without splitting", () => {
assert.deepStrictEqual(
resolveChatColumnTarget("panel", [1, 2]),
{ kind: "active" },
);
});

test("splitPanel mode always splits to the right of the active group", () => {
assert.deepStrictEqual(
resolveChatColumnTarget("splitPanel", [1, 2, 3]),
{ kind: "beside" },
);
});

test("secondarySideBar stacks new chats into the right-most chat column", () => {
assert.deepStrictEqual(
resolveChatColumnTarget("secondarySideBar", [1, 3]),
{ kind: "column", column: 3 },
);
});

test("secondarySideBar splits once when no chat column exists yet", () => {
assert.deepStrictEqual(
resolveChatColumnTarget("secondarySideBar", []),
{ kind: "beside" },
);
});

test("publishes a matching configuration property", () => {
const manifest = JSON.parse(
readFileSync(new URL("../../package.json", import.meta.url), "utf8"),
) as {
contributes: {
configuration: {
properties: Record<string, {
default: unknown;
enum?: unknown[];
enumDescriptions?: unknown[];
}>;
};
};
};
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", "secondarySideBar"]);
assert.strictEqual(property.enumDescriptions?.length, 3);
});

test("new-session creation consults the setting and panel column", () => {
const extension = readFileSync(
new URL("../../src/extension.ts", import.meta.url),
"utf8",
);
assert.match(extension, /parseChatPanelLocation\(/);
assert.match(extension, /getConfiguration\("pi-on-code"\)\.get\("chatPanelLocation"\)/);
assert.match(extension, /resolveChatColumnTarget\(location, openChatColumns\)/);
assert.match(extension, /sw\.webviewPanel\.show\(viewColumn\)/);
const panel = readFileSync(
new URL("../../src/webview-panel.ts", import.meta.url),
"utf8",
);
assert.match(panel, /async show\(column\?: vscode\.ViewColumn\)/);
assert.match(panel, /column \?\? vscode\.ViewColumn\.Two/);
});
});
5 changes: 3 additions & 2 deletions src/webview-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ export class PiWebviewPanel {
set onActivate(cb: (() => void) | null) { this._onActivateCb = cb; }
private _onActivateCb: (() => void) | null = null;
get isActive(): boolean { return this.panel?.active === true; }
get viewColumn(): vscode.ViewColumn | undefined { return this.panel?.viewColumn; }

async show(): Promise<void> {
async show(column?: vscode.ViewColumn): Promise<void> {
if (this.panel) {
this.panel.reveal();
// Retained webviews can keep the compositor surface from the size they
Expand All @@ -107,7 +108,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,
Expand Down
Loading