diff --git a/opencode.jsonc b/opencode.jsonc index 87c3b993..72c65673 100644 --- a/opencode.jsonc +++ b/opencode.jsonc @@ -3,7 +3,12 @@ "mcp": { "chrome-devtools": { "type": "local", - "command": ["npx", "-y", "chrome-devtools-mcp@latest"] + "command": [ + "npx", + "-y", + "chrome-devtools-mcp@latest" + ] } - } + }, + "model": "openai/gpt-5.2-codex" } diff --git a/packages/app/package.json b/packages/app/package.json index cd0ca560..b167c7fa 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,7 +1,7 @@ { "name": "@different-ai/openwork-ui", "private": true, - "version": "0.3.7", + "version": "0.4.0", "type": "module", "scripts": { "dev": "vite", diff --git a/packages/app/src/app/app.tsx b/packages/app/src/app/app.tsx index 15b50eda..1430650b 100644 --- a/packages/app/src/app/app.tsx +++ b/packages/app/src/app/app.tsx @@ -328,15 +328,11 @@ export default function App() { } for (const attachment of draft.attachments) { - if (attachment.kind === "image") { - parts.push({ type: "image", image: attachment.dataUrl, mediaType: attachment.mimeType } as Part); - continue; - } parts.push({ type: "file", - data: attachment.dataUrl, + url: attachment.dataUrl, filename: attachment.name, - mediaType: attachment.mimeType, + mime: attachment.mimeType, } as Part); } @@ -2665,6 +2661,31 @@ export default function App() { setLanguage: setLocale, }); + const searchWorkspaceFiles = async (query: string) => { + const trimmed = query.trim(); + if (!trimmed) return []; + if (isDemoMode()) { + const lower = trimmed.toLowerCase(); + return activeWorkingFiles().filter((file) => file.toLowerCase().includes(lower)); + } + const activeClient = client(); + if (!activeClient) return []; + try { + const directory = workspaceProjectDir().trim(); + const result = unwrap( + await activeClient.find.files({ + query: trimmed, + dirs: "true", + limit: 50, + directory: directory || undefined, + }), + ); + return result; + } catch { + return []; + } + }; + const sessionProps = () => ({ selectedSessionId: activeSessionId(), setView, @@ -2730,6 +2751,7 @@ export default function App() { openCommandRunModal: openRunModal, commandRegistryItems, registerCommand: commandRegistry.registerCommand, + searchFiles: searchWorkspaceFiles, onTryNotionPrompt: () => { setPrompt("setup my crm"); setTryNotionPromptVisible(false); diff --git a/packages/app/src/app/components/session/composer.tsx b/packages/app/src/app/components/session/composer.tsx index f6bf59bf..f9f51d17 100644 --- a/packages/app/src/app/components/session/composer.tsx +++ b/packages/app/src/app/components/session/composer.tsx @@ -1,6 +1,6 @@ import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js"; import type { Agent } from "@opencode-ai/sdk/v2/client"; -import { ArrowRight, AtSign, File, Paperclip, Terminal, X, Zap } from "lucide-solid"; +import { ArrowRight, AtSign, ChevronDown, File, Paperclip, X, Zap } from "lucide-solid"; import type { ComposerAttachment, ComposerDraft, ComposerPart, PromptMode } from "../../types"; @@ -33,6 +33,15 @@ type ComposerProps = { onInsertCommand: (commandId: string) => void; selectedModelLabel: string; onModelClick: () => void; + agentLabel: string; + selectedAgent: string | null; + agentPickerOpen: boolean; + agentPickerBusy: boolean; + agentPickerError: string | null; + agentOptions: Agent[]; + onToggleAgentPicker: () => void; + onSelectAgent: (agent: string | null) => void; + setAgentPickerRef: (el: HTMLDivElement) => void; showNotionBanner: boolean; onNotionBannerClick: () => void; toast: string | null; @@ -319,7 +328,9 @@ export default function Composer(props: ComposerProps) { const syncHeight = () => { if (!editorRef) return; editorRef.style.height = "auto"; - const nextHeight = Math.min(editorRef.scrollHeight, 160); + const baseHeight = 24; + const scrollHeight = editorRef.scrollHeight || baseHeight; + const nextHeight = Math.min(Math.max(scrollHeight, baseHeight), 160); editorRef.style.height = `${nextHeight}px`; editorRef.style.overflowY = editorRef.scrollHeight > 160 ? "auto" : "hidden"; }; @@ -697,8 +708,6 @@ export default function Composer(props: ComposerProps) { onCleanup(() => window.removeEventListener("openwork:focusPrompt", handler)); }); - const modeLabel = createMemo(() => (mode() === "shell" ? "Shell" : "Prompt")); - return (
@@ -880,23 +889,11 @@ export default function Composer(props: ComposerProps) {
-
+
-
-
- - {modeLabel()} -
- -
Remote workspace
-
-
+ +
Remote workspace
+
@@ -917,55 +914,133 @@ export default function Composer(props: ComposerProps) { onKeyUp={updateMentionQuery} onClick={updateMentionQuery} onPaste={handlePaste} - class="bg-transparent border-none p-0 text-gray-12 focus:ring-0 text-[15px] leading-relaxed resize-none min-h-[24px] outline-none relative z-10" + class="bg-transparent border-none p-0 pb-12 pr-20 text-gray-12 focus:ring-0 text-[15px] leading-relaxed resize-none min-h-[24px] outline-none relative z-10" /> + +
+ + + +
+
+ Session agent +
+
+ + Loading agents...
} + > + No agents available.
} + > + + {(agent: Agent) => ( + + )} + +
+ + +
{props.agentPickerError}
+
+
+
+ Tip: use /agent-next or /agent-prev to cycle. +
+
+ +
+ +
+ { + const target = event.currentTarget as HTMLInputElement; + const files = Array.from(target.files ?? []); + if (files.length) void addAttachments(files); + target.value = ""; + }} + /> + + + +
- -
- { - const target = event.currentTarget as HTMLInputElement; - const files = Array.from(target.files ?? []); - if (files.length) void addAttachments(files); - target.value = ""; - }} - /> - - - -
diff --git a/packages/app/src/app/components/session/message-list.tsx b/packages/app/src/app/components/session/message-list.tsx index 53cbb1fd..c9e5501f 100644 --- a/packages/app/src/app/components/session/message-list.tsx +++ b/packages/app/src/app/components/session/message-list.tsx @@ -39,6 +39,24 @@ type MessageBlockItem = MessageBlock | StepClusterBlock; export default function MessageList(props: MessageListProps) { const [copyingId, setCopyingId] = createSignal(null); let copyTimeout: number | undefined; + const isAttachmentPart = (part: Part) => { + if (part.type !== "file") return false; + const url = (part as { url?: string }).url; + return typeof url === "string" && !url.startsWith("file://"); + }; + const attachmentsForMessage = (message: MessageWithParts) => + message.parts + .filter(isAttachmentPart) + .map((part) => { + const record = part as { url?: string; filename?: string; mime?: string }; + return { + url: record.url ?? "", + filename: record.filename ?? "attachment", + mime: record.mime ?? "application/octet-stream", + }; + }) + .filter((attachment) => !!attachment.url); + const isImageAttachment = (mime: string) => mime.startsWith("image/"); onCleanup(() => { if (copyTimeout !== undefined) { @@ -269,6 +287,32 @@ export default function MessageList(props: MessageListProps) { : "max-w-[68ch] text-[15px] leading-7 text-gray-12 group pl-2" }`} > + 0}> +
+ + {(attachment) => ( +
+ } + > +
+ {attachment.filename} +
+
+
+
{attachment.filename}
+
{attachment.mime}
+
+
+ )} +
+
+
{(group, idx) => (
diff --git a/packages/app/src/app/context/session.ts b/packages/app/src/app/context/session.ts index 8fc12d57..4cdfb766 100644 --- a/packages/app/src/app/context/session.ts +++ b/packages/app/src/app/context/session.ts @@ -127,8 +127,9 @@ export function createSessionStore(options: { const reloadDetectionSet = new Set(); const skillPathPattern = /[\\/]\.opencode[\\/](skill|skills)[\\/]/i; - const opencodeConfigPattern = /(?:^|[\\/])opencode\.json\b/i; + const opencodeConfigPattern = /(?:^|[\\/])opencode\.jsonc?\b/i; const opencodePathPattern = /(?:^|[\\/])\.opencode[\\/]/i; + const mutatingTools = new Set(["write", "edit", "apply_patch"]); const extractSearchText = (value: unknown) => { if (!value) return ""; @@ -146,15 +147,37 @@ export function createSessionStore(options: { return null; }; + const detectReloadReasonDeep = (value: unknown): ReloadReason | null => { + if (!value) return null; + if (typeof value === "string" || typeof value === "number") { + return detectReloadReason(value); + } + if (Array.isArray(value)) { + for (const entry of value) { + const reason = detectReloadReasonDeep(entry); + if (reason) return reason; + } + return null; + } + if (typeof value === "object") { + for (const entry of Object.values(value as Record)) { + const reason = detectReloadReasonDeep(entry); + if (reason) return reason; + } + } + return null; + }; + const detectReloadFromPart = (part: Part): ReloadReason | null => { + if (part.type !== "tool") return null; const record = part as Record; + const toolName = typeof record.tool === "string" ? record.tool : ""; + if (!mutatingTools.has(toolName)) return null; + const state = (record.state ?? {}) as Record; return ( - detectReloadReason(record.text) || - detectReloadReason(record.path) || - detectReloadReason(record.title) || - detectReloadReason((record.state as { title?: unknown })?.title) || - detectReloadReason((record.state as { output?: unknown })?.output) || - detectReloadReason((record.state as { input?: unknown })?.input) + detectReloadReasonDeep(state.input) || + detectReloadReasonDeep(state.patch) || + detectReloadReasonDeep(state.diff) ); }; diff --git a/packages/app/src/app/pages/session.tsx b/packages/app/src/app/pages/session.tsx index 1387a575..621b8062 100644 --- a/packages/app/src/app/pages/session.tsx +++ b/packages/app/src/app/pages/session.tsx @@ -17,7 +17,6 @@ import type { import { AlertTriangle, - AtSign, ArrowRight, ChevronDown, HardDrive, @@ -94,6 +93,7 @@ export type SessionViewProps = { providers: Provider[]; providerConnectedIds: string[]; listAgents: () => Promise; + searchFiles: (query: string) => Promise; selectedSessionAgent: string | null; setSessionAgent: (sessionId: string, agent: string | null) => void; saveSession: (sessionId: string) => Promise; @@ -553,7 +553,6 @@ export default function SessionView(props: SessionViewProps) { const sessionId = requireSessionId(); if (!sessionId) return; props.setSessionAgent(sessionId, agent); - setCommandToast(agent ? `Agent set to ${agent}` : "Agent cleared"); }; const cycleAgent = async (direction: "next" | "prev") => { @@ -582,7 +581,6 @@ export default function SessionView(props: SessionViewProps) { return; } props.setSessionAgent(sessionId, nextAgent); - setCommandToast(`Agent set to ${nextAgent}`); } catch (error) { const message = error instanceof Error ? error.message : "Agent selection failed"; setCommandToast(message); @@ -740,8 +738,7 @@ export default function SessionView(props: SessionViewProps) { return; } - props.setSessionAgent(sessionId, match.name); - setCommandToast(`Agent set to ${match.name}`); + props.setSessionAgent(sessionId, match.name); clearPrompt(); } catch (error) { const message = error instanceof Error ? error.message : "Agent selection failed"; @@ -917,12 +914,6 @@ export default function SessionView(props: SessionViewProps) { props.setPrompt(draft.text); }; - const searchFiles = async (query: string) => { - const q = query.trim().toLowerCase(); - if (!q) return []; - return props.workingFiles.filter((file) => file.toLowerCase().includes(q)); - }; - return (
-
(agentPickerRef = el)}> - - - -
-
- Session agent -
-
- - Loading agents...
} - > - No agents available.
} - > - - {(agent: Agent) => ( - - )} - -
- - -
{agentPickerError()}
-
-
-
- Tip: use /agent-next or /agent-prev to cycle. -
- - - @@ -1195,13 +1101,27 @@ export default function SessionView(props: SessionViewProps) { onInsertCommand={handleInsertCommand} selectedModelLabel={props.selectedSessionModelLabel || "Model"} onModelClick={props.openSessionModelPicker} + agentLabel={agentLabel()} + selectedAgent={props.selectedSessionAgent} + agentPickerOpen={agentPickerOpen()} + agentPickerBusy={agentPickerBusy()} + agentPickerError={agentPickerError()} + agentOptions={agentOptions()} + onToggleAgentPicker={openAgentPicker} + onSelectAgent={(agent) => { + applySessionAgent(agent); + setAgentPickerOpen(false); + }} + setAgentPickerRef={(el) => { + agentPickerRef = el; + }} showNotionBanner={props.showTryNotionPrompt} onNotionBannerClick={props.onTryNotionPrompt} toast={commandToast()} onToast={(message) => setCommandToast(message)} listAgents={props.listAgents} recentFiles={props.workingFiles} - searchFiles={searchFiles} + searchFiles={props.searchFiles} isRemoteWorkspace={props.activeWorkspaceDisplay.workspaceType === "remote"} /> diff --git a/packages/app/src/app/utils/index.ts b/packages/app/src/app/utils/index.ts index 07e600d1..f6852021 100644 --- a/packages/app/src/app/utils/index.ts +++ b/packages/app/src/app/utils/index.ts @@ -438,7 +438,12 @@ export function groupMessageParts(parts: Part[], messageId: string): MessageGrou } if (part.type === "file") { - const record = part as { label?: string; path?: string; filename?: string }; + const record = part as { label?: string; path?: string; filename?: string; url?: string }; + const url = record.url; + if (typeof url === "string" && !url.startsWith("file://")) { + flushText(); + return; + } const label = record.label ?? record.path ?? record.filename ?? ""; textBuffer += label ? `@${label}` : "@file"; return; diff --git a/packages/desktop/package.json b/packages/desktop/package.json index ea242035..70a07b57 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@different-ai/openwork", "private": true, - "version": "0.3.7", + "version": "0.4.0", "type": "module", "scripts": { "dev": "tauri dev --config \"{\\\"build\\\":{\\\"devUrl\\\":\\\"http://localhost:${PORT:-5173}\\\"}}\"", diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index 9417a369..fcc8c0bc 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -2381,7 +2381,7 @@ dependencies = [ [[package]] name = "openwork" -version = "0.3.7" +version = "0.4.0" dependencies = [ "json5", "serde", diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index 3d815196..694fa01c 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openwork" -version = "0.3.7" +version = "0.4.0" description = "OpenWork" authors = ["Different AI"] edition = "2021" diff --git a/packages/desktop/src-tauri/tauri.conf.json b/packages/desktop/src-tauri/tauri.conf.json index 8c7eed95..7c243b4f 100644 --- a/packages/desktop/src-tauri/tauri.conf.json +++ b/packages/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenWork", - "version": "0.3.7", + "version": "0.4.0", "identifier": "com.differentai.openwork", "build": { "beforeDevCommand": "pnpm -C ../.. --filter @different-ai/openwork run prepare:sidecar && pnpm -w dev:ui",