From ae89c2b1df04252e98b137ddb1e99c39c4088a8b Mon Sep 17 00:00:00 2001 From: Tobias Garcia Date: Fri, 17 Jul 2026 11:24:41 +0900 Subject: [PATCH 1/2] Fix: cmd branch now invokes via subprocess.Popen(command, shell=True) instead of old list form that corrupted commands with embedded quote argument --- app/data/action/run_shell.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/data/action/run_shell.py b/app/data/action/run_shell.py index bbaa8e62..6ff741ef 100644 --- a/app/data/action/run_shell.py +++ b/app/data/action/run_shell.py @@ -353,10 +353,17 @@ def shell_exec_windows(input_data: dict) -> dict: command, ] else: - # Use /d and /s to ensure quoted commands (e.g., paths with spaces) are handled consistently. - args = ["cmd.exe", "/d", "/s", "/c", command] + # cmd.exe's own /C parser uses bespoke quote-stripping that does not + # understand the backslash-escaped quotes Python's list2cmdline() + # produces when a list is passed to Popen. That mismatch corrupts any + # command containing an embedded quoted argument (e.g. `foo -p "..."`) + # with stray literal quote characters. Invoke via shell=True below + # instead, passing this raw string so Python's own shell path drives + # cmd.exe without the extra escaping layer. + args = command creation_flags = getattr(subprocess, "CREATE_NO_WINDOW", 0) + use_shell = shell_choice == "cmd" # Background mode: start process and return immediately if background: @@ -365,6 +372,7 @@ def shell_exec_windows(input_data: dict) -> dict: bg_flags = creation_flags | subprocess.CREATE_NEW_PROCESS_GROUP process = subprocess.Popen( args, + shell=use_shell, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, @@ -396,6 +404,7 @@ def shell_exec_windows(input_data: dict) -> dict: fg_flags = creation_flags | subprocess.CREATE_NEW_PROCESS_GROUP process = subprocess.Popen( args, + shell=use_shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL, From 15ef3752e1648537d73b290065466a7859622d8f Mon Sep 17 00:00:00 2001 From: Tobias Garcia Date: Fri, 17 Jul 2026 13:59:51 +0900 Subject: [PATCH 2/2] Fix: Chat input persistency and silent BytePlus failure fixes --- agent_core/core/impl/llm/interface.py | 40 ++++++++++++++----- .../frontend/src/components/Chat/Chat.tsx | 19 ++++++--- .../frontend/src/store/selectors/chatInput.ts | 2 + .../src/store/slices/chatInputSlice.ts | 26 ++++++++---- 4 files changed, 65 insertions(+), 22 deletions(-) diff --git a/agent_core/core/impl/llm/interface.py b/agent_core/core/impl/llm/interface.py index 945cb82a..f6757d4f 100644 --- a/agent_core/core/impl/llm/interface.py +++ b/agent_core/core/impl/llm/interface.py @@ -1710,11 +1710,21 @@ def _generate_byteplus_with_session( cached_tokens, ) - return { - "tokens_used": total_tokens or 0, - "content": content or "", - "cached_tokens": cached_tokens or 0, - } + result = {"tokens_used": total_tokens or 0, "cached_tokens": cached_tokens or 0} + if exc_obj: + error_str = f"{type(exc_obj).__name__}: {str(exc_obj)}" + result["error"] = error_str + try: + result["error_info_obj"] = classify_llm_error( + exc_obj, provider=self.provider, model=self.model + ) + except Exception: + pass + result["content"] = "" + logger.error(f"[BYTEPLUS_SESSION_ERROR] {error_str}") + else: + result["content"] = content or "" + return result # ───────────────────── Provider‑specific private helpers ───────────────────── @profile("llm_openai_call", OperationCategory.LLM) @@ -2331,11 +2341,21 @@ def _generate_byteplus_with_prefix_cache( cached_tokens or 0, ) - return { - "tokens_used": total_tokens or 0, - "content": content or "", - "cached_tokens": cached_tokens or 0, - } + result = {"tokens_used": total_tokens or 0, "cached_tokens": cached_tokens or 0} + if exc_obj: + error_str = f"{type(exc_obj).__name__}: {str(exc_obj)}" + result["error"] = error_str + try: + result["error_info_obj"] = classify_llm_error( + exc_obj, provider=self.provider, model=self.model + ) + except Exception: + pass + result["content"] = "" + logger.error(f"[BYTEPLUS_PREFIX_CACHE_ERROR] {error_str}") + else: + result["content"] = content or "" + return result def _parse_responses_api_content(self, result: Dict[str, Any]) -> str: """Parse content from BytePlus Responses API response. diff --git a/app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx b/app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx index 10dfcb3d..22849aee 100644 --- a/app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx +++ b/app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx @@ -8,8 +8,8 @@ import type { SlashCommandAutocompleteHandle } from '../ui' import { useDerivedAgentStatus } from '../../hooks' import { ChatMessageItem } from '../../pages/Chat/ChatMessage' import { useAppDispatch, useAppSelector } from '../../store/hooks' -import { selectPendingPrefill } from '../../store/selectors/chatInput' -import { clearPendingPrefill } from '../../store/slices/chatInputSlice' +import { selectPendingPrefill, selectDraftText } from '../../store/selectors/chatInput' +import { clearPendingPrefill, setDraftText, clearDraftText } from '../../store/slices/chatInputSlice' import styles from './Chat.module.css' // Pending attachment type @@ -132,9 +132,16 @@ export function Chat({ livingUIId, placeholder, emptyMessage }: ChatProps) { return messages.slice().sort((a, b) => a.timestamp - b.timestamp) }, [messages]) - const [input, setInput] = useState('') - const [enhancing, setEnhancing] = useState(false) const dispatch = useAppDispatch() + const draftKey = livingUIId ?? 'main' + const input = useAppSelector(selectDraftText(draftKey)) + const inputValueRef = useRef(input) + inputValueRef.current = input + const setInput = useCallback((value: string | ((prev: string) => string)) => { + const resolved = typeof value === 'function' ? (value as (prev: string) => string)(inputValueRef.current) : value + dispatch(setDraftText({ key: draftKey, text: resolved })) + }, [dispatch, draftKey]) + const [enhancing, setEnhancing] = useState(false) const pendingPrefill = useAppSelector(selectPendingPrefill) const [pendingAttachments, setPendingAttachments] = useState([]) const [attachmentError, setAttachmentError] = useState(null) @@ -290,6 +297,8 @@ export function Chat({ livingUIId, placeholder, emptyMessage }: ChatProps) { // Consume a one-shot prefill payload from the chatInput slice (e.g. when the // user picks a playbook). Replaces the current input so the prompt is ready // to send or edit, then clears the payload so it doesn't re-apply. + // Deliberately overwrites any persisted draftText — an explicit prefill + // action takes precedence. useEffect(() => { if (pendingPrefill === null) return setInput(pendingPrefill) @@ -438,7 +447,7 @@ export function Chat({ livingUIId, placeholder, emptyMessage }: ChatProps) { if (!connected) { showToast('info', 'Reconnecting — your message will send when the connection is restored.') } - setInput('') + dispatch(clearDraftText(draftKey)) setPendingAttachments([]) setAttachmentError(null) clearReplyTarget() diff --git a/app/ui_layer/browser/frontend/src/store/selectors/chatInput.ts b/app/ui_layer/browser/frontend/src/store/selectors/chatInput.ts index d4ee9a57..05f0039a 100644 --- a/app/ui_layer/browser/frontend/src/store/selectors/chatInput.ts +++ b/app/ui_layer/browser/frontend/src/store/selectors/chatInput.ts @@ -1,3 +1,5 @@ import type { RootState } from '../index' export const selectPendingPrefill = (state: RootState) => state.chatInput.pendingPrefill + +export const selectDraftText = (key: string) => (state: RootState) => state.chatInput.drafts[key] ?? '' diff --git a/app/ui_layer/browser/frontend/src/store/slices/chatInputSlice.ts b/app/ui_layer/browser/frontend/src/store/slices/chatInputSlice.ts index a0459987..2d139e5e 100644 --- a/app/ui_layer/browser/frontend/src/store/slices/chatInputSlice.ts +++ b/app/ui_layer/browser/frontend/src/store/slices/chatInputSlice.ts @@ -1,18 +1,24 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -// Cross-component prefill channel for the chat input. +// Cross-component channel for the chat input, plus persisted draft text. // -// The chat input state itself stays local to Chat.tsx — this slice only -// carries a one-shot "pendingPrefill" payload that Chat consumes via -// useEffect and clears immediately. Used by the Playbook modal (and any -// future feature that needs to drop text into the composer from elsewhere -// in the app). +// `pendingPrefill` is a one-shot payload that Chat consumes via useEffect and +// clears immediately. Used by the Playbook modal (and any future feature +// that needs to drop text into the composer from elsewhere in the app). +// +// `drafts` persists each conversation's composer text, keyed by +// `livingUIId ?? 'main'` (Chat.tsx is shared between the main Chat page and +// every Living UI project's chat panel), so it survives the route unmount +// that happens when navigating to another tab (Settings, Tasks & Actions) +// and back. interface ChatInputState { pendingPrefill: string | null + drafts: Record } const initialState: ChatInputState = { pendingPrefill: null, + drafts: {}, } const chatInputSlice = createSlice({ @@ -25,8 +31,14 @@ const chatInputSlice = createSlice({ clearPendingPrefill(state) { state.pendingPrefill = null }, + setDraftText(state, action: PayloadAction<{ key: string; text: string }>) { + state.drafts[action.payload.key] = action.payload.text + }, + clearDraftText(state, action: PayloadAction) { + delete state.drafts[action.payload] + }, }, }) -export const { setPendingPrefill, clearPendingPrefill } = chatInputSlice.actions +export const { setPendingPrefill, clearPendingPrefill, setDraftText, clearDraftText } = chatInputSlice.actions export default chatInputSlice.reducer