feat: add OpenChamber sidebar command and icon for VS Code title bar

This commit is contained in:
Bohdan Triapitsyn
2025-12-31 00:06:59 +02:00
parent a9f3180439
commit 7b98f972f3
4 changed files with 208 additions and 4 deletions
+42
View File
@@ -0,0 +1,42 @@
<svg width="24" height="24" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<!-- OpenChamber logo - tuned for VS Code title bar (no theming) -->
<g transform="translate(16, 16) scale(0.48)">
<!-- Left face -->
<path
d="M0 0 L-26 -15 L-26 15 L0 30 Z"
fill="none"
stroke="#A0AF54"
stroke-width="4"
stroke-linejoin="round"
/>
<!-- Right face -->
<path
d="M0 0 L26 -15 L26 15 L0 30 Z"
fill="none"
stroke="#A0AF54"
stroke-width="4"
stroke-linejoin="round"
/>
<!-- Top face -->
<path
d="M0 -30 L-26 -15 L0 0 L26 -15 Z"
fill="none"
stroke="#A0AF54"
stroke-width="4"
stroke-linejoin="round"
/>
<!-- O logo - hollow rectangle frame with soft fill -->
<g transform="matrix(0.866, 0.5, -0.866, 0.5, 0, -15) scale(0.55)">
<rect
x="-12"
y="-14"
width="24"
height="28"
fill="#C4D36A"
fill-opacity="0.32"
stroke="#A0AF54"
stroke-width="8"
/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+19
View File
@@ -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"
+5 -1
View File
@@ -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;
+142 -3
View File
@@ -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<string | null> => {
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<boolean>('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<void>((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<string>('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');
})
);