Merge pull request 'chore: bring upstream v1.20.0 into custom' (#2) from release/v1.20.0 into custom
This commit is contained in:
@@ -1,6 +1,22 @@
|
||||
## [Unreleased]
|
||||
## [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,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",
|
||||
|
||||
@@ -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(() => {});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -327,6 +327,33 @@ describe('Z.ai quota provider (VS Code parity)', () => {
|
||||
assert.equal(windows['MCP Tools']!.windowSeconds, 30 * 24 * 60 * 60);
|
||||
assert.equal(windows['MCP Tools']!.resetAt, 1787128459979);
|
||||
});
|
||||
|
||||
test('maps CREDIT_LIMIT entries to windows with credit value labels and plan level', async () => {
|
||||
stubFetchReturning(() => Promise.resolve(mockResponse({
|
||||
code: 200,
|
||||
data: {
|
||||
limits: [
|
||||
{ type: 'CREDIT_LIMIT', unit: 3, number: 5, usage: 12000, currentValue: 65, remaining: 11934, percentage: 1, nextResetTime: 1787257978907 },
|
||||
{ type: 'CREDIT_LIMIT', unit: 6, number: 1, usage: 60000, currentValue: 65, remaining: 59934, percentage: 1, nextResetTime: 1787844668997 },
|
||||
],
|
||||
level: 'pro',
|
||||
},
|
||||
})));
|
||||
|
||||
const result = await fetchQuotaForProvider('zai-coding-plan');
|
||||
const windows = result.usage!.windows;
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.planLabel, 'pro');
|
||||
assert.equal(windows['5h']!.usedPercent, 1);
|
||||
assert.equal(windows['5h']!.windowSeconds, 5 * 60 * 60);
|
||||
assert.equal(windows['5h']!.resetAt, 1787257978907);
|
||||
assert.equal(windows['5h']!.valueLabel, '65 / 12k credits');
|
||||
assert.equal(windows.weekly!.usedPercent, 1);
|
||||
assert.equal(windows.weekly!.windowSeconds, 7 * 24 * 60 * 60);
|
||||
assert.equal(windows.weekly!.resetAt, 1787844668997);
|
||||
assert.equal(windows.weekly!.valueLabel, '65 / 60k credits');
|
||||
});
|
||||
});
|
||||
|
||||
describe('NeuralWatt quota provider (VS Code parity)', () => {
|
||||
|
||||
@@ -72,13 +72,31 @@ type ZaiLimit = {
|
||||
type?: string;
|
||||
number?: number;
|
||||
unit?: number;
|
||||
usage?: number;
|
||||
currentValue?: number;
|
||||
remaining?: number;
|
||||
nextResetTime?: number;
|
||||
percentage?: number;
|
||||
};
|
||||
|
||||
// CREDIT_LIMIT entries carry `usage` (total credits) and `currentValue` (consumed);
|
||||
// TOKENS_LIMIT entries only carry a percentage.
|
||||
const formatZaiCreditAmount = (value: number): string => {
|
||||
if (value < 1000) return value.toLocaleString('en-US');
|
||||
return `${Math.round(value / 100) / 10}k`;
|
||||
};
|
||||
|
||||
const formatZaiCreditValueLabel = (limit: ZaiLimit): string | null => {
|
||||
const used = toNumber(limit.currentValue);
|
||||
const total = toNumber(limit.usage);
|
||||
if (used === null || total === null) return null;
|
||||
return `${formatZaiCreditAmount(used)} / ${formatZaiCreditAmount(total)} credits`;
|
||||
};
|
||||
|
||||
type ZaiPayload = {
|
||||
data?: {
|
||||
limits?: ZaiLimit[];
|
||||
level?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -411,15 +429,20 @@ const buildResult = (data: {
|
||||
configured: boolean;
|
||||
usage?: ProviderUsage | null;
|
||||
error?: string;
|
||||
}): ProviderResult => ({
|
||||
providerId: data.providerId,
|
||||
providerName: data.providerName,
|
||||
ok: data.ok,
|
||||
configured: data.configured,
|
||||
usage: data.usage ?? null,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
fetchedAt: Date.now(),
|
||||
});
|
||||
planLabel?: string | null;
|
||||
}): ProviderResult => {
|
||||
const result: ProviderResult = {
|
||||
providerId: data.providerId,
|
||||
providerName: data.providerName,
|
||||
ok: data.ok,
|
||||
configured: data.configured,
|
||||
usage: data.usage ?? null,
|
||||
...(data.error ? { error: data.error } : {}),
|
||||
fetchedAt: Date.now(),
|
||||
};
|
||||
if (data.planLabel) result.planLabel = data.planLabel;
|
||||
return result;
|
||||
};
|
||||
|
||||
const resolveXaiAuth = (): XaiAuthEntry | null => {
|
||||
const entry = getProviderAuth('xai');
|
||||
@@ -1291,7 +1314,7 @@ const buildClaudeRateLimitResult = (): ProviderResult => (
|
||||
providerName: 'Claude',
|
||||
ok: false,
|
||||
configured: true,
|
||||
error: 'Rate limited by Anthropic. Retrying shortly.',
|
||||
error: 'Rate limited. Retrying soon.',
|
||||
})
|
||||
);
|
||||
|
||||
@@ -2059,16 +2082,19 @@ const fetchZaiQuota = async (): Promise<ProviderResult> => {
|
||||
const payload = await response.json() as ZaiPayload;
|
||||
const limits = Array.isArray(payload?.data?.limits) ? payload.data.limits : [];
|
||||
const windows: Record<string, UsageWindow> = {};
|
||||
for (const tokensLimit of limits.filter((limit) => limit?.type === 'TOKENS_LIMIT')) {
|
||||
const windowSeconds = resolveWindowSeconds(tokensLimit as Record<string, unknown>);
|
||||
// The API renamed TOKENS_LIMIT to CREDIT_LIMIT; field semantics stayed the same,
|
||||
// so both limit types map to the same windows.
|
||||
for (const limit of limits.filter((entry) => entry?.type === 'TOKENS_LIMIT' || entry?.type === 'CREDIT_LIMIT')) {
|
||||
const windowSeconds = resolveWindowSeconds(limit as Record<string, unknown>);
|
||||
const windowLabel = resolveWindowLabel(windowSeconds);
|
||||
const resetAt = tokensLimit.nextResetTime ? normalizeTimestamp(tokensLimit.nextResetTime) : null;
|
||||
const usedPercent = typeof tokensLimit.percentage === 'number' ? tokensLimit.percentage : null;
|
||||
const resetAt = limit.nextResetTime ? normalizeTimestamp(limit.nextResetTime) : null;
|
||||
const usedPercent = typeof limit.percentage === 'number' ? limit.percentage : null;
|
||||
|
||||
windows[windowLabel] = toUsageWindow({
|
||||
usedPercent,
|
||||
windowSeconds,
|
||||
resetAt,
|
||||
valueLabel: formatZaiCreditValueLabel(limit),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2087,6 +2113,7 @@ const fetchZaiQuota = async (): Promise<ProviderResult> => {
|
||||
ok: true,
|
||||
configured: true,
|
||||
usage: { windows },
|
||||
planLabel: payload?.data?.level || null,
|
||||
});
|
||||
} catch (error) {
|
||||
return buildResult({
|
||||
|
||||
Reference in New Issue
Block a user