diff --git a/packages/vscode/assets/icon-titlebar.svg b/packages/vscode/assets/icon-titlebar.svg
new file mode 100644
index 00000000..3c8c1408
--- /dev/null
+++ b/packages/vscode/assets/icon-titlebar.svg
@@ -0,0 +1,42 @@
+
diff --git a/packages/vscode/package.json b/packages/vscode/package.json
index 85087cc7..063ff4e3 100644
--- a/packages/vscode/package.json
+++ b/packages/vscode/package.json
@@ -54,6 +54,19 @@
]
},
"commands": [
+ {
+ "command": "openchamber.openSidebar",
+ "title": "Open OpenChamber Sidebar",
+ "icon": {
+ "light": "assets/icon.svg",
+ "dark": "assets/icon-titlebar.svg"
+ }
+ },
+ {
+ "command": "openchamber.focusChat",
+ "title": "OpenChamber: Focus Chat"
+ },
+
{
"command": "openchamber.restartApi",
"title": "OpenChamber: Restart API Connection"
@@ -88,6 +101,12 @@
"group": "navigation"
}
],
+ "editor/title": [
+ {
+ "command": "openchamber.openSidebar",
+ "group": "navigation@1"
+ }
+ ],
"openchamber.submenu": [
{
"command": "openchamber.explain"
diff --git a/packages/vscode/src/ChatViewProvider.ts b/packages/vscode/src/ChatViewProvider.ts
index f21b7dba..f9d7c799 100644
--- a/packages/vscode/src/ChatViewProvider.ts
+++ b/packages/vscode/src/ChatViewProvider.ts
@@ -8,7 +8,11 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'openchamber.chatView';
private _view?: vscode.WebviewView;
-
+
+ public isVisible() {
+ return this._view?.visible ?? false;
+ }
+
// Cache latest status/URL for when webview is resolved after connection is ready
private _cachedStatus: ConnectionStatus = 'connecting';
private _cachedError?: string;
diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts
index 2808db7c..0cf9cecc 100644
--- a/packages/vscode/src/extension.ts
+++ b/packages/vscode/src/extension.ts
@@ -26,6 +26,143 @@ const formatDurationMs = (value: number | null | undefined) => {
export async function activate(context: vscode.ExtensionContext) {
outputChannel = vscode.window.createOutputChannel('OpenChamber');
+ let moveToRightSidebarScheduled = false;
+
+ const isCursorLikeHost = () => /\bcursor\b/i.test(vscode.env.appName);
+
+ const findMoveToRightSidebarCommandId = async (): Promise => {
+ const commands = await vscode.commands.getCommands(true);
+
+ const preferred = [
+ // Newer VS Code naming
+ 'workbench.action.moveViewToSecondarySideBar',
+ 'workbench.action.moveViewToSecondarySidebar',
+ 'workbench.action.moveFocusedViewToSecondarySideBar',
+ 'workbench.action.moveFocusedViewToSecondarySidebar',
+
+ // Some builds use "Auxiliary Bar" naming
+ 'workbench.action.moveViewToAuxiliaryBar',
+ 'workbench.action.moveFocusedViewToAuxiliaryBar',
+ ];
+
+ for (const commandId of preferred) {
+ if (commands.includes(commandId)) return commandId;
+ }
+
+ const fuzzy = commands.find((commandId) => {
+ const id = commandId.toLowerCase();
+ const looksLikeMoveView = id.includes('workbench.action') && id.includes('move') && id.includes('view');
+ if (!looksLikeMoveView) return false;
+
+ // Support both "secondary sidebar" and "auxiliary bar" naming.
+ return (id.includes('secondary') && id.includes('side') && id.includes('bar')) || (id.includes('auxiliary') && id.includes('bar'));
+ });
+
+ return fuzzy || null;
+ };
+
+ const attemptMoveChatToRightSidebar = async (): Promise<'moved' | 'unsupported' | 'failed'> => {
+ const moveCommandId = await findMoveToRightSidebarCommandId();
+ if (!moveCommandId) return 'unsupported';
+
+ try {
+ await vscode.commands.executeCommand('openchamber.chatView.focus');
+ await vscode.commands.executeCommand(moveCommandId);
+ return 'moved';
+ } catch (error) {
+ outputChannel?.appendLine(
+ `[OpenChamber] Failed moving chat view to right sidebar (command=${moveCommandId}): ${error instanceof Error ? error.message : String(error)}`
+ );
+ return 'failed';
+ }
+ };
+
+ const maybeMoveChatToRightSidebarOnStartup = async () => {
+ if (isCursorLikeHost()) return;
+
+ const attempted = context.globalState.get('openchamber.sidebarAutoMoveAttempted') || false;
+ if (attempted) return;
+ await context.globalState.update('openchamber.sidebarAutoMoveAttempted', true);
+
+ if (moveToRightSidebarScheduled) return;
+ moveToRightSidebarScheduled = true;
+
+ // Defer until after activation to avoid stealing focus during startup.
+ setTimeout(() => {
+ void (async () => {
+ try {
+ await attemptMoveChatToRightSidebar();
+ } finally {
+ moveToRightSidebarScheduled = false;
+ }
+ })();
+ }, 800);
+ };
+
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
+
+ const tryExecuteCommand = async (commandId: string) => {
+ try {
+ await vscode.commands.executeCommand(commandId);
+ return true;
+ } catch {
+ return false;
+ }
+ };
+
+ const togglePrimarySidebar = async () => {
+ return await tryExecuteCommand('workbench.action.toggleSidebarVisibility');
+ };
+
+ const toggleSecondarySidebar = async () => {
+ const candidates = ['workbench.action.toggleSecondarySideBarVisibility', 'workbench.action.toggleAuxiliaryBar'];
+ for (const commandId of candidates) {
+ if (await tryExecuteCommand(commandId)) return true;
+ }
+ return false;
+ };
+
+ context.subscriptions.push(
+ vscode.commands.registerCommand('openchamber.openSidebar', async () => {
+ const isOpen = chatViewProvider?.isVisible() ?? false;
+
+ // Second click hides the sidebar that currently hosts the chat view.
+ if (isOpen) {
+ if (isCursorLikeHost()) {
+ await togglePrimarySidebar();
+ await sleep(50);
+ if (chatViewProvider?.isVisible()) {
+ await toggleSecondarySidebar();
+ }
+ return;
+ }
+
+ await toggleSecondarySidebar();
+ await sleep(50);
+ if (chatViewProvider?.isVisible()) {
+ await togglePrimarySidebar();
+ }
+ return;
+ }
+
+ // Best-effort: open the container (if available), then focus the chat view.
+ try {
+ await vscode.commands.executeCommand('workbench.view.extension.openchamber');
+ } catch {
+ // Ignore: not all VS Code forks expose this command.
+ }
+
+ await vscode.commands.executeCommand('openchamber.chatView.focus');
+ })
+ );
+
+ context.subscriptions.push(
+ vscode.commands.registerCommand('openchamber.focusChat', async () => {
+ await vscode.commands.executeCommand('openchamber.chatView.focus');
+ })
+ );
+
+
// Migration: clear legacy auto-set API URLs (ports 47680-47689 were auto-assigned by older extension versions)
const config = vscode.workspace.getConfiguration('openchamber');
const legacyApiUrl = config.get('apiUrl') || '';
@@ -48,6 +185,8 @@ export async function activate(context: vscode.ExtensionContext) {
)
);
+ void maybeMoveChatToRightSidebarOnStartup();
+
context.subscriptions.push(
vscode.commands.registerCommand('openchamber.restartApi', async () => {
try {
@@ -91,7 +230,7 @@ export async function activate(context: vscode.ExtensionContext) {
chatViewProvider?.addTextToInput(contextText);
// Focus the chat panel
- vscode.commands.executeCommand('openchamber.chatView.focus');
+ vscode.commands.executeCommand('openchamber.focusChat');
})
);
@@ -123,7 +262,7 @@ export async function activate(context: vscode.ExtensionContext) {
// Create new session and send the prompt
chatViewProvider?.createNewSessionWithPrompt(prompt);
- vscode.commands.executeCommand('openchamber.chatView.focus');
+ vscode.commands.executeCommand('openchamber.focusChat');
})
);
@@ -153,7 +292,7 @@ export async function activate(context: vscode.ExtensionContext) {
// Create new session and send the prompt
chatViewProvider?.createNewSessionWithPrompt(prompt);
- vscode.commands.executeCommand('openchamber.chatView.focus');
+ vscode.commands.executeCommand('openchamber.focusChat');
})
);