fix: restore desktop startup and align tool previews with SDK updates

Add reflect-metadata bootstrap so desktop sidecar no longer crashes on startup.
Update SDK v1.4 compatibility for model variant and diff payload handling.
Unify write/edit/apply patch expanded previews and hide write success output noise.
This commit is contained in:
Bohdan Triapitsyn
2026-04-12 00:40:49 +03:00
parent e6ac6badde
commit 844052e599
14 changed files with 222 additions and 189 deletions
+10 -9
View File
@@ -32,7 +32,7 @@
"@ibm/plex": "^6.4.1",
"@lezer/highlight": "^1.2.3",
"@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "^1.3.17",
"@opencode-ai/sdk": "^1.4.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -94,7 +94,7 @@
},
"packages/desktop": {
"name": "@openchamber/desktop",
"version": "1.9.3",
"version": "1.9.4",
"devDependencies": {
"@tauri-apps/cli": "^2",
"@types/node": "^24.3.1",
@@ -103,7 +103,7 @@
},
"packages/ui": {
"name": "@openchamber/ui",
"version": "1.9.3",
"version": "1.9.4",
"dependencies": {
"@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.1",
@@ -133,7 +133,7 @@
"@fontsource/ibm-plex-sans": "^5.1.1",
"@ibm/plex": "^6.4.1",
"@lezer/highlight": "^1.2.3",
"@opencode-ai/sdk": "^1.3.17",
"@opencode-ai/sdk": "^1.4.3",
"@pierre/diffs": "1.1.0-beta.13",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
@@ -208,10 +208,10 @@
},
"packages/vscode": {
"name": "openchamber",
"version": "1.9.3",
"version": "1.9.4",
"dependencies": {
"@openchamber/ui": "workspace:*",
"@opencode-ai/sdk": "^1.3.17",
"@opencode-ai/sdk": "^1.4.3",
"adm-zip": "^0.5.16",
"jsonc-parser": "^3.3.1",
"react": "^19.1.1",
@@ -231,7 +231,7 @@
},
"packages/web": {
"name": "@openchamber/web",
"version": "1.9.3",
"version": "1.9.4",
"bin": {
"openchamber": "./bin/cli.js",
},
@@ -243,7 +243,7 @@
"@fontsource/ibm-plex-sans": "^5.1.1",
"@ibm/plex": "^6.4.1",
"@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "^1.3.17",
"@opencode-ai/sdk": "^1.4.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -275,6 +275,7 @@
"react-dom": "^19.1.1",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^15.6.6",
"reflect-metadata": "^0.2.2",
"remark-gfm": "^4.0.1",
"simple-git": "^3.28.0",
"sonner": "^2.0.7",
@@ -826,7 +827,7 @@
"@openchamber/web": ["@openchamber/web@workspace:packages/web"],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.3.17", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-2+MGgu7wynqTBwxezR01VAGhILXlpcHDY/pF7SWB87WOgLt3kD55HjKHNj6PWxyY8n575AZolR95VUC3gtwfmA=="],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.4.3", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-X0CAVbwoGAjTY2iecpWkx2B+GAa2jSaQKYpJ+xILopeF/OGKZUN15mjqci+L7cEuwLHV5wk3x2TStUOVCa5p0A=="],
"@peculiar/asn1-android": ["@peculiar/asn1-android@2.6.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-cBRCKtYPF7vJGN76/yG8VbxRcHLPF3HnkoHhKOZeHpoVtbMYfY9ROKtH3DtYUY9m8uI1Mh47PRhHf2hSK3xcSQ=="],
+1 -1
View File
@@ -89,7 +89,7 @@
"@ibm/plex": "^6.4.1",
"@lezer/highlight": "^1.2.3",
"@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "^1.3.17",
"@opencode-ai/sdk": "^1.4.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
+1 -1
View File
@@ -39,7 +39,7 @@
"@fontsource/ibm-plex-sans": "^5.1.1",
"@ibm/plex": "^6.4.1",
"@lezer/highlight": "^1.2.3",
"@opencode-ai/sdk": "^1.3.17",
"@opencode-ai/sdk": "^1.4.3",
"@pierre/diffs": "1.1.0-beta.13",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
@@ -2,10 +2,10 @@
import React from 'react';
import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext';
import { RiArrowDownSLine, RiArrowRightSLine, RiExternalLinkLine } from '@remixicon/react';
import { File as PierreFile, PatchDiff } from '@pierre/diffs/react';
import { PatchDiff } from '@pierre/diffs/react';
import { cn } from '@/lib/utils';
import { SimpleMarkdownRenderer } from '../../MarkdownRenderer';
import { getToolMetadata, getLanguageFromExtension, isImageFile, getImageMimeType } from '@/lib/toolHelpers';
import { getToolMetadata } from '@/lib/toolHelpers';
import type { ToolPart as ToolPartType, ToolState as ToolStateUnion } from '@opencode-ai/sdk/v2';
import { toolDisplayStyles } from '@/lib/typography';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
@@ -185,9 +185,11 @@ const LiveDuration: React.FC<{ start: number; end?: number; active: boolean }> =
};
const parseDiffStats = (metadata?: Record<string, unknown>): { added: number; removed: number } | null => {
if (!metadata?.diff || typeof metadata.diff !== 'string') return null;
const diffText = getPatchText((metadata as { patch?: unknown } | undefined)?.patch)
?? getPatchText(metadata?.diff);
if (!diffText) return null;
const lines = metadata.diff.split('\n');
const lines = diffText.split('\n');
let added = 0;
let removed = 0;
@@ -254,22 +256,67 @@ const extractFirstChangedLineFromDiff = (diffText: string): number | undefined =
return firstHunkStart;
};
const getPatchText = (value: unknown): string | undefined => {
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
if (value && typeof value === 'object') {
const patch = (value as { patch?: unknown }).patch;
if (typeof patch === 'string') {
const trimmed = patch.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
}
return undefined;
};
const buildWritePreviewPatch = (filePath: string | undefined, content: string): string | undefined => {
const normalizedContent = content.replace(/\r\n/g, '\n');
if (!normalizedContent.trim()) {
return undefined;
}
const normalizedPath = (() => {
const candidate = (filePath ?? '').trim();
if (!candidate) {
return 'new-file';
}
return candidate.startsWith('/') ? candidate.slice(1) : candidate;
})();
const lines = normalizedContent.split('\n');
const hunkSize = lines.length;
const body = lines.map((line) => `+${line}`).join('\n');
return [
'--- /dev/null',
`+++ b/${normalizedPath}`,
`@@ -0,0 +1,${hunkSize} @@`,
body,
].join('\n');
};
const getFirstChangedLineFromMetadata = (tool: string, metadata?: Record<string, unknown>): number | undefined => {
if (!metadata || (tool !== 'edit' && tool !== 'multiedit' && tool !== 'apply_patch')) {
return undefined;
}
if (typeof metadata.diff === 'string') {
const line = extractFirstChangedLineFromDiff(metadata.diff);
const topLevelPatch = getPatchText((metadata as { patch?: unknown }).patch) ?? getPatchText(metadata.diff);
if (topLevelPatch) {
const line = extractFirstChangedLineFromDiff(topLevelPatch);
if (Number.isFinite(line)) {
return line;
}
}
const files = Array.isArray(metadata.files) ? metadata.files : [];
const firstFile = files[0] as { diff?: unknown } | undefined;
if (typeof firstFile?.diff === 'string') {
const line = extractFirstChangedLineFromDiff(firstFile.diff);
const firstFile = files[0] as { patch?: unknown; diff?: unknown } | undefined;
const filePatch = getPatchText(firstFile?.patch) ?? getPatchText(firstFile?.diff);
if (filePatch) {
const line = extractFirstChangedLineFromDiff(filePatch);
if (Number.isFinite(line)) {
return line;
}
@@ -303,15 +350,17 @@ const getPrimaryDiffFromMetadata = (
: files[0];
if (matched && typeof matched === 'object') {
const patch = (matched as { diff?: unknown }).diff;
if (typeof patch === 'string' && patch.trim().length > 0) {
const patch = getPatchText((matched as { patch?: unknown; diff?: unknown }).patch)
?? getPatchText((matched as { patch?: unknown; diff?: unknown }).diff);
if (patch) {
return patch;
}
}
}
if (typeof metadata.diff === 'string' && metadata.diff.trim().length > 0) {
return metadata.diff;
const topLevelPatch = getPatchText((metadata as { patch?: unknown }).patch) ?? getPatchText(metadata.diff);
if (topLevelPatch) {
return topLevelPatch;
}
return undefined;
@@ -1280,8 +1329,8 @@ const getDiffPatchEntries = (
return null;
}
const record = file as { relativePath?: unknown; filePath?: unknown; diff?: unknown };
const patch = typeof record.diff === 'string' ? record.diff.trim() : '';
const record = file as { relativePath?: unknown; filePath?: unknown; patch?: unknown; diff?: unknown };
const patch = getPatchText(record.patch) ?? getPatchText(record.diff) ?? '';
if (!patch) {
return null;
}
@@ -1344,97 +1393,6 @@ const DiffPreview: React.FC<DiffPreviewProps> = React.memo(({ diff, pierreTheme,
DiffPreview.displayName = 'DiffPreview';
interface WriteInputPreviewProps {
content: string;
filePath?: string;
displayPath: string;
pierreTheme: { light: string; dark: string };
pierreThemeType: 'light' | 'dark';
}
const WriteInputPreview: React.FC<WriteInputPreviewProps> = React.memo(({
content,
filePath,
displayPath,
pierreTheme,
pierreThemeType,
}) => {
const language = React.useMemo(
() => getLanguageFromExtension(filePath ?? '') || detectLanguageFromOutput(content, 'write', filePath ? { filePath } : undefined),
[content, filePath]
);
const lineCount = Math.max(content.split('\n').length, 1);
const headerLineLabel = lineCount === 1 ? 'line 1' : `lines 1-${lineCount}`;
return (
<div className="w-full min-w-0">
<div className="bg-muted/20 px-2 py-1 rounded-lg mb-1 flex items-center gap-2 min-w-0">
{renderPathLikeGitChanges(displayPath)}
<span className="typography-meta text-muted-foreground/80 flex-shrink-0">({headerLineLabel})</span>
</div>
<PierreFile
file={{
name: displayPath,
contents: content,
lang: language || undefined,
}}
options={{
disableFileHeader: true,
overflow: 'wrap',
theme: pierreTheme,
themeType: pierreThemeType,
}}
className="block w-full"
/>
</div>
);
});
WriteInputPreview.displayName = 'WriteInputPreview';
interface ImagePreviewProps {
content: string;
filePath: string;
displayPath: string;
}
const ImagePreview: React.FC<ImagePreviewProps> = React.memo(({ content, filePath, displayPath }) => {
const mimeType = getImageMimeType(filePath);
const isSvg = filePath.toLowerCase().endsWith('.svg');
// For SVG, content might be raw XML, otherwise assume base64
const imageSrc = React.useMemo(() => {
if (isSvg && !content.startsWith('data:')) {
// Raw SVG content
return `data:image/svg+xml;base64,${btoa(content)}`;
}
if (content.startsWith('data:')) {
return content;
}
// Assume base64 encoded
return `data:${mimeType};base64,${content}`;
}, [content, mimeType, isSvg]);
return (
<div className="w-full min-w-0">
<div className="bg-muted/20 px-2 py-1 rounded-lg mb-2 flex items-center min-w-0">
{renderPathLikeGitChanges(displayPath)}
</div>
<div className="flex justify-center p-4 bg-muted/10 rounded-lg">
<img
src={imageSrc}
alt={displayPath}
className="max-w-full max-h-96 object-contain rounded"
style={{ imageRendering: 'auto' }}
/>
</div>
</div>
);
});
ImagePreview.displayName = 'ImagePreview';
interface ToolExpandedContentProps {
part: ToolPartType;
state: ToolStateUnion;
@@ -1459,32 +1417,16 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
const hasStringOutput = typeof rawOutput === 'string' && rawOutput.length > 0;
const outputString = typeof rawOutput === 'string' ? rawOutput : '';
const diffContent = typeof metadata?.diff === 'string' ? (metadata.diff as string) : null;
const diffContent = getPatchText((metadata as { patch?: unknown } | undefined)?.patch)
?? getPatchText(metadata?.diff)
?? null;
const diffEntries = React.useMemo(
() => (diffContent ? getDiffPatchEntries(metadata, diffContent, currentDirectory) : []),
[currentDirectory, diffContent, metadata]
);
const writeFilePath = part.tool === 'write'
? typeof input?.filePath === 'string'
? input.filePath
: typeof input?.file_path === 'string'
? input.file_path
: typeof input?.path === 'string'
? input.path
: undefined
: undefined;
const writeInputContent = part.tool === 'write'
? typeof (input as { content?: unknown })?.content === 'string'
? (input as { content?: string }).content
: typeof (input as { text?: unknown })?.text === 'string'
? (input as { text?: string }).text
: null
: null;
const shouldShowWriteInputPreview = part.tool === 'write' && !!writeInputContent;
const isWriteImageFile = writeFilePath ? isImageFile(writeFilePath) : false;
const writeDisplayPath = shouldShowWriteInputPreview
? (writeFilePath ? getRelativePath(writeFilePath, currentDirectory) : 'New file')
: null;
const hideToolInputPreview = part.tool === 'apply_patch'
|| part.tool === 'edit'
|| part.tool === 'multiedit';
const diagnosticSection = React.useMemo(
() => getToolDiagnosticSection(part.tool, input, metadata, currentDirectory),
[currentDirectory, input, metadata, part.tool],
@@ -1505,7 +1447,21 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
return formatInputForDisplay(input, part.tool);
}, [input, part.tool]);
const hasInputText = part.tool !== 'apply_patch' && inputTextContent.trim().length > 0;
const hasInputText = !hideToolInputPreview && inputTextContent.trim().length > 0;
const isWriteLikeTool = part.tool === 'write' || part.tool === 'create' || part.tool === 'file_write';
const writeLikeInputPatch = React.useMemo(() => {
if (!isWriteLikeTool || !hasInputText) {
return undefined;
}
const filePath = typeof input?.filePath === 'string'
? input.filePath
: typeof input?.file_path === 'string'
? input.file_path
: typeof input?.path === 'string'
? input.path
: undefined;
return buildWritePreviewPatch(filePath, inputTextContent);
}, [hasInputText, input?.filePath, input?.file_path, input?.path, inputTextContent, isWriteLikeTool]);
React.useEffect(() => {
setDiffViewMode('unified');
@@ -1615,7 +1571,7 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
);
}
if ((part.tool === 'edit' || part.tool === 'multiedit' || part.tool === 'apply_patch') && (diffEntries.length > 0 || !!diagnosticSection)) {
if ((part.tool === 'edit' || part.tool === 'multiedit' || part.tool === 'apply_patch' || part.tool === 'write') && (diffEntries.length > 0 || !!diagnosticSection)) {
return renderScrollableBlock(
<div className="space-y-3">
{diffEntries.map((entry) => (
@@ -1648,6 +1604,10 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
);
}
if (isWriteLikeTool) {
return null;
}
if (hasStringOutput && outputString.trim()) {
return renderScrollableBlock(
<ToolScrollableTextOutput
@@ -1680,35 +1640,20 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
renderResultContent()
) : (
<>
{shouldShowWriteInputPreview && isWriteImageFile ? (
<div className="my-1">
{renderScrollableBlock(
<ImagePreview
content={writeInputContent as string}
filePath={writeFilePath as string}
displayPath={writeDisplayPath ?? 'New file'}
/>
)}
</div>
) : shouldShowWriteInputPreview ? (
<div className="my-1">
{renderScrollableBlock(
<WriteInputPreview
content={writeInputContent as string}
filePath={writeFilePath}
displayPath={writeDisplayPath ?? 'New file'}
pierreTheme={pierreTheme}
pierreThemeType={pierreThemeType}
/>
)}
</div>
) : hasInputText ? (
{hasInputText ? (
<div className="my-1">
{renderScrollableBlock(
part.tool === 'bash' ? (
<pre className="tool-input-text whitespace-pre-wrap break-words typography-code text-muted-foreground/90 m-0 p-0">
{inputTextContent}
</pre>
) : isWriteLikeTool && writeLikeInputPatch ? (
<DiffPreview
diff={writeLikeInputPatch}
pierreTheme={pierreTheme}
pierreThemeType={pierreThemeType}
diffViewMode={diffViewMode}
/>
) : (
<blockquote className="tool-input-text whitespace-pre-wrap break-words typography-meta italic text-muted-foreground/70">
{inputTextContent}
@@ -1722,9 +1667,9 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
</div>
) : null}
{part.tool !== 'write' && state.status === 'completed' && 'output' in state && (
{state.status === 'completed' && 'output' in state && (
<div>
{(part.tool === 'edit' || part.tool === 'multiedit' || part.tool === 'apply_patch') && diffContent ? (
{(part.tool === 'edit' || part.tool === 'multiedit' || part.tool === 'apply_patch' || part.tool === 'write') && diffContent ? (
<div className="mb-1 flex items-center justify-end gap-2">
<DiffViewToggle
mode={diffViewMode}
@@ -36,6 +36,47 @@ const formatInputForDisplay = (input: Record<string, unknown>, toolName?: string
return formatToolInput(input, toolName || '');
};
const getPatchText = (value: unknown): string | undefined => {
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
if (value && typeof value === 'object') {
const patch = (value as { patch?: unknown }).patch;
if (typeof patch === 'string') {
const trimmed = patch.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
}
return undefined;
};
const getToolMetadataPatch = (metadata?: Record<string, unknown>): string | undefined => {
if (!metadata || typeof metadata !== 'object') {
return undefined;
}
const topLevelPatch = getPatchText((metadata as { patch?: unknown }).patch) ?? getPatchText(metadata.diff);
if (topLevelPatch) {
return topLevelPatch;
}
const files = Array.isArray((metadata as { files?: unknown }).files) ? (metadata as { files: unknown[] }).files : [];
for (const file of files) {
if (!file || typeof file !== 'object') {
continue;
}
const patch = getPatchText((file as { patch?: unknown }).patch) ?? getPatchText((file as { diff?: unknown }).diff);
if (patch) {
return patch;
}
}
return undefined;
};
export const tryParseJsonOutput = (output: string): { data: unknown; isJson: boolean } => {
if (!output || typeof output !== 'string') {
return { data: null, isJson: false };
@@ -73,10 +114,11 @@ export const formatEditOutput = (output: string, toolName: string, metadata?: Re
cleaned = stripLspDiagnostics(cleaned);
}
if ((toolName === 'edit' || toolName === 'multiedit' || toolName === 'apply_patch') && cleaned.trim().length === 0 && metadata?.diff) {
const diff = metadata.diff;
return typeof diff === 'string' ? diff : String(diff);
if ((toolName === 'edit' || toolName === 'multiedit' || toolName === 'apply_patch') && cleaned.trim().length === 0) {
const diff = getToolMetadataPatch(metadata);
if (diff) {
return diff;
}
}
return cleaned;
+1 -2
View File
@@ -1,6 +1,5 @@
import type {
Event,
FileDiff,
Message,
Part,
PermissionRequest,
@@ -11,7 +10,7 @@ import type {
Todo,
} from "@opencode-ai/sdk/v2/client"
import { Binary } from "./binary"
import type { GlobalState, State } from "./types"
import type { FileDiff, GlobalState, State } from "./types"
import { dropSessionCaches } from "./session-cache"
import { stripSessionDiffSnapshots } from "./sanitize"
import { syncDebug } from "./debug"
+29 -10
View File
@@ -1,8 +1,9 @@
// ---------------------------------------------------------------------------
// Payload sanitization — strip oversized diff snapshot fields client-side.
//
// OpenCode session objects carry summary.diffs[].before/after with full file
// contents. The UI never uses these fields but they waste browser memory and
// OpenCode session/message snapshots may carry large full-content diff fields
// (legacy before/after or from/to). The UI never uses these fields but they
// waste browser memory and
// can crash tabs for large sessions.
//
// Applied at two points:
@@ -19,6 +20,8 @@ type DiffEntry = {
deletions?: number
before?: string
after?: string
from?: string
to?: string
}
type SessionSummary = {
@@ -26,16 +29,24 @@ type SessionSummary = {
[key: string]: unknown
}
/** Strip before/after from summary.diffs on a session object */
/** Strip oversized snapshot fields from summary.diffs on a session object */
export function stripSessionDiffSnapshots(session: Session): Session {
const summary = (session as { summary?: SessionSummary }).summary
if (!summary?.diffs || !Array.isArray(summary.diffs)) return session
let changed = false
const stripped = summary.diffs.map((d) => {
if (d && (typeof d.before === "string" || typeof d.after === "string")) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { before: _before, after: _after, ...rest } = d
if (d && (
typeof d.before === "string"
|| typeof d.after === "string"
|| typeof d.from === "string"
|| typeof d.to === "string"
)) {
const rest = { ...d }
delete rest.before
delete rest.after
delete rest.from
delete rest.to
changed = true
return rest
}
@@ -46,16 +57,24 @@ export function stripSessionDiffSnapshots(session: Session): Session {
return { ...session, summary: { ...summary, diffs: stripped } } as Session
}
/** Strip before/after from summary.diffs on a message object */
/** Strip oversized snapshot fields from summary.diffs on a message object */
export function stripMessageDiffSnapshots(message: Message): Message {
const summary = (message as { summary?: SessionSummary }).summary
if (!summary?.diffs || !Array.isArray(summary.diffs)) return message
let changed = false
const stripped = summary.diffs.map((d) => {
if (d && (typeof d.before === "string" || typeof d.after === "string")) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { before: _before, after: _after, ...rest } = d
if (d && (
typeof d.before === "string"
|| typeof d.after === "string"
|| typeof d.from === "string"
|| typeof d.to === "string"
)) {
const rest = { ...d }
delete rest.before
delete rest.after
delete rest.from
delete rest.to
changed = true
return rest
}
+1 -1
View File
@@ -1,5 +1,4 @@
import type {
FileDiff,
Message,
Part,
PermissionRequest,
@@ -7,6 +6,7 @@ import type {
SessionStatus,
Todo,
} from "@opencode-ai/sdk/v2/client"
import type { FileDiff } from "./types"
type SessionCache = {
session_status: Record<string, SessionStatus | undefined>
+4 -3
View File
@@ -1099,7 +1099,7 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
const messages = getSyncMessages(sessionId, directory)
for (let i = messages.length - 1; i >= 0; i -= 1) {
const message = messages[i] as Message & {
model?: { providerID?: string; modelID?: string }
model?: { providerID?: string; modelID?: string; variant?: string }
variant?: string
mode?: string
}
@@ -1116,8 +1116,9 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
const agent = typeof message.agent === "string" && message.agent.trim().length > 0
? message.agent
: (typeof message.mode === "string" && message.mode.trim().length > 0 ? message.mode : undefined)
const variant = typeof message.variant === "string" && message.variant.trim().length > 0
? message.variant
const variantCandidate = message.model?.variant ?? message.variant
const variant = typeof variantCandidate === "string" && variantCandidate.trim().length > 0
? variantCandidate
: undefined
return { agent, providerID, modelID, variant }
+9 -1
View File
@@ -2,7 +2,6 @@ import type {
Agent,
Command,
Config,
FileDiff,
LspStatus,
McpStatus,
Message,
@@ -19,6 +18,15 @@ import type {
VcsInfo,
} from "@opencode-ai/sdk/v2/client"
export type FileDiff = {
file?: string
status?: string
additions?: number
deletions?: number
patch?: string
[key: string]: unknown
}
export type ProjectMeta = {
name?: string
icon?: {
+1 -1
View File
@@ -243,7 +243,7 @@
},
"dependencies": {
"@openchamber/ui": "workspace:*",
"@opencode-ai/sdk": "^1.3.17",
"@opencode-ai/sdk": "^1.4.3",
"adm-zip": "^0.5.16",
"jsonc-parser": "^3.3.1",
"react": "^19.1.1",
+2 -1
View File
@@ -29,7 +29,7 @@
"@fontsource/ibm-plex-sans": "^5.1.1",
"@ibm/plex": "^6.4.1",
"@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "^1.3.17",
"@opencode-ai/sdk": "^1.4.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -61,6 +61,7 @@
"react-dom": "^19.1.1",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^15.6.6",
"reflect-metadata": "^0.2.2",
"remark-gfm": "^4.0.1",
"simple-git": "^3.28.0",
"sonner": "^2.0.7",
+1
View File
@@ -1,3 +1,4 @@
import 'reflect-metadata';
import express from 'express';
import path from 'path';
import { spawn, spawnSync } from 'child_process';
+16
View File
@@ -0,0 +1,16 @@
// Test file for UI verification
// Created: 2026-04-12
const testConfig = {
name: "OpenChamber UI Verification",
version: "2.0.0-beta",
features: {
chat: true,
terminal: true,
settings: true
},
debug: true,
environment: "development"
};
export default testConfig;