Align desktop plan mode activation with TUI
This commit is contained in:
1 parent
54e8d46603
commit
158aeabfa4
4 files changed
+66
-57
No files matched your search
@@ -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。
|
||||
|
||||
@@ -674,6 +674,6 @@
|
||||
<!-- highlight.js -->
|
||||
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js" crossorigin="anonymous"></script>
|
||||
<script src="phosphor-icons.js?v=2"></script>
|
||||
<script src="app.js?v=209"></script>
|
||||
<script src="app.js?v=210"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 = ""
|
||||
|
||||
+61
-34
@@ -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 = []
|
||||
|
||||
Reference in new issue
Block a user