From 158aeabfa4cf2a7ab72af090c6e30f3403a378ea Mon Sep 17 00:00:00 2001 From: dd3xp Date: Sun, 5 Jul 2026 05:14:52 -0400 Subject: [PATCH] Align desktop plan mode activation with TUI --- frontends/desktop/static/app.js | 14 ----- frontends/desktop/static/index.html | 2 +- frontends/desktop_bridge.py | 12 ++-- frontends/plan_state.py | 95 ++++++++++++++++++----------- 4 files changed, 66 insertions(+), 57 deletions(-) diff --git a/frontends/desktop/static/app.js b/frontends/desktop/static/app.js index 39d3d842..369d3f80 100644 --- a/frontends/desktop/static/app.js +++ b/frontends/desktop/static/app.js @@ -1788,10 +1788,6 @@ const POLL_MSG_LIMIT = 200; const PLAN_LOST_GRACE_MS = 1500; // tuiapp_v2._PLAN_LOST_GRACE_SEC const PLAN_COMPLETE_GRACE_MS = 3000; // tuiapp_v2._PLAN_GRACE_SEC -function isPlanPresetPrompt(text) { - const p = String(text || '').toLowerCase(); - return p.includes('plan_sop') || p.includes('plan 模式') || p.includes('plan mode'); -} let _submitInFlight = false; const runToggle = document.getElementById('run-toggle'); const chatStatus = pageStatusBar(runToggle); @@ -3343,16 +3339,6 @@ async function sendPrompt(text) { const previewFiles = usedFiles.filter(f => !f.isImage).map(f => ({ id: 'f-' + f.sid, name: f.name, path: f.path })); if (previewFiles.length) userMsg.files = previewFiles; sess.messages.push(userMsg); appendMessage(sess, userMsg); - if (isPlanPresetPrompt(text)) { - const pr = rt(sess); - pr.planCollapsed = false; - pr.planShowAll = false; - const sidHint = (sess.bridgeSessionId || sess.id || 'sess').replace(/\//g, '_'); - applyPlanPayload(sess, { - active: true, placeholder: true, done: 0, total: 0, complete: false, - step: '', pathHint: `plan_${sidHint}/plan.md`, items: [], - }); - } sess.lastActiveTs = Date.now(); // 仿 TUI:不再从首条消息自动改名 —— 标题在 newSession 时已设为 agent-N, // 之后只接受用户手动 rename。 diff --git a/frontends/desktop/static/index.html b/frontends/desktop/static/index.html index b6d48093..0833b6d7 100644 --- a/frontends/desktop/static/index.html +++ b/frontends/desktop/static/index.html @@ -674,6 +674,6 @@ - + diff --git a/frontends/desktop_bridge.py b/frontends/desktop_bridge.py index ed178e83..472542ac 100644 --- a/frontends/desktop_bridge.py +++ b/frontends/desktop_bridge.py @@ -113,14 +113,14 @@ def _load_plan_baseline(item: dict, msgs: list) -> int: def _sanitize_desktop_plan_path(session_id: str, plan_path: str) -> str: - """Desktop: drop shared plan_demo paths so sessions do not read the same file.""" + """Keep only real plan-mode paths; never invent a placeholder path on load.""" import plan_state p = (plan_path or "").strip() if not p: return "" - if plan_state.is_session_scoped_plan_path(p, session_id): - return p - return plan_state.default_session_plan_path(session_id) + if plan_state.is_plan_mode_path(p): + return p.lstrip("./\\") + return "" class AgentManager: @@ -606,10 +606,6 @@ class AgentManager: if image_metas: extra["images"] = image_metas user_msg = self.add_message(sess, "user", prompt, **extra) - import plan_state - if plan_state.is_plan_preset_prompt(prompt): - plan_state.bind_plan_session(sess, prompt) - self._persist() sess.status = "running" sess.cancel_requested = False sess.last_error = "" diff --git a/frontends/plan_state.py b/frontends/plan_state.py index bab87ee9..6b75e8fa 100644 --- a/frontends/plan_state.py +++ b/frontends/plan_state.py @@ -200,14 +200,20 @@ def _msg_content(m) -> str: return c if isinstance(c, str) else "" +def _msg_role(m) -> str: + if isinstance(m, dict): return str(m.get("role") or "") + return str(getattr(m, "role", "") or "") + + def plan_path_mention_in_messages(messages, start_idx: int = 0) -> Optional[str]: + """Latest assistant/tool `enter_plan_mode(...)` path; plain chat text is not a signal.""" for m in reversed(_slice(messages, start_idx)): + if _msg_role(m) == "user": + continue text = _msg_content(m) if not text: continue if "enter_plan_mode" in text and (hit := _ENTER_PLAN_RE.search(text)): return hit.group(1).strip().strip("\"'") - if "plan.md" in text and (hits := _PATH_RE.findall(text)): - return hits[-1].strip().strip("\"'") return None @@ -222,6 +228,8 @@ def _resolve_stashed_at(p: str, root: str) -> Optional[str]: def _find_path_at(messages, start_idx: int, root: str) -> Optional[str]: for m in reversed(_slice(messages, start_idx)): + if _msg_role(m) == "user": + continue text = _msg_content(m) if not text or "plan.md" not in text: continue for hit in reversed(_PATH_RE.findall(text)): @@ -229,32 +237,45 @@ def _find_path_at(messages, start_idx: int, root: str) -> Optional[str]: return None -def default_session_plan_path(session_id: str) -> str: - sid = (session_id or "sess").replace("/", "_") - return f"temp/plan_{sid}/plan.md" +def _store_plan_path(path: str, root: str) -> str: + p = (path or "").strip() + if not p: + return "" + if os.path.isabs(p) and root: + for base in (os.path.join(root, "temp"), root): + try: + rel = os.path.relpath(p, base) + except ValueError: + continue + if rel and not rel.startswith(".."): + return rel.replace("\\", "/") + return p.replace("\\", "/").lstrip("./") -def is_session_scoped_plan_path(path: str, session_id: str) -> bool: - """Desktop: only bind/adopt paths under this session's plan_{id}/ tree.""" - if not path: - return False - sid = (session_id or "sess").replace("/", "_") - norm = path.replace("\\", "/").lstrip("./") - return f"plan_{sid}/" in norm or norm.endswith(f"plan_{sid}/plan.md") +def is_plan_mode_path(path: str) -> bool: + norm = (path or "").replace("\\", "/").lstrip("./") + return re.fullmatch(r"(?:temp/)?plan_[A-Za-z0-9_\-]+/plan\.md", norm) is not None -def is_plan_preset_prompt(prompt: str) -> bool: - p = (prompt or "").lower() - return "plan_sop" in p or "plan 模式" in p or "plan mode" in p - - -def bind_plan_session(sess: Any, prompt: str = "", path: Optional[str] = None) -> str: - """Bind plan card to this session only (avoids sharing plan_demo/ across sessions).""" - rel = (path or "").strip() or default_session_plan_path(getattr(sess, "id", "") or "") - sess.plan_path = rel - msgs = list(getattr(sess, "messages", []) or []) - sess.plan_scan_baseline = len(msgs) - return rel +def ensure_agent_plan_mode(sess: Any, root: str) -> None: + agent = getattr(sess, "agent", None) + bound = (getattr(sess, "plan_path", "") or "").strip() + if not agent or not bound or _stashed_plan_path(agent): + return + if not _resolve_stashed_at(bound, root): + return + for src in (getattr(agent, "handler", None), agent): + if not src: + continue + enter = getattr(src, "enter_plan_mode", None) + if callable(enter): + enter(bound) + return + for src in (getattr(agent, "handler", None), agent): + working = getattr(src, "working", None) + if isinstance(working, dict): + working["in_plan_mode"] = bound + return def sync_plan_path_from_text(sess: Any, text: str, root: str) -> None: @@ -267,23 +288,20 @@ def sync_plan_path_from_text(sess: Any, text: str, root: str) -> None: raw = m.group(1).strip().strip("\"'") if not raw: return - sess.plan_path = raw.lstrip("./") + sess.plan_path = _store_plan_path(raw, root) def session_plan_active(sess: Any, agent, messages, start_idx: int, root: str) -> bool: """Desktop: active when this session has a bound plan_path (not global plan_*/ scan).""" bound = (getattr(sess, "plan_path", None) or "").strip() - if not bound: - return False if _stashed_plan_path(agent): return True + if not bound: + return False if _resolve_stashed_at(bound, root): return True if getattr(sess, "status", "") == "running": return True - # Plan preset bound this session — keep placeholder until plan.md appears or path changes - if is_session_scoped_plan_path(bound, getattr(sess, "id", "")): - return True return False @@ -315,15 +333,24 @@ def desktop_plan_payload_from_session(sess: Any, ga_root: str = "") -> dict: partial = getattr(sess, "partial", None) if isinstance(partial, dict) and isinstance(partial.get("content"), str): sync_plan_path_from_text(sess, partial["content"], root) - sid = getattr(sess, "id", "") or "" + live_path = _stashed_plan_path(agent) + if live_path and not (getattr(sess, "plan_path", None) or "").strip(): + sess.plan_path = _store_plan_path(live_path, root) + if not getattr(sess, "plan_scan_baseline", 0): + sess.plan_scan_baseline = len(raw) if not (getattr(sess, "plan_path", None) or "").strip(): mentioned = plan_path_mention_in_messages(raw, base) - if mentioned and is_session_scoped_plan_path(mentioned, sid): - sess.plan_path = mentioned.lstrip("./") + if mentioned: + sess.plan_path = _store_plan_path(mentioned, root) else: - return {"active": False} + found = _find_path_at(raw, base, root) + if found: + sess.plan_path = _store_plan_path(found, root) + else: + return {"active": False} if not session_plan_active(sess, agent, raw, base, root): return {"active": False} + ensure_agent_plan_mode(sess, root) bound = getattr(sess, "plan_path", "") or "" path = _desktop_resolve_plan_file(sess, bound, root, agent, raw, base) items = []