diff --git a/web/client/src/jsonRender/registry.jsx b/web/client/src/jsonRender/registry.jsx index 70fc322..0d70750 100644 --- a/web/client/src/jsonRender/registry.jsx +++ b/web/client/src/jsonRender/registry.jsx @@ -1049,18 +1049,19 @@ const { registry, handlers, executeAction } = defineRegistry(catalog, { //数据展示类 Table: ({ props, bindings }) => { const schema = props.__inPlaceEditSchema || {}; + const isInPlaceEditEnabled = Boolean(props.__inPlaceEditEnabled); const [editableValue, setEditableValue] = useEditableProp( - props.__inPlaceEditValue, - bindings?.__inPlaceEditValue, - props.dataSource || [] + props.dataSource || [], + isInPlaceEditEnabled ? bindings?.__inPlaceEditValue : undefined, + props.dataSource || [] ); const itemFields = schema.item_fields || []; const editableFields = new Map( itemFields.map((field) => [String(field.name), field]) - ); - const isInlineEditing = Boolean( - props.__inPlaceEditEnabled - && bindings?.__inPlaceEditValue + ); + const isInlineEditing = Boolean( + isInPlaceEditEnabled + && bindings?.__inPlaceEditValue && Array.isArray(editableValue) && schema.kind === "list" ); @@ -1109,18 +1110,19 @@ const { registry, handlers, executeAction } = defineRegistry(catalog, { Tree: ({ props, bindings }) => { const schema = props.__inPlaceEditSchema || {}; const childrenKey = schema.children_key || "children"; + const isInPlaceEditEnabled = Boolean(props.__inPlaceEditEnabled); const [editableValue, setEditableValue] = useEditableProp( - props.__inPlaceEditValue, - bindings?.__inPlaceEditValue, - [] + props.treeData || [], + isInPlaceEditEnabled ? bindings?.__inPlaceEditValue : undefined, + props.treeData || [] ); const itemFields = schema.item_fields || []; const titleField = itemFields.find( (field) => field.name === schema.item_title_field - ); - const isInlineEditing = Boolean( - props.__inPlaceEditEnabled - && bindings?.__inPlaceEditValue + ); + const isInlineEditing = Boolean( + isInPlaceEditEnabled + && bindings?.__inPlaceEditValue && Array.isArray(editableValue) && schema.kind === "tree" && titleField diff --git a/web/client/src/layout/index.jsx b/web/client/src/layout/index.jsx index 5035421..9717eeb 100644 --- a/web/client/src/layout/index.jsx +++ b/web/client/src/layout/index.jsx @@ -95,7 +95,7 @@ export default function index(props) { {outerRoutes} }> - } /> + } /> {innerRoutes} diff --git a/web/client/src/sections/superAgent/hooks/useSuperAgentStream.js b/web/client/src/sections/superAgent/hooks/useSuperAgentStream.js index 57c6faa..e7b5617 100644 --- a/web/client/src/sections/superAgent/hooks/useSuperAgentStream.js +++ b/web/client/src/sections/superAgent/hooks/useSuperAgentStream.js @@ -73,6 +73,51 @@ const getInterruptStatus = (item = {}) => { const isTerminalInterrupt = (item = {}) => TERMINAL_INTERRUPT_STATUSES.has(getInterruptStatus(item)); +const getInterruptInteractionKey = (item = {}) => { + const payload = item?.value || item?.payload || item || {}; + const interactionId = String( + payload.interaction_id || + payload.interactionId || + item.interaction_id || + item.interactionId || + "" + ).trim(); + if (!interactionId) return ""; + + const revision = payload.revision ?? item.revision ?? ""; + return `${interactionId}:${String(revision)}`; +}; + +const parseMessageContent = (content) => { + if (content && typeof content === "object") return content; + if (typeof content !== "string") return null; + + try { + return JSON.parse(content); + } catch (_) { + return null; + } +}; + +const collectResolvedInterruptInteractions = (messages = []) => { + const resolvedInteractions = new Set(); + (Array.isArray(messages) ? messages : []).forEach((message) => { + if (String(message?.type || "").toLowerCase() !== "tool") return; + + const result = parseMessageContent(message.content); + const decisionType = String(result?.type || "").toLowerCase(); + const interactionId = String(result?.interaction_id || "").trim(); + const revision = result?.revision ?? ""; + if ( + interactionId && + ["approve", "edit", "reject"].includes(decisionType) + ) { + resolvedInteractions.add(`${interactionId}:${String(revision)}`); + } + }); + return resolvedInteractions; +}; + const isSuperAgentEventRequest = (input) => { const requestUrl = String(input?.url || input || ""); return /\/threads\/[^/]+\/stream\/events(?:\?|$)/.test(requestUrl); @@ -394,6 +439,7 @@ const useSuperAgentStream = ({ const [runtimeInterrupts, setRuntimeInterrupts] = useState([]); const [dismissedInterruptIds, setDismissedInterruptIds] = useState(new Set()); const resolvedInterruptIdsRef = useRef(new Set()); + const resolvedInterruptInteractionsRef = useRef(new Set()); const [currentInterruptId, setCurrentInterruptId] = useState(""); const [hydratedThreadId, setHydratedThreadId] = useState(""); const [transportStateRevision, setTransportStateRevision] = useState(0); @@ -580,6 +626,7 @@ const useSuperAgentStream = ({ setRuntimeInterrupts([]); setDismissedInterruptIds(new Set()); resolvedInterruptIdsRef.current.clear(); + resolvedInterruptInteractionsRef.current.clear(); setCurrentInterruptId(""); setHydratedThreadId(""); }, [activeSessionId]); @@ -671,7 +718,12 @@ const useSuperAgentStream = ({ if (event?.method === "input.requested") { const data = event.params?.data || {}; const interruptId = String(data.interrupt_id || "").trim(); - if (interruptId && !resolvedInterruptIdsRef.current.has(interruptId)) { + const interactionKey = getInterruptInteractionKey(data.payload); + const isResolvedInterrupt = + resolvedInterruptIdsRef.current.has(interruptId) || + (interactionKey && + resolvedInterruptInteractionsRef.current.has(interactionKey)); + if (interruptId && !isResolvedInterrupt) { localRunActiveRef.current = false; setIsLocalRunActive(false); setCurrentInterruptId(interruptId); @@ -763,6 +815,7 @@ const useSuperAgentStream = ({ setIsBusinessRunning(false); setRuntimeInterrupts([]); resolvedInterruptIdsRef.current.clear(); + resolvedInterruptInteractionsRef.current.clear(); // 会话切换只断开当前浏览器订阅,不能发送 stop/cancel 命令。 // stopRun 只由用户点击停止按钮触发,否则后端会把原会话改为 suspended。 @@ -862,6 +915,12 @@ const useSuperAgentStream = ({ }).then((result) => { if (interruptId) { resolvedInterruptIdsRef.current.add(interruptId); + const interactionKey = getInterruptInteractionKey( + response?.value || response?.payload || response + ); + if (interactionKey) { + resolvedInterruptInteractionsRef.current.add(interactionKey); + } setRuntimeInterrupts((currentInterrupts) => currentInterrupts.filter( (item) => String(item?.id || "") !== interruptId @@ -947,12 +1006,25 @@ const useSuperAgentStream = ({ hydratedStateThreadId === currentSessionId ? eventTransport.latestState : null; - const rawStreamMessages = isStreamProjectionReady ? stream.messages : []; - const rawStreamValues = isStreamProjectionReady ? stream.values || {} : {}; - const hydratedMessages = Array.isArray(hydratedState?.values?.messages) - ? hydratedState.values.messages - : []; - // 只有 hydrate 真正拿到消息后,才能解除首次提交的 thread 投影保护。 + const rawStreamMessages = isStreamProjectionReady ? stream.messages : []; + const rawStreamValues = isStreamProjectionReady ? stream.values || {} : {}; + const hydratedMessages = Array.isArray(hydratedState?.values?.messages) + ? hydratedState.values.messages + : []; + const historicalResolvedInteractionKeys = useMemo( + () => + collectResolvedInterruptInteractions([ + ...hydratedMessages, + ...rawStreamMessages, + ]), + [hydratedMessages, rawStreamMessages] + ); + useEffect(() => { + historicalResolvedInteractionKeys.forEach((interactionKey) => { + resolvedInterruptInteractionsRef.current.add(interactionKey); + }); + }, [historicalResolvedInteractionKeys]); + // 只有 hydrate 真正拿到消息后,才能解除首次提交的 thread 投影保护。 // 仅有 task 快照不代表消息已完成落库,否则 task 先到时会让当前消息再次被隐藏。 const hasHydratedSessionContent = hydratedMessages.length > 0; useEffect(() => { @@ -1045,9 +1117,9 @@ const useSuperAgentStream = ({ const isPersistedTaskActive = ACTIVE_TASK_STATUSES.has(persistedTaskStatus); const hasPersistedNextNodes = Array.isArray(hydratedState?.next) && hydratedState.next.length > 0; - const hydratedInterrupts = [ - ...(Array.isArray(hydratedState?.interrupts) - ? hydratedState.interrupts + const hydratedInterrupts = [ + ...(Array.isArray(hydratedState?.interrupts) + ? hydratedState.interrupts : []), ...(Array.isArray(hydratedState?.values?.__interrupt__) ? hydratedState.values.__interrupt__.map((item) => ({ @@ -1056,15 +1128,23 @@ const useSuperAgentStream = ({ value: item?.value ?? item?.payload, namespace: Array.isArray(item?.namespace) ? item.namespace : [], })) - : []), - ]; - const hasPersistedInterrupts = hydratedInterrupts.length > 0; - const persistedInterrupts = - hydratedState - ? hydratedInterrupts - : isStreamProjectionReady - ? stream.interrupts - : []; + : []), + ]; + const pendingHydratedInterrupts = hydratedInterrupts.filter((item) => { + const interruptId = String(item?.id || item?.interruptId || "").trim(); + const interactionKey = getInterruptInteractionKey(item); + return ( + !resolvedInterruptIdsRef.current.has(interruptId) && + !historicalResolvedInteractionKeys.has(interactionKey) + ); + }); + const hasPersistedInterrupts = pendingHydratedInterrupts.length > 0; + const persistedInterrupts = + hydratedState + ? pendingHydratedInterrupts + : isStreamProjectionReady + ? stream.interrupts + : []; const streamInterrupts = [ ...(Array.isArray(persistedInterrupts) ? persistedInterrupts : []), ...runtimeInterrupts, @@ -1130,16 +1210,27 @@ const useSuperAgentStream = ({ }); }; - streamInterrupts.forEach((item) => appendInterrupt(item)); - return [...merged.values()].filter( - (item) => - !dismissedInterruptIds.has(String(item.id || "")) && - !isTerminalInterrupt(item) - ); + streamInterrupts.forEach((item) => appendInterrupt(item)); + return [...merged.values()].filter( + (item) => { + const interruptId = String(item.id || ""); + const interactionKey = getInterruptInteractionKey(item); + const isHistoricalResolved = + historicalResolvedInteractionKeys.has(interactionKey) || + resolvedInterruptInteractionsRef.current.has(interactionKey); + return ( + !dismissedInterruptIds.has(interruptId) && + !resolvedInterruptIdsRef.current.has(interruptId) && + !isHistoricalResolved && + !isTerminalInterrupt(item) + ); + } + ); }, [ dismissedInterruptIds, isInterruptProjectionReady, isStreamProjectionReady, + historicalResolvedInteractionKeys, streamInterrupts, ]); @@ -1168,9 +1259,9 @@ const useSuperAgentStream = ({ isLocalRunActive: effectiveIsLocalRunActive, runPhase: effectiveRunPhase, isPersistedThreadActive, - currentInterruptId: - hydratedState && - hydratedInterrupts.length === 0 && + currentInterruptId: + hydratedState && + pendingHydratedInterrupts.length === 0 && runtimeInterrupts.length === 0 ? "" : currentInterruptId,