Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
| 11 | Shell/Design Tokens | PR opened (W15-A11) — top 5 W14-A4 deltas applied: primary cyan->blue (`#3b80f4`, legacy fallback preserved), sidebar default 260->220 px, surface `#0f1626`->`#01101a`, surface2 `#141d33`->`#001420`, card radius 8->6 px. `npm run build` + `tsc --noEmit` + 4/4 Playwright smoke @ 1536/1672/1920 PASS. |
| 12 | Dashboard Modes | _pending_ |
| 13 | Source OS + 60 Apps | _pending_ |
| 14 | Primary Tabs A (Autopilot/Design/Gen3D/Jobs) | _pending_ |
| 14 | Primary Tabs A (Autopilot/Design/Gen3D/Jobs) | claude/w15-a14-tabs-a — Autopilot: +PlannerQueue/+AgentActivityFeed/+FreezeThawControls components wired to live `/api/workflows/active`, `/api/agents`, freeze/thaw endpoints (blocked-honest when not implemented). Design/Gen3D/Jobs already FULLY_WIRED per W15-A3 audit; verified no fake fallback + tsc/build PASS. 4 screenshots in `proof/screenshots/w15-a14/`. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update screenshot count to match recorded artifacts.

Line 37 says “4 screenshots,” but Line 60 lists 6 files. Keeping this consistent avoids confusion in proof tracking.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@03_implementation/docs/handoffs/GUI_VISUAL_E2E_COMPLETION_LEDGER_2026-05-10.md`
at line 37, Update the screenshot count in the "Primary Tabs A
(Autopilot/Design/Gen3D/Jobs)" entry in
GUI_VISUAL_E2E_COMPLETION_LEDGER_2026-05-10.md: change the phrase "4 screenshots
in `proof/screenshots/w15-a14/`" to "6 screenshots in
`proof/screenshots/w15-a14/`" so it matches the six files listed later; ensure
the updated sentence remains in the same entry that starts with "Primary Tabs A
(Autopilot/Design/Gen3D/Jobs)".

| 15 | Primary Tabs B (Printers/Observe/Agents/Learning) | _pending_ |
| 16 | Primary Tabs C (Artifacts/Approvals/Plugins/Roadmap) | _pending_ |
| 17 | Settings + Themes | _pending_ |
Expand All @@ -59,3 +59,4 @@
- 2026-05-10: Wave 15 launched; Phase 0 complete.
- 2026-05-10: W15-CLEANUP (PR 3 of 8 sequence per W15-A6 §6) — delete 9 unrouted UNACCEPTABLE_FAKE tabs + 11 orphan mock data files + 1 stale 14-tab `app/routes.tsx` manifest. Branch `claude/w15-cleanup-fake-tabs` from `origin/develop@8dccb7b`. Pre-deletion grep audit: 0 of 21 files had any production import (all consumer references were inside the deleted-tab set). Verification: `tsc --noEmit` PASS, `npm run build` PASS, `vitest run` 118 passed / 4 skipped / 0 failed. Diff: 21 deletions only, 2148 net lines removed. Cites W15-A4 §3, §6 + W15-A3 (stale routes.tsx). Sources: ESLint `no-unused-modules` + TypeScript dead-code-elimination patterns.
- 2026-05-10: Agent 11 (Shell/Design Tokens) opened PR `claude/w15-a11-shell-tokens`. Cites W14-A4 audit. Brand decision: primary shifted cyan->blue with `--h3d-color-primary-cyan-legacy` (`#22d3ee`) preserved for one-step revert; Tailwind `accent.cyan` alias kept verbatim so 88+ existing component refs are not visually moved. Self-audit: build PASS, tsc PASS, 4 smoke specs PASS @ 1536x1024 + 1672x941 + 1920x1080, theme toggle still works, no boot console errors.
- 2026-05-10: Agent 14 (Primary Tabs A) landed on `claude/w15-a14-tabs-a` from `origin/develop`. Added `components/autopilot/{PlannerQueue,AgentActivityFeed,FreezeThawControls}.tsx` wired into `tabs/Autopilot.tsx`. No fake data: planner queue reads `getActiveWorkflows()`, agent activity reads `getAgents()`, freeze/thaw posts to `/api/autopilot/freeze`+`/thaw` and renders explicit "blocked" when 404/405 (current backend, owned by A20). tsc/build PASS. Screenshots saved to `03_implementation/proof/screenshots/w15-a14/{autopilot,design,gen3d,jobs,autopilot-1536x1400,autopilot-freeze-thaw-detail}.png`. Locks: claude-w15-a14-tabs-a. Rebased onto `origin/develop@c2d4a13a` 2026-05-10 (W15-FIX-210-v2; cascade past PR #214/#209/#218/#215/#216/#217).
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
134 changes: 134 additions & 0 deletions 03_implementation/ui/src/components/autopilot/AgentActivityFeed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* AgentActivityFeed — Wave 15 A14.
*
* Display widget for live agent activity. Reads `Agent[]` from
* `adapters.getAgents()` (which hits `/api/agents`) and surfaces a
* compact, honest view of which agents are active right now.
*
* Strict rules:
* - No fake fallback data.
* - No write actions — read-only feed.
*/
import { useEffect, useState } from "react";
import { adapters } from "../../api/adapters";
import type { Agent, AgentStatus } from "../../types/agent";

type LoadState = "loading" | "ready" | "error";

export interface AgentActivityFeedProps {
agents?: Agent[];
}

export function AgentActivityFeed({ agents: propAgents }: AgentActivityFeedProps = {}) {
const [agents, setAgents] = useState<Agent[]>(propAgents ?? []);
const [state, setState] = useState<LoadState>(propAgents ? "ready" : "loading");
const [errorMsg, setErrorMsg] = useState<string | null>(null);

useEffect(() => {
if (propAgents) {
setAgents(propAgents);
setState("ready");
return;
}
let mounted = true;
const load = () => {
adapters
.getAgents()
.then((next) => {
if (!mounted) {
return;
}
setAgents(next);
setState("ready");
setErrorMsg(null);
})
.catch((error) => {
if (!mounted) {
return;
}
setState("error");
setErrorMsg(error instanceof Error ? error.message : "agents API unreachable");
});
};
load();
const timer = window.setInterval(load, 20_000);
return () => {
mounted = false;
window.clearInterval(timer);
};
}, [propAgents]);

// Sort: active first, then paused, then idle, then error.
const sorted = [...agents].sort((a, b) => statusWeight(a.status) - statusWeight(b.status));
const activeCount = agents.filter((a) => a.status === "active").length;

return (
<section
id="autopilot.agent_activity"
data-testid="autopilot-agent-activity"
className="flex min-h-0 flex-col rounded border border-border bg-surface p-4"
>
<header className="flex items-center justify-between gap-2">
<div>
<h2 className="text-base font-semibold text-fg">AGENT ACTIVITY</h2>
<p className="text-xs text-muted">Live agent roster from /api/agents.</p>
</div>
<span className="rounded bg-surface2 px-2 py-1 text-xs text-muted" data-testid="autopilot-agent-activity-count">
{state === "loading" ? "loading…" : `${activeCount}/${agents.length} active`}
</span>
</header>
<div className="mt-3 grid min-h-0 flex-1 content-start gap-2 overflow-auto">
{state === "error" && (
<div className="rounded border border-amber-700/60 bg-amber-950/30 p-3 text-xs text-amber-100">
Blocked: agents endpoint unreachable. {errorMsg ?? ""}
</div>
)}
{state === "ready" && agents.length === 0 && (
<div className="rounded border border-border bg-bg/40 p-3 text-xs text-muted">
No agents returned by the live agents API.
</div>
)}
{sorted.map((agent) => (
<article
key={agent.id}
data-testid="autopilot-agent-activity-row"
data-agent-id={agent.id}
data-agent-status={agent.status}
className="grid grid-cols-[auto_1fr_auto] items-center gap-2 rounded border border-border bg-bg/40 px-3 py-2 text-sm"
>
<span
aria-label={`status ${agent.status}`}
className={`h-2 w-2 rounded-full ${statusDot(agent.status)}`}
/>
<div className="min-w-0">
<div className="truncate font-medium text-fg">{agent.role}</div>
<div className="truncate text-xs text-muted">
{agent.task_count} task{agent.task_count === 1 ? "" : "s"} ·{" "}
<span className="font-mono">{agent.model_provider || "—"}</span>
</div>
</div>
<span className="rounded bg-surface2 px-2 py-1 text-[10px] uppercase text-muted">
{agent.last_activity_utc
? new Date(agent.last_activity_utc).toLocaleTimeString()
: "—"}
</span>
</article>
))}
</div>
</section>
);
}

function statusWeight(status: AgentStatus): number {
if (status === "active") return 0;
if (status === "paused") return 1;
if (status === "idle") return 2;
return 3; // error
}

function statusDot(status: AgentStatus): string {
if (status === "active") return "bg-accent-green";
if (status === "paused") return "bg-amber-400";
if (status === "error") return "bg-red-500";
return "bg-muted";
}
145 changes: 145 additions & 0 deletions 03_implementation/ui/src/components/autopilot/FreezeThawControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* FreezeThawControls — Wave 15 A14.
*
* Freeze / thaw the autopilot loop. The current Hermes3D backend exposes no
* `/api/autopilot/freeze` or `/api/autopilot/thaw` endpoint (verified
* 2026-05-10 against `src/hermes3d/api/routes/autopilot.py` — only
* /readiness, /next-gate, /write-plan, /write-report, /guardrails exist).
*
* Strict rules:
* - No fake "frozen" state that lies to the operator. We probe the
* endpoint and surface the real outcome.
* - When the endpoint is missing we render an explicit "blocked" notice
* so the operator knows the control is not wired yet (per W15 A20).
* - When the endpoint exists and accepts the call we record a proof event.
*/
import { useState } from "react";
import { Snowflake, Sun } from "lucide-react";
import { adapters } from "../../api/adapters";

type HermesImportMeta = ImportMeta & {
env: {
VITE_HERMES3D_BRIDGE_PORT?: string;
};
};

const DEFAULT_BRIDGE_PORT = "8765";
const LIVE_BRIDGE_PORT = (import.meta as HermesImportMeta).env.VITE_HERMES3D_BRIDGE_PORT ?? DEFAULT_BRIDGE_PORT;
const LIVE_BASE_URL = `http://127.0.0.1:${LIVE_BRIDGE_PORT}`;
Comment on lines +20 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The HermesImportMeta type definition and the LIVE_BASE_URL construction logic are duplicated from Autopilot.tsx. This duplication makes the configuration fragile and harder to maintain. Consider centralizing this logic in a shared constants file or within the adapters layer to ensure consistency across the application.


type Mode = "frozen" | "thawed" | "unknown";

export function FreezeThawControls() {
const [mode, setMode] = useState<Mode>("unknown");
const [message, setMessage] = useState<string | null>(null);
const [pending, setPending] = useState<boolean>(false);

const trigger = async (path: "freeze" | "thaw") => {
setPending(true);
let accepted = false;
let summary = "no response";
try {
const response = await fetch(`${LIVE_BASE_URL}/api/autopilot/${path}`, {
method: "POST",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify({ source: "autopilot.freeze_thaw_controls" }),
cache: "no-store",
});
Comment on lines +42 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This component uses fetch directly to interact with the backend API. Following the project's established pattern, these network calls should be abstracted into the adapters layer. This improves maintainability, allows for easier mocking in tests, and centralizes error handling and base URL management.

const payload: unknown = await response.json().catch(() => null);
if (response.status === 404 || response.status === 405) {
summary = `Blocked: /api/autopilot/${path} is not implemented on this backend (W15 A20 owns the endpoint).`;
} else if (!response.ok) {
summary = `Blocked: backend rejected ${path} (${response.status} ${response.statusText}).`;
} else {
accepted = true;
summary = payloadSummary(payload, path);
setMode(path === "freeze" ? "frozen" : "thawed");
}
} catch (error) {
summary = `Blocked: backend unreachable at ${LIVE_BASE_URL} (${describeError(error)}).`;
} finally {
setPending(false);
}
setMessage(summary);
await adapters
.emitProofEvent(`autopilot.${path}.requested`, { accepted, summary })
.catch(() => {/* best effort */});
};

return (
<section
id="autopilot.freeze_thaw"
data-testid="autopilot-freeze-thaw"
data-autopilot-mode={mode}
className="rounded border border-border bg-surface p-4"
>
<header className="flex items-center justify-between gap-2">
<div>
<h2 className="text-base font-semibold text-fg">FREEZE / THAW</h2>
<p className="text-xs text-muted">
Stop or resume the autopilot loop. Calls /api/autopilot/freeze + /thaw.
</p>
</div>
<span
data-testid="autopilot-freeze-thaw-state"
className={`rounded px-2 py-1 text-xs uppercase ${modeTone(mode)}`}
>
{mode}
</span>
</header>
<div className="mt-3 flex flex-wrap gap-2">
<button
type="button"
data-testid="autopilot-freeze-button"
disabled={pending}
onClick={() => void trigger("freeze")}
className="inline-flex items-center gap-2 rounded border border-border bg-bg/50 px-3 py-2 text-sm text-fg hover:border-amber-500 disabled:opacity-50"
>
<Snowflake size={14} className="text-cyan-300" />
Freeze autopilot
</button>
<button
type="button"
data-testid="autopilot-thaw-button"
disabled={pending}
onClick={() => void trigger("thaw")}
className="inline-flex items-center gap-2 rounded border border-border bg-bg/50 px-3 py-2 text-sm text-fg hover:border-accent-blue disabled:opacity-50"
>
<Sun size={14} className="text-amber-300" />
Thaw autopilot
</button>
</div>
{message && (
<div
data-testid="autopilot-freeze-thaw-message"
className="mt-3 rounded border border-border bg-bg/40 p-2 text-xs text-muted"
>
{message}
</div>
)}
</section>
);
}

function modeTone(mode: Mode): string {
if (mode === "frozen") return "bg-cyan-900/40 text-cyan-200";
if (mode === "thawed") return "bg-green-900/40 text-green-200";
return "bg-surface2 text-muted";
}

function payloadSummary(payload: unknown, fallback: string): string {
if (typeof payload === "object" && payload !== null && !Array.isArray(payload)) {
const record = payload as Record<string, unknown>;
if (typeof record.message === "string") return record.message;
if (typeof record.status === "string") return record.status;
if (typeof record.proof_event_id === "string") return `proof ${record.proof_event_id}`;
}
return `${fallback} accepted`;
}

function describeError(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return "unknown";
}
Loading
Loading