From f59bf0db9b98b982bd52da9f87c42a0959f1de3f Mon Sep 17 00:00:00 2001 From: ciregenz Date: Wed, 20 May 2026 05:36:17 -0700 Subject: [PATCH] defluff (frontend + backend): strip em-dashes + shorten docstrings + drop dead UI files (cosmetic only, no schedule code) --- backend/apps/agents/agents.py | 95 +--- backend/apps/agents/mcp_meta_server.py | 34 +- backend/apps/agents/models.py | 105 +---- backend/apps/agents/ws_manager.py | 71 +-- backend/apps/health/health.py | 6 - backend/apps/modes/models.py | 36 +- backend/apps/modes/modes.py | 5 +- backend/apps/outputs/models.py | 2 +- backend/apps/outputs/outputs.py | 30 +- backend/apps/outputs/runtime.py | 126 ++---- backend/apps/service/models.py | 6 +- backend/apps/settings/credentials.py | 71 +-- backend/apps/settings/models.py | 58 +-- backend/apps/settings/settings.py | 65 +-- backend/apps/skills/models.py | 6 +- frontend/src/app/Main.tsx | 71 +-- frontend/src/app/components/DynamicIsland.tsx | 96 +--- .../components/ElementSelectionContext.tsx | 11 +- frontend/src/app/components/ErrorBoundary.tsx | 18 +- frontend/src/app/components/ErrorSlime.tsx | 2 +- .../app/components/GlobalSearchPalette.tsx | 22 +- .../src/app/components/Layout/AppShell.tsx | 112 +---- .../app/components/Layout/DashboardHost.tsx | 21 +- frontend/src/app/components/Loading.tsx | 18 +- .../Onboarding/OnboardingDirector.ts | 45 +- .../components/Onboarding/OnboardingPanel.tsx | 119 +---- .../Onboarding/OnboardingProgressSlice.ts | 42 +- .../Onboarding/OnboardingRoadmapModal.tsx | 24 +- .../components/Onboarding/OnboardingRoot.tsx | 85 +--- .../components/Onboarding/ac/ACGestures.ts | 12 +- .../app/components/Onboarding/ac/ACPopup.tsx | 80 +--- .../components/Onboarding/ac/ACTypewriter.ts | 79 +--- .../Onboarding/ac/AgenticCursor.tsx | 126 +----- .../app/components/Onboarding/ac/acRuntime.ts | 421 ++---------------- .../components/Onboarding/ac/cursorStore.ts | 25 +- .../src/app/components/Onboarding/eventBus.ts | 45 +- .../app/components/Onboarding/selectors.ts | 82 +--- .../Onboarding/steps/skipPredicates.ts | 36 +- .../Onboarding/steps/step02_enableActions.ts | 15 +- .../Onboarding/steps/step03_launchAgent.ts | 19 +- .../Onboarding/steps/step04_useBrowser.ts | 9 +- .../steps/step05_agentUseBrowser.ts | 25 +- .../steps/step06_agentControlAgents.ts | 29 +- .../Onboarding/steps/step07_installSkill.ts | 5 +- .../Onboarding/steps/step08_makeApp.ts | 40 +- .../app/components/Onboarding/steps/types.ts | 41 +- .../app/components/Onboarding/telemetry.ts | 10 +- frontend/src/app/components/PixelBlast.tsx | 28 +- frontend/src/app/components/PlanPicker.tsx | 36 +- .../src/app/components/RichPromptEditor.tsx | 2 +- frontend/src/app/components/SignInGate.tsx | 34 +- .../app/components/useDomElementSelector.ts | 14 +- .../src/app/pages/AgentChat/ApprovalBar.tsx | 50 +-- .../AgentChat/BrowserAgentInlineFeed.tsx | 27 +- .../src/app/pages/AgentChat/ChatInput.tsx | 184 ++------ .../app/pages/AgentChat/CompactionMarker.tsx | 11 +- .../src/app/pages/AgentChat/ContextDrawer.tsx | 18 +- .../app/pages/AgentChat/MessageActionBar.tsx | 6 +- .../src/app/pages/AgentChat/MessageBubble.tsx | 54 +-- .../app/pages/AgentChat/StreamingBubble.tsx | 38 +- .../app/pages/AgentChat/ToolCallBubble.tsx | 34 +- .../app/pages/AgentChat/ToolGroupBubble.tsx | 1 - .../src/app/pages/AgentChat/toolLabels.ts | 25 +- .../src/app/pages/Analytics/Analytics.tsx | 4 +- frontend/src/app/pages/Commands/Commands.tsx | 5 - .../app/pages/Customization/Customization.tsx | 2 - .../pages/Dashboard/BrowserAgentOverlay.tsx | 18 +- .../src/app/pages/Dashboard/BrowserCard.tsx | 72 +-- .../app/pages/Dashboard/CanvasControls.tsx | 11 +- .../app/pages/Dashboard/CardSearchPalette.tsx | 1 - .../app/pages/Dashboard/DashboardViewCard.tsx | 40 +- .../app/pages/Dashboard/DirectionHints.tsx | 2 - frontend/src/app/pages/Dashboard/NoteCard.tsx | 13 +- .../Dashboard/captureDashboardThumbnail.ts | 6 +- .../app/pages/Dashboard/useCanvasControls.ts | 75 +--- .../Dashboard/useOverlayScrollPassthrough.ts | 12 +- frontend/src/app/pages/Modes/Modes.tsx | 10 +- frontend/src/app/pages/Settings/Settings.tsx | 325 +++----------- .../src/app/pages/Skills/SkillBuilderChat.tsx | 16 +- frontend/src/app/pages/Skills/Skills.tsx | 18 - frontend/src/app/pages/Tools/Tools.tsx | 96 +--- frontend/src/app/pages/Views/CodeEditor.tsx | 1 - .../src/app/pages/Views/TerminalPanel.tsx | 21 +- frontend/src/app/pages/Views/ViewCard.tsx | 16 +- frontend/src/app/pages/Views/ViewEditor.tsx | 363 +++------------ frontend/src/app/pages/Views/ViewPreview.tsx | 117 +---- frontend/src/app/pages/Views/Views.tsx | 4 +- .../app/pages/Views/captureViewThumbnail.ts | 20 +- .../pages/Views/useIframeElementSelector.ts | 2 - frontend/src/index.tsx | 15 +- frontend/src/shared/browserCommandHandler.ts | 59 +-- frontend/src/shared/canvasInteractionState.ts | 15 +- frontend/src/shared/config.ts | 46 +- .../src/shared/hooks/useDashboardActive.tsx | 13 +- frontend/src/shared/hooks/useDeepLink.ts | 30 +- .../shared/hooks/useInteractionHeartbeat.ts | 8 +- .../src/shared/hooks/useLastDashboardId.ts | 21 +- frontend/src/shared/hooks/useReducedMotion.ts | 23 +- frontend/src/shared/hooks/useRouteTracker.ts | 15 +- .../src/shared/hooks/useRuntimePreviewUrl.ts | 72 +-- frontend/src/shared/hooks/useWindowFocus.ts | 13 +- frontend/src/shared/migrations.ts | 36 +- frontend/src/shared/notifications.ts | 21 +- frontend/src/shared/resolveUrl.ts | 12 +- frontend/src/shared/serviceClient.ts | 41 +- frontend/src/shared/state/agentsSlice.ts | 126 ++---- frontend/src/shared/state/dashboardsSlice.ts | 4 +- frontend/src/shared/state/interactionSlice.ts | 18 +- frontend/src/shared/state/modelsSlice.ts | 7 +- frontend/src/shared/state/outputsSlice.ts | 11 +- frontend/src/shared/state/skillsSlice.ts | 4 +- frontend/src/shared/state/streamingSlice.ts | 38 +- .../src/shared/state/subscriptionsSlice.ts | 21 +- frontend/src/shared/state/tempStateSlice.ts | 1 - frontend/src/shared/styles/motionTokens.ts | 31 +- frontend/src/shared/subscription/checkout.ts | 13 +- frontend/src/shared/useBrowserActivity.ts | 6 +- frontend/src/types/electron.d.ts | 2 - 118 files changed, 853 insertions(+), 4202 deletions(-) diff --git a/backend/apps/agents/agents.py b/backend/apps/agents/agents.py index 68f4b91f..50885af5 100644 --- a/backend/apps/agents/agents.py +++ b/backend/apps/agents/agents.py @@ -11,12 +11,7 @@ import logging logger = logging.getLogger(__name__) -# In-flight dedup map for generate-group-meta. Keyed by (session_id, group_id). -# When the frontend issues N concurrent requests for the same group (which it -# can during heavy streaming), we only fire ONE upstream Anthropic call and -# return the same Future to all callers. Eliminates the 429 thundering herd -# without changing retry/fallback semantics — each unique (session, group) -# still gets its full retry budget, just not multiplied by N callers. +# Dedup concurrent generate-group-meta calls; collapses the 429 thundering herd by sharing one upstream Future per (session, group). _group_meta_inflight: dict[tuple[str, str], asyncio.Future] = {} @asynccontextmanager @@ -32,7 +27,6 @@ async def agents_lifespan(): agents = SubApp("agents", agents_lifespan) -# REST Endpoints @agents.router.get("/sessions") async def list_sessions(dashboard_id: str = ""): @@ -71,12 +65,7 @@ async def send_message(session_id: str, body: dict): if not prompt: raise HTTPException(status_code=400, detail="prompt is required") - # Pre-flight MCP suggestion (Phase 3, Layer N). Runs in parallel with - # the agent launch path — if it produces suggestions, they're - # surfaced inline in the chat via agent:mcp_suggestions WS event. - # Fails open: any error from the classifier is swallowed and the - # agent proceeds normally. The classifier is short-circuited for - # obviously-local prompts (greetings, shell commands, file paths). + # Run MCP-suggestion classifier in parallel with the agent launch; fails open. try: from backend.apps.agents.mcp_preflight import run_preflight from backend.apps.agents.ws_manager import ws_manager as _ws @@ -93,7 +82,6 @@ async def send_message(session_id: str, body: dict): except Exception: pass - # Non-blocking — don't gate the agent on the classifier. import asyncio as _asyncio _asyncio.create_task(_emit_preflight()) except Exception: @@ -161,11 +149,7 @@ async def generate_group_meta(session_id: str, body: dict): if not group_id or not tool_calls: raise HTTPException(status_code=400, detail="group_id and tool_calls are required") - # In-flight dedup. If an identical request is already running, await its - # result instead of firing another Anthropic call. This is the entire fix - # for the 429 storm we were seeing — N concurrent identical requests - # collapse to 1 upstream call. Refinement requests bypass dedup since - # they may legitimately want fresh results with different inputs. + # Dedup: share an in-flight Future across callers; refinement requests bypass since they may want fresh results. is_refinement = body.get("is_refinement", False) key = (session_id, group_id) if not is_refinement: @@ -174,8 +158,7 @@ async def generate_group_meta(session_id: str, body: dict): try: return await existing except Exception: - # If the in-flight call failed, fall through and try again - # ourselves rather than propagating someone else's error. + # In-flight call failed; retry ourselves rather than propagate someone else's error. pass future: asyncio.Future = asyncio.get_event_loop().create_future() @@ -197,7 +180,6 @@ async def generate_group_meta(session_id: str, body: dict): future.set_exception(e) raise finally: - # Always clear our slot if we own it, so the next request runs fresh. if not is_refinement and _group_meta_inflight.get(key) is future: _group_meta_inflight.pop(key, None) @@ -267,12 +249,7 @@ async def resume_session(session_id: str): @agents.router.post("/sessions/{session_id}/warm-cache") async def warm_session_cache(session_id: str): - """Fire a max_tokens=1 dummy request through the agent path so - Anthropic processes the system+tools prefix and writes the prompt - cache. The next real user turn lands a cache hit instead of paying - cold-start TTFT. Non-blocking, fire-and-forget on the frontend. - Returns 200 even on failure (best-effort). - """ + """Fire a max_tokens=1 dummy request to prime the Anthropic prompt cache; best-effort.""" try: await agent_manager.warm_prompt_cache(session_id) except Exception: @@ -280,10 +257,6 @@ async def warm_session_cache(session_id: str): return {"ok": True} -# --------------------------------------------------------------------------- -# 9Router / Subscription endpoints -# --------------------------------------------------------------------------- - @agents.router.get("/subscriptions/status") async def subscriptions_status(): """Check if 9Router is running and list connected providers.""" @@ -292,8 +265,7 @@ async def subscriptions_status(): return {"running": False, "providers": [], "models": []} connections = await get_providers() models = await get_models() - # Frontend consumers (OnboardingModal, Settings) read - # `data.providers.connections` — preserve that envelope here. + # Frontend reads data.providers.connections; preserve the envelope. return {"running": True, "providers": {"connections": connections}, "models": models} @@ -310,11 +282,7 @@ async def subscriptions_connect(body: dict): if not is_running(): raise HTTPException(status_code=503, detail="9Router not available. Please install Node.js.") - # If reconnecting a primary lane (e.g. gemini-cli), drop its cascade - # siblings first. The registry prefers antigravity over gemini-cli - # when both are present, so a stale antigravity token would keep - # 400ing even after gemini-cli refreshes. Wiping the sibling forces - # the registry onto the freshly reconnected lane. + # Reconnecting gemini-cli must wipe antigravity; registry prefers AG and a stale AG token would 400 after gemini-cli refreshes. cascade = _PROVIDER_CASCADE_REMOVES.get(provider, []) if cascade: try: @@ -325,7 +293,6 @@ async def subscriptions_connect(body: dict): try: result = await start_oauth(provider) - # For auth_code flows, store pending state so the callback can exchange if result.get("flow") == "authorization_code" and result.get("state"): from backend.main import _pending_oauth _pending_oauth[result["state"]] = { @@ -399,8 +366,7 @@ async def subscriptions_models(): @agents.router.post("/probe-model") async def probe_model(body: dict): - """1-token health probe. Returns {ok, latency_ms} or {ok:false, error} - or {ok:true, skipped:true} when the route's ambiguous (silent beats wrong).""" + """1-token health probe; returns latency or skipped when the route is ambiguous (silent beats wrong).""" import time as _time short_name = (body or {}).get("model") or "" if not short_name: @@ -459,8 +425,7 @@ async def probe_model(body: dict): except Exception as e: msg = str(e).splitlines()[0] if str(e) else type(e).__name__ low = msg.lower() - # Suppress transients — chat will retry naturally and probe-time aliasing - # 404s often differ from how the chat path resolves the same id. + # Suppress transients: chat retries naturally and probe-time alias 404s often differ from chat resolution. if any(s in low for s in ( "timeout", "timed out", "connection reset", "connection aborted", @@ -489,7 +454,7 @@ async def list_models(): try: conns = await _9r_providers() raw_providers = {c.get("provider", "") for c in conns if c.get("isActive") or c.get("testStatus") == "active"} - # 9Router uses "claude"; our models use api="anthropic" — map across. + # 9Router uses "claude"; our models use api="anthropic". Map across. _9R_TO_API = { "claude": "anthropic", "codex": "codex", @@ -501,8 +466,7 @@ async def list_models(): logger.debug(f"Failed to fetch 9Router providers: {e}") def _serialize(models: list[dict]) -> list[dict]: - # Native models. Tiers describe the model itself; billing_kind - # describes the user's wallet for it. Pricing is shown only for paid. + # Tiers describe the model; billing_kind describes the wallet. Pricing shown only for paid. from backend.apps.agents.providers.registry import ( COST_PER_1M_TOKENS, compute_tiers, @@ -533,7 +497,7 @@ async def list_models(): "reasoning": bool(m.get("reasoning", False)), "input_cost_per_1m": input_cost, "output_cost_per_1m": output_cost, - # Strict — subscription doesn't count. Pickerside uses Subscription chip. + # Strict free; subscriptions show via the picker's Subscription chip. "is_free": billing_kind == "free", "billing_kind": billing_kind, "tiers": list(tiers), @@ -554,8 +518,7 @@ async def list_models(): cc_variants = [m for m in anthropic_models if m.get("route") == "cc"] api_variants = [m for m in anthropic_models if m.get("route") == "api"] - # Pro mode shows two groups (Pro proxy + Anthropic alternates via cc/api); - # own-key mode collapses to one Anthropic group using adaptive routing. + # Pro mode splits into Pro proxy + Anthropic alternates; own-key collapses to one adaptive group. notes: list[dict] = [] if is_openswarm_pro: result["OpenSwarm Pro"] = _serialize(adaptive) @@ -620,8 +583,7 @@ async def list_models(): if visible: result[provider_name] = visible - # OR catalog fetched straight from openrouter.ai (independent of 9Router - # boot state) so picker populates the moment a key lands. + # Fetch OpenRouter catalog directly (independent of 9Router) so picker fills the moment a key lands. if has_openrouter_key: try: from backend.apps.agents.providers.registry import fetch_openrouter_models @@ -669,10 +631,7 @@ async def list_models(): entries = sorted(by_vendor[vendor], key=lambda x: x["label"].lower()) result[f"OpenRouter · {pretty}"] = entries - # User-configured custom OpenAI-compatible providers (Ollama Cloud, Together, etc). - # Each provider becomes its own group in the picker; each model is addressed via - # the `custom//` value, which `_find_builtin_model` synthesises - # into a route='api' / api='custom' entry at request time. + # Custom OpenAI-compatible providers (Ollama Cloud, Together, etc); addressed via custom//. from backend.apps.agents.providers.registry import _custom_provider_slug_for_lookup for cp in (getattr(settings, "custom_providers", None) or []): cp_name = (getattr(cp, "name", "") or "").strip() @@ -707,27 +666,14 @@ async def list_models(): return {"models": result, "notes": notes} -# Google's two OAuth lanes (gemini-cli and antigravity) share user-facing -# meaning (both = "Google subscription") but 9Router treats them as -# separate connections with independent token lifecycles. The registry -# prefers `ag/` over `gc/` whenever AG is active because AG bypasses the -# thoughtSignature validator that breaks multi-step tool turns. That -# preference becomes a footgun when AG's token expires silently: the -# user reconnects "Google", only gemini-cli refreshes, and every request -# still routes through the stale AG token -> 400 Invalid argument. -# -# Cascade is one-directional. gemini-cli is the primary lane the UI -# exposes; operations on it sweep antigravity too. Direct operations on -# antigravity (e.g. an explicit AG opt-in/out path) MUST NOT cascade -# back to gemini-cli or we'd nuke the user's main Google connection. +# gemini-cli and antigravity are two Google OAuth lanes; registry prefers AG, so we cascade-wipe AG when reconnecting gemini-cli to avoid stale-AG 400s. One-directional: AG operations MUST NOT cascade back. _PROVIDER_CASCADE_REMOVES: dict[str, list[str]] = { "gemini-cli": ["antigravity"], } async def _delete_provider_connections(providers: list[str]) -> int: - """Delete all 9Router connections whose provider is in the given list. - Returns the count actually removed. Silent if 9Router is unreachable.""" + """Delete 9Router connections in `providers`; returns count removed, silent on 9Router unreachable.""" import httpx from backend.apps.nine_router import NINE_ROUTER_API, get_providers try: @@ -748,12 +694,7 @@ async def _delete_provider_connections(providers: list[str]) -> int: @agents.router.post("/subscriptions/disconnect") async def subscriptions_disconnect(body: dict): - """Disconnect a subscription provider via 9Router. - - For Google's paired lanes (gemini-cli + antigravity), wipe BOTH so a - subsequent reconnect lands on a clean slate instead of resurrecting - a stale sibling. - """ + """Disconnect a subscription provider via 9Router; cascades-wipe Google's paired lanes.""" provider = body.get("provider", "") if not provider: raise HTTPException(status_code=400, detail="provider required") diff --git a/backend/apps/agents/mcp_meta_server.py b/backend/apps/agents/mcp_meta_server.py index 887388b4..f4672136 100644 --- a/backend/apps/agents/mcp_meta_server.py +++ b/backend/apps/agents/mcp_meta_server.py @@ -1,23 +1,5 @@ #!/usr/bin/env python3 -"""Stdio MCP server exposing the MCP activation gate. - -Tools: - - MCPList: enumerate installed MCP servers (active + available). - - MCPSearch(query): rank servers by relevance to a free-form query. - - MCPActivate(server_name): activate a server for the rest of the session. - -The activation gate is the dispatch-layer enforcement of the product invariant -"all MCP actions only via ToolSearch": the model can only reach an MCP server's -tools if the user has approved MCPActivate for that server, which appends to -session.active_mcps. _build_mcp_servers in agent_manager.py intersects connected -MCPs with that list before handing them to the SDK, so unactivated servers are -literally unreachable — the gate cannot be bypassed by ignoring prompt rules. - -HITL: the model's invocation of MCPActivate goes through agent_manager's pre- -tool approval hook just like any other tool call — the user is prompted to -approve activation in the standard ApprovalBar UI. No separate HITL inside this -server. -""" +"""Stdio MCP server exposing the MCP activation gate (MCPList/MCPSearch/MCPActivate).""" import json import os @@ -69,7 +51,7 @@ TOOLS = [ "description": ( "Request activation of an MCP server for this session. Triggers a " "user approval prompt; on approve the server's tools become callable " - "next turn. Always confirm the server name via MCPList/MCPSearch first — " + "next turn. Always confirm the server name via MCPList/MCPSearch first; " "invalid names return alternatives instead of activating." ), "inputSchema": { @@ -81,7 +63,7 @@ TOOLS = [ }, "reason": { "type": "string", - "description": "Why you need it — shown to the user in the approval prompt.", + "description": "Why you need it; shown to the user in the approval prompt.", }, }, "required": ["server_name"], @@ -133,7 +115,7 @@ def format_servers(servers: list[dict], heading: str = "") -> str: name = s.get("name", "") desc = s.get("description") or f"{name} integration" status = s.get("status", "available") - lines.append(f"- `{name}` [{status}] — {desc}") + lines.append(f"- `{name}` [{status}]; {desc}") return "\n".join(lines) @@ -189,15 +171,17 @@ def handle_tool_call(tool_name: str, arguments: dict) -> dict: "isError": True, } if result.get("status") == "already_active": - return {"content": [{"type": "text", "text": f"`{server_name}` is already active for this session — its tools should be callable now."}]} + return {"content": [{"type": "text", "text": f"`{server_name}` is already active for this session; its tools should be callable now."}]} if result.get("status") == "activated": return { "content": [{ "type": "text", "text": ( f"Activated `{server_name}`. Its tools (`mcp__{server_name}__*`) " - f"will be callable on the NEXT turn. End this turn now and the user's " - f"next message will see the new tools." + f"are NOT callable in this turn; the transport snapshot is " + f"already locked. This turn will end automatically and a " + f"hidden continuation turn will fire with the new tools " + f"loaded. Do not attempt any other tool call now." ), }], } diff --git a/backend/apps/agents/models.py b/backend/apps/agents/models.py index c36ef8a9..640b338d 100644 --- a/backend/apps/agents/models.py +++ b/backend/apps/agents/models.py @@ -11,7 +11,7 @@ class AgentConfig(BaseModel): system_prompt: Optional[str] = None allowed_tools: list[str] = Field(default_factory=lambda: ["Read", "Edit", "Write", "Bash", "Glob", "Grep", "AskUserQuestion"]) max_turns: Optional[int] = None - target_directory: Optional[str] = None # if None, uses repo root + target_directory: Optional[str] = None dashboard_id: Optional[str] = None class ApprovalRequest(BaseModel): @@ -55,26 +55,15 @@ class Message(BaseModel): forced_tools: Optional[list[str]] = None images: Optional[list[dict]] = None hidden: bool = False - # Optional client-generated id used by the frontend to reconcile an - # optimistic message bubble (rendered synchronously on send) with the - # server-confirmed echo. Plumbed through send_message and round-tripped - # back via the agent:message WS event so the frontend can dedupe. + # Frontend-generated id for optimistic-bubble dedup against the server echo. client_message_id: Optional[str] = None - # Wall-clock duration in milliseconds spent producing this message's - # content. For thinking blocks: time from content_block_start → - # content_block_stop. Lets the persisted ThinkingBubble show - # "Thought for Ns" on reload instead of falling back to the static - # "Thoughts" label. Optional for back-compat with messages saved - # before this field existed. + # Wall-clock ms producing this message's content; for thinking, content_block_start -> stop. Lets reloaded bubbles show "Thought for Ns". elapsed_ms: Optional[int] = None - # Approximate output tokens for this message's content. For thinking - # blocks we use the same char/3.6 heuristic the live UI uses so the - # number frozen on the persisted bubble matches what the user saw - # rising during the stream. Pure display, not billing. + # Approx output tokens; thinking uses char/3.6 to match the live UI's count. Display only. tokens: Optional[int] = None - # tool_count drives the "3 tools used" segment on the thinking pill. + # Drives the "N tools used" segment on the thinking pill. tool_count: Optional[int] = None - # combined input + output + children tokens for the turn (overloaded name). + # Combined input + output + children tokens for the turn (overloaded name). input_tokens: Optional[int] = None class MessageBranch(BaseModel): @@ -101,40 +90,22 @@ class AgentSession(BaseModel): allowed_tools: list[str] = Field(default_factory=list) max_turns: Optional[int] = None cwd: Optional[str] = None - # Origin remote and branch resolved at session start. Persisted so a - # resumed session reattaches to the same project even if the user has - # since `cd`'d elsewhere; also surfaced in the session list UI so the - # user can tell two sessions apart by repo. + # Resolved at session start so resume reattaches to the same repo even after the user cd's elsewhere. repo_url: Optional[str] = None branch: Optional[str] = None created_at: datetime = Field(default_factory=datetime.now) closed_at: Optional[datetime] = None - # Wall-clock of the first stream event from the agent SDK. Set once - # at the start of the first turn so resumed sessions can show "first - # response was at HH:MM" in the session list without rescanning the - # message log. + # Wall-clock of the first stream event so resumed sessions can show "first response at HH:MM" without rescan. first_response_at: Optional[datetime] = None - # Operational log of HITL approval decisions, one entry per request: - # {tool, behavior, decision_ms}. Persisted alongside the session so a - # reload restores the full approval timeline (which calls were - # approved, denied, and how long each took). + # HITL approval log: {tool, behavior, decision_ms} per entry. approval_decisions: list[dict] = Field(default_factory=list) cost_usd: float = 0.0 tokens: dict[str, int] = Field(default_factory=lambda: {"input": 0, "output": 0}) - # Total wall-clock ms the agent spent in `status="running"`. Accumulates - # across turns; persists across resume. Used by the session-close - # report so we can report "agent active time" alongside total session - # duration. Off by default so legacy sessions deserialize cleanly. + # Total ms in status="running", accumulated across turns/resume; powers session-close "agent active time". agent_active_ms: int = 0 - # Accumulated wall-clock ms spent on each model. Updated when the - # active model changes (model switch) or on close. Surfaced in the - # session header so the user can see "Sonnet: 45s · Haiku: 12s" - # without scanning turns by hand. + # Per-model wall-clock ms; updated on model switch or close. time_per_model: dict[str, int] = Field(default_factory=dict) - # Per-tool latency rollup: { tool_name: { count, total_ms, max_ms } }. - # Populated as tools complete. Surfaced in the session "tools used" - # row so the user can see which tool calls were slow without - # opening every turn. + # Per-tool latency: { tool_name: { count, total_ms, max_ms } }. tool_latencies: dict[str, dict] = Field(default_factory=dict) browser_domains: list[str] = Field(default_factory=list) messages: list[Message] = Field(default_factory=list) @@ -146,58 +117,20 @@ class AgentSession(BaseModel): browser_id: Optional[str] = None parent_session_id: Optional[str] = None needs_fork: bool = False - # Stronger than needs_fork: when True, the next turn drops `resume=` - # entirely and replays history into a brand-new sdk_session_id. This - # is the only way to make the bundled CLI re-read mcp_servers from - # the rebuilt options dict — `fork_session=True` only forks the - # conversation tree, it inherits the original transport's MCP server - # set. Set after MCPActivate when prior turns exist so the newly - # activated server's tools actually reach the model. + # Stronger than needs_fork: drop resume= and replay history into a fresh sdk_session_id; fork_session alone won't re-read mcp_servers. needs_fresh_session: bool = False - # Set when MCPActivate (or analogous activation) wants the agent to - # auto-continue immediately after the current turn ends — without - # requiring the user to type another message. The agent loop reads - # this at the end of `_run_agent_loop`; if set, it clears it and - # dispatches a new hidden turn with `pending_continuation_prompt` as - # the prompt. Race-free vs. the original asyncio-task approach. + # Auto-continue: agent loop dispatches a hidden turn at end-of-loop using pending_continuation_prompt. Race-free vs background tasks. pending_continuation: bool = False pending_continuation_prompt: Optional[str] = None - # Sanitized server names (matching tools_lib._sanitize_server_name) of MCP - # servers the model has explicitly activated this session via the - # MCPActivate meta-tool. Empty by default — the gate in - # _build_mcp_servers intersects connected MCPs with this list, so no - # MCP tool is callable until the model searches for and activates a - # server. The product invariant is that this is non-bypassable: the - # filter lives at the dispatch layer (mcp_servers passed to the SDK), - # not the prompt layer. + # Sanitized server names model has explicitly activated this session; _build_mcp_servers intersects connected MCPs with this. Non-bypassable; dispatch-layer gate. active_mcps: list[str] = Field(default_factory=list) - # Estimated framework preamble tokens (preset + tool defs + MCP descs + - # composed prompt). Subtracted from displayed input for honest "this turn" - # numbers. Heuristic; clamped >= 0. + # Heuristic preamble tokens (preset + tool defs + MCP descs + composed prompt); subtracted from displayed input. framework_overhead_tokens: int = 0 - # Compaction state. compact_threshold_pct is the live ctx_used ratio - # that triggers _maybe_compact at the next turn boundary — turn-based - # thresholds break under uneven workloads (one big Bash dump fills - # context fast; 30 chitchat turns barely move it). 0.65 = 130K of the - # 200K standard tier. compacted_through_msg_id is the last message id - # covered by the most recent summary so we don't re-summarize on - # every turn. + # Live ctx_used ratio triggering _maybe_compact at the next turn boundary; turn-based thresholds break under uneven workloads. 0.65 = 130K of 200K. compact_threshold_pct: float = 0.65 compacted_through_msg_id: Optional[str] = None - # Pre-send hard guard. Fires later than the compaction threshold — - # 0.90 of 200K = 180K — to give the auto-compact path a chance to - # bring the request back under the ceiling. If still over after - # compaction, LRU-trim the oldest active_mcps. Past this we surface - # the friendly context-overflow card instead of letting a 429 hit. + # Hard pre-send guard at 0.90 (= 180K); past compaction we LRU-trim active_mcps, then surface the overflow card. context_soft_cap_pct: float = 0.90 context_window: int = 200_000 - # How much the model should "think" before answering. Provider-agnostic - # value that gets translated per-API in agent_manager: - # off — no thinking - # low — minimal thinking (fastest) - # medium — balanced - # high — extensive thinking (slowest, smartest) - # auto — let the model / provider default decide (recommended) - # Only applies to models flagged with reasoning: True in the registry. - # Existing sessions without this field will default to "auto". + # Provider-agnostic thinking level (off/low/medium/high/auto), translated per-API in agent_manager; only affects reasoning-flagged models. thinking_level: Literal["off", "low", "medium", "high", "auto"] = "auto" diff --git a/backend/apps/agents/ws_manager.py b/backend/apps/agents/ws_manager.py index 67fde187..49fdd848 100644 --- a/backend/apps/agents/ws_manager.py +++ b/backend/apps/agents/ws_manager.py @@ -9,19 +9,7 @@ logger = logging.getLogger(__name__) class ConnectionManager: - """Manages WebSocket connections and bridges HITL approval requests. - - Every outbound event flows through the seq log so reconnecting - clients can replay missed events. The send happens *under* the - per-session lock yielded by `seq_log.stamp(...)`, which guarantees - wire order matches seq order even under concurrent broadcasts. - - A WS disconnect (`disconnect_session`) ONLY removes the socket - from the connection registry. It does NOT cancel the underlying - agent task. The task lives on `agent_manager.tasks`; only an - explicit `agent:stop`, REST `/close`, natural completion, or - process shutdown ends a run. - """ + """Manages WebSocket connections and HITL approval bridging; events flow through seq_log so reconnects can replay.""" def __init__(self): self.connections: dict[str, list[WebSocket]] = {} @@ -53,19 +41,7 @@ class ConnectionManager: ] async def send_to_session(self, session_id: str, event: str, data: dict): - """Broadcast a session event with monotonic sequencing. - - The send to every socket happens inside the seq_log lock so a - slow/dead WS doesn't reorder events on the fast ones. If a - single send raises (broken pipe, half-open socket), we log and - continue — the ring buffer still has the event so the client - will replay it on reconnect. - - For terminal status events (completed/stopped/error) we also - atomically persist the payload to disk; a client that returns - after a process restart can then resolve the spinner via - `seq_log.load_terminal(...)` instead of being stuck. - """ + """Broadcast a session event with monotonic sequencing; terminal statuses also persist to disk.""" async with seq_log.stamp(session_id, event, data) as (seq, payload_str): for ws in list(self.connections.get(session_id, [])): try: @@ -77,38 +53,17 @@ class ConnectionManager: await ws.send_text(payload_str) except Exception: logger.debug("send_to_session: global send failed", exc_info=True) - # Persist terminal events under the lock so a concurrent - # `agent:status: running` can't race past and overwrite - # the disk file with a stale state. + # Persist under the lock so a concurrent running status can't race past and overwrite with stale state. if event == "agent:status" and data.get("status") in TERMINAL_STATUSES: seq_log.persist_terminal(session_id, payload_str) async def replay_to( self, session_id: str, websocket: WebSocket, last_seq: int ) -> dict: - """Replay buffered events with seq > last_seq to one socket. - - Returns a small ack envelope describing what happened so the - caller (the WS handler) can send a `server:resume_ack` frame. - - Three cases: - 1. `events` non-empty: replay them in order; ack carries - `from_seq`, `to_seq`. - 2. No buffer at all (process restarted, session evicted) - but a persisted terminal exists: send it; ack signals - `terminal_only=True`. - 3. `last_seq` predates the oldest buffered seq: emit - `agent:gap_detected`; client REST-refreshes the session. - """ + """Replay buffered events with seq > last_seq; returns ack envelope for the resume handshake.""" oldest, newest, events = seq_log.replay(session_id, last_seq) - # Check for gap FIRST. If the client's last_seq is below the - # buffer's oldest seq, we can't deliver everything they - # missed — silently replaying only the in-buffer tail would - # leave a hole in their state. Tell them to REST-refresh - # instead, even if the tail looks safe to send. - # Treat last_seq=0 as "fresh client" — they want a full - # replay of whatever's in the buffer, not a gap signal. + # Gap-check first: if last_seq predates the buffer, signal REST-refresh; last_seq=0 means fresh client (full replay). if last_seq > 0 and oldest is not None and last_seq < oldest - 1: gap_payload = json.dumps({ "event": "agent:gap_detected", @@ -162,7 +117,6 @@ class ConnectionManager: "to_seq": newest, } - # Nothing in memory. Try a persisted terminal event. terminal = seq_log.load_terminal(session_id) if terminal is not None: try: @@ -171,7 +125,6 @@ class ConnectionManager: pass return {"ok": True, "replayed": 1, "terminal_only": True} - # Nothing missed, nothing to replay. Caller's caught up. return { "ok": True, "replayed": 0, @@ -225,12 +178,7 @@ class ConnectionManager: return out async def broadcast_global(self, event: str, data: dict): - """Send a message to all global (dashboard) connections. - - Dashboard-scoped events don't go through the per-session seq - log — they're not session-bound and the dashboard WS has its - own resume story (full state refetch on reconnect). - """ + """Send to all dashboard connections; bypasses seq_log (dashboard resumes via full state refetch).""" payload = json.dumps({"event": event, "data": data}) for ws in list(self.global_connections): try: @@ -245,12 +193,7 @@ class ConnectionManager: sensitive_label: str | None = None, sensitive_why: str | None = None, ) -> dict: - """Send an approval request and wait for the user's response. - - Returns the approval decision dict. Times out after `timeout` - seconds (default 10 minutes) so a forgotten request doesn't - permanently park the agent. - """ + """Send an approval request and wait for the user's decision; 10-minute timeout prevents permanent park.""" future = asyncio.get_event_loop().create_future() self.pending_futures[request_id] = future diff --git a/backend/apps/health/health.py b/backend/apps/health/health.py index 1ffbddfd..c24fb463 100644 --- a/backend/apps/health/health.py +++ b/backend/apps/health/health.py @@ -13,16 +13,10 @@ async def health_lifespan(): health = SubApp("health", health_lifespan) -###################################### -# Health Check Endpoints # -###################################### - @health.router.get("/check") @typechecked async def check() -> PlainTextResponse: debug("Health check successful") - # Use PlainTextResponse instead of JSONResponse for AWS ALB compatibility - # ALB health checks can be sensitive to JSON responses and Content-Length headers return PlainTextResponse( content="OK", status_code=status.HTTP_200_OK, diff --git a/backend/apps/modes/models.py b/backend/apps/modes/models.py index b639660a..09bb94c7 100644 --- a/backend/apps/modes/models.py +++ b/backend/apps/modes/models.py @@ -55,9 +55,9 @@ BUILTIN_MODES: list[Mode] = [ Mode( id="ask", name="Ask", - description="Read-only conversation. Browse the codebase, search the web, and discuss ideas — but no edits, shells, or file writes.", + description="Read-only conversation. Browse the codebase, search the web, and discuss ideas; but no edits, shells, or file writes.", system_prompt=( - "You are in Ask mode — a read-only assistant. Keep responses " + "You are in Ask mode; a read-only assistant. Keep responses " "natural and conversational. You CAN read files, search the " "codebase, and search/fetch the web. You CANNOT edit files, run " "shell commands, or otherwise modify anything; if the user asks " @@ -88,21 +88,21 @@ BUILTIN_MODES: list[Mode] = [ name="App Builder", description="Create and iterate on reusable App artifacts.", system_prompt=( - "You are an App Builder — an AI assistant that creates self-contained " + "You are an App Builder; an AI assistant that creates self-contained " "web apps rendered in an iframe preview.\n\n" "Your working directory is a dedicated workspace folder pre-seeded with " "template files. Read the existing files before making changes.\n\n" "## Critical rules\n\n" "- The entry point MUST be named `index.html`. Never rename it or create " "a different HTML file as the main entry point.\n" - "- Write files immediately when you have code ready — the user sees a " + "- Write files immediately when you have code ready; the user sees a " "live preview that auto-refreshes from these files.\n" "- Always write the complete file content on first creation (do not use " "Edit for partial patches on new files).\n" "- For complex apps, split code into separate files (JS, CSS, etc.) " "and reference them from index.html with relative paths.\n" "- Always update meta.json with a short name and one-sentence description.\n" - "- Build beautiful, polished UIs with modern design — dark themes, smooth " + "- Build beautiful, polished UIs with modern design; dark themes, smooth " "transitions, proper spacing, and responsive layouts.\n\n" "Read the SKILL.md reference in your workspace for the full technical " "specification of the App platform (available globals, file conventions, " @@ -120,17 +120,17 @@ BUILTIN_MODES: list[Mode] = [ name="Skill Builder", description="Create and iterate on skills using AI-assisted vibe coding.", system_prompt=( - "You are a Skill Builder — an AI assistant that helps users create, " + "You are a Skill Builder; an AI assistant that helps users create, " "refine, and iterate on Claude skills (SKILL.md files).\n\n" "## How Skills Work\n\n" "A skill is a Markdown file that teaches Claude how to perform a specific task. " "Skills have YAML frontmatter with `name` and `description` fields, followed by " - "the skill body in Markdown. The description is the primary triggering mechanism — " + "the skill body in Markdown. The description is the primary triggering mechanism; " "it tells Claude when to use the skill.\n\n" "## Your Working Directory\n\n" "Your working directory is a dedicated workspace folder for this skill. " "Write your output directly to these files using the Write tool:\n\n" - "1. **SKILL.md** — The complete skill file with YAML frontmatter and Markdown body. " + "1. **SKILL.md**; The complete skill file with YAML frontmatter and Markdown body. " "Example frontmatter:\n" " ```\n" " ---\n" @@ -138,34 +138,34 @@ BUILTIN_MODES: list[Mode] = [ " description: When to trigger and what this skill does.\n" " ---\n" " ```\n\n" - "2. **meta.json** — Metadata for the skill builder UI. Always write this file. Example:\n" + "2. **meta.json**; Metadata for the skill builder UI. Always write this file. Example:\n" ' {"name":"My Skill","description":"A short description","command":"my-skill"}\n\n' "Write these files immediately when you have content ready. The user can see " "a live preview that auto-refreshes from these files. Always write the " "complete file content (do not use Edit for partial patches on first creation).\n\n" "## Skill Creation Process\n\n" - "1. **Understand intent** — Ask what the skill should do, when it should trigger, " + "1. **Understand intent**; Ask what the skill should do, when it should trigger, " "and what the expected output format is.\n" - "2. **Draft the skill** — Write a SKILL.md with clear instructions, examples, " + "2. **Draft the skill**; Write a SKILL.md with clear instructions, examples, " "and good progressive disclosure.\n" - "3. **Iterate** — Refine based on user feedback. Update the files each time.\n\n" + "3. **Iterate**; Refine based on user feedback. Update the files each time.\n\n" "## Skill Writing Best Practices\n\n" "- Keep SKILL.md under 500 lines; use bundled reference files for large content.\n" - "- The `description` frontmatter is the primary trigger. Make it slightly \"pushy\" — " + "- The `description` frontmatter is the primary trigger. Make it slightly \"pushy\"; " "include both what the skill does AND specific contexts for when to use it.\n" "- Use imperative form in instructions.\n" "- Include examples with input/output pairs when helpful.\n" "- Define output formats explicitly with templates.\n" - "- Use theory of mind — explain *why* things matter rather than just MUST directives.\n" + "- Use theory of mind; explain *why* things matter rather than just MUST directives.\n" "- Think about edge cases, error handling, and progressive disclosure.\n\n" "## Skill Anatomy\n\n" "```\n" "skill-name/\n" - "├── SKILL.md (required) — YAML frontmatter + Markdown instructions\n" + "├── SKILL.md (required); YAML frontmatter + Markdown instructions\n" "└── Bundled Resources (optional)\n" - " ├── scripts/ — Executable code for repetitive tasks\n" - " ├── references/ — Docs loaded into context as needed\n" - " └── assets/ — Files used in output\n" + " ├── scripts/ ; Executable code for repetitive tasks\n" + " ├── references/; Docs loaded into context as needed\n" + " └── assets/ ; Files used in output\n" "```\n\n" "Be collaborative and flexible. If the user wants to \"just vibe\", skip the formal " "process and iterate freely. Always write updated files so the preview stays current." diff --git a/backend/apps/modes/modes.py b/backend/apps/modes/modes.py index ebf399ef..e5e77586 100644 --- a/backend/apps/modes/modes.py +++ b/backend/apps/modes/modes.py @@ -14,10 +14,7 @@ from backend.config.paths import MODES_DIR as DATA_DIR @asynccontextmanager async def modes_lifespan(): os.makedirs(DATA_DIR, exist_ok=True) - # One-time migration: Chat was merged into Ask. Remove a stale built-in - # chat.json if it still has its is_builtin=True signature so users don't - # see two near-identical modes in the picker. Leave alone if a user has - # diverged it (we don't want to wipe customizations). + # Migration: Chat merged into Ask; drop a stale built-in chat.json but leave customized copies alone. chat_path = os.path.join(DATA_DIR, "chat.json") if os.path.exists(chat_path): try: diff --git a/backend/apps/outputs/models.py b/backend/apps/outputs/models.py index b0ed9eec..d237d910 100644 --- a/backend/apps/outputs/models.py +++ b/backend/apps/outputs/models.py @@ -127,7 +127,7 @@ class OutputExecute(BaseModel): # running if the backend code touches anything outside the safe # data-shaping allowlist. The UI shows those warnings to the user and # re-submits with force=True after they click "Run Anyway." This is - # a UX gate, not a security one — anyone holding the auth token can + # a UX gate, not a security one; anyone holding the auth token can # set force=True; the value is providing the user explicit visibility # of what's about to execute. force: bool = False diff --git a/backend/apps/outputs/outputs.py b/backend/apps/outputs/outputs.py index 4fb41639..b401562f 100644 --- a/backend/apps/outputs/outputs.py +++ b/backend/apps/outputs/outputs.py @@ -139,7 +139,7 @@ def _inject_token_into_relative_urls(html: str, token: str) -> str: relative `` / `