Merge branch 'openchamber:main' into github-usage-rework

This commit is contained in:
Jakub Syty
2026-08-26 13:58:58 +02:00
committed by GitHub
513 changed files with 31772 additions and 12940 deletions
+30
View File
@@ -1,6 +1,36 @@
## [Unreleased]
- The chat view no longer stays stuck on its loading screen on slow or remote connections (for example code-server behind a reverse proxy) — the connection status is re-sent until the webview is ready to hear it (thanks @VinciYan).
- **Chat context attachments:** diff and file comments, terminal selections, and linked issues/PRs now show in the conversation as compact context cards — source header, captured content behind an expander, your comment below — instead of raw text inside the message.
- **Chat: comment on a reply.** Select text in a chat message and choose Comment to attach that quote with your note to the next message; the selection stays highlighted while you type. Add to chat is now Add to input.
- Diff: hovering a line shows a + button that opens a comment for the line; clicking a line or dragging across lines opens the editor for that range. The comment editor and saved-comment cards match the chat's comment style.
- Composer: hovering a context chip above the input opens a stacked preview of everything attached, where comments can be edited in place or items removed before sending.
- Chat: @ file mentions now rank files and directories together by how well they match, so the file you typed is at the top instead of below unrelated directories. Multi-word queries match in any order, and long paths keep the folder next to the file name visible.
- Search: Ctrl/Cmd+P now matches the whole file path, not just the file name — searching a folder name finds the files inside it.
- Search in dropdowns: searchable pickers (agents, models, providers, branches) now put the best matches first, match multi-word queries in any order, and ignore punctuation (so "gpt4o" finds "gpt-4o").
- Keyboard: dropdown menus and pickers answer Ctrl+N/Ctrl+P for down/up, the session switcher opens focused on your current session, and shortcut labels in tooltips and menus show the binding you actually have set (thanks @ChangeHow).
- Chat: OpenCode notices now share one style.
- The timeline dialog now fits small windows instead of squeezing the message list to a couple of rows (thanks to @gaojunran).
## [1.20.0] - 2026-08-23
- **/btw side questions:** type `/btw` followed by your question to ask something off-topic in a temporary session forked from the current conversation. The answer streams into a panel above the composer; collapse it, keep it as a full session, or discard it without touching the chat (thanks to @jaygupta17).
- **Skills catalog:** browse curated GitHub skill collections in a card-based catalog with cross-source search and direct links to each skill's repository.
- Settings: the workspace selector on Providers, Agents, MCP, Commands and Skills now only changes what those pages show instead of moving the chat, session list and file tree to another workspace.
- Settings/Projects: a project can now pin a thinking level next to its model, for models that offer levels.
- Settings/General: changing the default model, variant or agent no longer repoints an open chat that already carries a model you picked for it. Chats following the default still switch immediately.
- Settings/Providers: the provider you select no longer jumps to a different one when the chat selection or provider data changes.
- Settings/Integrations: the experimental page now only lists integrations that can be installed; unavailable and Coming soon entries were removed.
- Providers: expanded support for custom providers.
- Sessions created outside OpenChamber now appear in the sidebar and Recent list without a page refresh (thanks to @tomzx).
- If OpenCode restarts while a response is still running, the chat now stops with an interrupted state and a notification to continue instead of hanging silently (thanks to @sum117).
- Usage: Z.ai credit limits now appear alongside its other quota windows.
- Chat: file paths in messages now open from the session's workspace, even if you last browsed files in another workspace (thanks to @tomzx).
- Chat: app links such as `spotify://` now ask for confirmation before opening another app. You can trust an app link type on one device and manage trusted links in Settings.
- While a reply streams, the model status line under the last message now turns into the finished message's info row in place, instead of jumping when the reply completes.
- Chat: newly sent messages and syntax-highlighted code blocks no longer briefly flicker. Bash output can also grow with its content instead of being cut off.
- Chat: long user messages can be expanded even when their final layout finishes after they first appear.
- UI: the default dialog close button is easier to click or tap (thanks to @rockinrimmer).
## [1.19.0] - 2026-08-19
+2 -2
View File
@@ -2,7 +2,7 @@
"name": "openchamber",
"displayName": "OpenChamber",
"description": "%extension.description%",
"version": "1.19.0",
"version": "1.20.0",
"publisher": "fedaykindev",
"private": true,
"repository": {
@@ -245,7 +245,7 @@
},
"dependencies": {
"@openchamber/ui": "workspace:*",
"@opencode-ai/sdk": "1.18.18",
"@opencode-ai/sdk": "1.18.21",
"adm-zip": "^0.6.0",
"jsonc-parser": "^3.3.1",
"react": "^19.1.1",
@@ -1,4 +1,5 @@
import * as vscode from 'vscode';
import { scheduleCachedStateRetries } from './webviewCachedStateRetry';
import { handleBridgeMessage, type BridgeRequest, type BridgeResponse } from './bridge';
import { getThemeKindName } from './theme';
import type { OpenCodeManager, ConnectionStatus } from './opencode';
@@ -23,6 +24,19 @@ export class AgentManagerPanelProvider {
private _sseStreams = new Map<string, AbortController>();
private readonly _webviewDevServerUrl: string | null;
/**
* See webviewCachedStateRetry.ts — a single postMessage can be dropped
* before the webview bridge is ready, leaving the loading screen stuck.
*/
private _scheduleCachedStateRetries(targetPanel: vscode.WebviewPanel | undefined): void {
scheduleCachedStateRetries({
target: targetPanel ?? this._panel,
getCurrent: () => this._panel,
isConnected: () => this._cachedStatus === 'connected',
send: () => this._sendCachedState(),
});
}
constructor(
private readonly _context: vscode.ExtensionContext,
private readonly _extensionUri: vscode.Uri,
@@ -64,6 +78,9 @@ export class AgentManagerPanelProvider {
// Send cached connection status
this._sendCachedState();
// The webview bridge may not be ready yet; keep re-sending so a dropped
// `connectionStatus` can never leave the webview stuck on its loading screen.
this._scheduleCachedStateRetries(this._panel);
// Handle panel disposal
this._panel.onDidDispose(() => {
@@ -126,6 +143,13 @@ export class AgentManagerPanelProvider {
// Send to webview if it exists
this._sendCachedState();
// When we become connected, keep re-sending at staggered delays so the
// webview cannot miss the transition (postMessage is dropped if the
// webview bridge is not ready yet).
if (status === 'connected') {
this._scheduleCachedStateRetries(this._panel);
}
}
public notifySettingsSynced(settings: unknown): void {
+24
View File
@@ -1,4 +1,5 @@
import * as vscode from 'vscode';
import { scheduleCachedStateRetries } from './webviewCachedStateRetry';
import { handleBridgeMessage, type BridgeRequest, type BridgeResponse } from './bridge';
import { getThemeKindName } from './theme';
import type { OpenCodeManager, ConnectionStatus } from './opencode';
@@ -58,6 +59,19 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
private readonly _MESSAGE_TIMEOUT = 5000; // 5 seconds
private readonly _MAX_RETRIES = 3;
/**
* See webviewCachedStateRetry.ts — a single postMessage can be dropped
* before the webview bridge is ready, leaving the loading screen stuck.
*/
private _scheduleCachedStateRetries(targetView: vscode.WebviewView | undefined): void {
scheduleCachedStateRetries({
target: targetView ?? this._view,
getCurrent: () => this._view,
isConnected: () => this._cachedStatus === 'connected',
send: () => this._sendCachedState(),
});
}
private _createMessageId(): string {
return `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
}
@@ -102,6 +116,9 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
// Send cached connection status and API URL (may have been set before webview was resolved)
this._sendCachedState();
// The webview bridge may not be ready yet; keep re-sending so a dropped
// `connectionStatus` can never leave the webview stuck on its loading screen.
this._scheduleCachedStateRetries(webviewView);
// Send current active editor file state to the new webview
this._lastActiveEditorFilePayload = null;
@@ -185,6 +202,13 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
// Send to webview if it exists
this._sendCachedState();
// When we become connected, keep re-sending at staggered delays so the
// webview cannot miss the transition (postMessage is dropped if the
// webview bridge is not ready yet).
if (status === 'connected') {
this._scheduleCachedStateRetries(this._view);
}
}
public addTextToInput(text: string) {
+2
View File
@@ -44,6 +44,8 @@ Keep `bridge.ts` as a thin orchestration layer that delegates message handling t
The webview CSP permits `blob:` only for `worker-src` so shared UI parsers can run bounded local decompression off the main thread. Blob scripts remain disallowed by `script-src`.
The webview build emits each worker as one self-contained file. VS Code webviews cannot load workers directly from extension resource URLs or load module imports from inside a worker. The shared Shiki client therefore fetches the built worker, starts it from a `blob:` URL, and relies on the worker CSP allowance above.
- `bridge-localfs-proxy-runtime.ts`
- Local `/api/fs/read` and `/api/fs/raw` proxy helpers and shared proxy utility helpers.
- Workspace-contained Markdown gallery images use these local filesystem
@@ -1,4 +1,5 @@
import * as vscode from 'vscode';
import { scheduleCachedStateRetries } from './webviewCachedStateRetry';
import { handleBridgeMessage, type BridgeRequest, type BridgeResponse } from './bridge';
import { getThemeKindName } from './theme';
import type { OpenCodeManager, ConnectionStatus } from './opencode';
@@ -49,6 +50,19 @@ export class SessionEditorPanelProvider {
private _lastActiveEditorFilePayload: ActiveEditorFilePayload | null = null;
private readonly _webviewDevServerUrl: string | null;
/**
* See webviewCachedStateRetry.ts a single postMessage can be dropped
* before the webview bridge is ready, leaving the loading screen stuck.
*/
private _scheduleCachedStateRetries(panelId: string, entry: SessionPanelState): void {
scheduleCachedStateRetries({
target: entry.panel,
getCurrent: () => this._panels.get(panelId)?.panel,
isConnected: () => this._cachedStatus === 'connected',
send: () => this._sendCachedStateToPanel(entry),
});
}
constructor(
private readonly _context: vscode.ExtensionContext,
private readonly _extensionUri: vscode.Uri,
@@ -116,6 +130,9 @@ export class SessionEditorPanelProvider {
void this.updateTheme(vscode.window.activeColorTheme.kind);
this._sendCachedStateToPanel(state);
// The webview bridge may not be ready yet; keep re-sending so a dropped
// `connectionStatus` can never leave the webview stuck on its loading screen.
this._scheduleCachedStateRetries(panelId, state);
void this._broadcastActiveEditorFile();
panel.onDidDispose(() => {
@@ -187,6 +204,15 @@ export class SessionEditorPanelProvider {
for (const entry of this._panels.values()) {
this._sendCachedStateToPanel(entry);
}
// When we become connected, keep re-sending at staggered delays so the
// webview cannot miss the transition (postMessage is dropped if the
// webview bridge is not ready yet).
if (status === 'connected') {
for (const [panelId, entry] of this._panels.entries()) {
this._scheduleCachedStateRetries(panelId, entry);
}
}
}
public notifySettingsSynced(settings: unknown): void {
@@ -173,17 +173,20 @@ const readSharedSettingsFromDisk = (): Record<string, unknown> => {
};
const writeSharedSettingsToDisk = async (changes: Record<string, unknown>): Promise<void> => {
let tmp: string | null = null;
try {
await fs.promises.mkdir(path.dirname(OPENCHAMBER_SHARED_SETTINGS_PATH), { recursive: true });
const current = readSharedSettingsFromDisk();
const next: Record<string, unknown> = { ...current, ...changes };
// Atomic write: tmp file + rename. Readers never see a partial/truncated
// JSON that would fail to parse and silently get coerced to {}.
const tmp = `${OPENCHAMBER_SHARED_SETTINGS_PATH}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
tmp = `${OPENCHAMBER_SHARED_SETTINGS_PATH}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
await fs.promises.writeFile(tmp, JSON.stringify(next, null, 2), 'utf8');
await fs.promises.rename(tmp, OPENCHAMBER_SHARED_SETTINGS_PATH);
} catch {
// ignore
if (tmp) {
await fs.promises.rm(tmp, { force: true }).catch(() => {});
}
}
};
+11
View File
@@ -0,0 +1,11 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { describe, test } from 'node:test';
const source = readFileSync(new URL('../vite.config.ts', import.meta.url), 'utf8');
describe('VS Code webview worker build', () => {
test('bundles worker imports into one file', () => {
assert.match(source, /worker:\s*\{[\s\S]*?inlineDynamicImports:\s*true/);
});
});
@@ -0,0 +1,31 @@
/**
* The webview only leaves its initial loading screen once it receives a
* `connectionStatus: connected` message. VS Code drops postMessage calls made
* before the webview's acquireVsCodeApi bridge is ready (common in
* code-server / slow or flaky networks), so a single send can be lost
* forever. Re-sending the cached state at staggered delays bounds the wait
* without needing a webview-side ack protocol; the payload is idempotent
* (connection status + window focus), so duplicate deliveries are harmless.
*/
const CACHED_STATE_RETRY_DELAYS_MS = [500, 1500, 3500, 7000, 12000, 20000];
export function scheduleCachedStateRetries<Target>(input: {
/** The panel/view the retries belong to. */
target: Target | undefined;
/** Reads the provider's CURRENT panel/view, so a replaced target stops its stale retries. */
getCurrent: () => Target | undefined;
/** Retries only make sense for the connected transition. */
isConnected: () => boolean;
/** Re-sends the provider's cached state. */
send: () => void;
}): void {
if (!input.isConnected()) return;
const target = input.target;
if (!target) return;
for (const delayMs of CACHED_STATE_RETRY_DELAYS_MS) {
setTimeout(() => {
if (input.getCurrent() !== target) return;
input.send();
}, delayMs);
}
}
+7
View File
@@ -25,6 +25,13 @@ export default defineConfig(({ mode }) => ({
},
worker: {
format: 'es',
// VS Code webviews cannot load module imports from inside a web worker.
// Keep the Shiki worker self-contained instead of emitting grammar chunks.
rollupOptions: {
output: {
inlineDynamicImports: true,
},
},
},
define: {
'process.env.NODE_ENV': JSON.stringify(mode === 'production' ? 'production' : 'development'),