From 7a4d45210926c6e990e2a96be3d8afe509143048 Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 16 Oct 2026 16:36:43 +0200 Subject: [PATCH] u --- .agents/skills/backend/SKILL.md | 1 - src/automats/chatAutomat.js | 168 +------------------- src/automats/draftSync.js | 159 ------------------ src/automats/index.js | 1 - src/automats/sseAutomat.js | 9 -- src/components/chat/ChatAttachmentItems.jsx | 4 +- src/components/chat/ChatDrawer.jsx | 46 +----- src/components/chat/DraftRequestPrompt.jsx | 118 -------------- src/components/chat/index.js | 1 - src/components/manifest/Attachment.jsx | 2 +- src/routes/lieferung/neu.jsx | 2 +- 11 files changed, 13 insertions(+), 498 deletions(-) delete mode 100644 src/automats/draftSync.js delete mode 100644 src/components/chat/DraftRequestPrompt.jsx diff --git a/.agents/skills/backend/SKILL.md b/.agents/skills/backend/SKILL.md index d387ea7..598b9fb 100644 --- a/.agents/skills/backend/SKILL.md +++ b/.agents/skills/backend/SKILL.md @@ -491,7 +491,6 @@ _none_ - `modelconversation`: `{}` - `modelconversation-`: `{}` - `brand`: `{ sender, brandId }` - - `draft`: `{ sender, action, name, content }` ### Section: /health diff --git a/src/automats/chatAutomat.js b/src/automats/chatAutomat.js index 09d61a2..c1db8a0 100644 --- a/src/automats/chatAutomat.js +++ b/src/automats/chatAutomat.js @@ -285,66 +285,16 @@ function closeConversationStream() { } activeStreamConversationId = null; } -/** - * Normalizes tool arguments to detect a virtual draft read (drafts/delivery, drafts/quote, drafts/order). - * @param {object} args - * @returns {{ filename: string, draftKey: string } | null} - */ -export function getDraftInfoFromToolArgs(args) { - if (!args || typeof args !== 'object') return null; - const raw = args.filename || args.path || args.file || args.name || ''; - if (typeof raw !== 'string' || !raw.trim()) return null; - const normalized = raw.trim().replace(/^\/?(api\/)?vfs\//, ''); - if (normalized.startsWith('drafts/')) { - const draftKey = normalized.replace(/^drafts\//, '').replace(/\.json$/, '').toLowerCase(); - return { - filename: normalized, - draftKey, - }; - } - return null; -} - /** * Checks if a tool call or stream tool event represents an interactive tool - * that awaits user intervention (ask_user or virtual draft read). + * that awaits user intervention (ask_user). * @param {object} item * @returns {boolean} */ export function isInteractiveTool(item) { if (!item) return false; const name = item.name || item.function?.name; - if (name === 'ask_user') return true; - if (name === 'read_file') { - let args = {}; - const rawContent = item.content !== undefined ? item.content : (item.arguments || item.parameters || item.function?.arguments); - if (typeof rawContent === 'string') { - try { - args = JSON.parse(rawContent); - } catch { - if (rawContent.includes('drafts/')) { - args = { filename: rawContent }; - } - } - } else if (rawContent && typeof rawContent === 'object') { - args = rawContent; - } - const secondaryRaw = item.arguments || item.function?.arguments; - if ((!args.filename && !args.path && !args.file) && secondaryRaw && secondaryRaw !== rawContent) { - try { - const parsed = typeof secondaryRaw === 'string' ? JSON.parse(secondaryRaw) : secondaryRaw; - if (parsed && typeof parsed === 'object') { - args = { ...args, ...parsed }; - } - } catch { - if (typeof secondaryRaw === 'string' && secondaryRaw.includes('drafts/')) { - args = { filename: secondaryRaw }; - } - } - } - return Boolean(getDraftInfoFromToolArgs(args)); - } - return false; + return name === 'ask_user'; } const actions = { @@ -397,7 +347,7 @@ const actions = { insertExtractionSnippet({ text = '', label = 'parsed invoice', - instruction = "use 'Process Incoming Product and Invoices' SKILL to fill drafts/delivery vfs file", + instruction = "use 'Process Incoming Product and Invoices' SKILL", } = {}) { const trimmed = String(text || '').trim(); if (!trimmed) return; @@ -408,7 +358,7 @@ const actions = { pendingExtraction: { text: trimmed, label: label || 'parsed invoice', - instruction: instruction || "use 'Process Incoming Product and Invoices' SKILL to fill drafts/delivery vfs file", + instruction: instruction || "use 'Process Incoming Product and Invoices' SKILL", }, }); ensureCompanyEventsConnected(); @@ -580,7 +530,7 @@ const actions = { const messages = msgsRes?.messages || []; const updatedConv = convRes?.conversation; const hasPendingInteractive = Boolean( - selectPendingAskUser({ messages }) || selectPendingDraftRead({ messages }) + selectPendingAskUser({ messages }) ); chatAutomat.setState((prev) => ({ @@ -1661,112 +1611,6 @@ export function selectPendingAskUser(state = chatAutomat.state) { } export const selectHasPendingAsk = (state = chatAutomat.state) => - Boolean(selectPendingAskUser(state) || selectPendingDraftRead(state)); - -/** - * Returns pending un-answered `read_file` tool call for a virtual draft (`drafts/*`). - * @param {object} [state] - * @returns {{ toolCallId: string, filename: string, draftKey: string } | null} - */ -export function selectPendingDraftRead(state = chatAutomat.state) { - const { messages, streamingText, activeTurns } = state; - - // 1. Check live activeTurns for any read_file draft tool call - if (activeTurns && activeTurns.length > 0) { - for (let i = activeTurns.length - 1; i >= 0; i--) { - const turn = activeTurns[i]; - const tools = turn.tools || []; - for (let j = tools.length - 1; j >= 0; j--) { - const tool = tools[j]; - if (tool.name === 'read_file') { - let args = {}; - try { - if (typeof tool.content === 'string') { - args = JSON.parse(tool.content); - } else if (tool.content && typeof tool.content === 'object') { - args = tool.content; - } - } catch {} - if (!args.filename && !args.path && tool.arguments) { - try { - args = typeof tool.arguments === 'string' ? JSON.parse(tool.arguments) : tool.arguments; - } catch {} - } - const draftInfo = getDraftInfoFromToolArgs(args); - if (draftInfo) { - return { - toolCallId: tool.id, - filename: draftInfo.filename, - draftKey: draftInfo.draftKey, - }; - } - } - } - } - } - - // 2. Check message history for any un-answered read_file for drafts/* - if (streamingText || !messages || messages.length === 0) return null; - - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i]; - if (msg.role === 'system' || msg.role === 'usage' || msg.role === 'reasoning') continue; - if (msg.role === 'user') return null; - - if (msg.role === 'tool') { - return null; - } - - if (msg.role === 'assistant' || msg.role === 'model') { - let payload = null; - if (typeof msg.message === 'string') { - try { - payload = JSON.parse(msg.message); - } catch {} - } else if (typeof msg.message === 'object' && msg.message !== null) { - payload = msg.message; - } - if (!payload?.tool_calls || !Array.isArray(payload.tool_calls) || payload.tool_calls.length === 0) { - return null; - } - - const draftCalls = payload.tool_calls.filter((tc) => tc.function?.name === 'read_file'); - for (const tc of draftCalls) { - let args = {}; - try { - if (typeof tc.function?.arguments === 'string') { - args = JSON.parse(tc.function.arguments); - } else if (tc.function?.arguments) { - args = tc.function.arguments; - } - } catch {} - const draftInfo = getDraftInfoFromToolArgs(args); - if (draftInfo) { - const answered = messages.slice(i + 1).some((m) => { - if (m.role !== 'tool') return false; - try { - const p = typeof m.message === 'string' ? JSON.parse(m.message) : m.message; - return p?.tool_call_id === tc.id; - } catch { - return false; - } - }); - if (!answered) { - return { - toolCallId: tc.id, - filename: draftInfo.filename, - draftKey: draftInfo.draftKey, - }; - } - } - } - return null; - } - return null; - } - return null; -} - -export const selectHasPendingDraftRead = (state = chatAutomat.state) => Boolean(selectPendingDraftRead(state)); + Boolean(selectPendingAskUser(state)); export default chatAutomat; diff --git a/src/automats/draftSync.js b/src/automats/draftSync.js deleted file mode 100644 index ff21662..0000000 --- a/src/automats/draftSync.js +++ /dev/null @@ -1,159 +0,0 @@ -/** - * @file draftSync.js - * @description Virtual Drafts synchronizer for VFS drafts (drafts/quote, drafts/order, drafts/delivery). - * Connects frontend draft automats (quoteDraftAutomat, orderDraftAutomat, deliveryEditorAutomat) - * with backend VFS requests and broadcasts. - * - * Incoming Signals: - * - SSE 'draft': Backend broadcasts an external update to a draft. - */ - -import { deliveryEditorAutomat, createEmptyLineitem } from './deliveryEditorAutomat'; -import { quoteDraftAutomat, orderDraftAutomat } from './manifestDraftAutomat'; -import { getStoredClientId } from '../api/http'; -import { getSseClientId } from './sseAutomat'; - -/** - * Normalizes a draft filename/path to its canonical key ('quote', 'order', 'delivery'). - * Handles paths like 'drafts/quote', 'drafts/quote.json', '/api/vfs/drafts/order', 'quote', etc. - * - * @param {string} [name] - * @returns {string} Normalized key - */ -export function normalizeDraftName(name) { - if (!name) return ''; - return String(name) - .trim() - .replace(/^\/?(api\/)?vfs\//, '') - .replace(/^drafts\//, '') - .replace(/\.json$/, '') - .toLowerCase(); -} - -/** - * Supported virtual draft names. - */ -export const SUPPORTED_DRAFTS = ['quote', 'order', 'delivery']; - -/** - * Timestamps of last modifications per draft. - * @type {Map} - */ -const draftTimestamps = new Map(); - -/** - * Resolves active draft content for a given draft name from its managing automat. - * - * @param {string} rawName - * @returns {object|null} - */ -export function getActiveDraftContent(rawName) { - const name = normalizeDraftName(rawName); - if (name === 'quote') { - return quoteDraftAutomat.actions.getManifest(); - } - if (name === 'order') { - return orderDraftAutomat.actions.getManifest(); - } - if (name === 'delivery') { - return deliveryEditorAutomat.actions.getManifest(); - } - return null; -} - -/** - * Generates the list of virtual draft files for the VFS listing. - * - * @returns {Array<{ name: string, size: number, updated_at: string }>} - */ -export function getActiveDraftsList() { - const now = new Date().toISOString(); - return SUPPORTED_DRAFTS.map((name) => { - const content = getActiveDraftContent(name); - let size = 0; - if (content != null) { - try { - size = JSON.stringify(content).length; - } catch {} - } - const updatedAt = draftTimestamps.get(name) || (size > 0 ? now : '2026-10-14T00:00:00.000Z'); - return { - name, - size, - updated_at: updatedAt, - }; - }); -} - -/** - * Applies incoming draft content update to the managing automat. - * - * @param {string} rawName - * @param {object|string} rawContent - */ -export function applyDraftUpdate(rawName, rawContent) { - const name = normalizeDraftName(rawName); - if (!name) return; - - let content = rawContent; - if (typeof content === 'string') { - try { - content = JSON.parse(content); - } catch (err) { - console.error(`[draft] failed to parse incoming draft string for ${name}:`, err); - return; - } - } - - draftTimestamps.set(name, new Date().toISOString()); - - try { - if (name === 'quote') { - quoteDraftAutomat.actions.setManifest(content); - } else if (name === 'order') { - orderDraftAutomat.actions.setManifest(content); - } else if (name === 'delivery') { - const manifest = content?.manifest || content || {}; - const rawLines = manifest.lines ?? manifest.lineitems ?? manifest.items ?? []; - const lines = Array.isArray(rawLines) - ? rawLines.map((l) => (l && l.key ? l : createEmptyLineitem(l))) - : []; - const partyId = manifest.partyId ?? manifest.deliveryEntity ?? manifest.supplierId ?? null; - const externalNumber = manifest.externalNumber ?? manifest.external_number ?? null; - - deliveryEditorAutomat.actions.setDelivery({ - ...manifest, - partyId, - deliveryEntity: partyId, - externalNumber, - external_number: externalNumber, - lines, - }); - } - } catch (err) { - console.error(`[draft] error applying draft update to ${name} automat:`, err); - } -} - - - -/** - * Handles incoming 'draft' SSE broadcast from backend. - * - * @param {{ sender?: string, action: string, name?: string, target?: string, content?: any }} payload - */ -export function handleDraftEvent(payload) { - if (!payload) return; - const { sender, action, name, target, content } = payload; - - const myClientId = getStoredClientId() || (typeof getSseClientId === 'function' ? getSseClientId() : null); - if (sender && myClientId && sender === myClientId) { - // Ignore self-echo - return; - } - - if (action === 'update') { - const draftName = normalizeDraftName(name || target); - applyDraftUpdate(draftName, content); - } -} diff --git a/src/automats/index.js b/src/automats/index.js index e68e1cc..5bf03f0 100644 --- a/src/automats/index.js +++ b/src/automats/index.js @@ -6,7 +6,6 @@ export * from './deliveryEditorAutomat'; export * from './manifestDraftAutomat'; -export * from './draftSync'; export { default as authAutomat } from './authAutomat'; export * from './authAutomat'; diff --git a/src/automats/sseAutomat.js b/src/automats/sseAutomat.js index 60163de..2f511ff 100644 --- a/src/automats/sseAutomat.js +++ b/src/automats/sseAutomat.js @@ -35,7 +35,6 @@ import supplierAutomat from './supplierAutomat'; import orderAutomat from './orderAutomat'; import quoteAutomat from './quoteAutomat'; import shipmentAutomat from './shipmentAutomat'; -import { handleDraftEvent } from './draftSync'; /** @@ -267,14 +266,6 @@ function connect() { - addTrackedListener(es, 'draft', (event) => { - try { - const payload = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; - handleDraftEvent(payload); - } catch (err) { - console.error('[sse] draft event handling failed', err); - } - }); addTrackedListener(es, 'open', () => { sseAutomat.setState({ status: 'connected' }); diff --git a/src/components/chat/ChatAttachmentItems.jsx b/src/components/chat/ChatAttachmentItems.jsx index 327cf2a..4490c0e 100644 --- a/src/components/chat/ChatAttachmentItems.jsx +++ b/src/components/chat/ChatAttachmentItems.jsx @@ -238,8 +238,8 @@ export function parseExtractionsFromText(rawText) { } // 2. Legacy / fallback: message contains "use 'Process Incoming Product and Invoices' SKILL" + code block - const instructionPhrase = "use 'Process Incoming Product and Invoices' SKILL to fill drafts/delivery vfs file"; - if (cleanText.includes(instructionPhrase)) { + const hasInvoiceSkill = cleanText.includes("Process Incoming Product and Invoices"); + if (hasInvoiceSkill) { const codeBlockRegex = /```[a-z]*\s*\n([\s\S]*?)```/g; let cbMatch; while ((cbMatch = codeBlockRegex.exec(cleanText)) !== null) { diff --git a/src/components/chat/ChatDrawer.jsx b/src/components/chat/ChatDrawer.jsx index 0791152..6696d3c 100644 --- a/src/components/chat/ChatDrawer.jsx +++ b/src/components/chat/ChatDrawer.jsx @@ -39,15 +39,12 @@ import CycloneRoundedIcon from '@mui/icons-material/CycloneRounded'; import { setting as settingApi } from '../../api'; import chatAutomat, { selectPendingAskUser, - selectPendingDraftRead, } from '../../automats/chatAutomat'; -import { getActiveDraftContent } from '../../automats/draftSync'; import userSettingsAutomat from '../../automats/userSettingsAutomat'; import vfsAutomat from '../../automats/vfsAutomat'; import { VfsEditorPanel, VfsFileIcon } from '../vfs'; import ChatFlowList from './ChatFlowList'; import AskUserPrompt from './AskUserPrompt'; -import DraftRequestPrompt from './DraftRequestPrompt'; import { ACCEPT_STRING, readFileAttachment } from './chatAttachments'; import { ChatStagedAttachmentsTray } from './ChatAttachmentItems'; import { @@ -57,7 +54,7 @@ import { } from '../../colors'; const DEFAULT_DRAWER_WIDTH = 460; -const EXTRACTION_MODEL_INSTRUCTION = "use 'Process Incoming Product and Invoices' SKILL to fill drafts/delivery vfs file"; +const EXTRACTION_MODEL_INSTRUCTION = "use 'Process Incoming Product and Invoices' SKILL"; /** Sort by updated_at desc; subagents (started_by set) directly below their parent. */ function sortConversations(list) { @@ -103,7 +100,7 @@ function conversationTitle(c) { .replace(/\[EXTRACTION(?:\s+label="[^"]*")?\][\s\S]*?\[\/EXTRACTION\]/gi, '') .replace(/[\s\S]*?<\/extraction>/gi, '') .replace(/```[\s\S]*?```/g, '') - .replace(/use 'Process Incoming Product and Invoices' SKILL to fill drafts\/delivery vfs file/gi, '') + .replace(/use 'Process Incoming Product and Invoices' SKILL.*/gi, '') .replace(/[\s\S]*?<\/USERENV>/gi, '') .replace(/\s+/g, ' ') .trim(); @@ -841,33 +838,7 @@ export default class ChatDrawer extends PureComponent { }; getPendingAskUser = () => selectPendingAskUser(this.state); - getPendingDraftRead = () => selectPendingDraftRead(this.state); - handleProvideDraft = async ({ toolCallId, draftKey, empty = false }) => { - try { - let content; - if (empty) { - content = { error: 'Draft not open or empty' }; - } else { - const manifest = getActiveDraftContent(draftKey); - content = { - manifest: manifest || { - kind: draftKey, - mode: 'create', - lines: [], - }, - }; - } - - await chatAutomat.actions.sendToolResponse({ - tool_call_id: toolCallId, - name: 'read_file', - content, - }); - } catch (err) { - console.error('[ChatDrawer] Failed to provide draft response:', err); - } - }; renderHeader() { const { @@ -1364,7 +1335,6 @@ export default class ChatDrawer extends PureComponent { } const pendingAsk = this.getPendingAskUser(); - const pendingDraftRead = this.getPendingDraftRead(); return ( @@ -1399,14 +1369,6 @@ export default class ChatDrawer extends PureComponent { disabled={this.state.isSending} /> )} - - {pendingDraftRead && ( - - )} ); } @@ -1429,13 +1391,11 @@ export default class ChatDrawer extends PureComponent { } = this.state; const pendingAsk = this.getPendingAskUser(); - const pendingDraftRead = this.getPendingDraftRead(); const isBusy = conversationState === 'busy'; const isWorking = isBusy || isSending; const canChat = (Boolean(selectedConversationId) || draftConversation) && - !pendingAsk && - !pendingDraftRead; + !pendingAsk; const canSend = !isWorking && canChat && diff --git a/src/components/chat/DraftRequestPrompt.jsx b/src/components/chat/DraftRequestPrompt.jsx deleted file mode 100644 index 7447b35..0000000 --- a/src/components/chat/DraftRequestPrompt.jsx +++ /dev/null @@ -1,118 +0,0 @@ -import React from 'react'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import Button from '@mui/material/Button'; -import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined'; -import SendRoundedIcon from '@mui/icons-material/SendRounded'; -import BlockRoundedIcon from '@mui/icons-material/BlockRounded'; -import { getActiveDraftContent } from '../../automats/draftSync'; -import { - ASK_PROMPT_BG_PAIR, - ASK_PROMPT_BORDER_PAIR, - byMode, -} from '../../colors'; - -export default function DraftRequestPrompt({ draftData, onProvideDraft, disabled = false }) { - const { toolCallId, filename, draftKey } = draftData; - const manifest = getActiveDraftContent(draftKey); - const rawLines = manifest?.lines ?? manifest?.lineitems ?? []; - const linesCount = Array.isArray(rawLines) ? rawLines.length : 0; - const hasContent = Boolean(manifest && (linesCount > 0 || manifest.partyId || manifest.supplierId)); - - const handleSendDraft = () => { - if (disabled) return; - onProvideDraft({ toolCallId, draftKey, empty: false }); - }; - - const handleSendEmpty = () => { - if (disabled) return; - onProvideDraft({ toolCallId, draftKey, empty: true }); - }; - - const draftLabel = - draftKey === 'delivery' - ? 'Lieferung' - : draftKey === 'quote' - ? 'Angebot' - : draftKey === 'order' - ? 'Bestellung' - : draftKey; - - return ( - ({ - mx: 2, - my: 1, - p: 2, - borderRadius: 3, - bgcolor: byMode(theme, ASK_PROMPT_BG_PAIR), - border: `1px solid ${byMode(theme, ASK_PROMPT_BORDER_PAIR)}`, - display: 'flex', - flexDirection: 'column', - gap: 1.5, - })} - > - - - - Entwurfsanfrage: {filename || `drafts/${draftKey}`} - - - - - {hasContent ? ( - <> - Das Modell möchte den aktuellen {draftLabel}-Entwurf ({linesCount} Position{linesCount === 1 ? '' : 'en'}) einlesen, um die Bearbeitung fortzusetzen. - - ) : ( - <> - Das Modell fordert {filename} an. Aktuell ist kein {draftLabel}-Entwurf geöffnet. - - )} - - - - - - - - - ); -} diff --git a/src/components/chat/index.js b/src/components/chat/index.js index 5f7336f..91d239f 100644 --- a/src/components/chat/index.js +++ b/src/components/chat/index.js @@ -2,4 +2,3 @@ export { default as ChatFab } from './ChatFab'; export { default as ChatDrawer } from './ChatDrawer'; export { default as ChatFlowList } from './ChatFlowList'; export { default as NewConversationDialog } from './NewConversationDialog'; -export { default as DraftRequestPrompt } from './DraftRequestPrompt'; diff --git a/src/components/manifest/Attachment.jsx b/src/components/manifest/Attachment.jsx index 180ef7b..3fd7fb1 100644 --- a/src/components/manifest/Attachment.jsx +++ b/src/components/manifest/Attachment.jsx @@ -114,7 +114,7 @@ export class Attachment extends PureComponent { chatAutomat.actions.insertExtractionSnippet({ text: trimmed, label: 'parsed invoice', - instruction: "use 'Process Incoming Product and Invoices' SKILL to fill drafts/delivery vfs file", + instruction: "use 'Process Incoming Product and Invoices' SKILL", }); }; diff --git a/src/routes/lieferung/neu.jsx b/src/routes/lieferung/neu.jsx index a7adc94..bda6313 100644 --- a/src/routes/lieferung/neu.jsx +++ b/src/routes/lieferung/neu.jsx @@ -718,7 +718,7 @@ class LieferungNeuPage extends PureComponent { chatAutomat.actions.insertExtractionSnippet({ text: trimmed, label: 'parsed invoice', - instruction: "use 'Process Incoming Product and Invoices' SKILL to fill drafts/delivery vfs file", + instruction: "use 'Process Incoming Product and Invoices' SKILL", }); }