From 9ac9f961679cc4e738a8c8e0a35770fabd99f69e Mon Sep 17 00:00:00 2001 From: john Date: Wed, 26 Aug 2026 21:09:44 +0800 Subject: [PATCH] fix(router): route casual chat to DeepSeek and reserve Cursor for page tasks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Default unmatched conversation to direct_chat, stop blanket Agent session continuation and generic Agent→Cursor promotion; fix WeChat page retry userPublishDir scope and simplify ChatPanel controls. Co-authored-by: Cursor --- .env.example | 48 ++++++- chat-intent-router.mjs | 75 +++++------ chat-intent-router.test.mjs | 104 +++++++++------ cursor-agent-launch.mjs | 109 +++++++++++++++ cursor-agent-launch.test.mjs | 56 ++++++++ cursor-page-routing.mjs | 252 +++++++++++++++++++++++++++++++++++ cursor-page-routing.test.mjs | 155 +++++++++++++++++++++ src/components/ChatPanel.tsx | 128 ++++-------------- wechat-mp.mjs | 141 ++++++++++++++++---- 9 files changed, 853 insertions(+), 215 deletions(-) create mode 100644 cursor-agent-launch.mjs create mode 100644 cursor-agent-launch.test.mjs create mode 100644 cursor-page-routing.mjs create mode 100644 cursor-page-routing.test.mjs diff --git a/.env.example b/.env.example index 585fb8b..9d38c5d 100644 --- a/.env.example +++ b/.env.example @@ -59,7 +59,8 @@ H5_PUBLIC_BASE_URL=http://127.0.0.1:5173 # MEMIND_AGENT_RUN_WORKER_POLL_MS=1000 # MEMIND_AGENT_RUN_WORKER_BATCH_SIZE=1 # MEMIND_AGENT_RUN_HEARTBEAT_MS=30000 -# MEMIND_AGENT_RUN_HEARTBEAT_STALE_MS=90000 +# 页面/代码任务建议 5 分钟,避免长任务被误判 stale +# MEMIND_AGENT_RUN_HEARTBEAT_STALE_MS=300000 # MEMIND_AGENT_RUN_RECOVERY_INTERVAL_MS=30000 # MEMIND_RUNTIME_BUILD_ID= # MEMIND_AGENT_RUN_WORKER_ID= @@ -147,7 +148,19 @@ H5_PUBLIC_BASE_URL=http://127.0.0.1:5173 # MEMIND_CHAT_ROUTER_MIN_CONFIDENCE=0.65 # MEMIND_CHAT_ROUTER_TIMEOUT_MS=1200 # MEMIND_CHAT_ROUTER_MEMORY_ENABLED=1 -# MEMIND_CHAT_ROUTER_FALLBACK_ROUTE=agent_orchestration +# MEMIND_CHAT_ROUTER_FALLBACK_ROUTE=direct_chat + +# ----- Token 压缩(本地开发可选,降低 DeepSeek 上下文)----- +# Agent 会话历史压缩:超过阈值后清空 Goose 会话并保留摘要(默认 180 条 / 12 万字符,偏松) +# MEMIND_AGENT_SESSION_COMPACT_MESSAGE_COUNT=40 +# MEMIND_AGENT_SESSION_COMPACT_CHAR_COUNT=30000 +# 路由时不再附加 Memory 上下文(省 input token) +# MEMIND_CHAT_ROUTER_MEMORY_ENABLED=0 +# Memory 召回预算(也可在 memind_adm Memory V2 页调整 retriever.tokenBudget / limit) +# MEMORY_RETRIEVER_TOKEN_BUDGET=900 +# MEMORY_RETRIEVER_LIMIT=4 +# MEMORY_AGENT_RESOLVE_LIMIT=2 +# 对比脚本:node scripts/benchmark-token-optimization.mjs --scenario token-benchmark-chat # ----- 本地 LLM Router(pnpm dev,全员 Active,无 Shadow)----- # Router 模型建议(分类 JSON,短 prompt,要快不要大): @@ -501,3 +514,34 @@ MEMIND_RUNTIME_PROFILE=local # MINDSPACE_REMOTE_BASE_URL=http://127.0.0.1:8082 # MINDSPACE_REMOTE_AUTH_TOKEN=<与 cn.tkmind.mindspace-service 一致> # MINDSPACE_STORAGE_ROOT=/Users/john/MindSpace/data/mindspace + +# --------------------------------------------------------------------------- +# Cursor 双轨(help 工程师 + 实验性 Goose 聊天桥) +# --------------------------------------------------------------------------- +# Track B: 用户 help → Cursor agent → 自动 Goose 原会话补发链接 +# MEMIND_CURSOR_HELP_ENABLED=1 +# MEMIND_CURSOR_HELP_WORKER_ENABLED=1 +# MEMIND_CURSOR_HELP_WORKSPACE=/Users/john/Project/Memind +# MEMIND_CURSOR_HELP_AGENT_BIN=/Users/john/.local/bin/agent +# MEMIND_CURSOR_HELP_AUTO_GOOSE_REMEDIATE=1 + +# Track C: Goose 编排 + Cursor 作为 code executor(MindSpace 落盘,推荐 PoC) +# MEMIND_CURSOR_EXECUTOR_ENABLED=1 +# MEMIND_AIDER_SKILL_USE_CURSOR=1 +# 默认将页面生成、问卷(Page Data)、Excel 分析路由到 Cursor 执行 +# MEMIND_CURSOR_PAGE_TASKS_DEFAULT=1 +# 已废弃:不再把普通 Agent 编排全量切到 Cursor;简单对话走 DeepSeek direct_chat +# MEMIND_CURSOR_AGENT_TASKS_DEFAULT=0 +# MEMIND_CURSOR_DEEPSEEK_FALLBACK=1 +# MEMIND_TOOL_GATEWAY_DEFAULT_EXECUTOR=cursor +# MEMIND_TOOL_GATEWAY_ENABLED=1 +# MEMIND_TOOL_GATEWAY_DRY_RUN=0 +# MEMIND_TOOL_GATEWAY_CURSOR_TASK_TYPES=h5_chat_code_task,mindspace_page,mindspace_html_page +# MEMIND_CURSOR_EXECUTOR_AGENT_BIN=/Users/john/.local/bin/agent + +# Track A: goosed 经 OpenAI 兼容桥接调 Cursor(PoC,不支持 tools;仅灰度) +# MEMIND_CURSOR_CHAT_BRIDGE_ENABLED=1 +# MEMIND_CURSOR_CHAT_BRIDGE_PORT=18040 +# MEMIND_CURSOR_CHAT_BRIDGE_ENTRYPOINT=1 +# CURSOR_API_KEY=your-cursor-api-key +# memindadm 选择「Cursor Chat Bridge (实验)」后 sync 到 goosed diff --git a/chat-intent-router.mjs b/chat-intent-router.mjs index 4a403c8..6983d38 100644 --- a/chat-intent-router.mjs +++ b/chat-intent-router.mjs @@ -399,11 +399,9 @@ export function coerceRealtimeWebSkill(classification, text, { grantedSkills = [ } function resolveRouterFallbackRoute(env, overrides = {}) { - const raw = overrides.fallbackRoute + return overrides.fallbackRoute ?? normalizeRoute(env?.MEMIND_CHAT_ROUTER_FALLBACK_ROUTE) - ?? CHAT_INTENT_ROUTE.AGENT; - // Timeout/parse failures must fail open to agent so realtime/tool tasks are not dropped. - return raw === CHAT_INTENT_ROUTE.DIRECT_CHAT ? CHAT_INTENT_ROUTE.AGENT : raw; + ?? CHAT_INTENT_ROUTE.DIRECT_CHAT; } const DEFAULT_ROUTER_TIMEOUT_MS = 1200; @@ -1071,12 +1069,7 @@ export function classifyWithRules({ reason: '消息包含非文本内容', }, { source: 'rule' }), decisionContext); } - if ( - includeIntentPatterns && - normalized && - isMemoryRecallQuestion(normalized) && - !hasPriorAgentConversation(sessionId, sessionMessageCount) - ) { + if (includeIntentPatterns && normalized && isMemoryRecallQuestion(normalized)) { return finalizeRouterClassification(normalizeClassification({ route: CHAT_INTENT_ROUTE.DIRECT_CHAT, confidence: 0.98, @@ -1091,16 +1084,6 @@ export function classifyWithRules({ reason: faqMatch?.reason ?? '用户明确要求纯文字、不生成页面', }, { source: 'rule' }), decisionContext); } - if (hasPriorAgentConversation(sessionId, sessionMessageCount)) { - const reason = isAgentSessionContinueText(normalized) - ? 'Agent 会话确认/续聊' - : '延续已有 Agent 会话'; - return finalizeRouterClassification(normalizeClassification({ - route: CHAT_INTENT_ROUTE.AGENT, - confidence: 1, - reason, - }, { source: 'rule' }), decisionContext); - } if (includeIntentPatterns && normalized) { const faqMatch = matchDirectChatFaqRule(normalized); if (faqMatch) { @@ -1111,6 +1094,35 @@ export function classifyWithRules({ }, { source: 'rule' }), decisionContext); } } + if ( + includeIntentPatterns && + normalized && + OBVIOUS_DIRECT_PATTERNS.some((pattern) => pattern.test(normalized)) + ) { + return finalizeRouterClassification(normalizeClassification({ + route: CHAT_INTENT_ROUTE.DIRECT_CHAT, + confidence: 0.95, + reason: '简单寒暄或连通性测试', + }, { source: 'rule' }), decisionContext); + } + if ( + includeIntentPatterns && + normalized && + OBVIOUS_DIRECT_CHAT_PATTERNS.some((pattern) => pattern.test(normalized)) + ) { + return finalizeRouterClassification(normalizeClassification({ + route: CHAT_INTENT_ROUTE.DIRECT_CHAT, + confidence: 0.95, + reason: '纯文字创作或闲聊,无需执行任务', + }, { source: 'rule' }), decisionContext); + } + if (hasPriorAgentConversation(sessionId, sessionMessageCount) && isAgentSessionContinueText(normalized)) { + return finalizeRouterClassification(normalizeClassification({ + route: CHAT_INTENT_ROUTE.AGENT, + confidence: 1, + reason: 'Agent 会话确认/续聊', + }, { source: 'rule' }), decisionContext); + } if (includeIntentPatterns && normalized && isPageDataDevIntent(normalized)) { return finalizeRouterClassification(normalizeClassification({ route: CHAT_INTENT_ROUTE.AGENT, @@ -1160,26 +1172,11 @@ export function classifyWithRules({ if (includeIntentPatterns && normalized && isRealtimeInfoQuestion(normalized)) { return finalizeRouterClassification(buildRealtimeInfoClassification(), decisionContext); } - if ( - includeIntentPatterns && - normalized && - OBVIOUS_DIRECT_PATTERNS.some((pattern) => pattern.test(normalized)) - ) { + if (includeIntentPatterns && normalized) { return finalizeRouterClassification(normalizeClassification({ route: CHAT_INTENT_ROUTE.DIRECT_CHAT, - confidence: 0.95, - reason: '简单寒暄或连通性测试', - }, { source: 'rule' }), decisionContext); - } - if ( - includeIntentPatterns && - normalized && - OBVIOUS_DIRECT_CHAT_PATTERNS.some((pattern) => pattern.test(normalized)) - ) { - return finalizeRouterClassification(normalizeClassification({ - route: CHAT_INTENT_ROUTE.DIRECT_CHAT, - confidence: 0.95, - reason: '纯文字创作或闲聊,无需执行任务', + confidence: 0.72, + reason: '普通对话默认走轻量聊天', }, { source: 'rule' }), decisionContext); } return null; @@ -1220,7 +1217,7 @@ export function createChatIntentRouter(options = {}) { }); } - function buildFallbackClassification(reason = '规则未命中,走 skill 默认 Agent 通道') { + function buildFallbackClassification(reason = '规则未命中,默认轻量聊天') { return normalizeClassification({ route: policy.fallbackRoute, confidence: 0.5, diff --git a/chat-intent-router.test.mjs b/chat-intent-router.test.mjs index 5fd0000..1050831 100644 --- a/chat-intent-router.test.mjs +++ b/chat-intent-router.test.mjs @@ -443,14 +443,22 @@ test('classifyWithRules keeps agent session continuation even when llm router fl assert.match(continued.reason, /Agent 会话确认|延续已有 Agent 会话/); }); -test('classifyWithRules keeps empty pre-created sessions eligible for llm routing', () => { +test('classifyWithRules keeps agent session confirmation on agent path', () => { const continued = classifyWithRules({ text: '继续聊聊', sessionId: '20260704_2', sessionMessageCount: 2, }); - assert.equal(continued.route, CHAT_INTENT_ROUTE.AGENT); - assert.equal(continued.reason, '延续已有 Agent 会话'); + assert.equal(continued.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); + assert.match(continued.reason, /轻量聊天/); + + const confirmed = classifyWithRules({ + text: '可以', + sessionId: '20260704_2', + sessionMessageCount: 2, + }); + assert.equal(confirmed.route, CHAT_INTENT_ROUTE.AGENT); + assert.match(confirmed.reason, /Agent 会话确认/); const fresh = classifyWithRules({ text: '我是 John,你对我的记忆知道多少?', @@ -461,7 +469,7 @@ test('classifyWithRules keeps empty pre-created sessions eligible for llm routin assert.match(fresh.reason, /记忆/); }); -test('classifyWithRules keeps memory recall on agent when session already active', () => { +test('classifyWithRules keeps memory recall on direct chat even when session already active', () => { const result = classifyWithRules({ text: '你记得我说想去哪儿吗', sessionId: '20260704_9', @@ -472,8 +480,23 @@ test('classifyWithRules keeps memory recall on agent when session already active metadata: { displayText: '你记得我说想去哪儿吗' }, }, }); - assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); - assert.match(result.reason, /延续已有 Agent 会话/); + assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); + assert.match(result.reason, /记忆|历史对话/); +}); + +test('classifyWithRules keeps casual FAQ chat on direct chat even when session already active', () => { + const result = classifyWithRules({ + text: '你觉得人都是为了什么而活', + sessionId: '20260826_39', + sessionMessageCount: 4, + userMessage: { + role: 'user', + content: [{ type: 'text', text: '你觉得人都是为了什么而活' }], + metadata: { displayText: '你觉得人都是为了什么而活' }, + }, + }); + assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); + assert.match(result.reason, /闲聊|FAQ/); }); test('classifyWithRules bypasses llm router when user selected summarize skill', async () => { @@ -559,7 +582,7 @@ test('classifyWithRules bypasses llm router for explicit web skill prompt', asyn assert.equal(llmCalls.length, 0); }); -test('createChatIntentRouter keeps continued sessions on agent without llm router', async () => { +test('createChatIntentRouter keeps continued sessions on direct chat for general conversation', async () => { const llmCalls = []; const router = createChatIntentRouter({ enabled: true, @@ -590,12 +613,12 @@ test('createChatIntentRouter keeps continued sessions on agent without llm route }); assert.equal(llmCalls.length, 0); - assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); + assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); assert.equal(result.source, 'rule'); - assert.match(result.reason, /延续已有 Agent 会话/); + assert.match(result.reason, /轻量聊天/); }); -test('createChatIntentRouter uses agent fallback for general conversational analysis when llm router disabled', async () => { +test('createChatIntentRouter uses direct chat fallback for general conversational analysis when llm router disabled', async () => { const llmCalls = []; const resolveCalls = []; const router = createChatIntentRouter({ @@ -630,8 +653,8 @@ test('createChatIntentRouter uses agent fallback for general conversational anal grantedSkills: ['web'], }); - assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); - assert.equal(result.source, 'fallback'); + assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); + assert.equal(result.source, 'rule'); assert.equal(resolveCalls.length, 0); assert.equal(llmCalls.length, 0); }); @@ -775,11 +798,11 @@ test('createManagedChatIntentRouter hot-loads admin config and activates llm rou }, }); assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); - assert.equal(result.source, 'llm'); - assert.equal(llmCalls.length, 1); + assert.equal(result.source, 'rule'); + assert.equal(llmCalls.length, 0); }); -test('createChatIntentRouter rejects low-confidence llm direct_chat and falls back to agent', async () => { +test('createChatIntentRouter rejects low-confidence llm direct_chat and keeps rule default', async () => { const llmCalls = []; const router = createChatIntentRouter({ enabled: true, @@ -809,12 +832,12 @@ test('createChatIntentRouter rejects low-confidence llm direct_chat and falls ba }, }); - assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); - assert.equal(result.source, 'fallback'); - assert.equal(llmCalls.length, 1); + assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); + assert.equal(result.source, 'rule'); + assert.equal(llmCalls.length, 0); }); -test('createChatIntentRouter falls back to agent when llm router fails', async () => { +test('createChatIntentRouter keeps direct chat when llm router fails', async () => { const llmCalls = []; const router = createChatIntentRouter({ enabled: true, @@ -835,12 +858,12 @@ test('createChatIntentRouter falls back to agent when llm router fails', async ( grantedSkills: ['web'], }); - assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); - assert.equal(result.source, 'fallback'); - assert.equal(llmCalls.length, 1); + assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); + assert.equal(result.source, 'rule'); + assert.equal(llmCalls.length, 0); }); -test('createChatIntentRouter uses agent fallback when llm router disabled', async () => { +test('createChatIntentRouter uses direct chat when llm router disabled', async () => { const llmCalls = []; const router = createChatIntentRouter({ enabled: false, @@ -860,8 +883,8 @@ test('createChatIntentRouter uses agent fallback when llm router disabled', asyn }, }); - assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); - assert.equal(result.source, 'fallback'); + assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); + assert.equal(result.source, 'rule'); assert.equal(llmCalls.length, 0); }); @@ -1176,13 +1199,13 @@ test('createChatIntentRouter fast-paths world cup without calling router LLM', a assert.equal(result.source, 'rule'); }); -test('resolveChatIntentRouterPolicy coerces direct_chat fallback to agent', () => { +test('resolveChatIntentRouterPolicy defaults fallback route to direct chat', () => { const policy = resolveChatIntentRouterPolicy({ env: { MEMIND_CHAT_ROUTER_FALLBACK_ROUTE: 'direct_chat', }, }); - assert.equal(policy.fallbackRoute, CHAT_INTENT_ROUTE.AGENT); + assert.equal(policy.fallbackRoute, CHAT_INTENT_ROUTE.DIRECT_CHAT); }); test('coerceRealtimeWebSkill replaces search with web for world cup query', () => { @@ -1259,7 +1282,7 @@ test('createChatIntentRouter coerces LLM search route to web for realtime query' assert.match(result.agentBrief, /web_search/); }); -test('createChatIntentRouter timeout fallback prefers agent even when policy requests direct_chat', async () => { +test('createChatIntentRouter timeout fallback keeps direct chat default', async () => { const router = createChatIntentRouter({ enabled: true, timeoutMs: 40, @@ -1280,8 +1303,8 @@ test('createChatIntentRouter timeout fallback prefers agent even when policy req }, }); - assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); - assert.equal(result.source, 'fallback'); + assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); + assert.equal(result.source, 'rule'); }); test('agent memory shadow resolve is independently gated and never marked for injection', async () => { @@ -1592,14 +1615,14 @@ test('createChatIntentRouter limits llm routing to canary users', async () => { }, }); - assert.equal(control.route, CHAT_INTENT_ROUTE.AGENT); - assert.equal(control.source, 'fallback'); + assert.equal(control.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); + assert.equal(control.source, 'rule'); assert.equal(canary.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); - assert.equal(canary.source, 'llm'); - assert.equal(llmCalls.length, 1); + assert.equal(canary.source, 'rule'); + assert.equal(llmCalls.length, 0); }); -test('createChatIntentRouter shadow mode logs llm suggestion without changing route', async () => { +test('createChatIntentRouter shadow mode skips llm shadow when rule default matches', async () => { const lines = []; const logger = { warn(line) { lines.push(line); }, info(line) { lines.push(line); } }; const router = createChatIntentRouter({ @@ -1631,13 +1654,10 @@ test('createChatIntentRouter shadow mode logs llm suggestion without changing ro }, }); - assert.equal(result.route, CHAT_INTENT_ROUTE.AGENT); - assert.equal(result.source, 'fallback'); - assert.equal(result.llmShadow?.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); - assert.equal(result.llmShadow?.wouldChangeRoute, true); - assert.equal(lines.length, 1); - assert.match(lines[0], /\[chat-llm-router-shadow\]/); - assert.match(lines[0], /wouldChangeRoute":true/); + assert.equal(result.route, CHAT_INTENT_ROUTE.DIRECT_CHAT); + assert.equal(result.source, 'rule'); + assert.equal(result.llmShadow, undefined); + assert.equal(lines.length, 0); }); test('logChatLlmRouterShadow emits structured payload', () => { diff --git a/cursor-agent-launch.mjs b/cursor-agent-launch.mjs new file mode 100644 index 0000000..5d34131 --- /dev/null +++ b/cursor-agent-launch.mjs @@ -0,0 +1,109 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +function envFlag(value, fallback = false) { + const raw = String(value ?? '').trim().toLowerCase(); + if (!raw) return fallback; + return ['1', 'true', 'yes', 'on'].includes(raw); +} + +export function expandHome(value) { + const text = String(value ?? '').trim(); + if (!text) return text; + if (text.startsWith('~/')) return path.join(os.homedir(), text.slice(2)); + if (text === '~') return os.homedir(); + return text; +} + +export function cursorExecutorEnabled(env = process.env) { + return envFlag(env.MEMIND_CURSOR_EXECUTOR_ENABLED, false); +} + +export function resolvePreferredCodeExecutor(env = process.env) { + if (!cursorExecutorEnabled(env)) return 'aider'; + if (envFlag(env.MEMIND_AIDER_SKILL_USE_CURSOR, true)) return 'cursor'; + return 'aider'; +} + +export function cursorPageTasksDefaultEnabled(env = process.env) { + if (!cursorExecutorEnabled(env)) return false; + return envFlag(env.MEMIND_CURSOR_PAGE_TASKS_DEFAULT, true); +} + +/** @deprecated 普通 Agent 编排不再默认走 Cursor;仅页面/问卷/Excel 等代码任务使用 enforcePageGenerationCursorRuntime */ +export function cursorAgentTasksDefaultEnabled(env = process.env) { + if (!cursorExecutorEnabled(env)) return false; + return envFlag(env.MEMIND_CURSOR_AGENT_TASKS_DEFAULT, false); +} + +/** 智趣执行失败时回退 DeepSeek / Goose 会话 */ +export function cursorDeepseekFallbackEnabled(env = process.env) { + return envFlag(env.MEMIND_CURSOR_DEEPSEEK_FALLBACK, false); +} + +export function resolveCursorAgentBin(env = process.env) { + const configured = expandHome( + env.MEMIND_CURSOR_EXECUTOR_AGENT_BIN + ?? env.MEMIND_CURSOR_HELP_AGENT_BIN + ?? '~/.local/bin/agent', + ); + if (configured && fs.existsSync(configured)) return configured; + return 'agent'; +} + +export function buildCursorExecutorInstruction(instruction) { + const base = String(instruction ?? '').trim(); + if (!base) return base; + return [ + base, + '', + '执行要求:', + '- 在当前 MindSpace 工作区(cwd)内完成改动', + '- 页面产物写入 public/ 目录下的 .html 文件', + '- 直接完成可交付产物,不要等待用户确认', + '- 不要执行 git commit、git push、git checkout、git reset、生产发布或远端同步命令', + '- 不要修改当前工作区之外的文件', + '- 完成后在最终输出中简述改动文件与验证结果', + ].join('\n'); +} + +export function buildCursorExecutorLaunchPlan({ + cwd, + instruction, + env = process.env, + agentBin = resolveCursorAgentBin(env), +} = {}) { + const workspace = path.resolve(String(cwd ?? env.MEMIND_CURSOR_EXECUTOR_WORKSPACE ?? process.cwd())); + const prompt = buildCursorExecutorInstruction(instruction); + if (!prompt) { + return { + ok: false, + executor: 'cursor', + message: 'TKMind 智趣执行器需要 instruction', + }; + } + return { + ok: true, + executor: 'cursor', + executorLabel: 'TKMind 智趣', + cwd: workspace, + command: agentBin, + args: [ + '--print', + '--trust', + '--force', + '--approve-mcps', + '--output-format', + 'text', + '--workspace', + workspace, + prompt, + ], + env: {}, + notes: [ + 'TKMind 智趣执行器在 MindSpace 工作区 headless 执行。', + 'Goose 仍负责对话编排,智趣只负责代码/页面落盘。', + ], + }; +} diff --git a/cursor-agent-launch.test.mjs b/cursor-agent-launch.test.mjs new file mode 100644 index 0000000..82457a6 --- /dev/null +++ b/cursor-agent-launch.test.mjs @@ -0,0 +1,56 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + buildCursorExecutorLaunchPlan, + cursorExecutorEnabled, + resolvePreferredCodeExecutor, +} from './cursor-agent-launch.mjs'; + +test('cursor executor is disabled unless explicitly enabled', () => { + assert.equal(cursorExecutorEnabled({}), false); + assert.equal(cursorExecutorEnabled({ MEMIND_CURSOR_EXECUTOR_ENABLED: '1' }), true); +}); + +test('resolvePreferredCodeExecutor prefers cursor when enabled', () => { + assert.equal(resolvePreferredCodeExecutor({}), 'aider'); + assert.equal( + resolvePreferredCodeExecutor({ MEMIND_CURSOR_EXECUTOR_ENABLED: '1' }), + 'cursor', + ); + assert.equal( + resolvePreferredCodeExecutor({ + MEMIND_CURSOR_EXECUTOR_ENABLED: '1', + MEMIND_AIDER_SKILL_USE_CURSOR: '0', + }), + 'aider', + ); +}); + +test('buildCursorExecutorLaunchPlan includes workspace and mindspace hints', async () => { + const fs = await import('node:fs/promises'); + const os = await import('node:os'); + const path = await import('node:path'); + const agentStub = path.join(await fs.mkdtemp(path.join(os.tmpdir(), 'cursor-agent-')), 'agent'); + await fs.writeFile(agentStub, '#!/bin/sh\nexit 0\n', { mode: 0o755 }); + + const plan = buildCursorExecutorLaunchPlan({ + cwd: '/tmp/mindspace/user-1', + instruction: '写一首春日诗并生成 public/spring-poem.html', + env: { MEMIND_CURSOR_EXECUTOR_AGENT_BIN: agentStub }, + }); + assert.equal(plan.ok, true); + assert.equal(plan.executor, 'cursor'); + assert.equal(plan.cwd, '/tmp/mindspace/user-1'); + assert.equal(plan.command, agentStub); + assert.ok(plan.args.includes('--workspace')); + assert.ok(plan.args.includes('/tmp/mindspace/user-1')); + assert.match(plan.args.at(-1) ?? '', /public\/.*\.html/); + assert.match(plan.args.at(-1) ?? '', /不要执行 git commit/); + assert.match(plan.args.at(-1) ?? '', /不要修改当前工作区之外的文件/); +}); + +test('buildCursorExecutorLaunchPlan rejects empty instruction', () => { + const plan = buildCursorExecutorLaunchPlan({ cwd: '/tmp/work', instruction: ' ' }); + assert.equal(plan.ok, false); + assert.match(plan.message ?? '', /instruction/i); +}); diff --git a/cursor-page-routing.mjs b/cursor-page-routing.mjs new file mode 100644 index 0000000..aaf8152 --- /dev/null +++ b/cursor-page-routing.mjs @@ -0,0 +1,252 @@ +import { + AIDER_DEVELOPMENT_SKILL_NAME, + EXCEL_ANALYST_SKILL_NAME, + PAGE_DATA_COLLECT_SKILL_NAME, + extractAiderDevelopmentTask, + isExcelAnalysisIntent, + isGenericPageGenerationRequest, + isPageDataDevIntent, + isPageDataIntent, + isPageGenerationIntent, +} from './chat-skills.mjs'; +import { + cursorAgentTasksDefaultEnabled, + cursorPageTasksDefaultEnabled, + resolvePreferredCodeExecutor, +} from './cursor-agent-launch.mjs'; + +const PUBLISH_SKILL_NAME = 'static-page-publish'; + +const PAGE_CURSOR_TASK_TYPE = 'h5_chat_code_task'; + +const CURSOR_TASK_KIND = { + PAGE_GENERATION: 'page_generation', + PAGE_DATA: 'page_data', + EXCEL_ANALYSIS: 'excel_analysis', +}; + +function selectedChatSkill(userMessage) { + const metadata = userMessage?.metadata; + const runMetadata = metadata?.memindRun ?? metadata?.agentRun ?? {}; + return String(runMetadata.selectedChatSkill ?? '').trim(); +} + +export function resolveCursorTaskKind(taskText, skill = '') { + const normalized = String(taskText ?? '').trim(); + if (!normalized || isPageDataDevIntent(normalized)) return null; + + const selectedSkill = String(skill ?? '').trim(); + if ( + selectedSkill === PAGE_DATA_COLLECT_SKILL_NAME + || isPageDataIntent(normalized) + ) { + return CURSOR_TASK_KIND.PAGE_DATA; + } + if ( + selectedSkill === EXCEL_ANALYST_SKILL_NAME + || isExcelAnalysisIntent(normalized) + ) { + return CURSOR_TASK_KIND.EXCEL_ANALYSIS; + } + if ( + selectedSkill === PUBLISH_SKILL_NAME + || isPageGenerationIntent(normalized) + ) { + if (isGenericPageGenerationRequest(normalized)) return null; + return CURSOR_TASK_KIND.PAGE_GENERATION; + } + return null; +} + +export function isPageCursorDefaultCandidate(userMessage, { + rawToolMode = 'chat', + env = process.env, +} = {}) { + if (!cursorPageTasksDefaultEnabled(env)) return false; + if (String(rawToolMode ?? '').trim().toLowerCase() === 'code') return false; + + const skill = selectedChatSkill(userMessage); + if (skill === AIDER_DEVELOPMENT_SKILL_NAME) return false; + + const runMetadata = userMessage?.metadata?.memindRun ?? userMessage?.metadata?.agentRun ?? {}; + if (runMetadata.pageTemplateSkill || runMetadata.pageTemplateStrict) return false; + + const taskText = extractAiderDevelopmentTask(userMessage); + if (!taskText) return false; + + return resolveCursorTaskKind(taskText, skill) !== null; +} + +function buildCursorPageInstruction(taskText) { + return [ + taskText, + '', + '[Memind page task via TKMind 智趣 executor]', + '在当前用户 MindSpace 工作区生成或更新 public/*.html 页面。', + '完成后确保页面可通过 /MindSpace//public/.html 访问。', + '不要只回复文字,必须实际落盘 HTML 文件。', + ].join('\n'); +} + +function buildCursorPageDataInstruction(taskText) { + return [ + taskText, + '', + '[Memind Page Data survey task via TKMind 智趣 executor]', + '在当前用户 MindSpace 工作区完成可提交、可持久化的问卷/数据收集与结果分析页面。', + '必须先阅读 docs/page-data-api-usage.md 与 docs/examples/page-data-survey-test.md。', + '需要:1) 创建/更新 public/*.html 问卷页(含 MindSpacePageData.createClient);', + '2) 按需创建后台分析页;3) 注册 dataset 与 page policy(可参考 scripts/setup-page-data-survey-demo.mjs);', + '4) 数据必须持久化到 PG(通过 Page Data API),不要旁路自建 API。', + '完成后确保页面可通过 /MindSpace//public/.html 访问。', + '不要只回复文字,必须实际落盘 HTML 与相关配置文件。', + ].join('\n'); +} + +function buildCursorExcelInstruction(taskText) { + return [ + taskText, + '', + '[Memind Excel analysis task via TKMind 智趣 executor]', + '在当前用户 MindSpace 工作区分析用户上传的 Excel/CSV 等表格文件。', + '先在工作区 attachments/、uploads/ 或用户消息提及的路径定位附件文件,读取并分析数据。', + '输出分析结论,并生成可访问的结果页面 public/*.html 或分析报告文件。', + '不要只回复文字,必须实际读取文件并产出可交付结果。', + ].join('\n'); +} + +function buildCursorTaskInstruction(taskText, taskKind) { + switch (taskKind) { + case CURSOR_TASK_KIND.PAGE_DATA: + return buildCursorPageDataInstruction(taskText); + case CURSOR_TASK_KIND.EXCEL_ANALYSIS: + return buildCursorExcelInstruction(taskText); + case CURSOR_TASK_KIND.PAGE_GENERATION: + default: + return buildCursorPageInstruction(taskText); + } +} + +function resolveCursorTaskSkill(taskKind, runMetadataSkill = '') { + const existing = String(runMetadataSkill ?? '').trim(); + if (existing && existing !== AIDER_DEVELOPMENT_SKILL_NAME) return existing; + switch (taskKind) { + case CURSOR_TASK_KIND.PAGE_DATA: + return PAGE_DATA_COLLECT_SKILL_NAME; + case CURSOR_TASK_KIND.EXCEL_ANALYSIS: + return EXCEL_ANALYST_SKILL_NAME; + case CURSOR_TASK_KIND.PAGE_GENERATION: + default: + return PUBLISH_SKILL_NAME; + } +} + +function resolveCursorSuggestedDelivery(taskKind) { + switch (taskKind) { + case CURSOR_TASK_KIND.PAGE_DATA: + return 'mindspace_page_data'; + case CURSOR_TASK_KIND.EXCEL_ANALYSIS: + return 'mindspace_analysis_html'; + case CURSOR_TASK_KIND.PAGE_GENERATION: + default: + return 'mindspace_public_html'; + } +} + +function rewriteUserMessageForCursorTask(userMessage, taskText, taskKind) { + const message = userMessage && typeof userMessage === 'object' && !Array.isArray(userMessage) + ? { ...userMessage } + : { value: userMessage }; + const metadata = message.metadata && typeof message.metadata === 'object' && !Array.isArray(message.metadata) + ? { ...message.metadata } + : {}; + const instruction = buildCursorTaskInstruction(taskText, taskKind); + metadata.displayText = taskText; + metadata.userVisible = true; + const content = Array.isArray(message.content) + ? message.content.map((item) => ( + item?.type === 'text' + ? { ...item, text: instruction } + : item + )) + : [{ type: 'text', text: instruction }]; + return { ...message, metadata, content }; +} + +export function enforcePageGenerationCursorRuntime(userMessage, { + rawToolMode = 'chat', + taskType = null, + forceDeepReasoning = false, + env = process.env, +} = {}) { + if (!isPageCursorDefaultCandidate(userMessage, { rawToolMode, env })) { + return { + userMessage, + rawToolMode, + taskType, + forceDeepReasoning, + requiredExecutor: null, + }; + } + + const taskText = extractAiderDevelopmentTask(userMessage); + const skill = selectedChatSkill(userMessage); + const taskKind = resolveCursorTaskKind(taskText, skill); + if (!taskKind) { + return { + userMessage, + rawToolMode, + taskType, + forceDeepReasoning, + requiredExecutor: null, + }; + } + + const codeExecutor = resolvePreferredCodeExecutor(env); + const message = rewriteUserMessageForCursorTask(userMessage, taskText, taskKind); + const metadata = message.metadata && typeof message.metadata === 'object' && !Array.isArray(message.metadata) + ? { ...message.metadata } + : {}; + const runMetadata = metadata.memindRun && typeof metadata.memindRun === 'object' && !Array.isArray(metadata.memindRun) + ? { ...metadata.memindRun } + : {}; + + metadata.memindRun = { + ...runMetadata, + executor: codeExecutor, + pageCursorDefault: true, + cursorTaskKind: taskKind, + suggestedDelivery: resolveCursorSuggestedDelivery(taskKind), + selectedChatSkill: resolveCursorTaskSkill(taskKind, runMetadata.selectedChatSkill), + }; + + return { + userMessage: { ...message, metadata }, + rawToolMode: 'code', + taskType: PAGE_CURSOR_TASK_TYPE, + forceDeepReasoning: true, + requiredExecutor: codeExecutor, + }; +} + +export function applyCursorFirstAgentExecution(userMessage, runOptions = {}, { + routingDecision = null, + env = process.env, +} = {}) { + // Cursor 仅服务于显式页面/代码任务(enforcePageGenerationCursorRuntime)。 + // 普通 Agent 编排走 Goose/DeepSeek,不因 MEMIND_CURSOR_AGENT_TASKS_DEFAULT 全量切 Cursor。 + void userMessage; + void runOptions; + void routingDecision; + void env; + return { userMessage, runOptions, cursorFirst: false }; +} + +export function __cursorPageRoutingTestExports() { + return { + PAGE_CURSOR_TASK_TYPE, + CURSOR_TASK_KIND, + cursorPageTasksDefaultEnabled, + cursorAgentTasksDefaultEnabled, + }; +} diff --git a/cursor-page-routing.test.mjs b/cursor-page-routing.test.mjs new file mode 100644 index 0000000..f2fa44b --- /dev/null +++ b/cursor-page-routing.test.mjs @@ -0,0 +1,155 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + enforcePageGenerationCursorRuntime, + isPageCursorDefaultCandidate, + resolveCursorTaskKind, + applyCursorFirstAgentExecution, +} from './cursor-page-routing.mjs'; + +const enabledEnv = { + MEMIND_CURSOR_EXECUTOR_ENABLED: '1', + MEMIND_CURSOR_PAGE_TASKS_DEFAULT: '1', +}; + +test('page cursor default matches poem page requests', () => { + const userMessage = { + role: 'user', + content: [{ type: 'text', text: '帮我写首诗,并做成页面' }], + metadata: { displayText: '帮我写首诗,并做成页面', userVisible: true }, + }; + assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv }), true); + assert.equal(resolveCursorTaskKind('帮我写首诗,并做成页面'), 'page_generation'); +}); + +test('page cursor default matches survey page-data requests', () => { + const userMessage = { + role: 'user', + content: [{ type: 'text', text: '帮我做一个调查问卷,结果存 PG' }], + metadata: { displayText: '帮我做一个调查问卷,结果存 PG' }, + }; + assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv }), true); + assert.equal(resolveCursorTaskKind('帮我做一个调查问卷,结果存 PG'), 'page_data'); +}); + +test('page cursor default matches excel analysis requests', () => { + const userMessage = { + role: 'user', + content: [{ type: 'text', text: '帮我分析这个 Excel 表格的数据趋势' }], + metadata: { displayText: '帮我分析这个 Excel 表格的数据趋势' }, + }; + assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv }), true); + assert.equal(resolveCursorTaskKind('帮我分析这个 Excel 表格的数据趋势'), 'excel_analysis'); +}); + +test('page cursor default skips generic empty page requests', () => { + const userMessage = { + role: 'user', + content: [{ type: 'text', text: '帮我做一个页面吧' }], + metadata: { displayText: '帮我做一个页面吧' }, + }; + assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv }), false); +}); + +test('page cursor default skips page-data dev repair requests', () => { + const userMessage = { + role: 'user', + content: [{ type: 'text', text: '问卷页面提交失败,帮我排查 Page Data 绑定' }], + metadata: { displayText: '问卷页面提交失败,帮我排查 Page Data 绑定' }, + }; + assert.equal(isPageCursorDefaultCandidate(userMessage, { env: enabledEnv }), false); +}); + +test('enforcePageGenerationCursorRuntime switches page generation to code cursor executor', () => { + const userMessage = { + role: 'user', + content: [{ type: 'text', text: '帮我写首诗,并做成页面' }], + metadata: { displayText: '帮我写首诗,并做成页面' }, + }; + const enforced = enforcePageGenerationCursorRuntime(userMessage, { env: enabledEnv }); + assert.equal(enforced.rawToolMode, 'code'); + assert.equal(enforced.taskType, 'h5_chat_code_task'); + assert.equal(enforced.requiredExecutor, 'cursor'); + assert.equal(enforced.userMessage.metadata.memindRun.executor, 'cursor'); + assert.equal(enforced.userMessage.metadata.memindRun.pageCursorDefault, true); + assert.equal(enforced.userMessage.metadata.memindRun.cursorTaskKind, 'page_generation'); + assert.equal(enforced.userMessage.metadata.memindRun.selectedChatSkill, 'static-page-publish'); + assert.match( + enforced.userMessage.content[0].text, + /MindSpace 工作区生成或更新 public\/\*\.html/, + ); +}); + +test('enforcePageGenerationCursorRuntime switches survey tasks to cursor page-data flow', () => { + const userMessage = { + role: 'user', + content: [{ type: 'text', text: '帮我做一个调查问卷,结果存 PG' }], + metadata: { displayText: '帮我做一个调查问卷,结果存 PG' }, + }; + const enforced = enforcePageGenerationCursorRuntime(userMessage, { env: enabledEnv }); + assert.equal(enforced.rawToolMode, 'code'); + assert.equal(enforced.requiredExecutor, 'cursor'); + assert.equal(enforced.userMessage.metadata.memindRun.cursorTaskKind, 'page_data'); + assert.equal(enforced.userMessage.metadata.memindRun.selectedChatSkill, 'page-data-collect'); + assert.equal(enforced.userMessage.metadata.memindRun.suggestedDelivery, 'mindspace_page_data'); + assert.match(enforced.userMessage.content[0].text, /Page Data survey task via TKMind 智趣 executor/); + assert.match(enforced.userMessage.content[0].text, /page-data-api-usage\.md/); +}); + +test('enforcePageGenerationCursorRuntime switches excel analysis to cursor flow', () => { + const userMessage = { + role: 'user', + content: [{ type: 'text', text: '帮我分析这个 Excel 表格的数据趋势' }], + metadata: { displayText: '帮我分析这个 Excel 表格的数据趋势' }, + }; + const enforced = enforcePageGenerationCursorRuntime(userMessage, { env: enabledEnv }); + assert.equal(enforced.rawToolMode, 'code'); + assert.equal(enforced.requiredExecutor, 'cursor'); + assert.equal(enforced.userMessage.metadata.memindRun.cursorTaskKind, 'excel_analysis'); + assert.equal(enforced.userMessage.metadata.memindRun.selectedChatSkill, 'excel-analyst'); + assert.match(enforced.userMessage.content[0].text, /Excel analysis task via TKMind 智趣 executor/); +}); + +test('page cursor default is disabled without env flag', () => { + const userMessage = { + role: 'user', + content: [{ type: 'text', text: '帮我写首诗,并做成页面' }], + metadata: { displayText: '帮我写首诗,并做成页面' }, + }; + assert.equal(isPageCursorDefaultCandidate(userMessage, { env: {} }), false); +}); + +test('applyCursorFirstAgentExecution no longer forces cursor for generic agent tasks', () => { + const enabledEnv = { + MEMIND_CURSOR_EXECUTOR_ENABLED: '1', + MEMIND_CURSOR_AGENT_TASKS_DEFAULT: '1', + }; + const userMessage = { + role: 'user', + content: [{ type: 'text', text: '帮我查一下今天的热点新闻并总结' }], + metadata: { memindRun: {} }, + }; + const applied = applyCursorFirstAgentExecution(userMessage, { toolMode: 'chat' }, { + routingDecision: 'agent_orchestration', + env: enabledEnv, + }); + assert.equal(applied.cursorFirst, false); + assert.equal(applied.runOptions.toolMode, 'chat'); +}); + +test('applyCursorFirstAgentExecution skips direct chat routes', () => { + const enabledEnv = { + MEMIND_CURSOR_EXECUTOR_ENABLED: '1', + MEMIND_CURSOR_AGENT_TASKS_DEFAULT: '1', + }; + const userMessage = { + role: 'user', + content: [{ type: 'text', text: '你好' }], + metadata: { memindRun: {} }, + }; + const applied = applyCursorFirstAgentExecution(userMessage, { toolMode: 'chat' }, { + routingDecision: 'direct_chat', + env: enabledEnv, + }); + assert.equal(applied.cursorFirst, false); +}); diff --git a/src/components/ChatPanel.tsx b/src/components/ChatPanel.tsx index d6c9284..335f06e 100644 --- a/src/components/ChatPanel.tsx +++ b/src/components/ChatPanel.tsx @@ -1,5 +1,5 @@ import { ChangeEvent, useCallback, useEffect, useLayoutEffect, useRef, useState, type ClipboardEvent, type MutableRefObject } from 'react'; -import { BrainCircuit, Database, Image, ImageOff, ImagePlus } from 'lucide-react'; +import { BrainCircuit } from 'lucide-react'; import { useNetworkStatus } from '../hooks/useNetworkStatus'; import { openAvatarPicker } from '../utils/userAvatar'; import { @@ -11,11 +11,7 @@ import { } from '../utils/chatSkills'; import { getMessageSaveActions } from '../utils/messageSave'; import { getDisplayText } from '../utils/message'; -import { - IMAGE_GENERATION_MODE_LABELS, - nextImageGenerationMode, - type ImageGenerationMode, -} from '../utils/imageGeneration'; +import type { ImageGenerationMode } from '../utils/imageGeneration'; import { downloadChatMessageDocx, } from '../api/client'; @@ -39,6 +35,7 @@ import { ChatSkillPicker } from './ChatSkillPicker'; import { ChatSkillIcon } from './ChatSkillIcons'; import { PageTemplateShopModal } from './PageTemplateShopModal'; import { ChatLoadingSpinner } from './ChatLoadingSpinner'; +import { useTaskLoadingTip } from '../hooks/useTaskLoadingTip'; import { ChatSharePreviewModal } from './ChatSharePreviewModal'; import { ChatPlazaPublishModal } from './ChatPlazaPublishModal'; import { MessageList } from './MessageList'; @@ -183,6 +180,7 @@ export function ChatPanel({ onBalanceUpdate, onGrantedSkillsUpdate, onOpenRecharge, + taskLoadingTip = null, }: { variant: 'full' | 'compact'; user?: PortalUser | null; @@ -231,6 +229,7 @@ export function ChatPanel({ onBalanceUpdate?: (balanceCents: number) => void; onGrantedSkillsUpdate?: (skills: string[]) => void; onOpenRecharge?: () => void; + taskLoadingTip?: string | null; }) { const online = useNetworkStatus(); const [input, setInput] = useState(''); @@ -240,12 +239,8 @@ export function ChatPanel({ const [plazaPublishSource, setPlazaPublishSource] = useState(null); const [voiceNotice, setVoiceNotice] = useState(null); const [forceDeepReasoning, setForceDeepReasoning] = useState(false); - const [pgRequired, setPgRequired] = useState(false); - const [imageGenerationMode, setImageGenerationMode] = useState('auto'); - const [imageGenerationTipVisible, setImageGenerationTipVisible] = useState(false); - const [pgTipVisible, setPgTipVisible] = useState(false); const [deepReasoningTipVisible, setDeepReasoningTipVisible] = useState(false); - const [chatControlOnboardingStep, setChatControlOnboardingStep] = useState<0 | 1 | 2 | null>(null); + const [chatControlOnboardingStep, setChatControlOnboardingStep] = useState<0 | 1 | null>(null); const [templateShopOpen, setTemplateShopOpen] = useState(false); const [activeTemplatePrefill, setActiveTemplatePrefill] = useState<{ skillId: string; @@ -261,8 +256,6 @@ export function ChatPanel({ const [voiceStopSignal, setVoiceStopSignal] = useState(0); const pendingSkillRef = useRef(null); const chatInputRef = useRef(null); - const imageGenerationTipTimerRef = useRef | null>(null); - const pgTipTimerRef = useRef | null>(null); const deepReasoningTipTimerRef = useRef | null>(null); const [randomPrompt] = useState( () => CHAT_PLACEHOLDER_PROMPTS[Math.floor(Math.random() * CHAT_PLACEHOLDER_PROMPTS.length)], @@ -282,6 +275,7 @@ export function ChatPanel({ }>(null); const prevMessageCountRef = useRef(messages.length); const showAssistantTyping = chatState === 'waiting' || chatState === 'streaming'; + const displayLoadingTip = useTaskLoadingTip(showAssistantTyping, taskLoadingTip); inputRef.current = input; pendingImagesRef.current = pendingImages; @@ -337,19 +331,12 @@ export function ChatPanel({ setChatControlOnboardingStep(0); const timers = [ window.setTimeout(() => setChatControlOnboardingStep(1), 2_200), - window.setTimeout(() => setChatControlOnboardingStep(2), 4_400), - window.setTimeout(() => setChatControlOnboardingStep(null), 6_600), + window.setTimeout(() => setChatControlOnboardingStep(null), 4_400), ]; return () => timers.forEach((timer) => window.clearTimeout(timer)); }, [session?.id]); useEffect(() => () => { - if (imageGenerationTipTimerRef.current) { - clearTimeout(imageGenerationTipTimerRef.current); - } - if (pgTipTimerRef.current) { - clearTimeout(pgTipTimerRef.current); - } if (deepReasoningTipTimerRef.current) { clearTimeout(deepReasoningTipTimerRef.current); } @@ -372,26 +359,6 @@ export function ChatPanel({ [], ); - const showImageGenerationSlowTip = useCallback(() => { - showTransientControlTip(setImageGenerationTipVisible, imageGenerationTipTimerRef); - }, [showTransientControlTip]); - - const handlePgToggle = useCallback(() => { - setPgRequired((current) => { - const next = !current; - if (next) { - showTransientControlTip(setPgTipVisible, pgTipTimerRef); - } else { - setPgTipVisible(false); - if (pgTipTimerRef.current) { - clearTimeout(pgTipTimerRef.current); - pgTipTimerRef.current = null; - } - } - return next; - }); - }, [showTransientControlTip]); - const handleDeepReasoningToggle = useCallback(() => { setForceDeepReasoning((current) => { const next = !current; @@ -408,11 +375,6 @@ export function ChatPanel({ }); }, [showTransientControlTip]); - const handleImageGenerationClick = useCallback(() => { - setImageGenerationMode((current) => nextImageGenerationMode(current)); - showImageGenerationSlowTip(); - }, [showImageGenerationSlowTip]); - useLayoutEffect(() => { const container = mainRef.current; if (!container) return; @@ -876,8 +838,8 @@ export function ChatPanel({ await onSubmit(trimmed, imagesToSend, previewImagesToSend, { messageId: outgoingMessageId, forceDeepReasoning, - pgRequired, - imageGenerationMode, + pgRequired: false, + imageGenerationMode: 'auto', selectedChatSkill, fileAttachments: fileAttachmentsToSend, }); @@ -907,8 +869,6 @@ export function ChatPanel({ }, [ activeTemplatePrefill, forceDeepReasoning, - pgRequired, - imageGenerationMode, input, onSubmit, onUploadFile, @@ -1118,6 +1078,7 @@ export function ChatPanel({ - {taskInputStatusText} -