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.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",