diff --git a/emrg/__main__.py b/emrg/__main__.py index 3882670a..90fd5b23 100644 --- a/emrg/__main__.py +++ b/emrg/__main__.py @@ -49,7 +49,7 @@ def _build_parser() -> argparse.ArgumentParser: parser.add_argument( "--init-auto-evolve", action="store_true", - help="Enable auto-evolution for the current project on connect.", + help="Enable auto-evolution for the current agent on connect.", ) sub = parser.add_subparsers(dest="command", metavar="[command]") @@ -99,7 +99,7 @@ def _build_parser() -> argparse.ArgumentParser: description="Update emrg by pulling latest source and reinstalling.", ) - # emrg rant [--project ] + # emrg rant [--agent ] rant_parser = sub.add_parser( "rant", help="Send feedback/complaint to EMRG for evolution analysis", @@ -107,8 +107,8 @@ def _build_parser() -> argparse.ArgumentParser: "evolution system will use to discover improvement opportunities.", ) rant_parser.add_argument( - "-p", "--project", - help="Target project (from agents.yml). Omit for emrg itself.", + "-p", "--agent", + help="Target agent (from agents.yml). Omit for emrg itself.", default=None, ) rant_parser.add_argument( @@ -160,7 +160,7 @@ def main() -> None: else: _run_daemon() elif parsed.command == "rant": - _send_rant(" ".join(parsed.message), project=parsed.project) + _send_rant(" ".join(parsed.message), agent=parsed.agent) elif parsed.command == "stop": # Windows installer pre-stop depends on the non-zero exit code when # residual processes remain (emrg/_stop_all.py owns the full logic). @@ -417,13 +417,13 @@ def _run_daemon() -> None: # ── Rant ────────────────────────────────────────────────────── -def _send_rant(message: str, project: str | None = None) -> None: +def _send_rant(message: str, agent: str | None = None) -> None: """Send a rant/feedback message to the daemon for evolution analysis.""" - # Parse @project from message if -p/--project not explicitly set - # (matching TUI /rant @project behavior — app.py:1365-1369) - if project is None and message.startswith("@"): + # Parse @agent from message if -p/--agent not explicitly set + # (matching TUI /rant @agent behavior — app.py:1365-1369) + if agent is None and message.startswith("@"): parts = message.split(None, 1) - project = parts[0][1:] # strip @ + agent = parts[0][1:] # strip @ message = parts[1] if len(parts) > 1 else "" async def _do() -> None: @@ -444,8 +444,8 @@ async def _do() -> None: "message": message, "timestamp": datetime.now().isoformat(), } - if project: - payload["project"] = project + if agent: + payload["agent"] = agent await ws.send(json.dumps(payload, ensure_ascii=False)) diff --git a/emrg/client/app.py b/emrg/client/app.py index 9044f856..5d34ca45 100644 --- a/emrg/client/app.py +++ b/emrg/client/app.py @@ -17,7 +17,7 @@ from emrg.client.python_tui.pending_rows import ParkedUserRows from emrg.client.python_tui.widgets.markdown import StreamingMarkdown from emrg.client.widgets import ( - InputWidget, RewindSelector, SessionSelector, ProjectSelector, + InputWidget, RewindSelector, SessionSelector, AgentSelector, TaskSelector, ModelSelector, SandboxSelector, CommandDropdown, ChatHistory, SelectorState, _COMMAND_HELP, ToolSelector, @@ -171,7 +171,7 @@ def _auto_title_from_prompt(text: str, max_len: int = 30) -> str | None: def _task_session_target(task: dict | None) -> tuple[str, str] | None: """The ``(cwd, session_id)`` that opening a task's session needs. - ``list_tasks`` rows already carry both (`handler.status()` → `project_path`, + ``list_tasks`` rows already carry both (`handler.status()` → `agent_path`, `session_id`), so /task-session needs no server change (rant 2026-09-17T18:36:08). Returns ``None`` when they cannot be used — a task that has never run, or a row without them — and the caller then reports that @@ -181,11 +181,11 @@ def _task_session_target(task: dict | None) -> tuple[str, str] | None: Module-level and pure so it is unit-testable without a terminal: the branch it feeds is otherwise only reachable by driving the TUI. Missing, empty and non-string fields all count as unusable — `os.getcwd`-style falsiness is not - an answer to "where is this project". + an answer to "where is this agent". """ if not isinstance(task, dict): return None - cwd = task.get("project_path") + cwd = task.get("agent_path") sid = task.get("session_id") if not isinstance(cwd, str) or not cwd.strip(): return None @@ -202,7 +202,7 @@ def _task_open_switch(pending: tuple[str, str] | None, resumed_sid: str) -> str answer is about a *different* session than the one the open asked for. The second is not hypothetical — an open whose verdict never arrived (the connection dropped) leaves its target behind, and a later `/resume ` - succeeding must not then drag the host into a project they never picked, + succeeding must not then drag the host into an agent they never picked, which is the defect a cwd is the wrong place to discover. """ if pending is None or pending[1] != resumed_sid: @@ -217,7 +217,7 @@ def _resume_target_from_resolution( Rant 2026-09-29T15:52:49 (requirement 6, the client half). The client used to send its **own** cwd with every ``resume_session``, which is only ever right - by accident: a session belonging to another project — a scheduled task's + by accident: a session belonging to another agent — a scheduled task's cycle above all — lives under a different ``.emrg/sessions/``, and the daemon's broadcast filter is keyed by ``(session, cwd)``, so the wrong cwd both fails to open it and, where it opens, leaves the live frames addressed @@ -227,12 +227,12 @@ def _resume_target_from_resolution( names a different session than the one asked for. Same discipline as ``_task_open_switch`` at the other end of the same round trip, and for the same reason — a verdict that arrives after its request was abandoned must not - move the host into a project they never picked. + move the host into an agent they never picked. An ``unknown`` answer is **not** the same reading as an answer that carries a - cwd. The daemon cannot vouch for the session's project when the global index + cwd. The daemon cannot vouch for the session's agent when the global index does not know it, so the client keeps the cwd it has (correct for a session in - the current project) rather than treating a guess as a fact. + the current agent) rather than treating a guess as a fact. """ if not pending_sid or result.get("session_id") != pending_sid: return None @@ -282,7 +282,7 @@ def _cards_from_tool_calls(tool_calls) -> list[ToolCard]: status="pending", tool_call_id=str(call.get("id") or ""), arguments=args, - # The daemon projects `call_label` into every history entry it returns, + # The daemon agents `call_label` into every history entry it returns, # so a resumed session shows the same label the live one did — including # a session recorded before the field existed (rant 2026-10-20T12:22:21). call_label=str(call.get("call_label") or ""), @@ -384,7 +384,7 @@ def _replay_rows(messages) -> list[tuple[str, object, str]]: contract: `kind: "message"` for the user, assistant and system roles, `kind: "tool_result"` for tool output, and `kind: "summary"` for a compaction boundary. A record outside that set is skipped rather than rendered as a blank - row — a role the file on disk may hold and the records mode does not project is + row — a role the file on disk may hold and the records mode does not agent is invented nowhere here, because inventing it would put the TUI and the GUI back on two different content paths, which is the defect that requirement removes. @@ -1124,7 +1124,7 @@ async def interactive(init_auto_evolve: bool = False, console=None): # Session setup cwd = os.getcwd() session_id = generate_session_id(Path(cwd)) - project_name = Path(cwd).name + agent_name = Path(cwd).name # Send init_auto_evolve if requested (before ping, so daemon # processes it before any user interaction starts) @@ -1210,7 +1210,7 @@ def _narrate_the_stop() -> None: status.elapsed = "" # Rant 2026-09-02T10:31:11:与 done 路径(:617)一致的标题复位——中断 # 漏清 term.set_title 会让标题残留 [m:ss] 计时停住。 - term.set_title(f"{session_title or session_id} @ {project_name}") + term.set_title(f"{session_title or session_id} @ {agent_name}") chat.add("system", CANCELLED_LINE) _last_center = server_id or "emrg" status.update(center=_last_center) @@ -1272,7 +1272,7 @@ async def _run_elapsed_timer() -> None: mins, secs = divmod(elapsed, 60) timer = f"[{mins}:{secs:02d}]" status.elapsed = timer - term.set_title(f"{timer} {session_title or session_id} @ {project_name}") + term.set_title(f"{timer} {session_title or session_id} @ {agent_name}") term.render() except Exception as e: logger.error("elapsed timer error: %s", e) @@ -1297,13 +1297,13 @@ def _ensure_elapsed_timer() -> None: # Selector state — each selector has an active flag, widget ref, and pending flag. session_sel = SelectorState() delete_sel = SelectorState() - project_sel = SelectorState() + agent_sel = SelectorState() model_sel = SelectorState() sandbox_sel = SelectorState() rewind_sel = SelectorState() task_sel = SelectorState() tool_sel = SelectorState() # Tab: the tool-detail selector (rant 2026-09-30T09:17:54) - _rant_project: str | None = None # Set after project selection, used on next Enter + _rant_agent: str | None = None # Set after agent selection, used on next Enter _skills_confirm: tuple | None = None # (skill_name, install_cmd) — next Enter answers the prompt # (request_id, question, deadline) while the daemon is waiting on this # client's answer to an escalation request — the next line typed is the @@ -1362,8 +1362,8 @@ async def read_server(): nonlocal _last_center, _elapsed_task, conn nonlocal _request_start, turn_running # /task-session: the daemon's verdict on a task's session decides whether - # the client moves into that task's project at all (rant 2026-09-17T18:36:08). - nonlocal cwd, project_name, _task_list_intent, _task_open_pending + # the client moves into that task's agent at all (rant 2026-09-17T18:36:08). + nonlocal cwd, agent_name, _task_list_intent, _task_open_pending nonlocal _resume_pending_sid nonlocal _replay_pending nonlocal _rewind_fill, _rewind_replay @@ -1447,7 +1447,7 @@ async def _reconnect(): ver = getattr(emrg, "__version__", "dev") chat.add("system", f"EMRG {ver} | {server_id}\nType /help for shortcuts, or just start chatting.") status.update(left=_status_left(session_title, session_id, current_model, current_vision, current_sandbox), center=server_id) - term.set_title(f"{session_title or session_id} @ {project_name}") + term.set_title(f"{session_title or session_id} @ {agent_name}") term.render(); continue # P1 queue-injection client side (daemon #655): messages sent @@ -1744,7 +1744,7 @@ async def _reconnect(): chat.add("system", f"⚠ {resp.content} Try '继续' to resume.") _last_center = server_id or "emrg" status.update(center=_last_center) - term.set_title(f"{session_title or session_id} @ {project_name}") + term.set_title(f"{session_title or session_id} @ {agent_name}") # rant 21:52:18: daemon's done frame reports the authoritative # current-context message count (system + history + tool results); # fall back to the local +1 approximation when absent. @@ -1816,7 +1816,7 @@ async def _reconnect(): chat.dirty = True chat.add("system", f"Created new session {new_sid} — continue chatting.") status.update(left=_status_left("", new_sid, current_model, current_vision, current_sandbox), center=server_id or "emrg") - term.set_title(f"{new_sid} @ {project_name}") + term.set_title(f"{new_sid} @ {agent_name}") msg_count = 0 _update_left_extra() status.update(center=server_id or "emrg") @@ -2116,7 +2116,7 @@ async def _reconnect(): f"({count} messages, " f"created {_created}, updated {_updated})") status.update(left=_status_left(session_title, session_id, current_model, current_vision, current_sandbox), center=server_id or "emrg") - term.set_title(f"{session_title or pending_sid} @ {project_name}") + term.set_title(f"{session_title or pending_sid} @ {agent_name}") _update_left_extra() term.render() continue @@ -2138,29 +2138,29 @@ async def _reconnect(): term.render() continue - # Projects list - if data.get("type") == "projects_list": - nonlocal project_sel - projects = data.get("projects", []) + # Agents list + if data.get("type") == "agents_list": + nonlocal agent_sel + agents = data.get("agents", []) err = data.get("error", "") if err: chat.add("system", f"Error: {err}") - project_sel.pending = False - elif project_sel.pending and projects: - project_sel.pending = False - project_sel.widget = ProjectSelector(projects) - project_sel.active = True - chat.add(project_sel.widget) - status.update(center="select project: ↑↓ Enter Esc (j/k vim)") + agent_sel.pending = False + elif agent_sel.pending and agents: + agent_sel.pending = False + agent_sel.widget = AgentSelector(agents) + agent_sel.active = True + chat.add(agent_sel.widget) + status.update(center="select agent: ↑↓ Enter Esc (j/k vim)") else: - project_sel.pending = False - if projects: - project_sel.widget = ProjectSelector(projects) - project_sel.active = True - chat.add(project_sel.widget) - status.update(center="select project: ↑↓ Enter Esc (j/k vim)") + agent_sel.pending = False + if agents: + agent_sel.widget = AgentSelector(agents) + agent_sel.active = True + chat.add(agent_sel.widget) + status.update(center="select agent: ↑↓ Enter Esc (j/k vim)") else: - chat.add("system", "No projects configured. Use emrg in a git repo to auto-register.") + chat.add("system", "No agents configured. Use emrg in a git repo to auto-register.") term.render() continue @@ -2429,7 +2429,7 @@ async def _reconnect(): # Where a session lives, asked before resuming it (rant # 2026-09-29T15:52:49, requirement 6). The client used to send its # own cwd with every resume; the daemon answers with the session's - # real project instead, and the pair is then handed to the same + # real agent instead, and the pair is then handed to the same # `_task_open_pending` machinery that moves the client into it. if data.get("type") == "session_cwd_result": target = _resume_target_from_resolution( @@ -2492,13 +2492,13 @@ async def _reconnect(): current_roots = list(meta.get("sandbox_roots") or []) # /task-session: the session exists, so the client now moves - # into that task's project — before the history replay below, + # into that task's agent — before the history replay below, # which reads it from `cwd`. switch_to = _task_open_switch(_task_open_pending, new_sid) _task_open_pending = None if switch_to is not None: cwd = switch_to - project_name = Path(cwd).name + agent_name = Path(cwd).name # Clear and replay: the material comes from the daemon, not # from disk (rant 2026-09-29T15:52:49, requirement 4 — two @@ -2547,7 +2547,7 @@ async def _reconnect(): session_title = new_title chat.add("system", f"Session renamed to: {new_title}") status.update(left=_status_left(session_title, session_id, current_model, current_vision, current_sandbox), center=server_id or "emrg") - term.set_title(f"{session_title} @ {project_name}") + term.set_title(f"{session_title} @ {agent_name}") term.render() continue @@ -2557,7 +2557,7 @@ async def _reconnect(): if err: chat.add("system", f"Error: {err}") else: - scope = data.get("scope", "project") + scope = data.get("scope", "agent") directory = data.get("directory", "") memories = data.get("memories", []) lines = [f"Memories ({scope}):", f" Directory: {directory}", ""] @@ -2574,7 +2574,7 @@ async def _reconnect(): lines.append("") lines.append("Type /memory to read a specific memory.") lines.append("Type /memory session for session memories.") - lines.append("Type /memory project for project memories.") + lines.append("Type /memory agent for agent memories.") else: lines.append(" (no memories yet)") chat.add("system", "\n".join(lines)) @@ -2713,8 +2713,8 @@ def _handle_selector_nav(data: bytes, widget) -> bool: async def handle_key(data: bytes) -> bool: nonlocal inp, status, history, paste_mode, stream_buffer, conn, chat, busy, need_new_assistant, session_id, session_title, msg_count, cwd nonlocal turn_ended_cancelled, cancel_receipt_held - nonlocal current_model, project_name, current_sandbox - nonlocal session_sel, delete_sel, project_sel, model_sel, rewind_sel, task_sel + nonlocal current_model, agent_name, current_sandbox + nonlocal session_sel, delete_sel, agent_sel, model_sel, rewind_sel, task_sel nonlocal _rewind_fill nonlocal sandbox_sel, tool_sel nonlocal _task_list_intent, _task_open_pending @@ -2874,35 +2874,35 @@ async def handle_key(data: bytes) -> bool: return True return True - # ── Project selector mode ────────────────────────── - if project_sel.active and project_sel.widget: + # ── Agent selector mode ────────────────────────── + if agent_sel.active and agent_sel.widget: if data == b"\x1b": # Esc — cancel selection - project_sel.active = False - chat.add("system", "Project selection cancelled.") - chat.remove(project_sel.widget) - project_sel.widget = None + agent_sel.active = False + chat.add("system", "Agent selection cancelled.") + chat.remove(agent_sel.widget) + agent_sel.widget = None status.update(center=server_id or "emrg") chat.dirty = True; term.render() return True if data == b"\r" or data == b"\n": # Enter — confirm - pname = project_sel.widget.selected_project_name - project_sel.active = False - chat.remove(project_sel.widget) - project_sel.widget = None + pname = agent_sel.widget.selected_agent_name + agent_sel.active = False + chat.remove(agent_sel.widget) + agent_sel.widget = None if pname: - nonlocal _rant_project - _rant_project = pname - chat.add("system", f"Rant to project '@{pname}' — type your message and press Enter:") + nonlocal _rant_agent + _rant_agent = pname + chat.add("system", f"Rant to agent '@{pname}' — type your message and press Enter:") status.update(center=f"rant to @{pname}") else: - chat.add("system", "No project selected.") + chat.add("system", "No agent selected.") status.update(center=server_id or "emrg") chat.dirty = True; term.render() return True - if _handle_selector_nav(data, project_sel.widget): + if _handle_selector_nav(data, agent_sel.widget): chat.dirty = True; term.render() return True - # Ignore other keys when in project selector mode + # Ignore other keys when in agent selector mode return True # ── Model selector mode ────────────────────────── @@ -3026,7 +3026,7 @@ async def handle_key(data: bytes) -> bool: if task_intent == "session": # /task-session (rant 2026-09-17T18:36:08): open the task's # session instead of triggering it. The cwd the daemon is - # asked with is the TASK's project — the client's own cwd + # asked with is the TASK's agent — the client's own cwd # only moves once the daemon confirms the session exists, so # a never-run task leaves the host where they were. target = _task_session_target(task_row) @@ -3242,14 +3242,14 @@ async def handle_key(data: bytes) -> bool: if text: if text.lower() in ("quit", "exit"): return False - # If a rant project was selected, use this message as the rant + # If a rant agent was selected, use this message as the rant # (rant 2026-08-17T11:51:59: routes through the agent for # polish/confirm, then the submit_rant tool records it) - if _rant_project: + if _rant_agent: hint = ( f"[Host wants to submit this rant — polish it, ask for " f"confirmation if needed, then call submit_rant " - f"(project: {_rant_project})]\n{text}" + f"(agent: {_rant_agent})]\n{text}" ) # This path echoes its own user row too, so it parks it the # same way the ordinary submit does — the id is minted here, @@ -3259,7 +3259,7 @@ async def handle_key(data: bytes) -> bool: # submit's). rid = new_task_id() parked_user_rows.park( - rid, chat.add("user", f"/rant @{_rant_project} {text}")) + rid, chat.add("user", f"/rant @{_rant_agent} {text}")) chat.add("assistant", "") msg_count += 1; _update_left_extra() _last_center = "thinking..." @@ -3269,14 +3269,14 @@ async def handle_key(data: bytes) -> bool: # path does before ITS send: `was_busy` is this function's # own local (the assignment further down is what binds it), # so a read that does not follow one raises - # UnboundLocalError — the /rant-with-project path crashed + # UnboundLocalError — the /rant-with-agent path crashed # on every Enter (issue #1759). was_busy = busy rid = await conn.send_task(session_id=session_id, cwd=cwd, prompt=hint, id=rid) if was_busy: _queued_sends.append({"id": rid, "prompt": hint, "images": None}) - _rant_project = None + _rant_agent = None status.update(center=server_id or "emrg") inp.text = ""; inp.cursor = 0; inp.dirty = True; term.render() return True @@ -3336,15 +3336,15 @@ async def handle_key(data: bytes) -> bool: if text.lower().startswith("/memory"): parts = text.split(None, 1) sub = parts[1].strip() if len(parts) > 1 else "" - scope = "project" + scope = "agent" mem_id = "" if sub: # /memory session → list session memories # /memory → read specific memory if sub.lower() == "session": scope = "session" - elif sub.lower() == "project": - scope = "project" + elif sub.lower() == "agent": + scope = "agent" else: mem_id = sub # Could be a read request — send as read @@ -3433,7 +3433,7 @@ async def handle_key(data: bytes) -> bool: await conn.send_command("skills_update") status.update(center="checking skill updates…") else: - # The session's directory, not this process's: the project + # The session's directory, not this process's: the agent # skills a session has are the ones under its own root. skills = load_skills(Path(cwd) if cwd else None) if skills: @@ -3541,19 +3541,19 @@ def _is_image_token(s, i): Commands /help Show this help - /skills List loaded skills (user + project) + /skills List loaded skills (user + agent) /version Show EMRG version and instance info /image Insert clipboard image into input field /compact Compress conversation history /clear Clear current session and start fresh - /memory [session|project|] Browse memories + /memory [session|agent|] Browse memories /sessions Interactive session picker (↑↓/j/k to select) /resume [id] Switch to session (no args = interactive picker, ↑↓/j/k) /rename [title] Rename current session /delete [id] Delete session (no args = interactive picker, ↑↓/j/k) /rant Send feedback to evolution system - /rant @ Rant to a specific project - /rant Interactive project picker, then type message + /rant @ Rant to a specific agent + /rant Interactive agent picker, then type message /model [name] Switch LLM model (no args = interactive picker) /trigger List scheduled tasks (type name to trigger) /trigger Manually trigger a scheduled task now @@ -3594,29 +3594,29 @@ def _is_image_token(s, i): if text.lower().startswith("/rant"): parts = text.split(None, 2) message = parts[1].strip() if len(parts) > 1 else "" - project = None + agent = None if message.startswith("@"): - # /rant @ — project-targeted rant + # /rant @ — agent-targeted rant sub = text.split(None, 2) - project = sub[1][1:] # strip @ + agent = sub[1][1:] # strip @ message = sub[2].strip() if len(sub) > 2 else "" if not message: - if project: - chat.add("system", "Usage: /rant @ ") + if agent: + chat.add("system", "Usage: /rant @ ") inp.text = ""; inp.cursor = 0; inp.dirty = True; term.render() return True - # /rant without args → interactive project selector - project_sel.pending = True - await conn.send_command("list_projects") + # /rant without args → interactive agent selector + agent_sel.pending = True + await conn.send_command("list_agents") - status.update(center="loading projects...") + status.update(center="loading agents...") inp.text = ""; inp.cursor = 0; inp.dirty = True; term.render() return True - target = f" (@{project})" if project else "" + target = f" (@{agent})" if agent else "" hint = ( f"[Host wants to submit this rant — polish it, ask for " f"confirmation if needed, then call submit_rant " - f"(project: {project if project else 'emrg'})]\n{message}" + f"(agent: {agent if agent else 'emrg'})]\n{message}" ) # Parked before the send, under an id minted here — the other # `/rant` shape does the same, so an echoed row on either path @@ -3756,7 +3756,7 @@ def _is_image_token(s, i): session_sel.widget = None session_sel.pending = False # Ask where the session lives instead of assuming it is - # here: the id may name another project's session — a + # here: the id may name another agent's session — a # scheduled task's, above all — and `cwd` is this # client's, not that session's (rant 2026-09-29T15:52:49, # requirement 6). The answer drives the resume. diff --git a/emrg/client/widgets.py b/emrg/client/widgets.py index 7361e9f7..76364979 100644 --- a/emrg/client/widgets.py +++ b/emrg/client/widgets.py @@ -1,6 +1,6 @@ """EMRG client — TUI widget classes extracted from app.py. -Contains InputWidget, selectors (Session/Project/Model/Rewind/Command), +Contains InputWidget, selectors (Session/Agent/Model/Rewind/Command), ChatHistory, _COMMAND_HELP, and SelectorState. """ @@ -657,15 +657,15 @@ def _format_tool_input(arguments: dict) -> list[str]: return text.split("\n") -class ProjectSelector(Widget): - """Interactive project picker — arrow-key navigation with highlight. +class AgentSelector(Widget): + """Interactive agent picker — arrow-key navigation with highlight. - Renders a list of projects from agents.yml with the selected one in - reverse video. Used by /rant when invoked without @project. + Renders a list of agents from agents.yml with the selected one in + reverse video. Used by /rant when invoked without @agent. """ - def __init__(self, projects: list[dict] | None = None): - self.projects: list[dict] = projects or [] + def __init__(self, agents: list[dict] | None = None): + self.agents: list[dict] = agents or [] self.selected_index: int = 0 self._dirty: bool = True @@ -683,25 +683,25 @@ def move_up(self) -> None: self._dirty = True def move_down(self) -> None: - if self.selected_index < len(self.projects) - 1: + if self.selected_index < len(self.agents) - 1: self.selected_index += 1 self._dirty = True @property - def selected_project_name(self) -> str | None: - if 0 <= self.selected_index < len(self.projects): - return self.projects[self.selected_index].get("name", "") + def selected_agent_name(self) -> str | None: + if 0 <= self.selected_index < len(self.agents): + return self.agents[self.selected_index].get("name", "") return None def render(self, ctx): lines: list[Line] = [] pstyle = Style.parse("bold cyan") avail = getattr(ctx, "height", 0) or 0 - n = len(self.projects) + n = len(self.agents) chrome, start, end, markers = window_plan(avail, n, self.selected_index) if chrome: lines.append(Line( - spans=[Span("○ ", style="dim"), Span("Select a project (↑↓/j/k to move, Enter to confirm, Esc to cancel):", style="bold")], + spans=[Span("○ ", style="dim"), Span("Select an agent (↑↓/j/k to move, Enter to confirm, Esc to cancel):", style="bold")], style=ctx.style, )) if markers and start > 0: @@ -710,7 +710,7 @@ def render(self, ctx): style=ctx.style, )) for i in range(start, end): - p = self.projects[i] + p = self.agents[i] name = p.get("name", "?") repo = p.get("repo", "") label = f" {name}" @@ -878,7 +878,7 @@ def selected_task_name(self) -> str | None: @property def selected_task(self) -> dict | None: - """The whole selected row — /task-session needs `project_path` and + """The whole selected row — /task-session needs `agent_path` and `session_id`, which `selected_task_name` does not carry.""" if 0 <= self.selected_index < len(self.tasks): return self.tasks[self.selected_index] @@ -1030,18 +1030,18 @@ def render(self, ctx): "/sessions": "Browse and switch between saved sessions (↑↓/j/k to navigate)", "/rewind": "Rewind session — pick a user message to truncate history to", "/compact": "Compress conversation history to save context", - "/memory": "Browse and search memories [session|project|]", + "/memory": "Browse and search memories [session|agent|]", "/rename": "Rename current session [title]", "/delete": "Delete a session [/delete | /delete ]", "/clear": "Clear current session history and start fresh", - "/rant": "Send feedback to the evolution system [/rant | /rant @ ]", + "/rant": "Send feedback to the evolution system [/rant | /rant @ ]", "/model": "Switch LLM model [/model | /model ]", "/sandbox": "Set this session's sandbox tier, or its extra writable roots " "[/sandbox | /sandbox | /sandbox add|remove|list ] " "[/sandbox add|remove --registry names a registry key]", "/trigger": "List or manually trigger scheduled tasks [/trigger | /trigger ]", "/task-session": "Open a scheduled task's session (no args = interactive picker)", - "/skills": "List loaded skills (user + project)", + "/skills": "List loaded skills (user + agent)", "/version": "Show EMRG version and instance info", "/image": "Insert clipboard image into input field", "/help": "Show keyboard shortcuts and commands", @@ -1259,7 +1259,7 @@ def render(self, ctx): class SelectorState: - """Unified state for interactive selectors (session, project, model). + """Unified state for interactive selectors (session, agent, model). Consolidates 9 separate variables into 3 typed instances to eliminate nonlocal declaration errors (rant #31). @@ -1268,5 +1268,5 @@ class SelectorState: def __init__(self) -> None: self.active: bool = False - self.widget: 'SessionSelector | ProjectSelector | ModelSelector | None' = None + self.widget: 'SessionSelector | AgentSelector | ModelSelector | None' = None self.pending: bool = False diff --git a/emrg/gui/daemon_client.js b/emrg/gui/daemon_client.js index 789f8f48..77cd930c 100644 --- a/emrg/gui/daemon_client.js +++ b/emrg/gui/daemon_client.js @@ -123,7 +123,7 @@ function isSessionId(id) { const RESPONSE_TYPES = { list_sessions: "sessions_list", list_models: "models_list", - list_projects: "projects_list", + list_agents: "agents_list", list_history: "history_list", // The prompts the host typed in one session (issue #2349, part 2/2): the daemon // answers a `list_input_history` command with an `input_history` frame, so the @@ -164,8 +164,8 @@ const RESPONSE_TYPES = { * * The daemon answers where a session lives so that a client does not have to * guess (issue #2484). Two answers are deliberately not the same value: `index` - * carries the project the global session index holds, and `unknown` carries - * `null` — the daemon cannot vouch for that session's project, so the requested + * carries the agent the global session index holds, and `unknown` carries + * `null` — the daemon cannot vouch for that session's agent, so the requested * cwd comes back only as a candidate and must not be relabelled as a fact. * * So everything except a non-empty `index` cwd answers the caller's fallback, @@ -547,7 +547,7 @@ class DaemonClient { errFd === null ? null : EMRGD_START_ERR() ); } - // G125:spawn 设 cwd=project_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills) + // G125:spawn 设 cwd=agent_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills) const python = this._findPython(); const args = ["-m", "emrg.server"]; this.logger.info(`[gui] spawning daemon: ${python} ${args.join(" ")} cwd=${os.homedir()}`); @@ -1172,7 +1172,7 @@ class DaemonClient { // both (`app.py:1345` / `:1353`), so this layer was the only client that // dropped them: they matched no branch, fell to the unknown-frame log at // the bottom, and reached no renderer. The catch-all's list of *expected* - // drops (`templates_list` / `compact_result` / `projects_list`) never named + // drops (`templates_list` / `compact_result` / `agents_list`) never named // them. this._emit(frame.type, frame); return; @@ -1200,7 +1200,7 @@ class DaemonClient { // 日志以 `type=` 开头,下一次可以 `grep 'unknown frame type=' | sort | uniq -c` // 直接数,而不必从截断的 JSON 里抠类型。 // 注意:本支对某些帧是**预期**的——daemon 会把 `templates_list` / `compact_result` - // / `projects_list` 广播给全部订阅者(`daemon.py:1518`),而这是别的会话发出的 + // / `agents_list` 广播给全部订阅者(`daemon.py:1518`),而这是别的会话发出的 // 命令的回执,本连接既不认领也不该转发。 const unknownType = frame.type === undefined ? "" : String(frame.type); this.logger.warn(`[gui] unknown frame type=${unknownType}: ${JSON.stringify(frame).slice(0, 200)}`); diff --git a/emrg/gui/gui-state.js b/emrg/gui/gui-state.js index 0b270371..47607c91 100644 --- a/emrg/gui/gui-state.js +++ b/emrg/gui/gui-state.js @@ -8,7 +8,7 @@ // path (restore on boot + sidebar UI) lands in P4 slice 2. // // Design notes (from the rant): -// - Open sessions = cross-project tabs (aligned with the TUI multi-open model). +// - Open sessions = cross-agent tabs (aligned with the TUI multi-open model). // - Cap 20: never persist more than the cap; restore (slice 2) takes the most // recent 20 by lastActive. // - Write is atomic (.tmp + rename) so a crash mid-write never corrupts state. diff --git a/emrg/gui/main.js b/emrg/gui/main.js index fa85733d..d370d9b4 100644 --- a/emrg/gui/main.js +++ b/emrg/gui/main.js @@ -576,8 +576,8 @@ vision = false } // P6(rant 15:07:19 边界):projectPath 校验(跨项目打开时传项目路径) if (projectPath !== undefined && (typeof projectPath !== "string" || !projectPath.trim())) { - logger.warn(`[gui:switch] REJECT invalid project path=${projectPath}`); - throw new Error("invalid project path"); + logger.warn(`[gui:switch] REJECT invalid agent path=${projectPath}`); + throw new Error("invalid agent path"); } // P6(rant 15:07:19 上限 20):显式打开新会话超限 → 提示不自动关(已打开 sid 复用不拦) if (openSessions.size >= DEFAULT_CAP && !openSessions.has(sessionId)) { @@ -669,7 +669,7 @@ vision = false ipcMain.handle("emrg:newSession", async (_e, { projectPath } = {}) => { // P6(rant 15:07:19 边界):projectPath 校验(新建会话指定项目时) if (projectPath !== undefined && (typeof projectPath !== "string" || !projectPath.trim())) { - throw new Error("invalid project path"); + throw new Error("invalid agent path"); } // G14/G81:本地生成 session_id(无 new_session 消息) const sid = generateSessionId(); @@ -747,7 +747,7 @@ vision = false // // `cwd` resolves from the session exactly as `emrg:listHistory` does (rant // 2026-08-25T17:38:56): a hardcoded DEFAULT_CWD reads every session from the - // wrong project and answers an empty list that looks like "nothing typed yet". + // wrong agent and answers an empty list that looks like "nothing typed yet". if (!validateSessionId(sessionId)) throw new Error("invalid session_id"); const payload = { session_id: sessionId, cwd: resolveSessionCwd(sessionId) || DEFAULT_CWD }; const frame = await requireConn().sendCommandAndWait("list_input_history", payload, 5000); @@ -782,9 +782,9 @@ vision = false return { ok: true, removedCount: frame.removed_count ?? 0 }; }); - ipcMain.handle("emrg:listMemories", async (_e, { scope = "project", sessionId } = {}) => { + ipcMain.handle("emrg:listMemories", async (_e, { scope = "agent", sessionId } = {}) => { // GUI / 指令 P3:/memory — 列出记忆(daemon list_memories → memories_list) - // 记忆命令同样按会话真实项目取 cwd(scope=session 时用该会话,project 时用当前会话) + // 记忆命令同样按会话真实项目取 cwd(scope=session 时用该会话,agent 时用当前会话) const memCwd = resolveSessionCwd(sessionId || currentSessionId) || DEFAULT_CWD; const params = { scope, cwd: memCwd }; if (scope === "session") { @@ -795,7 +795,7 @@ vision = false return frame.memories || []; }); - ipcMain.handle("emrg:readMemory", async (_e, { memoryId, scope = "project", sessionId } = {}) => { + ipcMain.handle("emrg:readMemory", async (_e, { memoryId, scope = "agent", sessionId } = {}) => { // GUI / 指令 P3:/memory — 读取单条记忆(daemon read_memory → memory_content) if (typeof memoryId !== "string" || !memoryId.trim()) throw new Error("invalid memory_id"); const params = { scope, memory_id: memoryId.trim(), cwd: resolveSessionCwd(sessionId || currentSessionId) || DEFAULT_CWD }; @@ -860,11 +860,11 @@ vision = false ipcMain.handle("emrg:listSkills", async () => { // GUI / 指令 P3:/skills — 读取技能列表(TUI 本地 load_skills 等价物,daemon 无协议) - // 技能在 ~/.emrg/skills/*.md(user)与 /.emrg/skills/*.md(project) + // 技能在 ~/.emrg/skills/*.md(user)与 /.emrg/skills/*.md(agent) const skills = []; const dirs = [ { dir: path.join(os.homedir(), ".emrg", "skills"), source: "user" }, - { dir: path.join(DEFAULT_CWD, ".emrg", "skills"), source: "project" }, + { dir: path.join(DEFAULT_CWD, ".emrg", "skills"), source: "agent" }, ]; for (const { dir, source } of dirs) { let files = []; @@ -883,40 +883,40 @@ vision = false return skills; }); - ipcMain.handle("emrg:listProjects", async () => { - // GUI / 指令 P4:/rant 项目下拉 — daemon list_projects → projects_list - const frame = await requireConn().sendCommandAndWait("list_projects", {}, 5000); - return frame.projects || []; + ipcMain.handle("emrg:listAgents", async () => { + // GUI / 指令 P4:/rant 项目下拉 — daemon list_agents → agents_list + const frame = await requireConn().sendCommandAndWait("list_agents", {}, 5000); + return frame.agents || []; }); // P5(rant 15:07:19):某项目的会话列表(list_sessions(cwd=projectPath)) - ipcMain.handle("emrg:listProjectSessions", async (_e, { projectPath }) => { - if (typeof projectPath !== "string" || !projectPath) throw new Error("invalid project path"); + ipcMain.handle("emrg:listAgentSessions", async (_e, { projectPath }) => { + if (typeof projectPath !== "string" || !projectPath) throw new Error("invalid agent path"); const frame = await requireConn().sendCommandAndWait("list_sessions", { cwd: projectPath }, 5000); return { sessions: frame.sessions || [] }; }); - // P5:新建项目 = 轻量命令带 cwd → daemon 隐式 _touch_project 注册(零改动) - ipcMain.handle("emrg:registerProject", async (_e, { path: p }) => { - if (typeof p !== "string" || !p) throw new Error("invalid project path"); + // P5:新建项目 = 轻量命令带 cwd → daemon 隐式 _touch_agent 注册(零改动) + ipcMain.handle("emrg:registerAgent", async (_e, { path: p }) => { + if (typeof p !== "string" || !p) throw new Error("invalid agent path"); try { fs.accessSync(p, fs.constants.W_OK); // 目录可写校验(G121) } catch { - throw new Error("project directory not writable"); + throw new Error("agent directory not writable"); } await requireConn().sendCommandAndWait("list_sessions", { cwd: p }, 5000); // 隐式注册 return { ok: true, path: p }; }); // P5 slice 2:删除项目 = 只删 agents.yml 条目(保留磁盘数据)+ 关闭该项目已打开会话 - // 受保护项目不可删除(内置 project emrg / 内置 task emrg-task → 演化依赖,删了悬空); - // `.emrg` 是 _touch_project 历史记录**非内置 → 可删**(再次访问重新注册)。 + // 受保护项目不可删除(内置 agent emrg / 内置 task emrg-task → 演化依赖,删了悬空); + // `.emrg` 是 _touch_agent 历史记录**非内置 → 可删**(再次访问重新注册)。 // 返回 { ok, removed, closed: [sids] };renderer 收到后负责切换激活会话。 - ipcMain.handle("emrg:removeProject", async (_e, { name, path: p } = {}) => { - if (typeof name !== "string" || !name.trim()) throw new Error("invalid project name"); - // 受保护项目:内置 project `emrg` + 内置 task `emrg-task` + ipcMain.handle("emrg:removeAgent", async (_e, { name, path: p } = {}) => { + if (typeof name !== "string" || !name.trim()) throw new Error("invalid agent name"); + // 受保护项目:内置 agent `emrg` + 内置 task `emrg-task` if (name === "emrg" || name === "emrg-task") { - return { ok: false, protected: true, error: "protected system project" }; + return { ok: false, protected: true, error: "protected system agent" }; } // 该项目已打开的会话 → 关闭连接 + 移除 + 写盘(激活中先切相邻由 renderer 处理) const closed = []; @@ -930,7 +930,7 @@ vision = false if (currentSessionId && closed.includes(currentSessionId)) { currentSessionId = null; // 激活会话被关 → 无激活(renderer 切相邻) } - const frame = await requireConn().sendCommandAndWait("remove_project", { name: name.trim() }, 5000); + const frame = await requireConn().sendCommandAndWait("remove_agent", { name: name.trim() }, 5000); schedulePersistGuiState(); broadcastOpenSessions(); return { ok: true, removed: Boolean(frame.removed), closed, name }; @@ -1037,12 +1037,12 @@ vision = false return frame.rants || []; }); - ipcMain.handle("emrg:sendRant", async (_e, { message, project = "" } = {}) => { + ipcMain.handle("emrg:sendRant", async (_e, { message, agent = "" } = {}) => { // GUI / 指令 P4:/rant — 提交反馈到演化系统(daemon rant 协议,字段序与 rants.jsonl 一致) if (typeof message !== "string" || !message.trim()) throw new Error("invalid rant message"); const frame = await requireConn().sendCommandAndWait("rant", { message: message.trim().slice(0, 10000), - project: String(project || "").trim(), + agent: String(agent || "").trim(), // timestamp deliberately NOT sent: daemon stamps rants with local time // (rant 2026-08-07T13:34Z — GUI previously sent new Date().toISOString(), // which is UTC and 8h behind on UTC+8 hosts) @@ -1259,7 +1259,7 @@ vision = false return { ok: true }; }); - ipcMain.handle("emrg:pickProjectDir", async () => { + ipcMain.handle("emrg:pickAgentDir", async () => { // G70/G115:只选不写盘(保存才落盘) const res = await dialog.showOpenDialog(win, { properties: ["openDirectory"] }); return res.canceled ? null : res.filePaths[0]; diff --git a/emrg/gui/preload.js b/emrg/gui/preload.js index a59f8488..30ac9aea 100644 --- a/emrg/gui/preload.js +++ b/emrg/gui/preload.js @@ -50,10 +50,10 @@ const api = { panelResized: (payload) => ipcRenderer.invoke("emrg:panelResized", payload), // P2.3:面板布局上报 → bounds 同步 getPreviewState: () => ipcRenderer.invoke("emrg:getPreviewState"), // P2.3:renderer 崩溃恢复拉取 listSkills: () => ipcRenderer.invoke("emrg:listSkills"), - listProjects: () => ipcRenderer.invoke("emrg:listProjects"), - listProjectSessions: (payload) => ipcRenderer.invoke("emrg:listProjectSessions", payload), - registerProject: (payload) => ipcRenderer.invoke("emrg:registerProject", payload), - removeProject: (payload) => ipcRenderer.invoke("emrg:removeProject", payload), + listAgents: () => ipcRenderer.invoke("emrg:listAgents"), + listAgentSessions: (payload) => ipcRenderer.invoke("emrg:listAgentSessions", payload), + registerAgent: (payload) => ipcRenderer.invoke("emrg:registerAgent", payload), + removeAgent: (payload) => ipcRenderer.invoke("emrg:removeAgent", payload), listTasks: () => ipcRenderer.invoke("emrg:listTasks"), // 间隔下限由 daemon 上报(issue #2490)——渲染层不留第二份数字 taskLimits: () => ipcRenderer.invoke("emrg:taskLimits"), @@ -79,7 +79,7 @@ const api = { saveSettings: (payload) => ipcRenderer.invoke("emrg:saveSettings", payload), getSettings: () => ipcRenderer.invoke("emrg:getSettings"), cancel: (sessionId) => ipcRenderer.invoke("emrg:cancel", sessionId), - pickProjectDir: () => ipcRenderer.invoke("emrg:pickProjectDir"), + pickAgentDir: () => ipcRenderer.invoke("emrg:pickAgentDir"), // 渲染进程日志桥(rant 2026-08-24:打开会话链路排障——renderer 点击/调用步骤 // 经 emrg:log IPC 上报,由 main 写入 ~/.emrg/emrg-gui.log,与 main 日志同文件时序对齐) log: (level, msg) => ipcRenderer.invoke("emrg:log", { level, msg }), diff --git a/emrg/gui/renderer/css/components.css b/emrg/gui/renderer/css/components.css index 4362c607..eaafd885 100644 --- a/emrg/gui/renderer/css/components.css +++ b/emrg/gui/renderer/css/components.css @@ -930,8 +930,8 @@ dialog::backdrop { .rant-row .rant-col-time { flex: 0 0 150px; } -.rant-head .rant-col-project, -.rant-row .rant-col-project { +.rant-head .rant-col-agent, +.rant-row .rant-col-agent { flex: 0 0 90px; } .rant-head .rant-col-status, @@ -950,7 +950,7 @@ dialog::backdrop { } .rant-head span, .rant-row .rant-col-time, -.rant-row .rant-col-project, +.rant-row .rant-col-agent, .rant-row .rant-col-progress, .rant-row .rant-col-content { overflow: hidden; @@ -958,7 +958,7 @@ dialog::backdrop { white-space: nowrap; } .rant-row .rant-col-time, -.rant-row .rant-col-project, +.rant-row .rant-col-agent, .rant-row .rant-col-progress, .rant-row .rant-col-content { font-size: var(--fs-aux); diff --git a/emrg/gui/renderer/src/components/DialogHost.test.tsx b/emrg/gui/renderer/src/components/DialogHost.test.tsx index 901a251d..a0236ccf 100644 --- a/emrg/gui/renderer/src/components/DialogHost.test.tsx +++ b/emrg/gui/renderer/src/components/DialogHost.test.tsx @@ -66,11 +66,11 @@ function mockEmrg(over: Record = {}) { return mock; }; const bridge = { - listProjects: fn("listProjects", [{ name: "emrg", path: "/p/emrg" }, { name: "demo", path: "/p/demo", latest_session_at: "2026-08-27T00:00:00Z" }]), - listProjectSessions: fn("listProjectSessions", { sessions: [{ session_id: "s9", title: "Demo session" }] }), + listAgents: fn("listAgents", [{ name: "emrg", path: "/p/emrg" }, { name: "demo", path: "/p/demo", latest_session_at: "2026-08-27T00:00:00Z" }]), + listAgentSessions: fn("listAgentSessions", { sessions: [{ session_id: "s9", title: "Demo session" }] }), newSession: fn("newSession", { session_id: "s-new" }), - pickProjectDir: fn("pickProjectDir", { path: "/p/new" }), - registerProject: fn("registerProject", { ok: true, path: "/p/new" }), + pickAgentDir: fn("pickAgentDir", { path: "/p/new" }), + registerAgent: fn("registerAgent", { ok: true, path: "/p/new" }), switchSession: fn("switchSession", { ok: true }), deleteSession: fn("deleteSession", { ok: true }), renameSession: fn("renameSession", { ok: true }), @@ -81,7 +81,7 @@ function mockEmrg(over: Record = {}) { listMemories: fn("listMemories", [{ id: "m1", title: "记忆一", summary: "摘要内容" }]), readMemory: fn("readMemory", { id: "m1", title: "记忆一", content: "详情正文" }), listSkills: fn("listSkills", [{ name: "browser-harness", description: "web automation", source: "user" }]), - removeProject: fn("removeProject", { ok: true }), + removeAgent: fn("removeAgent", { ok: true }), ...over, }; (window as unknown as { emrg?: unknown }).emrg = bridge; @@ -147,7 +147,7 @@ describe("DialogHost (Batch 5 slice 4)", () => { await waitFor(() => expect(screen.getByText("记忆一")).toBeInTheDocument()); fireEvent.click(screen.getByText("记忆一")); await waitFor(() => expect(screen.getByTestId("memory-detail")).toBeInTheDocument()); - expect(calls.readMemory[0][0]).toMatchObject({ memoryId: "m1", scope: "project" }); + expect(calls.readMemory[0][0]).toMatchObject({ memoryId: "m1", scope: "agent" }); }); it("记忆详情按 vanilla 截断:title 80 / body 2000", async () => { @@ -277,7 +277,7 @@ describe("DialogHost (Batch 5 slice 4)", () => { await waitFor(() => expect(screen.getByTestId("open-session-dialog")).toBeInTheDocument()); await waitFor(() => expect(screen.getByText("demo")).toBeInTheDocument()); fireEvent.click(screen.getByText("demo")); - await waitFor(() => expect(calls.listProjectSessions.length).toBe(1)); + await waitFor(() => expect(calls.listAgentSessions.length).toBe(1)); await waitFor(() => expect(screen.getByText("Demo session")).toBeInTheDocument()); fireEvent.click(screen.getByText("Demo session")); await waitFor(() => expect(calls.switchSession.length).toBe(1)); @@ -292,7 +292,7 @@ describe("DialogHost (Batch 5 slice 4)", () => { const del = screen.getAllByTestId("open-session-delete"); fireEvent.click(del[0]); // emrg 项目 await waitFor(() => expect(screen.getByTestId("confirm-dialog")).toBeInTheDocument()); - expect(screen.getByText(/system project/i)).toBeInTheDocument(); + expect(screen.getByText(/system agent/i)).toBeInTheDocument(); }); it("新建会话:点选项目 → newSession + onSwitchSession(新会话)", async () => { diff --git a/emrg/gui/renderer/src/components/DialogHost.tsx b/emrg/gui/renderer/src/components/DialogHost.tsx index a8ba898d..7475baed 100644 --- a/emrg/gui/renderer/src/components/DialogHost.tsx +++ b/emrg/gui/renderer/src/components/DialogHost.tsx @@ -4,10 +4,10 @@ import { COMMANDS, hintText } from "../lib/commands"; import { helpRows, type MemoryRow, type SkillRow } from "../lib/dialogLists"; import { rewindPointRow, type RewindPoint } from "../lib/rewind"; import { - isProtectedProject, - projectRowView, + isProtectedAgent, + agentRowView, sessionRowView, - type ProjectRow, + type AgentRow, type SessionRow, } from "../lib/openSession"; import { dialogReducer, initialDialogState } from "../lib/dialog"; @@ -42,11 +42,11 @@ import { SandboxRootsDialog } from "./SandboxRootsDialog"; /** DialogHost 用到的 preload 桥方法(与 preload.js 的 52 通道子集对齐) */ interface EmrgBridge { - listProjects(): Promise; - listProjectSessions(p: { projectPath: string }): Promise<{ sessions: SessionRow[] }>; + listAgents(): Promise; + listAgentSessions(p: { projectPath: string }): Promise<{ sessions: SessionRow[] }>; newSession(p: { projectPath: string }): Promise<{ session_id: string }>; - pickProjectDir(): Promise<{ path?: string } | null>; - registerProject(p: { path: string }): Promise<{ ok: boolean; path: string }>; + pickAgentDir(): Promise<{ path?: string } | null>; + registerAgent(p: { path: string }): Promise<{ ok: boolean; path: string }>; switchSession(p: { sessionId: string }): Promise; deleteSession(p: { sessionId: string }): Promise; renameSession(p: { sessionId: string; title: string }): Promise; @@ -54,14 +54,14 @@ interface EmrgBridge { compactSession(p: { sessionId: string }): Promise; listHistory(p: { sessionId: string }): Promise<{ messages: RewindPoint[]; hasMore?: boolean }>; rewindSession(p: { sessionId: string; recordIndex: number }): Promise; - listMemories(p: { scope: "session" | "project"; sessionId?: string }): Promise; - readMemory(p: { memoryId: string; scope: "session" | "project"; sessionId?: string }): Promise<{ + listMemories(p: { scope: "session" | "agent"; sessionId?: string }): Promise; + readMemory(p: { memoryId: string; scope: "session" | "agent"; sessionId?: string }): Promise<{ id?: string; title?: string; content?: string; }>; listSkills(): Promise; - removeProject(p: { name: string; path: string }): Promise<{ ok: boolean; error?: string }>; + removeAgent(p: { name: string; path: string }): Promise<{ ok: boolean; error?: string }>; } function bridge(): EmrgBridge | undefined { @@ -144,11 +144,11 @@ export const DialogHost = forwardRef(function // confirm/rename 走 lib/dialog 的 reducer(vanilla showConfirm/showRename 请求槽) const [dialogState, dispatch] = useReducer(dialogReducer, initialDialogState); const [listDialog, setListDialog] = useState(CLOSED); - const [memoryScope, setMemoryScope] = useState<"session" | "project">("project"); + const [memoryScope, setMemoryScope] = useState<"session" | "agent">("agent"); // 列表数据(null = loading) - const [projects, setProjects] = useState(null); - const [openStep, setOpenStep] = useState("projects"); + const [agents, setAgents] = useState(null); + const [openStep, setOpenStep] = useState("agents"); const [openSessions, setOpenSessions] = useState(null); const [openProjectName, setOpenProjectName] = useState(""); const [sessionsError, setSessionsError] = useState(null); @@ -172,17 +172,17 @@ export const DialogHost = forwardRef(function // ── 数据加载(打开即拉取;bridge 缺失时 no-op 降级) ── - // projects:open-session / new-session 共用 + // agents:open-session / new-session 共用 useEffect(() => { if (!listDialog.sessions && !listDialog.newSession) return; const b = bridge(); if (!b) return; let cancelled = false; - setProjects(null); + setAgents(null); setSessionsError(null); - b.listProjects() + b.listAgents() .then((rows) => { - if (!cancelled) setProjects(rows); + if (!cancelled) setAgents(rows); }) .catch((e) => { if (!cancelled) setSessionsError((e as Error)?.message ?? String(e)); @@ -192,7 +192,7 @@ export const DialogHost = forwardRef(function }; }, [listDialog.sessions, listDialog.newSession]); - // memories(/memory [session|project]) + // memories(/memory [session|agent]) useEffect(() => { if (!listDialog.memory) return; const b = bridge(); @@ -256,7 +256,7 @@ export const DialogHost = forwardRef(function // ── 命令路由动作 ── function openMemory(scopeArg?: string): void { - setMemoryScope(scopeArg === "session" ? "session" : "project"); + setMemoryScope(scopeArg === "session" ? "session" : "agent"); openDialog("memory"); } @@ -323,7 +323,7 @@ export const DialogHost = forwardRef(function } /** 打开会话两步流:项目 → 会话(vanilla showOpenSessionDialog) */ - async function pickProject(p: ProjectRow): Promise { + async function pickProject(p: AgentRow): Promise { const b = bridge(); if (!b || !p.path) return; setOpenStep("sessions"); @@ -331,21 +331,21 @@ export const DialogHost = forwardRef(function setOpenSessions(null); setSessionsError(null); try { - const res = await b.listProjectSessions({ projectPath: p.path }); + const res = await b.listAgentSessions({ projectPath: p.path }); setOpenSessions(res.sessions ?? []); } catch (e) { setSessionsError((e as Error)?.message ?? String(e)); } } - /** 删除项目:受保护 → 拒绝提示;否则确认后 removeProject + 重载列表 */ - function deleteProject(p: ProjectRow): void { - if (isProtectedProject(p)) { + /** 删除项目:受保护 → 拒绝提示;否则确认后 removeAgent + 重载列表 */ + function deleteAgent(p: AgentRow): void { + if (isProtectedAgent(p)) { dispatch({ type: "open-confirm", payload: { - title: t("deleteProject.title"), - message: t("deleteProject.protectedBody", { name: String(p.name || p.path || "") }), + title: t("deleteAgent.title"), + message: t("deleteAgent.protectedBody", { name: String(p.name || p.path || "") }), okText: "dlg.gotIt", danger: false, }, @@ -355,36 +355,36 @@ export const DialogHost = forwardRef(function dispatch({ type: "open-confirm", payload: { - title: t("deleteProject.title"), - message: t("deleteProject.body", { name: String(p.name || p.path || "") }), + title: t("deleteAgent.title"), + message: t("deleteAgent.body", { name: String(p.name || p.path || "") }), okText: "dlg.delete", danger: true, onOk: async () => { const b = bridge(); if (!b) return; - const res = await b.removeProject({ name: p.name || "", path: p.path || "" }); + const res = await b.removeAgent({ name: p.name || "", path: p.path || "" }); if (res && res.error) { - transcript.addSystemMessage(t("deleteProject.failed", { msg: String(res.error) }), sid); + transcript.addSystemMessage(t("deleteAgent.failed", { msg: String(res.error) }), sid); } else { - transcript.addSystemMessage(t("deleteProject.removed", { name: String(p.name || p.path || "") }), sid); - setProjects(null); // 重载项目列表 + transcript.addSystemMessage(t("deleteAgent.removed", { name: String(p.name || p.path || "") }), sid); + setAgents(null); // 重载项目列表 } }, }, }); } - /** 新建项目 = 选目录 → 注册 → 重载项目列表(vanilla new-session 的 new-project 路径) */ - async function newProject(): Promise { + /** 新建项目 = 选目录 → 注册 → 重载项目列表(vanilla new-session 的 new-agent 路径) */ + async function newAgent(): Promise { const b = bridge(); if (!b) return; try { - const res = await b.pickProjectDir(); + const res = await b.pickAgentDir(); if (!res?.path) return; - await b.registerProject({ path: res.path }); - setProjects(null); - const rows = await b.listProjects(); - setProjects(rows); + await b.registerAgent({ path: res.path }); + setAgents(null); + const rows = await b.listAgents(); + setAgents(rows); } catch (e) { setSessionsError((e as Error)?.message ?? String(e)); } @@ -444,7 +444,7 @@ export const DialogHost = forwardRef(function openRename, openDelete, openSessions: () => { - setOpenStep("projects"); + setOpenStep("agents"); openDialog("sessions"); }, openNewSession: () => openDialog("newSession"), @@ -517,20 +517,20 @@ export const DialogHost = forwardRef(function { const b = bridge(); if (b) await b.switchSession({ sessionId: s.session_id }); onSwitchSession(s.session_id); closeDialog("sessions"); }} - onDeleteProject={deleteProject} - onNewProject={newProject} + onDeleteAgent={deleteAgent} + onNewAgent={newAgent} onNewSession={() => { closeDialog("sessions"); openDialog("newSession"); @@ -539,12 +539,12 @@ export const DialogHost = forwardRef(function /> { + onPickAgent={(p) => { if (p.path) void createSession(p.path); }} - onNewProject={newProject} + onNewAgent={newAgent} onDismiss={() => closeDialog("newSession")} /> (function }); // 类型 re-export(测试/调用方构造行视图用) -export { projectRowView, sessionRowView, rewindPointRow }; +export { agentRowView, sessionRowView, rewindPointRow }; diff --git a/emrg/gui/renderer/src/components/MemoryDialog.test.tsx b/emrg/gui/renderer/src/components/MemoryDialog.test.tsx index 5f01ed89..cf63efcb 100644 --- a/emrg/gui/renderer/src/components/MemoryDialog.test.tsx +++ b/emrg/gui/renderer/src/components/MemoryDialog.test.tsx @@ -38,7 +38,7 @@ describe("MemoryDialog", () => { expect(screen.getByTestId("memory-loading")).toHaveTextContent("加载中"); }); - it("空列表 → app.noMemories(project scope 文案)", () => { + it("空列表 → app.noMemories(agent scope 文案)", () => { setup({ memories: [] }); expect(screen.getByTestId("memory-empty")).toHaveTextContent("项目"); }); diff --git a/emrg/gui/renderer/src/components/MemoryDialog.tsx b/emrg/gui/renderer/src/components/MemoryDialog.tsx index 0078d9b0..fb5758fa 100644 --- a/emrg/gui/renderer/src/components/MemoryDialog.tsx +++ b/emrg/gui/renderer/src/components/MemoryDialog.tsx @@ -7,7 +7,7 @@ import { DialogActions } from "./HelpDialog"; * MemoryDialog — /memory 记忆浏览器对话框(Batch 4 slice 2)。 * 镜像 vanilla `showMemoryDialog(sub)`: * - loading(memories=null)→ dlg.loading 行 - * - 空列表 → app.noMemories(scope 区分 session/project 文案) + * - 空列表 → app.noMemories(scope 区分 session/agent 文案) * - 行 = 标题(40 截断) + 摘要(50 截断);点击 → onSelect(id)(vanilla 内联 readMemory) * - detail 传入 → 显示详情块(title + pre body) * - error → app.memFailed @@ -22,7 +22,7 @@ export interface MemoryDialogProps { open: boolean; /** null = loading;[] = 空;有值 = 列表 */ memories?: MemoryRow[] | null; - scope?: "session" | "project"; + scope?: "session" | "agent"; error?: string | null; detail?: MemoryDetailView | null; onSelect?: (id: string) => void; @@ -32,7 +32,7 @@ export interface MemoryDialogProps { export function MemoryDialog({ open, memories = null, - scope = "project", + scope = "agent", error = null, detail = null, onSelect, diff --git a/emrg/gui/renderer/src/components/NewSessionDialog.test.tsx b/emrg/gui/renderer/src/components/NewSessionDialog.test.tsx index e10a9ed6..fa1bafb0 100644 --- a/emrg/gui/renderer/src/components/NewSessionDialog.test.tsx +++ b/emrg/gui/renderer/src/components/NewSessionDialog.test.tsx @@ -3,21 +3,21 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { NewSessionDialog } from "./NewSessionDialog"; import { I18nProvider } from "../lib/i18n"; -import type { ProjectRow } from "../lib/openSession"; +import type { AgentRow } from "../lib/openSession"; /** * NewSessionDialog.test.tsx — 新建会话对话框测试(Batch 4 slice 3)。 * 镜像 vanilla showNewSessionDialog:loading/空/error/列表 + 新建项目按钮。 */ -const projects: ProjectRow[] = [ +const agents: AgentRow[] = [ { name: "emrg", path: "/x/emrg", latest_session_at: new Date().toISOString() }, ]; function setup(props: Partial[0]> = {}) { return render( - + , ); } @@ -33,12 +33,12 @@ describe("NewSessionDialog", () => { }); it("loading → dlg.loading 行", () => { - setup({ projects: null }); + setup({ agents: null }); expect(screen.getByTestId("new-session-loading")).toHaveTextContent("加载中"); }); - it("空项目列表 → noProjects", () => { - setup({ projects: [] }); + it("空项目列表 → noAgents", () => { + setup({ agents: [] }); expect(screen.getByTestId("new-session-empty")).toHaveTextContent("还没有项目"); }); @@ -47,21 +47,21 @@ describe("NewSessionDialog", () => { expect(screen.getByTestId("new-session-error")).toHaveTextContent("加载失败:boom"); }); - it("项目行:name/path/活跃;点击 → onPickProject", async () => { - const onPickProject = vi.fn(); - setup({ onPickProject }); - const row = screen.getByTestId("new-session-project"); + it("项目行:name/path/活跃;点击 → onPickAgent", async () => { + const onPickAgent = vi.fn(); + setup({ onPickAgent }); + const row = screen.getByTestId("new-session-agent"); expect(row).toHaveTextContent("emrg"); expect(row).toHaveTextContent("/x/emrg"); await userEvent.click(row); - expect(onPickProject).toHaveBeenCalledWith(projects[0]); + expect(onPickAgent).toHaveBeenCalledWith(agents[0]); }); - it("新建项目按钮 → onNewProject", async () => { - const onNewProject = vi.fn(); - setup({ onNewProject }); - await userEvent.click(screen.getByTestId("new-session-new-project")); - expect(onNewProject).toHaveBeenCalledTimes(1); + it("新建项目按钮 → onNewAgent", async () => { + const onNewAgent = vi.fn(); + setup({ onNewAgent }); + await userEvent.click(screen.getByTestId("new-session-new-agent")); + expect(onNewAgent).toHaveBeenCalledTimes(1); }); it("关闭按钮 → onDismiss", async () => { diff --git a/emrg/gui/renderer/src/components/NewSessionDialog.tsx b/emrg/gui/renderer/src/components/NewSessionDialog.tsx index 276b7cd3..a7163f66 100644 --- a/emrg/gui/renderer/src/components/NewSessionDialog.tsx +++ b/emrg/gui/renderer/src/components/NewSessionDialog.tsx @@ -1,32 +1,32 @@ import { useI18n } from "../lib/i18n"; -import { projectRowView, type ProjectRow } from "../lib/openSession"; +import { agentRowView, type AgentRow } from "../lib/openSession"; import { Dialog } from "./Dialog"; import { DialogActions } from "./HelpDialog"; /** * NewSessionDialog — 新建会话对话框(Batch 4 slice 3)。 * 镜像 vanilla `showNewSessionDialog()`: - * - loading(projects=null)/ empty(noProjects)/ error(loadFailed)四态 - * - 项目行(name/path/最近活跃)→ 点击 onPickProject(p)(vanilla: newSession({projectPath})) - * - 底部 "+ 新建项目…" → onNewProject(vanilla: pickProjectDir → registerProject → newSession) - * 数据注入式:projects 与回调由调用方(Batch 5 接线 window.emrg.listProjects/pickProjectDir)提供。 + * - loading(agents=null)/ empty(noAgents)/ error(loadFailed)四态 + * - 项目行(name/path/最近活跃)→ 点击 onPickAgent(p)(vanilla: newSession({projectPath})) + * - 底部 "+ 新建项目…" → onNewAgent(vanilla: pickAgentDir → registerAgent → newSession) + * 数据注入式:agents 与回调由调用方(Batch 5 接线 window.emrg.listAgents/pickAgentDir)提供。 */ export interface NewSessionDialogProps { open: boolean; /** null = loading;[] = 空;有值 = 列表 */ - projects?: ProjectRow[] | null; + agents?: AgentRow[] | null; error?: string | null; - onPickProject?: (p: ProjectRow) => void; - onNewProject?: () => void; + onPickAgent?: (p: AgentRow) => void; + onNewAgent?: () => void; onDismiss?: () => void; } export function NewSessionDialog({ open, - projects = null, + agents = null, error = null, - onPickProject, - onNewProject, + onPickAgent, + onNewAgent, onDismiss, }: NewSessionDialogProps) { const { t } = useI18n(); @@ -39,29 +39,29 @@ export function NewSessionDialog({ ); } - if (projects === null) { + if (agents === null) { return (
{t("dlg.loading")}
); } - if (projects.length === 0) { + if (agents.length === 0) { return (
- {t("openSession.noProjects")} + {t("openSession.noAgents")}
); } - return projects.map((p) => { - const v = projectRowView(p, t); + return agents.map((p) => { + const v = agentRowView(p, t); return ( diff --git a/emrg/gui/renderer/src/components/OpenSessionDialog.test.tsx b/emrg/gui/renderer/src/components/OpenSessionDialog.test.tsx index d50948d3..5c8f63ed 100644 --- a/emrg/gui/renderer/src/components/OpenSessionDialog.test.tsx +++ b/emrg/gui/renderer/src/components/OpenSessionDialog.test.tsx @@ -3,14 +3,14 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { OpenSessionDialog } from "./OpenSessionDialog"; import { I18nProvider } from "../lib/i18n"; -import type { ProjectRow, SessionRow } from "../lib/openSession"; +import type { AgentRow, SessionRow } from "../lib/openSession"; /** * OpenSessionDialog.test.tsx — 两步打开会话对话框测试(Batch 4 slice 3)。 - * 镜像 vanilla showOpenSessionDialog/showProjectSessions:项目列表(含删除)→ 会话列表。 + * 镜像 vanilla showOpenSessionDialog/showAgentSessions:项目列表(含删除)→ 会话列表。 */ -const projects: ProjectRow[] = [ +const agents: AgentRow[] = [ { name: "emrg", path: "/x/emrg", latest_session_at: new Date().toISOString() }, { name: "aitokenpool", path: "/x/aitokenpool" }, ]; @@ -23,7 +23,7 @@ const sessions: SessionRow[] = [ function setup(props: Partial[0]> = {}) { return render( - + , ); } @@ -32,19 +32,19 @@ describe("OpenSessionDialog", () => { it("open=false → 不渲染", () => { render( - + , ); expect(screen.queryByTestId("open-session-dialog")).toBeNull(); }); - it("projects loading → dlg.loading 行", () => { - setup({ projects: null }); + it("agents loading → dlg.loading 行", () => { + setup({ agents: null }); expect(screen.getByTestId("open-session-loading")).toHaveTextContent("加载中"); }); - it("空项目列表 → noProjects", () => { - setup({ projects: [] }); + it("空项目列表 → noAgents", () => { + setup({ agents: [] }); expect(screen.getByTestId("open-session-empty")).toHaveTextContent("还没有项目"); }); @@ -53,22 +53,22 @@ describe("OpenSessionDialog", () => { expect(screen.getByTestId("open-session-error")).toHaveTextContent("加载失败:boom"); }); - it("项目行:name/path/活跃 + 删除按钮;点击 → onPickProject", async () => { - const onPickProject = vi.fn(); - setup({ onPickProject }); - const rows = screen.getAllByTestId("open-session-project"); + it("项目行:name/path/活跃 + 删除按钮;点击 → onPickAgent", async () => { + const onPickAgent = vi.fn(); + setup({ onPickAgent }); + const rows = screen.getAllByTestId("open-session-agent"); expect(rows).toHaveLength(2); expect(rows[0]).toHaveTextContent("emrg"); expect(rows[0]).toHaveTextContent("/x/emrg"); await userEvent.click(screen.getAllByTestId("open-session-pick")[1]); - expect(onPickProject).toHaveBeenCalledWith(projects[1]); + expect(onPickAgent).toHaveBeenCalledWith(agents[1]); }); - it("删除按钮 → onDeleteProject", async () => { - const onDeleteProject = vi.fn(); - setup({ onDeleteProject }); + it("删除按钮 → onDeleteAgent", async () => { + const onDeleteAgent = vi.fn(); + setup({ onDeleteAgent }); await userEvent.click(screen.getAllByTestId("open-session-delete")[0]); - expect(onDeleteProject).toHaveBeenCalledWith(projects[0]); + expect(onDeleteAgent).toHaveBeenCalledWith(agents[0]); }); it("sessions 步:标题插值 + 会话行(当前标记)→ onPickSession", async () => { @@ -91,11 +91,11 @@ describe("OpenSessionDialog", () => { it("底部:新建会话 + 新建项目按钮 → 各自回调", async () => { const onNewSession = vi.fn(); - const onNewProject = vi.fn(); - setup({ onNewSession, onNewProject }); + const onNewAgent = vi.fn(); + setup({ onNewSession, onNewAgent }); await userEvent.click(screen.getByTestId("open-session-new-session")); expect(onNewSession).toHaveBeenCalledTimes(1); - await userEvent.click(screen.getByTestId("open-session-new-project")); - expect(onNewProject).toHaveBeenCalledTimes(1); + await userEvent.click(screen.getByTestId("open-session-new-agent")); + expect(onNewAgent).toHaveBeenCalledTimes(1); }); }); diff --git a/emrg/gui/renderer/src/components/OpenSessionDialog.tsx b/emrg/gui/renderer/src/components/OpenSessionDialog.tsx index 19fb55f2..e73cba41 100644 --- a/emrg/gui/renderer/src/components/OpenSessionDialog.tsx +++ b/emrg/gui/renderer/src/components/OpenSessionDialog.tsx @@ -1,35 +1,35 @@ import { useI18n } from "../lib/i18n"; -import { projectRowView, sessionRowView, type ProjectRow, type SessionRow } from "../lib/openSession"; +import { agentRowView, sessionRowView, type AgentRow, type SessionRow } from "../lib/openSession"; import { Dialog } from "./Dialog"; import { DialogActions } from "./HelpDialog"; /** * OpenSessionDialog — 两步打开会话对话框(Batch 4 slice 3)。 - * 镜像 vanilla `showOpenSessionDialog()` / `showProjectSessions()`: - * - step="projects":项目行(name/path/最近活跃 + 删除按钮)→ onPickProject(p) - * - step="sessions":标题 = 打开会话 — {project};会话行(title||id + 当前/活跃标记)→ onPickSession(s) - * - loading / empty / error 四态对齐 vanilla(dlg.loading / openSession.noProjects / noSessions / loadFailed) - * - 删除按钮 → onDeleteProject(p)(Batch 5 接线 confirmDeleteProject:受保护 → 拒绝;否则 ConfirmDialog) - * 数据注入式:step/projects/sessions 由调用方(Batch 5 接线 window.emrg.listProjects/listProjectSessions) + * 镜像 vanilla `showOpenSessionDialog()` / `showAgentSessions()`: + * - step="agents":项目行(name/path/最近活跃 + 删除按钮)→ onPickAgent(p) + * - step="sessions":标题 = 打开会话 — {agent};会话行(title||id + 当前/活跃标记)→ onPickSession(s) + * - loading / empty / error 四态对齐 vanilla(dlg.loading / openSession.noAgents / noSessions / loadFailed) + * - 删除按钮 → onDeleteAgent(p)(Batch 5 接线 confirmDeleteProject:受保护 → 拒绝;否则 ConfirmDialog) + * 数据注入式:step/agents/sessions 由调用方(Batch 5 接线 window.emrg.listAgents/listAgentSessions) * 驱动状态机。 */ -export type OpenSessionStep = "projects" | "sessions"; +export type OpenSessionStep = "agents" | "sessions"; export interface OpenSessionDialogProps { open: boolean; step: OpenSessionStep; /** null = loading;[] = 空;有值 = 列表 */ - projects?: ProjectRow[] | null; + agents?: AgentRow[] | null; /** step="sessions" 时的会话列表 */ sessions?: SessionRow[] | null; /** step="sessions" 时的当前项目名(标题插值) */ projectName?: string; currentSid?: string | null; error?: string | null; - onPickProject?: (p: ProjectRow) => void; + onPickAgent?: (p: AgentRow) => void; onPickSession?: (s: SessionRow) => void; - onDeleteProject?: (p: ProjectRow) => void; - onNewProject?: () => void; + onDeleteAgent?: (p: AgentRow) => void; + onNewAgent?: () => void; onNewSession?: () => void; onDismiss?: () => void; } @@ -37,20 +37,20 @@ export interface OpenSessionDialogProps { export function OpenSessionDialog({ open, step, - projects = null, + agents = null, sessions = null, projectName = "", currentSid = null, error = null, - onPickProject, + onPickAgent, onPickSession, - onDeleteProject, - onNewProject, + onDeleteAgent, + onNewAgent, onNewSession, onDismiss, }: OpenSessionDialogProps) { const { t } = useI18n(); - const title = step === "sessions" ? t("openSession.titleProject", { project: projectName || "…" }) : t("openSession.title"); + const title = step === "sessions" ? t("openSession.titleAgent", { agent: projectName || "…" }) : t("openSession.title"); const renderProjects = () => { if (error) { @@ -60,29 +60,29 @@ export function OpenSessionDialog({ ); } - if (projects === null) { + if (agents === null) { return (
{t("dlg.loading")}
); } - if (projects.length === 0) { + if (agents.length === 0) { return (
- {t("openSession.noProjects")} + {t("openSession.noAgents")}
); } - return projects.map((p) => { - const v = projectRowView(p, t); + return agents.map((p) => { + const v = agentRowView(p, t); return ( -
+
); @@ -151,14 +151,14 @@ export function OpenSessionDialog({ {t("openSession.desc")}

- {step === "projects" ? renderProjects() : renderSessions()} + {step === "agents" ? renderProjects() : renderSessions()}
-
diff --git a/emrg/gui/renderer/src/components/RantDialog.test.tsx b/emrg/gui/renderer/src/components/RantDialog.test.tsx index a0f6d4b3..a8827bd1 100644 --- a/emrg/gui/renderer/src/components/RantDialog.test.tsx +++ b/emrg/gui/renderer/src/components/RantDialog.test.tsx @@ -2,19 +2,19 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { fireEvent, render, screen } from "@testing-library/react"; import { I18nProvider } from "../lib/i18n"; import { RantDialog } from "./RantDialog"; -import type { ProjectRec } from "../lib/workspaceView"; +import type { AgentRec } from "../lib/workspaceView"; /** * RantDialog.test.tsx — Batch 5 slice 8:Rant 提交对话框。 * 镜像 vanilla openRantForm/submitRantForm 语义:打开清空 + 聚焦、消息必填、 - * 项目下拉(已注册项目)、提交 payload {message, project}。 + * 项目下拉(已注册项目)、提交 payload {message, agent}。 */ function wrapper(node: React.ReactNode) { return {node}; } -const PROJECTS: ProjectRec[] = [ +const AGENTS: AgentRec[] = [ { name: "emrg", path: "/p/emrg" }, { name: "sci", path: "/p/sci" }, ]; @@ -25,39 +25,39 @@ afterEach(() => { describe("RantDialog", () => { it("open:渲染消息文本域 + 项目下拉(已注册项目)", () => { - render(wrapper()); + render(wrapper()); expect(screen.getByTestId("rant-dialog")).toBeInTheDocument(); expect((screen.getByTestId("rant-message") as HTMLTextAreaElement).value).toBe(""); - const sel = screen.getByTestId("rant-project") as HTMLSelectElement; - expect(sel.options.length).toBe(PROJECTS.length + 1); // + 空选项 + const sel = screen.getByTestId("rant-agent") as HTMLSelectElement; + expect(sel.options.length).toBe(AGENTS.length + 1); // + 空选项 expect(sel.options[1].value).toBe("emrg"); }); it("空消息 → 内联错误,不提交", () => { const onSubmit = vi.fn(); - render(wrapper()); + render(wrapper()); fireEvent.click(screen.getByTestId("rant-submit")); expect(screen.getByTestId("rant-error")).toHaveTextContent("required"); expect(onSubmit).not.toHaveBeenCalled(); }); - it("提交 payload {message, project}(选项目)", async () => { + it("提交 payload {message, agent}(选项目)", async () => { const onSubmit = vi.fn().mockResolvedValue(undefined); - render(wrapper()); + render(wrapper()); fireEvent.change(screen.getByTestId("rant-message"), { target: { value: "侧栏太宽了" } }); - fireEvent.change(screen.getByTestId("rant-project"), { target: { value: "emrg" } }); + fireEvent.change(screen.getByTestId("rant-agent"), { target: { value: "emrg" } }); fireEvent.click(screen.getByTestId("rant-submit")); await vi.waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); - expect(onSubmit.mock.calls[0][0]).toEqual({ message: "侧栏太宽了", project: "emrg" }); + expect(onSubmit.mock.calls[0][0]).toEqual({ message: "侧栏太宽了", agent: "emrg" }); }); - it("项目选填:留空提交 project=''", async () => { + it("项目选填:留空提交 agent=''", async () => { const onSubmit = vi.fn().mockResolvedValue(undefined); - render(wrapper()); + render(wrapper()); fireEvent.change(screen.getByTestId("rant-message"), { target: { value: "hello" } }); fireEvent.click(screen.getByTestId("rant-submit")); await vi.waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); - expect(onSubmit.mock.calls[0][0]).toEqual({ message: "hello", project: "" }); + expect(onSubmit.mock.calls[0][0]).toEqual({ message: "hello", agent: "" }); }); it("closed (open=false) → 不渲染", () => { diff --git a/emrg/gui/renderer/src/components/RantDialog.tsx b/emrg/gui/renderer/src/components/RantDialog.tsx index 425bad64..9671b7b9 100644 --- a/emrg/gui/renderer/src/components/RantDialog.tsx +++ b/emrg/gui/renderer/src/components/RantDialog.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from "react"; import { useI18n } from "../lib/i18n"; -import type { ProjectRec } from "../lib/workspaceView"; +import type { AgentRec } from "../lib/workspaceView"; import { Dialog } from "./Dialog"; /** @@ -8,19 +8,19 @@ import { Dialog } from "./Dialog"; * 镜像 vanilla `openRantForm()` / `submitRantForm()`(renderer/js/dialogs.js): * * - 打开即清空消息、聚焦输入; - * - 项目下拉 = 已注册项目(选填,value=name||path,与 daemon rant 协议 project 字段一致); + * - 项目下拉 = 已注册项目(选填,value=name||path,与 daemon rant 协议 agent 字段一致); * - 消息必填(空 → 内联错误;vanilla 用 showConfirm 提示); - * - 提交 → onSubmit({message, project})(调用方接线 sendRant + 刷新列表 + 反馈)。 + * - 提交 → onSubmit({message, agent})(调用方接线 sendRant + 刷新列表 + 反馈)。 */ export interface RantDialogProps { open: boolean; /** 项目下拉选项(仅已注册项目;选填) */ - projects?: ProjectRec[]; - onSubmit?: (payload: { message: string; project: string }) => void | Promise; + agents?: AgentRec[]; + onSubmit?: (payload: { message: string; agent: string }) => void | Promise; onDismiss?: () => void; } -export function RantDialog({ open, projects = [], onSubmit, onDismiss }: RantDialogProps) { +export function RantDialog({ open, agents = [], onSubmit, onDismiss }: RantDialogProps) { const { t } = useI18n(); const [err, setErr] = useState(null); const msgRef = useRef(null); @@ -41,7 +41,7 @@ export function RantDialog({ open, projects = [], onSubmit, onDismiss }: RantDia return; } try { - await onSubmit?.({ message: msg, project: projectRef.current?.value ?? "" }); + await onSubmit?.({ message: msg, agent: projectRef.current?.value ?? "" }); } finally { onDismiss?.(); } @@ -76,10 +76,10 @@ export function RantDialog({ open, projects = [], onSubmit, onDismiss }: RantDia