mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
feat(daemon): enhance conversation session mode handling
- Added a new `mode` flag to CLI commands for project and conversation creation, allowing users to specify 'design' or 'chat' modes. - Implemented `normalizeChatSessionModeFlag` function to validate and normalize session mode inputs. - Updated project routes to handle session mode during conversation creation and updates. - Enhanced web components to support session mode changes, including new props and handlers for managing session modes in conversations. - Adjusted UI elements to reflect the current session mode, improving user experience and interaction flexibility. This update provides a more robust framework for managing conversation modes, catering to diverse user needs and enhancing overall functionality.
This commit is contained in:
parent
79c039efdf
commit
44492af1fa
9 changed files with 102 additions and 16 deletions
|
|
@ -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 "<title>"] [--skill <id>] [--design-system <id>]
|
||||
[--plugin <id>] [--inputs <json>] [--metadata-json <path|->]
|
||||
[--mode design|chat]
|
||||
od project import <baseDir> [--name "<title>"]
|
||||
od project list List projects.
|
||||
od project info <id> 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 <projectId> [--title "<title>"] [--seed-from <cid>]
|
||||
od conversation new <projectId> [--title "<title>"] [--seed-from <cid>] [--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 <id> [--seed-from <cid>] [--title "<title>"] [--json]
|
||||
od chat new --project <id> [--seed-from <cid>] [--title "<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:
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>) : null;
|
||||
if (!resolveBody) {
|
||||
if (!resolveBody && initialSessionMode === 'design') {
|
||||
const fallbackPluginId = defaultScenarioPluginIdForProjectMetadata(projectMetadata);
|
||||
if (fallbackPluginId && getInstalledPlugin(db, fallbackPluginId)) {
|
||||
resolveBody = { ...(req.body || {}), pluginId: fallbackPluginId };
|
||||
|
|
|
|||
|
|
@ -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<string | null>;
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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<string> => {
|
||||
const composedSystemPrompt = useCallback(async (
|
||||
sessionModeOverride: ChatSessionMode = activeSessionMode,
|
||||
): Promise<string> => {
|
||||
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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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<string, AgentInfo>;
|
||||
/** 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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ export interface Conversation {
|
|||
id: string;
|
||||
projectId: string;
|
||||
title: string | null;
|
||||
sessionMode: ChatSessionMode;
|
||||
sessionMode?: ChatSessionMode;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
latestRun?: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue