From 12db70e1ae814f974fe9de2e262f8797d70e6f6d Mon Sep 17 00:00:00 2001 From: seb Date: Tue, 6 Oct 2026 01:53:19 +0200 Subject: [PATCH] u --- src/automats/chatAutomat.js | 96 ++++++++++-- src/components/chat/ChatFlowList.jsx | 212 ++++++++++++++++++++++----- vite.config.js | 74 +++++++++- 3 files changed, 328 insertions(+), 54 deletions(-) diff --git a/src/automats/chatAutomat.js b/src/automats/chatAutomat.js index a839393..87ccdd1 100644 --- a/src/automats/chatAutomat.js +++ b/src/automats/chatAutomat.js @@ -630,15 +630,31 @@ const actions = { * Adds brain icon with text as chunks stream in. */ applyStreamReasoning(data) { - const text = typeof data?.text === 'string' ? data.text : (typeof data === 'string' ? data : ''); + let text = ''; + if (typeof data === 'string') { + text = data; + } else if (data && typeof data === 'object') { + if (typeof data.plain === 'string') { + text = data.plain; + } else if (data.encrypted) { + text = ''; + } else if (typeof data.text === 'string') { + text = data.text; + } + } if (!text || !text.trim()) return; + const trimmed = text.trim(); + if (trimmed.startsWith('{') && (trimmed.includes('"encrypted"') || trimmed.includes('"plain"'))) { + return; + } + chatAutomat.setState((prev) => { const turns = prev.activeTurns ? [...prev.activeTurns] : []; if (turns.length === 0) { turns.push({ id: `turn-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, handshake: 'green', - reasonings: [{ id: `r-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, text, sealed: false }], + reasonings: [{ id: `r-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, text: trimmed, sealed: false }], tools: [], usage: null, content: '', @@ -648,6 +664,7 @@ const actions = { } const lastIdx = turns.length - 1; const lastTurn = { ...turns[lastIdx] }; + lastTurn.tools = (lastTurn.tools || []).filter((tool) => Boolean(tool.name)); const rList = (lastTurn.reasonings || []).map((r) => ({ ...r })); let unsealedIdx = -1; for (let i = rList.length - 1; i >= 0; i--) { @@ -657,9 +674,9 @@ const actions = { } } if (unsealedIdx >= 0) { - rList[unsealedIdx].text = text; + rList[unsealedIdx].text = trimmed; } else { - rList.push({ id: `r-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, text, sealed: false }); + rList.push({ id: `r-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, text: trimmed, sealed: false }); } lastTurn.reasonings = rList; turns[lastIdx] = lastTurn; @@ -790,6 +807,7 @@ const actions = { lastTurn.reasonings = (lastTurn.reasonings || []) .filter((r) => r.text && r.text.trim()) .map((r) => ({ ...r, sealed: true })); + lastTurn.tools = (lastTurn.tools || []).filter((tool) => Boolean(tool.name)); turns[lastIdx] = lastTurn; return { activeTurns: turns, streamPhase: 'working' }; }); @@ -815,6 +833,7 @@ const actions = { } const lastIdx = turns.length - 1; const lastTurn = { ...turns[lastIdx] }; + lastTurn.tools = (lastTurn.tools || []).filter((tool) => Boolean(tool.name)); const currentText = typeof data.text === 'string' ? data.text : ((lastTurn.content || '') + (data.delta || '')); @@ -874,7 +893,9 @@ const actions = { reasonings: (t.reasonings || []) .filter((r) => r.text && r.text.trim()) .map((r) => ({ ...r, sealed: true })), - tools: (t.tools || []).map((tool) => (tool.state === 'black' ? { ...tool, state: 'green' } : tool)), + tools: (t.tools || []) + .filter((tool) => Boolean(tool.name)) + .map((tool) => (tool.state === 'black' ? { ...tool, state: 'green' } : tool)), })); // Gather full cumulative content from all turns @@ -1039,6 +1060,7 @@ const actions = { const payload = { role: 'tool', tool_call_id, + name: 'ask_user', content: answer, answer, }; @@ -1062,16 +1084,60 @@ const actions = { sealed: false, }; - chatAutomat.setState((prev) => ({ - messages: [...prev.messages, optimisticMsg], - activeTurns: [initialTurn], - conversationState: 'busy', - streamPhase: 'thinking', - streamingText: '', - lastUsage: null, - isSending: true, - sendError: null, - })); + chatAutomat.setState((prev) => { + const turns = prev.activeTurns ? [...prev.activeTurns] : []; + const sealedTurns = turns.map((t) => ({ + ...t, + sealed: true, + reasonings: (t.reasonings || []) + .filter((r) => r.text && r.text.trim()) + .map((r) => ({ ...r, sealed: true })), + tools: (t.tools || []) + .filter((tool) => Boolean(tool.name)) + .map((tool) => (tool.state === 'black' ? { ...tool, state: 'green' } : tool)), + })); + + const hasActiveTurns = sealedTurns.some( + (t) => (t.reasonings && t.reasonings.length > 0) || (t.tools && t.tools.length > 0) || t.content || t.usage + ); + + let foundExisting = false; + const nextMessages = prev.messages.map((m) => { + if (m.role === 'assistant' && !m.turns) { + try { + const p = typeof m.message === 'string' ? JSON.parse(m.message) : m.message; + if (p?.tool_calls?.some((tc) => tc.id === tool_call_id || tc.function?.name === 'ask_user')) { + foundExisting = true; + return { ...m, turns: sealedTurns }; + } + } catch {} + } + return m; + }); + + if (!foundExisting && hasActiveTurns) { + nextMessages.push({ + id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, + conversation_id: selectedConversationId, + role: 'assistant', + message: '', + turns: sealedTurns, + created_at: new Date().toISOString(), + }); + } + nextMessages.push(optimisticMsg); + + return { + messages: nextMessages, + activeTurns: [initialTurn], + conversationState: 'busy', + streamPhase: 'thinking', + streamingText: '', + lastUsage: null, + isSending: true, + sendError: null, + }; + }); try { const res = await modelconversationApi.addMessage(selectedConversationId, payload); diff --git a/src/components/chat/ChatFlowList.jsx b/src/components/chat/ChatFlowList.jsx index f71d82c..c80cc67 100644 --- a/src/components/chat/ChatFlowList.jsx +++ b/src/components/chat/ChatFlowList.jsx @@ -10,6 +10,17 @@ import SearchRoundedIcon from '@mui/icons-material/SearchRounded'; import ComputerRoundedIcon from '@mui/icons-material/ComputerRounded'; import ImageSearchRoundedIcon from '@mui/icons-material/ImageSearchRounded'; import CompareRoundedIcon from '@mui/icons-material/CompareRounded'; +import SupportIcon from '@mui/icons-material/Support'; +import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver'; +import PhonelinkIcon from '@mui/icons-material/Phonelink'; +import SaveAsIcon from '@mui/icons-material/SaveAs'; +import DescriptionIcon from '@mui/icons-material/Description'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import DeleteIcon from '@mui/icons-material/Delete'; +import GoogleIcon from '@mui/icons-material/Google'; +import WebIcon from '@mui/icons-material/Web'; +import EscalatorWarningIcon from '@mui/icons-material/EscalatorWarning'; +import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; import HealingMarkdown from './HealingMarkdown'; import { CHROMA, @@ -23,6 +34,17 @@ const TOOL_ICON_MAP = { javascript_execute: ComputerRoundedIcon, search_images: ImageSearchRoundedIcon, analyze_image: CompareRoundedIcon, + apisuggestionsorissues: SupportIcon, + ask_user: RecordVoiceOverIcon, + dashback_request: PhonelinkIcon, + patch: SaveAsIcon, + read_file: DescriptionIcon, + reset_vfs: DeleteForeverIcon, + rm: DeleteIcon, + web_search: GoogleIcon, + url_get: WebIcon, + sub_agent: EscalatorWarningIcon, + pdf_read: PictureAsPdfIcon, }; function formatMessageTime(isoString) { @@ -173,27 +195,108 @@ function HandshakeIconView({ state }) { ); } +function extractReasoningText(msg, payload) { + if (payload && typeof payload === 'object') { + if (payload.plain !== undefined) { + if (typeof payload.plain === 'string' && payload.plain.trim()) { + return payload.plain.trim(); + } + return ''; + } + if (payload.encrypted) { + return ''; + } + const candidate = payload.text || payload.content || payload.thinking || payload.reasoning; + if (candidate && typeof candidate === 'string') { + const trimmed = candidate.trim(); + if (trimmed.startsWith('{') && (trimmed.includes('"encrypted"') || trimmed.includes('"plain"'))) { + try { + const inner = JSON.parse(trimmed); + if (inner.plain && typeof inner.plain === 'string' && inner.plain.trim()) { + return inner.plain.trim(); + } + } catch {} + return ''; + } + return trimmed; + } + } + + if (typeof msg?.message === 'string') { + const raw = msg.message.trim(); + if (raw.startsWith('{') && (raw.includes('"encrypted"') || raw.includes('"plain"'))) { + try { + const parsed = JSON.parse(raw); + if (parsed.plain && typeof parsed.plain === 'string' && parsed.plain.trim()) { + return parsed.plain.trim(); + } + } catch {} + return ''; + } + if (!raw.startsWith('{')) { + return raw; + } + } + + return ''; +} + +class ReasoningTooltipContent extends PureComponent { + boxRef = createRef(); + + scrollToBottom = () => { + const el = this.boxRef.current; + if (el) { + el.scrollTop = el.scrollHeight; + requestAnimationFrame(() => { + if (this.boxRef.current) { + this.boxRef.current.scrollTop = this.boxRef.current.scrollHeight; + } + }); + } + }; + + componentDidMount() { + this.scrollToBottom(); + } + + componentDidUpdate(prevProps) { + if (this.props.text !== prevProps.text) { + this.scrollToBottom(); + } + } + + render() { + const { text } = this.props; + return ( + + {text} + + ); + } +} + function BrainIconView({ reasoning }) { const tooltipContent = reasoning?.text?.trim() || ''; if (!tooltipContent) return null; + if (tooltipContent.startsWith('{') && (tooltipContent.includes('"encrypted"') || tooltipContent.includes('"plain"'))) { + return null; + } return ( - {tooltipContent} - - } + title={} > { + if (!toolCallId) return null; + if (currentTurn?.tools) { + const t = currentTurn.tools.find((tool) => tool.id === toolCallId); + if (t) return t; + } + for (let i = currentAssistantTurns.length - 1; i >= 0; i--) { + const t = currentAssistantTurns[i].tools?.find((tool) => tool.id === toolCallId); + if (t) return t; + } + return null; + }; + const getOrCreateTurn = (msg) => { const shouldStartNew = !currentTurn || @@ -481,29 +598,39 @@ export function normalizeChatMessages(rawMessages = []) { } if (msg.role === 'reasoning') { - const turn = getOrCreateTurn(msg); - const text = - payload.plain || - payload.text || - payload.content || - payload.thinking || - payload.reasoning || - (typeof msg.message === 'string' ? msg.message : ''); - turn.reasonings.push({ - id: msg.id || `r-${turn.reasonings.length}`, - text, - sealed: true, - }); + const text = extractReasoningText(msg, payload); + if (text) { + const turn = getOrCreateTurn(msg); + turn.reasonings.push({ + id: msg.id || `r-${turn.reasonings.length}`, + text, + sealed: true, + }); + } continue; } if (msg.role === 'tool') { + const existingTool = payload.tool_call_id ? findExistingTool(payload.tool_call_id) : null; + const isAskUser = payload.name === 'ask_user' || existingTool?.name === 'ask_user'; + + if (!isAskUser && existingTool) { + existingTool.result = payload.result !== undefined ? payload.result : (payload.content !== undefined ? payload.content : ''); + if (payload.parameters) { + existingTool.parameters = payload.parameters; + } + if (payload.name && !existingTool.name) { + existingTool.name = payload.name; + } + continue; + } + const turn = getOrCreateTurn(msg); turn.tools.push({ id: payload.tool_call_id || msg.id, state: 'green', - name: payload.name || '', - content: payload.content !== undefined ? payload.content : (payload.parameters || ''), + name: isAskUser ? 'ask_user' : (payload.name || ''), + content: payload.content !== undefined ? payload.content : (payload.answer || payload.parameters || ''), parameters: payload.parameters, result: payload.result, }); @@ -526,13 +653,22 @@ export function normalizeChatMessages(rawMessages = []) { try { args = typeof tc.function?.arguments === 'string' ? JSON.parse(tc.function.arguments) : (tc.function?.arguments || {}); } catch {} - turn.tools.push({ - id: tc.id || `tc-${Date.now()}`, - state: 'green', - name: tc.function?.name || 'tool', - content: JSON.stringify(args, null, 2), - result: null, - }); + + const isAskUser = tc.function?.name === 'ask_user'; + const existing = !isAskUser && tc.id ? findExistingTool(tc.id) : null; + if (existing) { + existing.parameters = tc.function?.arguments || args; + if (tc.function?.name && !existing.name) existing.name = tc.function.name; + } else { + turn.tools.push({ + id: tc.id || `tc-${Date.now()}`, + state: 'green', + name: tc.function?.name || 'tool', + parameters: tc.function?.arguments || args, + content: null, + result: null, + }); + } } } diff --git a/vite.config.js b/vite.config.js index d27e03f..7522c50 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,9 +1,11 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { defineConfig, searchForWorkspaceRoot } from 'vite'; +import { execSync } from 'node:child_process'; +import { defineConfig, searchForWorkspaceRoot, createLogger } from 'vite'; import react from '@vitejs/plugin-react'; import { VitePWA } from 'vite-plugin-pwa'; +import pc from 'picocolors'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const useHttps = process.env.VITE_HTTPS === '1'; @@ -209,7 +211,76 @@ function nginxHeadersPreviewPlugin() { }; } +function getListeningProcess(port) { + try { + const stdout = execSync(`lsof -nP -ti :${port} -sTCP:LISTEN`, { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'ignore'], + }).trim(); + const pid = stdout.split('\n')[0]; + if (!pid) return null; + let cmd = ''; + try { + cmd = execSync(`ps -p ${pid} -o comm=`, { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'ignore'], + }).trim(); + } catch { + // ignore + } + return { pid, cmd }; + } catch { + return null; + } +} + +function createPidLogger() { + const logger = createLogger(); + const originalInfo = logger.info.bind(logger); + logger.info = (msg, options) => { + if (typeof msg === 'string') { + const match = msg.match(/Port (\d+) is in use/); + if (match) { + const port = match[1]; + const owner = getListeningProcess(port); + if (owner) { + const detail = owner.cmd ? ` (PID ${owner.pid}: ${owner.cmd})` : ` (PID ${owner.pid})`; + msg = msg.replace(`Port ${port} is in use`, `Port ${port} is in use${pc.yellow(detail)}`); + } + } + } + originalInfo(msg, options); + }; + return logger; +} + +function pidReporterPlugin() { + const patchPrintUrls = (server) => { + if (!server || server._pidReporterPatched) return; + server._pidReporterPatched = true; + const origPrintUrls = server.printUrls.bind(server); + server.printUrls = () => { + origPrintUrls(); + const pmId = process.env.pm_id ? pc.dim(` (PM2 #${process.env.pm_id})`) : ''; + server.config.logger.info( + ` ${pc.green('➜')} ${pc.bold('PID')}: ${pc.cyan(process.pid)}${pmId}`, + ); + }; + }; + + return { + name: 'pid-reporter', + configureServer(server) { + patchPrintUrls(server); + }, + configurePreviewServer(server) { + patchPrintUrls(server); + }, + }; +} + export default defineConfig(({ command }) => ({ + customLogger: createPidLogger(), resolve: sharedResolve, build: { emptyOutDir: false, @@ -298,6 +369,7 @@ export default defineConfig(({ command }) => ({ }, }, plugins: [ + pidReporterPlugin(), react(), nginxHeadersPreviewPlugin(), ...(command === 'build'