diff --git a/bun.lock b/bun.lock index efeb920d..562665f6 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index f897aa9a..a489b9d9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/ui/package.json b/packages/ui/package.json index 66f2eca5..2a8aa713 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -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", diff --git a/packages/ui/src/components/chat/message/parts/ToolPart.tsx b/packages/ui/src/components/chat/message/parts/ToolPart.tsx index 5496bb4f..0ce75da7 100644 --- a/packages/ui/src/components/chat/message/parts/ToolPart.tsx +++ b/packages/ui/src/components/chat/message/parts/ToolPart.tsx @@ -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): { 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): 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 = 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 = 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 ( -
-
- {renderPathLikeGitChanges(displayPath)} - ({headerLineLabel}) -
- -
- ); -}); - -WriteInputPreview.displayName = 'WriteInputPreview'; - -interface ImagePreviewProps { - content: string; - filePath: string; - displayPath: string; -} - -const ImagePreview: React.FC = 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 ( -
-
- {renderPathLikeGitChanges(displayPath)} -
-
- {displayPath} -
-
- ); -}); - -ImagePreview.displayName = 'ImagePreview'; - interface ToolExpandedContentProps { part: ToolPartType; state: ToolStateUnion; @@ -1459,32 +1417,16 @@ const ToolExpandedContent: React.FC = 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 = 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 = 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(
{diffEntries.map((entry) => ( @@ -1648,6 +1604,10 @@ const ToolExpandedContent: React.FC = React.memo(({ ); } + if (isWriteLikeTool) { + return null; + } + if (hasStringOutput && outputString.trim()) { return renderScrollableBlock( = React.memo(({ renderResultContent() ) : ( <> - {shouldShowWriteInputPreview && isWriteImageFile ? ( -
- {renderScrollableBlock( - - )} -
- ) : shouldShowWriteInputPreview ? ( -
- {renderScrollableBlock( - - )} -
- ) : hasInputText ? ( + {hasInputText ? (
{renderScrollableBlock( part.tool === 'bash' ? (
                                         {inputTextContent}
                                     
+ ) : isWriteLikeTool && writeLikeInputPatch ? ( + ) : (
{inputTextContent} @@ -1722,9 +1667,9 @@ const ToolExpandedContent: React.FC = React.memo(({
) : null} - {part.tool !== 'write' && state.status === 'completed' && 'output' in state && ( + {state.status === 'completed' && 'output' in state && (
- {(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 ? (
, 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 | 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; diff --git a/packages/ui/src/sync/event-reducer.ts b/packages/ui/src/sync/event-reducer.ts index d0d61830..bb2b1aaa 100644 --- a/packages/ui/src/sync/event-reducer.ts +++ b/packages/ui/src/sync/event-reducer.ts @@ -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" diff --git a/packages/ui/src/sync/sanitize.ts b/packages/ui/src/sync/sanitize.ts index 9e6aa195..3df7fb67 100644 --- a/packages/ui/src/sync/sanitize.ts +++ b/packages/ui/src/sync/sanitize.ts @@ -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 } diff --git a/packages/ui/src/sync/session-cache.ts b/packages/ui/src/sync/session-cache.ts index eb0be49f..1ae6532d 100644 --- a/packages/ui/src/sync/session-cache.ts +++ b/packages/ui/src/sync/session-cache.ts @@ -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 diff --git a/packages/ui/src/sync/session-ui-store.ts b/packages/ui/src/sync/session-ui-store.ts index cd373485..c265eda5 100644 --- a/packages/ui/src/sync/session-ui-store.ts +++ b/packages/ui/src/sync/session-ui-store.ts @@ -1099,7 +1099,7 @@ export const useSessionUIStore = create()((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()((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 } diff --git a/packages/ui/src/sync/types.ts b/packages/ui/src/sync/types.ts index 99262737..402254e2 100644 --- a/packages/ui/src/sync/types.ts +++ b/packages/ui/src/sync/types.ts @@ -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?: { diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 161ccefc..7feea0e5 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -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", diff --git a/packages/web/package.json b/packages/web/package.json index 658fc940..b8d8eaaa 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -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", diff --git a/packages/web/server/index.js b/packages/web/server/index.js index 8745c69c..6b4330db 100644 --- a/packages/web/server/index.js +++ b/packages/web/server/index.js @@ -1,3 +1,4 @@ +import 'reflect-metadata'; import express from 'express'; import path from 'path'; import { spawn, spawnSync } from 'child_process'; diff --git a/test-ui-verification.ts b/test-ui-verification.ts new file mode 100644 index 00000000..4d7fd3a3 --- /dev/null +++ b/test-ui-verification.ts @@ -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;