diff --git a/apps/daemon/src/cli.ts b/apps/daemon/src/cli.ts
index 1fd3d025b..9d1351dc3 100644
--- a/apps/daemon/src/cli.ts
+++ b/apps/daemon/src/cli.ts
@@ -152,7 +152,7 @@ const PROJECT_STRING_FLAGS = new Set([
'daemon-url', 'name', 'skill', 'design-system', 'plugin', 'metadata-json',
'pending-prompt', 'project', 'conversation', 'message', 'path', 'as',
'agent', 'model', 'snapshot-id', 'inputs', 'grant-caps', 'editor',
- 'title', 'against', 'seed-from',
+ 'title', 'against', 'seed-from', 'mode',
]);
const PROJECT_BOOLEAN_FLAGS = new Set(['help', 'h', 'json', 'follow']);
// `od automation …` mirrors the Automations tab. Same surface, same
@@ -4319,6 +4319,14 @@ async function projectDaemonUrl(flags) {
return cliDaemonUrl(flags);
}
+function normalizeChatSessionModeFlag(value) {
+ if (value == null) return undefined;
+ const mode = String(value).trim().toLowerCase();
+ if (mode === 'design' || mode === 'chat') return mode;
+ console.error('--mode must be one of: design, chat');
+ process.exit(2);
+}
+
function safeReadJsonFile(p) {
try {
const fs = (require ? require('node:fs') : null);
@@ -4335,6 +4343,7 @@ async function runProject(args) {
console.log(`Usage:
od project create [--name "
"] [--skill ] [--design-system ]
[--plugin ] [--inputs ] [--metadata-json ]
+ [--mode design|chat]
od project import [--name ""]
od project list List projects.
od project info Print one project.
@@ -4406,6 +4415,8 @@ Common options:
skillId: flags.skill ?? null,
designSystemId: flags['design-system'] ?? null,
};
+ const conversationMode = normalizeChatSessionModeFlag(flags.mode);
+ if (conversationMode) body.conversationMode = conversationMode;
if (flags['pending-prompt']) body.pendingPrompt = flags['pending-prompt'];
if (flags['metadata-json']) {
const mj = safeReadJsonFile(flags['metadata-json']);
@@ -5103,7 +5114,7 @@ function renderDiffLineContent(value) {
async function runConversation(args) {
if (args.length === 0 || args[0] === 'help' || args.includes('--help') || args.includes('-h')) {
console.log(`Usage:
- od conversation new [--title ""] [--seed-from ]
+ od conversation new [--title ""] [--seed-from ] [--mode design|chat]
Create a conversation in a project.
--seed-from copies another
conversation's messages in (Side Chat).
@@ -5128,6 +5139,8 @@ Common options:
}
const body = {};
if (typeof flags.title === 'string') body.title = flags.title;
+ const sessionMode = normalizeChatSessionModeFlag(flags.mode);
+ if (sessionMode) body.sessionMode = sessionMode;
if (typeof flags['seed-from'] === 'string' && flags['seed-from']) {
body.seedFromConversationId = flags['seed-from'];
}
@@ -5139,7 +5152,8 @@ Common options:
if (!resp.ok) return structuredHttpFailure(resp, 'project-not-found');
const data = await resp.json();
if (flags.json) return process.stdout.write(JSON.stringify(data, null, 2) + '\n');
- console.log(`[conversation] created ${data.conversation?.id ?? '-'}`);
+ const conv = data.conversation;
+ console.log(`[conversation] created ${conv?.id ?? '-'} (mode ${conv?.sessionMode ?? sessionMode ?? 'design'})`);
return;
}
case 'list': {
@@ -5185,7 +5199,7 @@ Common options:
async function runChat(args) {
if (args.length === 0 || args[0] === 'help' || args.includes('--help') || args.includes('-h')) {
console.log(`Usage:
- od chat new --project [--seed-from ] [--title ""] [--json]
+ od chat new --project [--seed-from ] [--title ""] [--mode design|chat] [--json]
Create a Side Chat — a new conversation
that copies in another conversation's
context (--seed-from) so the new chat
@@ -5213,6 +5227,8 @@ Common options:
}
const body = {};
if (typeof flags.title === 'string') body.title = flags.title;
+ const sessionMode = normalizeChatSessionModeFlag(flags.mode);
+ if (sessionMode) body.sessionMode = sessionMode;
if (typeof flags['seed-from'] === 'string' && flags['seed-from']) {
body.seedFromConversationId = flags['seed-from'];
}
@@ -5228,7 +5244,7 @@ Common options:
const seeded = body.seedFromConversationId
? ` (seeded from ${body.seedFromConversationId})`
: '';
- console.log(`[chat] created ${conv?.id ?? '-'}${conv?.title ? ` "${conv.title}"` : ''}${seeded}`);
+ console.log(`[chat] created ${conv?.id ?? '-'}${conv?.title ? ` "${conv.title}"` : ''}${seeded} (mode ${conv?.sessionMode ?? sessionMode ?? 'design'})`);
return;
}
default:
diff --git a/apps/daemon/src/project-routes.ts b/apps/daemon/src/project-routes.ts
index 4379558b2..2a6072ee3 100644
--- a/apps/daemon/src/project-routes.ts
+++ b/apps/daemon/src/project-routes.ts
@@ -323,13 +323,14 @@ export function registerProjectRoutes(app: Express, ctx: RegisterProjectRoutesDe
});
// Seed a default conversation so the UI always has somewhere to write.
const cid = randomId();
+ const initialSessionMode = normalizeChatSessionMode(
+ req.body?.conversationMode ?? req.body?.sessionMode,
+ );
insertConversation(db, {
id: cid,
projectId: id,
title: null,
- sessionMode: normalizeChatSessionMode(
- req.body?.conversationMode ?? req.body?.sessionMode,
- ),
+ sessionMode: initialSessionMode,
createdAt: now,
updatedAt: now,
});
@@ -341,7 +342,7 @@ export function registerProjectRoutes(app: Express, ctx: RegisterProjectRoutesDe
&& req.body.appliedPluginSnapshotId.trim().length > 0;
let resolveBody =
explicitPlugin ? (req.body as Record) : null;
- if (!resolveBody) {
+ if (!resolveBody && initialSessionMode === 'design') {
const fallbackPluginId = defaultScenarioPluginIdForProjectMetadata(projectMetadata);
if (fallbackPluginId && getInstalledPlugin(db, fallbackPluginId)) {
resolveBody = { ...(req.body || {}), pluginId: fallbackPluginId };
diff --git a/apps/web/src/components/FileWorkspace.tsx b/apps/web/src/components/FileWorkspace.tsx
index 26a4d54aa..d76739adc 100644
--- a/apps/web/src/components/FileWorkspace.tsx
+++ b/apps/web/src/components/FileWorkspace.tsx
@@ -6,6 +6,7 @@ import {
type DragEvent as ReactDragEvent,
} from 'react';
import type { TrackingProjectKind } from '@open-design/contracts/analytics';
+import type { ChatSessionMode } from '@open-design/contracts';
import { useAnalytics } from '../analytics/provider';
import {
trackFileManagerClick,
@@ -136,6 +137,7 @@ interface Props {
onSelectConversation?: (id: string) => void;
onDeleteConversation?: (id: string) => void;
onRenameConversation?: (id: string, title: string) => void;
+ onConversationSessionModeChange?: (id: string, mode: ChatSessionMode) => void;
onNewConversation?: () => void;
/** Create a context-seeded conversation and resolve its id (backs the launcher). */
onCreateSideChat?: (seedFromConversationId: string | null) => Promise;
@@ -265,6 +267,7 @@ export function FileWorkspace({
onSelectConversation,
onDeleteConversation,
onRenameConversation,
+ onConversationSessionModeChange,
onNewConversation,
onCreateSideChat,
}: Props) {
@@ -1246,6 +1249,7 @@ export function FileWorkspace({
onSelectConversation={onSelectConversation ?? (() => {})}
onDeleteConversation={onDeleteConversation ?? (() => {})}
onRenameConversation={onRenameConversation}
+ onSessionModeChange={onConversationSessionModeChange}
onNewConversation={onNewConversation}
onRequestOpenFile={openFile}
/>
diff --git a/apps/web/src/components/ProjectView.tsx b/apps/web/src/components/ProjectView.tsx
index 9bc8f5bdd..de06ac88c 100644
--- a/apps/web/src/components/ProjectView.tsx
+++ b/apps/web/src/components/ProjectView.tsx
@@ -107,7 +107,7 @@ import {
type SaveMessageOptions,
waitGeneratedPluginShareTask,
} from '../state/projects';
-import type { AppliedPluginSnapshot } from '@open-design/contracts';
+import type { AppliedPluginSnapshot, ChatSessionMode } from '@open-design/contracts';
import type {
AgentEvent,
AgentInfo,
@@ -115,7 +115,6 @@ import type {
Artifact,
ChatAttachment,
ChatCommentAttachment,
- ChatSessionMode,
ChatMessage,
ChatMessageFeedbackChange,
Conversation,
@@ -1434,7 +1433,9 @@ export function ProjectView({
return project.id;
}, [project.id]);
- const composedSystemPrompt = useCallback(async (): Promise => {
+ const composedSystemPrompt = useCallback(async (
+ sessionModeOverride: ChatSessionMode = activeSessionMode,
+ ): Promise => {
let skillBody: string | undefined;
let skillName: string | undefined;
let skillMode: SkillSummary['mode'] | undefined;
@@ -1537,6 +1538,7 @@ export function ProjectView({
audioVoiceOptions,
audioVoiceOptionsError: audioVoiceOptionsLookupError,
streamFormat: config.mode === 'api' ? 'plain' : undefined,
+ sessionMode: sessionModeOverride,
locale,
userInstructions: config.customInstructions,
projectInstructions: project.customInstructions,
@@ -1551,6 +1553,7 @@ export function ProjectView({
designSystems,
config.mode,
config.customInstructions,
+ activeSessionMode,
locale,
]);
@@ -2267,6 +2270,7 @@ export function ProjectView({
) => {
if (!activeConversationId) return;
if (messagesConversationIdRef.current !== activeConversationId) return;
+ const runSessionMode = meta?.sessionMode ?? activeSessionMode;
const retryTarget = meta?.retryOfAssistantId
? resolveRetryTarget(messages, meta.retryOfAssistantId)
: null;
@@ -2285,7 +2289,7 @@ export function ProjectView({
prompt,
attachments,
commentAttachments,
- ...(meta === undefined ? {} : { meta }),
+ meta: { ...(meta ?? {}), sessionMode: runSessionMode },
createdAt: Date.now(),
});
if (commentAttachments.length > 0) {
@@ -2770,6 +2774,7 @@ export function ProjectView({
designSystemId: project.designSystemId ?? null,
attachments: runAttachments.map((a) => a.path),
commentAttachments: runCommentAttachments,
+ sessionMode: runSessionMode,
research: meta?.research,
mediaExecution: mediaExecutionPolicyForProjectMetadata(project.metadata),
model: choice?.model ?? null,
@@ -2863,7 +2868,7 @@ export function ProjectView({
// on the next event.
}
}
- const systemPrompt = await composedSystemPrompt();
+ const systemPrompt = await composedSystemPrompt(runSessionMode);
const apiHistory = await historyWithApiAttachmentContext(
historyWithCommentAttachmentContext(nextHistory, userMsg.id),
userMsg.id,
@@ -2911,6 +2916,7 @@ export function ProjectView({
[
attachedComments,
activeConversationId,
+ activeSessionMode,
currentConversationBusy,
enqueueChatSend,
messages,
@@ -3638,6 +3644,33 @@ export function ProjectView({
[project.id],
);
+ const handleConversationSessionModeChange = useCallback(
+ async (id: string, sessionMode: ChatSessionMode) => {
+ setConversations((curr) =>
+ curr.map((conversation) =>
+ conversation.id === id ? { ...conversation, sessionMode } : conversation,
+ ),
+ );
+ const updated = await patchConversation(project.id, id, { sessionMode });
+ if (updated) {
+ setConversations((curr) =>
+ curr.map((conversation) =>
+ conversation.id === id ? { ...conversation, ...updated } : conversation,
+ ),
+ );
+ }
+ },
+ [project.id],
+ );
+
+ const handleActiveConversationSessionModeChange = useCallback(
+ (sessionMode: ChatSessionMode) => {
+ if (!activeConversationId) return;
+ void handleConversationSessionModeChange(activeConversationId, sessionMode);
+ },
+ [activeConversationId, handleConversationSessionModeChange],
+ );
+
// Side Chat launcher: create a NEW conversation seeded with the current
// chat's context (the daemon copies the source conversation's messages) and
// resolve its id. The new conversation is a normal conversation, so it shows
@@ -4480,6 +4513,8 @@ export function ProjectView({
queuedItems={currentConversationQueuedItems}
error={conversationLoadError ?? error ?? audioVoiceOptionsError}
projectId={project.id}
+ sessionMode={activeSessionMode}
+ onSessionModeChange={handleActiveConversationSessionModeChange}
projectKindForTracking={projectKindToTracking(project.metadata?.kind)}
projectFiles={projectFiles}
hasActiveDesignSystem={!!project.designSystemId}
@@ -4615,6 +4650,7 @@ export function ProjectView({
onSelectConversation={handleSelectConversation}
onDeleteConversation={handleDeleteConversation}
onRenameConversation={handleRenameConversation}
+ onConversationSessionModeChange={handleConversationSessionModeChange}
onNewConversation={handleNewConversation}
onCreateSideChat={handleCreateSideChat}
/>
diff --git a/apps/web/src/components/workspace/SideChatTab.tsx b/apps/web/src/components/workspace/SideChatTab.tsx
index be5286e4c..2c173b8d2 100644
--- a/apps/web/src/components/workspace/SideChatTab.tsx
+++ b/apps/web/src/components/workspace/SideChatTab.tsx
@@ -7,6 +7,7 @@ import type {
Conversation,
ProjectFile,
} from '../../types';
+import type { ChatSessionMode } from '@open-design/contracts';
import { useConversationChat } from './useConversationChat';
import styles from './SideChatTab.module.css';
@@ -28,6 +29,7 @@ interface Props {
onSelectConversation: (id: string) => void;
onDeleteConversation: (id: string) => void;
onRenameConversation?: (id: string, title: string) => void;
+ onSessionModeChange?: (id: string, mode: ChatSessionMode) => void;
onNewConversation?: () => void;
/** Forward produced-file / tool-card open requests to the workspace. */
onRequestOpenFile?: (name: string) => void;
@@ -49,14 +51,19 @@ export function SideChatTab({
onSelectConversation,
onDeleteConversation,
onRenameConversation,
+ onSessionModeChange,
onNewConversation,
onRequestOpenFile,
}: Props) {
const t = useT();
+ const sessionMode =
+ conversations.find((conversation) => conversation.id === conversationId)?.sessionMode
+ ?? 'design';
const chat = useConversationChat(projectId, conversationId, {
config,
agentsById,
locale,
+ sessionMode,
});
return (
@@ -73,6 +80,8 @@ export function SideChatTab({
streaming={chat.streaming}
error={chat.error}
projectId={projectId}
+ sessionMode={sessionMode}
+ onSessionModeChange={(mode) => onSessionModeChange?.(conversationId, mode)}
projectFiles={projectFiles}
projectFileNames={projectFileNames}
onEnsureProject={async () => projectId}
diff --git a/apps/web/src/components/workspace/useConversationChat.ts b/apps/web/src/components/workspace/useConversationChat.ts
index d7b0a86ca..c461a795d 100644
--- a/apps/web/src/components/workspace/useConversationChat.ts
+++ b/apps/web/src/components/workspace/useConversationChat.ts
@@ -18,6 +18,7 @@ import type {
ChatCommentAttachment,
ChatMessage,
} from '../../types';
+import type { ChatSessionMode } from '@open-design/contracts';
// ---------------------------------------------------------------------------
// useConversationChat — drives a secondary ChatPane bound to a single
@@ -51,6 +52,7 @@ export interface ConversationChatContext {
agentsById: Map;
/** UI locale forwarded to the daemon so prompts compose in-language. */
locale: string;
+ sessionMode: ChatSessionMode;
}
export interface UseConversationChatResult {
@@ -137,7 +139,12 @@ export function useConversationChat(
commentAttachments: ChatCommentAttachment[],
retryOfAssistantId?: string,
) => {
- const { config: cfg, agentsById: agents, locale: loc } = ctxRef.current;
+ const {
+ config: cfg,
+ agentsById: agents,
+ locale: loc,
+ sessionMode,
+ } = ctxRef.current;
if (cfg.mode !== 'daemon') {
setError('Side Chat needs a local agent. Pick one in the top bar.');
return;
@@ -277,6 +284,7 @@ export function useConversationChat(
model: choice?.model ?? null,
reasoning: choice?.reasoning ?? null,
locale: loc,
+ sessionMode,
onRunCreated: (runId) => {
updateAssistant(assistantId, (prev) => ({
...prev,
diff --git a/apps/web/src/providers/daemon.ts b/apps/web/src/providers/daemon.ts
index 596de7f3d..0c4780740 100644
--- a/apps/web/src/providers/daemon.ts
+++ b/apps/web/src/providers/daemon.ts
@@ -17,6 +17,7 @@ import type {
ChatRunStatus,
ChatRunStatusResponse,
ChatRequest,
+ ChatSessionMode,
ChatSseEvent,
ChatSseStartPayload,
DaemonAgentPayload,
@@ -204,6 +205,7 @@ export interface DaemonStreamOptions {
// workspace.
projectId?: string | null;
conversationId?: string | null;
+ sessionMode?: ChatSessionMode;
assistantMessageId?: string | null;
clientRequestId?: string | null;
skillId?: string | null;
@@ -300,6 +302,7 @@ export async function streamViaDaemon({
handlers,
projectId,
conversationId,
+ sessionMode,
assistantMessageId,
clientRequestId,
skillId,
@@ -333,6 +336,7 @@ export async function streamViaDaemon({
currentPrompt: latestUserPromptFromHistory(history),
projectId: projectId ?? null,
conversationId: conversationId ?? null,
+ sessionMode,
assistantMessageId: assistantMessageId ?? null,
clientRequestId: clientRequestId ?? null,
skillId: skillId ?? null,
diff --git a/apps/web/src/styles/chat.css b/apps/web/src/styles/chat.css
index 39eca1bf6..225bc049b 100644
--- a/apps/web/src/styles/chat.css
+++ b/apps/web/src/styles/chat.css
@@ -629,6 +629,14 @@
overflow: hidden;
text-overflow: ellipsis;
}
+@media (max-width: 700px) {
+ .session-mode-toggle__option {
+ padding: 0 6px;
+ }
+ .session-mode-toggle__label {
+ display: none;
+ }
+}
.composer-import {
background: transparent;
border: 1px solid var(--border);
diff --git a/packages/contracts/src/api/projects.ts b/packages/contracts/src/api/projects.ts
index 71f410788..0167cbe7d 100644
--- a/packages/contracts/src/api/projects.ts
+++ b/packages/contracts/src/api/projects.ts
@@ -193,7 +193,7 @@ export interface Conversation {
id: string;
projectId: string;
title: string | null;
- sessionMode: ChatSessionMode;
+ sessionMode?: ChatSessionMode;
createdAt: number;
updatedAt: number;
latestRun?: {