feat: add OpenChamber sidebar command and icon for VS Code title bar
This commit is contained in:
@@ -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 |
@@ -54,6 +54,19 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"commands": [
|
"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",
|
"command": "openchamber.restartApi",
|
||||||
"title": "OpenChamber: Restart API Connection"
|
"title": "OpenChamber: Restart API Connection"
|
||||||
@@ -88,6 +101,12 @@
|
|||||||
"group": "navigation"
|
"group": "navigation"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"editor/title": [
|
||||||
|
{
|
||||||
|
"command": "openchamber.openSidebar",
|
||||||
|
"group": "navigation@1"
|
||||||
|
}
|
||||||
|
],
|
||||||
"openchamber.submenu": [
|
"openchamber.submenu": [
|
||||||
{
|
{
|
||||||
"command": "openchamber.explain"
|
"command": "openchamber.explain"
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
|||||||
|
|
||||||
private _view?: vscode.WebviewView;
|
private _view?: vscode.WebviewView;
|
||||||
|
|
||||||
|
public isVisible() {
|
||||||
|
return this._view?.visible ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
// Cache latest status/URL for when webview is resolved after connection is ready
|
// Cache latest status/URL for when webview is resolved after connection is ready
|
||||||
private _cachedStatus: ConnectionStatus = 'connecting';
|
private _cachedStatus: ConnectionStatus = 'connecting';
|
||||||
private _cachedError?: string;
|
private _cachedError?: string;
|
||||||
|
|||||||
@@ -26,6 +26,143 @@ const formatDurationMs = (value: number | null | undefined) => {
|
|||||||
export async function activate(context: vscode.ExtensionContext) {
|
export async function activate(context: vscode.ExtensionContext) {
|
||||||
outputChannel = vscode.window.createOutputChannel('OpenChamber');
|
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)
|
// Migration: clear legacy auto-set API URLs (ports 47680-47689 were auto-assigned by older extension versions)
|
||||||
const config = vscode.workspace.getConfiguration('openchamber');
|
const config = vscode.workspace.getConfiguration('openchamber');
|
||||||
const legacyApiUrl = config.get<string>('apiUrl') || '';
|
const legacyApiUrl = config.get<string>('apiUrl') || '';
|
||||||
@@ -48,6 +185,8 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
void maybeMoveChatToRightSidebarOnStartup();
|
||||||
|
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.commands.registerCommand('openchamber.restartApi', async () => {
|
vscode.commands.registerCommand('openchamber.restartApi', async () => {
|
||||||
try {
|
try {
|
||||||
@@ -91,7 +230,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
chatViewProvider?.addTextToInput(contextText);
|
chatViewProvider?.addTextToInput(contextText);
|
||||||
|
|
||||||
// Focus the chat panel
|
// 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
|
// Create new session and send the prompt
|
||||||
chatViewProvider?.createNewSessionWithPrompt(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
|
// Create new session and send the prompt
|
||||||
chatViewProvider?.createNewSessionWithPrompt(prompt);
|
chatViewProvider?.createNewSessionWithPrompt(prompt);
|
||||||
vscode.commands.executeCommand('openchamber.chatView.focus');
|
vscode.commands.executeCommand('openchamber.focusChat');
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user