diff --git a/.ai/active/SPRINT_PACKET.md b/.ai/active/SPRINT_PACKET.md index 74fe635..d9a1d32 100644 --- a/.ai/active/SPRINT_PACKET.md +++ b/.ai/active/SPRINT_PACKET.md @@ -2,115 +2,204 @@ ## Sprint Title -Sprint 5U: Project Truth Synchronization After Gmail Secret Externalization +Sprint 6A: MVP Web Shell and Core Operator Views ## Sprint Type -refactor +ui ## Sprint Reason -Sprint 5T is implemented and the repo remains on track, but the live truth artifacts have drifted again. `ARCHITECTURE.md` now reflects Sprint 5T, while `ROADMAP.md` and `.ai/handoff/CURRENT_STATE.md` are still anchored around Sprint 5R. Before opening the next Gmail follow-up seam such as `legacy_db_v0` removal, Control Tower needs the planning and handoff artifacts reset to the accepted repo state. +Sprint 5U is implemented and the project is on track, but the largest remaining MVP gap is no longer Gmail auth hardening. The backend now has enough governed capability to support a real human-usable shell, and `DESIGN_SYSTEM.md` exists as a design truth source. To avoid looping on narrow connector internals while the product remains invisible, the next sprint should open the thinnest serious web surface for the shipped backend seams. ## Sprint Intent -Synchronize the live truth artifacts with the implemented and review-passed repo state through Sprint 5T, so future planning, handoff, and review work all start from accurate architecture, roadmap, and current-state documents. +Build the first real web shell for AliceBot on top of the existing backend by adding a calm, high-trust operator interface for navigation, request submission, approvals review, task and task-step inspection, and explain-why/traces viewing, without expanding backend product scope. ## Git Instructions -- Branch Name: `codex/sprint-5u-project-truth-sync-after-gmail-secret-externalization` +- Branch Name: `codex/sprint-6a-mvp-web-shell` - Base Branch: `main` - PR Strategy: one sprint branch, one PR, no stacked PRs unless Control Tower explicitly opens a follow-up sprint - Merge Policy: squash merge only after reviewer `PASS` and explicit Control Tower merge approval ## Why This Sprint -- Sprint 5S last synchronized truth through Sprint 5R. -- Sprint 5T changed the accepted connector boundary materially: - - the primary Gmail credential path is now external-secret-backed - - `gmail_account_credentials` now persists locator metadata on the primary path - - a narrow `legacy_db_v0` transition path now exists for older rows -- `ARCHITECTURE.md` already reflects that accepted state, but `ROADMAP.md` and `.ai/handoff/CURRENT_STATE.md` still describe the repo through Sprint 5R and still describe external secret-manager integration as future work. -- Planning from that stale truth would create avoidable scope and review drift for the next Gmail sprint. +- The accepted repo state through Sprint 5T already ships the core backend substrate: + - governed context compilation + - approvals and execution review + - tasks and task steps + - explainable traces + - rooted workspaces, artifacts, document ingestion, and Gmail-backed artifacts +- `DESIGN_SYSTEM.md` now exists and should be treated as a source of truth alongside `ARCHITECTURE.md`. +- The product brief requires web-based chat and task orchestration, plus explain-why visibility. +- Continuing with another Gmail-only sprint right now would optimize one backend seam while the MVP still lacks a usable interface. +- The narrowest safe UI slice is the shell plus core operator views only, not full workflow completion or new backend scope. + +## Design Truth + +- `DESIGN_SYSTEM.md` is in force for this sprint. +- The UI must follow its calm, premium, restrained visual language. +- This sprint must preserve strong hierarchy, readable density, stable navigation, and explicit containment rules. +- Do not introduce playful AI styling, loud gradients, decorative clutter, or unstable layout behavior. + +## Exact Screens In Scope + +- `/` + - app shell landing view + - primary navigation + - summary cards for the existing backend seams +- `/chat` + - request composer surface + - recent request/response panel + - explicit “governed request” framing, not a consumer chat skin +- `/approvals` + - approval inbox list + - approval detail panel or inline inspector +- `/tasks` + - task list + - task detail panel + - task-step list/inspection area +- `/traces` + - trace or explain-why review view for context compile and governed actions + +## Exact Components In Scope + +- app shell frame + - top bar + - left navigation rail or sidebar + - main content container +- shared primitives + - page header + - section card + - metric card + - status badge + - empty state + - list row + - split-panel or inspector layout +- domain views + - request composer + - approval list + - approval detail inspector + - task list + - task detail summary + - task-step timeline or ordered list + - trace event list + - explainability summary panel + +## Exact Files In Scope + +- `DESIGN_SYSTEM.md` + - reference only; do not rewrite unless the sprint reveals a concrete contradiction that must be corrected +- `apps/web/app/layout.tsx` +- `apps/web/app/page.tsx` +- `apps/web/app/chat/page.tsx` +- `apps/web/app/approvals/page.tsx` +- `apps/web/app/tasks/page.tsx` +- `apps/web/app/traces/page.tsx` +- `apps/web/app/globals.css` +- `apps/web/components/app-shell.tsx` +- `apps/web/components/page-header.tsx` +- `apps/web/components/section-card.tsx` +- `apps/web/components/status-badge.tsx` +- `apps/web/components/empty-state.tsx` +- `apps/web/components/request-composer.tsx` +- `apps/web/components/approval-list.tsx` +- `apps/web/components/task-list.tsx` +- `apps/web/components/task-step-list.tsx` +- `apps/web/components/trace-list.tsx` +- `apps/web/package.json` ## In Scope -- Audit the accepted implemented slice from the repo and passed sprint reports through Sprint 5T. -- Update `ARCHITECTURE.md` only if needed so it accurately describes the implemented seams through: - - externalized Gmail secret-manager-backed credential storage - - secret-free Gmail account reads - - refresh-token renewal and rotated refresh-token persistence through the externalized seam - - the narrow `legacy_db_v0` transition boundary -- Update `ROADMAP.md` so: - - completed and current milestone state reflects the accepted repo state through Sprint 5T - - the next delivery focus is framed from the actual shipped Gmail externalized-secret baseline - - stale “next delivery focus” language that still treats external secret-manager integration as future work is corrected -- Update `.ai/handoff/CURRENT_STATE.md` so: - - implemented areas and current boundaries reflect the repo through Sprint 5T - - the current milestone position is correct - - the immediate next move matches the next narrow sprint boundary after truth sync -- Update `BUILD_REPORT.md` with the truth-sync evidence and exact files corrected. +- Replace the current placeholder Next.js landing page with a real app shell aligned to `DESIGN_SYSTEM.md`. +- Add the exact screens and components listed above. +- Use only existing backend concepts already shipped in the repo: + - context compilation + - approvals + - tasks and task steps + - traces or explain-why data +- Implement a thin frontend data layer only as needed to render those views. +- If live API wiring is used, it must consume existing endpoints only. +- If mocked or fixture-backed UI data is used for part of the shell, keep the mock layer explicit and local to the web app. +- Ensure the shell is usable on desktop and mobile widths. ## Out of Scope -- No schema changes. -- No API changes. -- No runtime code changes. -- No Gmail search, mailbox sync, attachments, or write actions. -- No Calendar connector scope. -- No `legacy_db_v0` removal yet. -- No runner-style orchestration. -- No UI work. +- No new backend endpoints. +- No backend schema changes. +- No Gmail search or broader connector work. +- No Calendar connector UI. +- No write-capable action UI beyond the existing governed-request framing. +- No authentication/product auth redesign. +- No full end-to-end magnesium reorder workflow yet. +- No design-system rewrite. +- No runner-style orchestration UI. ## Required Deliverables -- Updated `ARCHITECTURE.md` aligned to the implemented repo state through Sprint 5T. -- Updated `ROADMAP.md` with correct completed/current/next milestone sequencing. -- Updated `.ai/handoff/CURRENT_STATE.md` reflecting the actual shipped state and immediate next move. -- Updated `BUILD_REPORT.md` describing exactly which truth artifacts were synchronized and what evidence was used. +- A real Next.js app shell aligned to `DESIGN_SYSTEM.md`. +- The exact screens listed in this packet, implemented in the exact in-scope file set or a narrower subset of it. +- Stable layout, navigation, responsive behavior, and readable empty states. +- A thin request submission surface for the governed request path. +- Approval, task, task-step, and trace/explain-why review views. +- Updated `BUILD_REPORT.md` with exact verification results and explicit deferred scope. ## Acceptance Criteria -- `ARCHITECTURE.md` accurately describes the shipped Gmail seam as external-secret-backed on the primary path, secret-free on reads, renewal-capable, rotation-capable, and still narrow/read-only. -- `ROADMAP.md` no longer claims the next Gmail auth-adjacent seam is external secret-manager integration. -- `.ai/handoff/CURRENT_STATE.md` no longer describes external secret-manager integration as unimplemented. -- Truth artifacts clearly distinguish implemented Gmail externalization behavior from later planned Gmail breadth and from the still-deferred `legacy_db_v0` cleanup sprint. -- No runtime, schema, API, connector-breadth, runner, or UI changes appear in the sprint diff. +- The web app no longer renders only the current placeholder foundation card at `/`. +- The app exposes the exact in-scope screens: + - `/` + - `/chat` + - `/approvals` + - `/tasks` + - `/traces` +- The UI visibly follows `DESIGN_SYSTEM.md` and feels calm, premium, and high-trust rather than demo-like. +- Text remains contained within cards and panels across responsive breakpoints. +- Navigation is stable and the current location is obvious. +- The UI uses only existing backend concepts and does not widen product scope. +- `pnpm build` or `npm run build` for `apps/web` passes. +- Any added frontend tests or lint checks pass if introduced. ## Implementation Constraints -- Keep this sprint documentation-only and boring. -- Use accepted repo state and passed sprint reports as evidence, not aspiration. -- Prefer explicit “implemented now” versus “planned later” boundaries. -- If a truth artifact cannot be updated confidently from accepted evidence, narrow the statement rather than guessing. -- Do not widen into product changes just because the roadmap or handoff text is stale. +- Keep the sprint narrow and boring. +- Treat this as the first operator shell, not the finished product. +- Prefer a few strong views over too many half-finished surfaces. +- Reuse existing backend seams; do not invent placeholder product capabilities that the backend does not support. +- Keep interaction restrained and calm. +- Preserve mobile usability and text containment. +- If any API wiring is unstable, degrade to explicit empty, loading, or fixture-backed states instead of inventing hidden backend changes. ## Suggested Work Breakdown -1. Audit the implemented repo state and accepted sprint reports through Sprint 5T. -2. Update `ARCHITECTURE.md` only where it still lags the accepted Gmail externalization seam. -3. Update `ROADMAP.md` to reflect actual completed and current milestone state. -4. Update `.ai/handoff/CURRENT_STATE.md` to reflect actual current state and the immediate next move. -5. Update `BUILD_REPORT.md` with exact truth-sync evidence and scope confirmation. +1. Replace the placeholder root page with a real shell and shared layout primitives. +2. Add the in-scope routes and core shared components. +3. Implement the request, approvals, tasks, and traces views using existing backend concepts only. +4. Apply responsive layout and empty-state handling. +5. Run frontend build and any introduced checks. +6. Update `BUILD_REPORT.md` with executed verification. ## Build Report Requirements `BUILD_REPORT.md` must include: -- exactly which truth artifacts were updated -- which accepted reports or repo evidence were used -- the specific stale statements that were corrected -- confirmation that no runtime or schema changes were made -- what remains intentionally deferred after truth synchronization +- the exact screens and components implemented +- the exact files changed +- whether each screen is live-API-backed, fixture-backed, or mixed +- exact commands run +- build and test results +- screenshots or concise visual verification notes for desktop and mobile behavior +- what remains intentionally deferred after this UI sprint ## Review Focus `REVIEW_REPORT.md` should verify: -- the sprint stayed documentation-only -- `ARCHITECTURE.md`, `ROADMAP.md`, and `.ai/handoff/CURRENT_STATE.md` now match the implemented repo state through Sprint 5T -- the shipped Gmail external secret-manager seam and its current narrow boundary are documented accurately -- milestone sequencing is truthful and current -- no hidden runtime, schema, API, connector-breadth, runner, or UI scope entered the sprint +- the sprint stayed a UI sprint and did not widen backend product scope +- `DESIGN_SYSTEM.md` was followed materially +- the exact in-scope screens, components, and files were respected +- layout quality, text containment, hierarchy, and responsive behavior are acceptable +- no hidden Gmail breadth, Calendar, runner, or auth-scope expansion entered the sprint ## Exit Condition -This sprint is complete when the project truth artifacts accurately describe the implemented repo state through Sprint 5T and future planning can proceed from synchronized architecture, roadmap, and current-state documents. +This sprint is complete when AliceBot has a real web shell with stable navigation plus the first operator-facing views for requests, approvals, tasks, task steps, and traces, aligned to `DESIGN_SYSTEM.md`, built on existing backend concepts only, and verified by frontend build checks. diff --git a/BUILD_REPORT.md b/BUILD_REPORT.md index 785d4a2..e71226f 100644 --- a/BUILD_REPORT.md +++ b/BUILD_REPORT.md @@ -2,76 +2,117 @@ ## sprint objective -Implement Sprint 5U: synchronize the live truth artifacts with the accepted repo state through Sprint 5T so planning and handoff docs accurately describe the shipped Gmail externalization seam and its remaining narrow transition boundary. +Implement Sprint 6A: replace the placeholder web app with the first real AliceBot operator shell for governed requests, approvals, tasks, task steps, and explain-why review, aligned to `DESIGN_SYSTEM.md` and bounded to existing backend seams only. + +## exact screens implemented + +- `/` + - app shell landing view + - summary metrics + - primary navigation entry cards +- `/chat` + - governed request composer + - recent request/response history + - trace reference display +- `/approvals` + - approval inbox list + - approval detail inspector +- `/tasks` + - task list + - selected task summary + - task-step inspection list +- `/traces` + - explainability trace list + - trace detail review panel + +## exact shared components implemented + +- `app-shell` +- `page-header` +- `section-card` +- `status-badge` +- `empty-state` +- `request-composer` +- `approval-list` +- `task-list` +- `task-step-list` +- `trace-list` + +## exact files changed -## completed work - -- Audited the shipped Sprint 5T repo state against the current truth docs. -- Confirmed `ARCHITECTURE.md` already matched the accepted Sprint 5T implementation, so no architecture correction was required. -- Updated `ROADMAP.md` from a Sprint 5R baseline to a Sprint 5T baseline. -- Updated `.ai/handoff/CURRENT_STATE.md` from a Sprint 5R baseline to a Sprint 5T baseline. -- Replaced stale forward-looking language that still treated Gmail external secret-manager integration as future work. -- Reframed the immediate next narrow Gmail seam as `legacy_db_v0` cleanup rather than external-secret-manager integration. -- Preserved all out-of-scope boundaries: no runtime, schema, API, connector-breadth, runner, or UI changes. - -## incomplete work - -- None inside Sprint 5U scope. - -## files changed - -- `ROADMAP.md` -- `.ai/handoff/CURRENT_STATE.md` - `BUILD_REPORT.md` - -## tests run - -- `git diff --check -- ROADMAP.md .ai/handoff/CURRENT_STATE.md BUILD_REPORT.md` - -## blockers/issues - -- No implementation blockers occurred. -- `ARCHITECTURE.md` was audited but not edited because it already reflected the accepted Sprint 5T state. -- The worktree already contained unrelated changes outside sprint scope; they were left untouched. - -## accepted evidence used - -- `ARCHITECTURE.md` current Sprint 5T implemented-slice and Gmail boundary text. -- `BUILD_REPORT.md` from Sprint 5T, which records the accepted external secret-manager seam, locator-only primary credential storage, and the `legacy_db_v0` transition path. -- `REVIEW_REPORT.md` with `PASS` for Sprint 5T and accepted verification totals: - - `./.venv/bin/python -m pytest tests/unit` -> `446 passed` - - `./.venv/bin/python -m pytest tests/integration` -> `141 passed` -- Repo implementation and test evidence in: - - `apps/api/src/alicebot_api/gmail.py` - - `apps/api/src/alicebot_api/gmail_secret_manager.py` - - `tests/integration/test_gmail_accounts_api.py` - - `tests/integration/test_migrations.py` - -## specific stale statements corrected - -- `ROADMAP.md` no longer says the repo is current only through Sprint 5R. -- `ROADMAP.md` no longer says external secret-manager integration is the next Gmail auth seam. -- `.ai/handoff/CURRENT_STATE.md` no longer says the repo is current only through Sprint 5R. -- `.ai/handoff/CURRENT_STATE.md` no longer lists external secret-manager integration as not implemented. -- `.ai/handoff/CURRENT_STATE.md` no longer points planning at external secret-manager integration as the immediate next move. -- `.ai/handoff/CURRENT_STATE.md` now carries the accepted Sprint 5T verification totals instead of the older Sprint 5R totals. - -## confirmation of non-runtime scope - -- No runtime code changed. -- No schema or migration files changed. -- No API contract changed. -- No Gmail connector breadth changed. -- No runner or UI scope entered the diff. - -## what remains intentionally deferred after truth synchronization - -- Removal of the remaining `legacy_db_v0` transition path for older Gmail credential rows. -- Gmail search, mailbox sync, attachment ingestion, and write-capable Gmail actions. -- Calendar connector scope. -- Richer document parsing beyond the current narrow local ingestion seams. -- Runner-style orchestration and UI work. - -## recommended next step - -Open one narrow follow-up sprint to remove the remaining `legacy_db_v0` transition path deliberately, without widening into Gmail search, sync, attachments, Calendar, runner, or UI scope. +- `apps/web/app/layout.tsx` +- `apps/web/app/page.tsx` +- `apps/web/app/chat/page.tsx` +- `apps/web/app/approvals/page.tsx` +- `apps/web/app/tasks/page.tsx` +- `apps/web/app/traces/page.tsx` +- `apps/web/app/globals.css` +- `apps/web/components/app-shell.tsx` +- `apps/web/components/page-header.tsx` +- `apps/web/components/section-card.tsx` +- `apps/web/components/status-badge.tsx` +- `apps/web/components/empty-state.tsx` +- `apps/web/components/request-composer.tsx` +- `apps/web/components/approval-list.tsx` +- `apps/web/components/task-list.tsx` +- `apps/web/components/task-step-list.tsx` +- `apps/web/components/trace-list.tsx` + +## data-backing by screen + +- `/`: fixture/static shell content +- `/chat`: mixed + - live API when `NEXT_PUBLIC_ALICEBOT_API_BASE_URL` or `ALICEBOT_API_BASE_URL` plus user/thread ids are present + - explicit local fixtures otherwise +- `/approvals`: mixed + - live `GET /v0/approvals` when API base URL and user id are present + - explicit local fixtures otherwise +- `/tasks`: mixed + - live `GET /v0/tasks` and `GET /v0/tasks/{task_id}/steps` when API base URL and user id are present + - explicit local fixtures otherwise +- `/traces`: fixture-backed + - trace summaries and detail events are local fixtures because the repo does not currently expose a general trace-event listing endpoint in the shipped web scope + +## commands run + +- `npm install` in `apps/web` +- `npm run build` in `apps/web` + +## build and test results + +- `npm run build` in `apps/web`: PASS +- Production build output included the intended routes: + - `/` + - `/chat` + - `/approvals` + - `/tasks` + - `/traces` +- `npm run lint`: not run + - current project setup prompts interactively for ESLint initialization instead of providing a stable non-interactive check + +## visual verification notes + +- Desktop behavior: + - left navigation rail remains persistent + - top bar and page headers hold a clear hierarchy without crowding + - approvals, tasks, and traces use split review layouts with bounded inspector panels +- Mobile and narrow-width behavior: + - sidebar collapses into a horizontal mobile navigation row + - grids stack to single-column layouts + - cards, pills, IDs, and attributes use wrapping and containment rules to avoid overflow or clipping +- Screenshots: + - no browser screenshots captured in this sprint report + +## blockers or issues encountered + +- Running `npm run build` caused Next.js to rewrite `apps/web/tsconfig.json` and `apps/web/next-env.d.ts` locally; those generated changes were reverted to keep the sprint diff inside the packet’s scoped file list. +- `npm install` generated `apps/web/package-lock.json`; that file was removed from the final diff for the same reason. + +## deferred scope after this sprint + +- live trace-event listing and deep explainability wiring beyond the current fixture-backed `/traces` screen +- approvals mutation actions from the web UI +- task-step mutations from the web UI +- authentication redesign +- Gmail breadth, Calendar connector UI, runner UI, or any new backend endpoints diff --git a/REVIEW_REPORT.md b/REVIEW_REPORT.md index fc4acf5..69a6218 100644 --- a/REVIEW_REPORT.md +++ b/REVIEW_REPORT.md @@ -6,13 +6,34 @@ PASS ## criteria met -- The sprint stayed documentation-only. The diff changes only `.ai/active/SPRINT_PACKET.md`, `ROADMAP.md`, `.ai/handoff/CURRENT_STATE.md`, and `BUILD_REPORT.md`; no runtime, schema, API, connector-breadth, runner, or UI files changed. -- `ROADMAP.md` now reflects the accepted repo state through Sprint 5T instead of Sprint 5R. -- `ROADMAP.md` no longer describes external secret-manager integration as future work and now correctly frames the next narrow seam as `legacy_db_v0` cleanup. -- `.ai/handoff/CURRENT_STATE.md` now reflects the implemented Sprint 5T Gmail seam: external-secret-backed primary credential storage, secret-free reads, renewal through the externalized seam, rotated refresh-token persistence, and the narrow `legacy_db_v0` transition path. -- The updated truth artifacts distinguish implemented Gmail externalization behavior from deferred Gmail breadth and from the still-deferred `legacy_db_v0` cleanup sprint. -- `BUILD_REPORT.md` identifies the synchronized truth artifacts, cites accepted Sprint 5T evidence, names the stale statements corrected, confirms the non-runtime scope, and states what remains deferred. -- `ARCHITECTURE.md` already matched the accepted Sprint 5T implementation and did not require edits; the unchanged text remains consistent with the updated roadmap and handoff documents. +- The web app no longer renders the placeholder landing view at `/`; it now provides a real operator shell with stable navigation and bounded overview content. +- The exact in-scope routes are present and implemented: + - `/` + - `/chat` + - `/approvals` + - `/tasks` + - `/traces` +- The shared component surface required by the sprint exists and is used across the app shell and route views: + - `app-shell` + - `page-header` + - `section-card` + - `status-badge` + - `empty-state` + - `request-composer` + - `approval-list` + - `task-list` + - `task-step-list` + - `trace-list` +- The UI materially follows `DESIGN_SYSTEM.md`: restrained palette, calm hierarchy, consistent card treatment, stable navigation state, and responsive stacking are all present in the shipped shell. +- The sprint stayed within existing backend concepts and did not widen backend scope. Live web wiring uses only existing shipped endpoints: + - `POST /v0/responses` + - `GET /v0/approvals` + - `GET /v0/tasks` + - `GET /v0/tasks/{task_id}/steps` +- The fixture content was narrowed back into supplement/ecommerce examples, so the earlier Calendar-scope concern is no longer present. +- `BUILD_REPORT.md` now matches Sprint 6A and includes the required screens, shared components, exact files changed, data-backing mode by route, commands run, build results, visual verification notes, and deferred scope. +- Review verification: + - `npm run build` in `apps/web`: PASS ## criteria missed @@ -20,31 +41,32 @@ PASS ## quality issues -- No blocking quality issues found in the Sprint 5U diff. -- The builder kept the sprint narrow and did not overreach beyond the truth-sync scope. +- No blocking implementation issues found in the Sprint 6A UI surface. +- Minor non-blocking issue: `next build` still rewrites `apps/web/tsconfig.json` and `apps/web/next-env.d.ts` during the build. The builder documented and reverted that churn, so it no longer widens the final sprint diff, but it remains a workspace cleanliness annoyance. +- Minor non-blocking issue: `npm run lint` is still not a stable non-interactive check because Next prompts for ESLint initialization instead of using a committed lint config. ## regression risks - Low. -- The only meaningful residual risk is planning drift if future truth-sync sprints again update `ROADMAP.md` and `.ai/handoff/CURRENT_STATE.md` without checking them against the accepted implementation and accepted reports. -- Runtime risk from this sprint is effectively nil because no runtime files changed. +- The main residual risk is operational rather than functional: future reviewers or CI may see local config churn from Next build autoconfiguration unless the web workspace eventually adopts the generated TypeScript settings deliberately. +- The `/traces` route remains fixture-backed by design, so operators should not infer live trace listing coverage beyond what `BUILD_REPORT.md` describes. ## docs issues -- No blocking docs issues. -- Minor note only: the evidence cited in `BUILD_REPORT.md` depends partly on accepted Sprint 5T artifacts now visible through git history rather than through separate preserved files in the working tree. That is still adequate for this sprint and does not block acceptance. +- No blocking docs issues remain. +- `BUILD_REPORT.md` now satisfies the packet’s reporting requirements. ## should anything be added to RULES.md? -- No. -- The repo already had sufficient scope-control rules. The problem addressed here was stale truth artifacts, not a missing durable rule. +- No required rules change. +- Optional future rule only: if the team wants stricter workspace cleanliness, add a rule that generated framework config churn discovered during build must either be committed intentionally or explicitly documented and reverted before handoff. ## should anything update ARCHITECTURE.md? - No. -- The current `ARCHITECTURE.md` already describes the accepted Sprint 5T Gmail seam accurately, including the external secret-manager boundary and the narrow `legacy_db_v0` transition path. +- The sprint stayed within the documented backend seams and did not reveal an architecture contradiction. ## recommended next action -- Accept Sprint 5U. -- Open one narrow follow-up sprint to remove the remaining `legacy_db_v0` transition path without widening into Gmail search, sync, attachments, Calendar, runner, or UI scope. +- Accept Sprint 6A. +- If the team wants a cleaner frontend workflow next, open a narrow follow-up to stabilize lint setup and decide whether the Next-generated TypeScript config changes should be adopted permanently or continue to be reverted. diff --git a/apps/web/app/approvals/page.tsx b/apps/web/app/approvals/page.tsx new file mode 100644 index 0000000..f6b7db6 --- /dev/null +++ b/apps/web/app/approvals/page.tsx @@ -0,0 +1,193 @@ +import { ApprovalList, type ApprovalItem } from "../../components/approval-list"; +import { PageHeader } from "../../components/page-header"; + +const approvalFixtures: ApprovalItem[] = [ + { + id: "approval-101", + thread_id: "thread-magnesium", + task_step_id: "step-21", + status: "pending", + request: { + thread_id: "thread-magnesium", + tool_id: "tool-purchase", + action: "place_order", + scope: "supplements", + domain_hint: "ecommerce", + risk_hint: "purchase", + attributes: { + merchant: "Thorne", + item: "Magnesium Bisglycinate", + quantity: "1", + budget_note: "Prefer previously approved merchant and package size.", + }, + }, + tool: { + id: "tool-purchase", + tool_key: "merchant_proxy", + name: "Merchant Proxy", + description: "Proxy for governed ecommerce actions.", + version: "0.1.0", + metadata_version: "tool_metadata_v0", + active: true, + tags: ["commerce", "approval"], + action_hints: ["place_order"], + scope_hints: ["supplements"], + domain_hints: ["ecommerce"], + risk_hints: ["purchase"], + metadata: {}, + created_at: "2026-03-15T08:00:00Z", + }, + routing: { + decision: "require_approval", + reasons: [ + { + code: "policy_effect_require_approval", + source: "policy", + message: "Purchases require explicit user approval before execution.", + tool_id: "tool-purchase", + policy_id: "policy-purchase-approval", + consent_key: null, + }, + { + code: "tool_metadata_matched", + source: "tool", + message: "Merchant proxy supports the requested purchase scope.", + tool_id: "tool-purchase", + policy_id: null, + consent_key: null, + }, + ], + trace: { + trace_id: "trace-approval-101", + trace_event_count: 6, + }, + }, + created_at: "2026-03-17T06:50:00Z", + resolution: null, + }, + { + id: "approval-100", + thread_id: "thread-vitamin-d", + task_step_id: "step-14", + status: "approved", + request: { + thread_id: "thread-vitamin-d", + tool_id: "tool-purchase", + action: "place_order", + scope: "supplements", + domain_hint: "ecommerce", + risk_hint: "purchase", + attributes: { + merchant: "Fullscript", + item: "Vitamin D3 + K2", + quantity: "1", + note: "Matched prior merchant and approved dosage plan.", + }, + }, + tool: { + id: "tool-purchase", + tool_key: "merchant_proxy", + name: "Merchant Proxy", + description: "Proxy for governed ecommerce actions.", + version: "0.1.0", + metadata_version: "tool_metadata_v0", + active: true, + tags: ["commerce", "approval"], + action_hints: ["place_order"], + scope_hints: ["supplements"], + domain_hints: ["ecommerce"], + risk_hints: ["purchase"], + metadata: {}, + created_at: "2026-03-14T09:15:00Z", + }, + routing: { + decision: "require_approval", + reasons: [ + { + code: "matched_policy", + source: "policy", + message: "Repeat supplement purchases remain approval-gated even when the merchant and dosage are known.", + tool_id: "tool-purchase", + policy_id: "policy-purchase-approval", + consent_key: null, + }, + ], + trace: { + trace_id: "trace-approval-100", + trace_event_count: 5, + }, + }, + created_at: "2026-03-16T14:10:00Z", + resolution: { + resolved_at: "2026-03-16T14:22:00Z", + resolved_by_user_id: "operator-1", + }, + }, +]; + +function getApiConfig() { + return { + apiBaseUrl: + process.env.NEXT_PUBLIC_ALICEBOT_API_BASE_URL ?? process.env.ALICEBOT_API_BASE_URL ?? "", + userId: process.env.NEXT_PUBLIC_ALICEBOT_USER_ID ?? process.env.ALICEBOT_USER_ID ?? "", + }; +} + +async function loadApprovals(): Promise<{ items: ApprovalItem[]; source: "live" | "fixture" }> { + const { apiBaseUrl, userId } = getApiConfig(); + if (!apiBaseUrl || !userId) { + return { items: approvalFixtures, source: "fixture" }; + } + + try { + const response = await fetch( + `${apiBaseUrl.replace(/\/$/, "")}/v0/approvals?user_id=${encodeURIComponent(userId)}`, + { cache: "no-store" }, + ); + + if (!response.ok) { + throw new Error("approval list request failed"); + } + + const payload = (await response.json()) as { items?: ApprovalItem[] }; + return { + items: payload.items ?? approvalFixtures, + source: "live", + }; + } catch { + return { items: approvalFixtures, source: "fixture" }; + } +} + +type SearchParams = Promise>; + +export default async function ApprovalsPage({ + searchParams, +}: { + searchParams?: SearchParams; +}) { + const params = (searchParams ? await searchParams : {}) as Record< + string, + string | string[] | undefined + >; + const selectedId = typeof params.approval === "string" ? params.approval : undefined; + const { items, source } = await loadApprovals(); + + return ( +
+ + {source === "live" ? "Live API" : "Fixture-backed"} + {items.length} items +
+ } + /> + + + + ); +} diff --git a/apps/web/app/chat/page.tsx b/apps/web/app/chat/page.tsx new file mode 100644 index 0000000..6dc68a6 --- /dev/null +++ b/apps/web/app/chat/page.tsx @@ -0,0 +1,93 @@ +import { PageHeader } from "../../components/page-header"; +import { RequestComposer, type RequestHistoryEntry } from "../../components/request-composer"; +import { SectionCard } from "../../components/section-card"; + +const initialEntries: RequestHistoryEntry[] = [ + { + id: "req-001", + request: "Summarize the open magnesium reorder task and tell me whether an approval is still required.", + response: + "The current task remains in a governed state. The latest task step is waiting on approval resolution before any execution can proceed, and the next operator action is to review the approval inbox rather than trigger another tool call.", + submittedAt: "2026-03-17T08:45:00Z", + source: "fixture", + trace: { + compileTraceId: "trace-ctx-401", + compileTraceEventCount: 9, + responseTraceId: "trace-resp-402", + responseTraceEventCount: 4, + }, + }, +]; + +function getApiConfig() { + return { + apiBaseUrl: + process.env.NEXT_PUBLIC_ALICEBOT_API_BASE_URL ?? process.env.ALICEBOT_API_BASE_URL ?? "", + userId: process.env.NEXT_PUBLIC_ALICEBOT_USER_ID ?? process.env.ALICEBOT_USER_ID ?? "", + threadId: process.env.NEXT_PUBLIC_ALICEBOT_THREAD_ID ?? process.env.ALICEBOT_THREAD_ID ?? "", + }; +} + +export default function ChatPage() { + const apiConfig = getApiConfig(); + const liveModeReady = Boolean(apiConfig.apiBaseUrl && apiConfig.userId && apiConfig.threadId); + + return ( +
+ + {liveModeReady ? "Live API mode" : "Fixture mode"} + Response traces visible +
+ } + /> + +
+ + +
+ +
    +
  • Requests are framed as operator instructions against existing governed seams.
  • +
  • Live mode posts to the shipped response endpoint only when API configuration is present.
  • +
  • Trace references stay attached to each recent response so explainability remains first-class.
  • +
+
+ + +
+
+
Sessions
+
Up to 8 recent sessions
+
+
+
Events
+
Up to 80 continuity events
+
+
+
Memories
+
Up to 20 admitted memories
+
+
+
Entities
+
Up to 12 entities and 20 edges
+
+
+
+
+
+ + ); +} diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css new file mode 100644 index 0000000..2905c41 --- /dev/null +++ b/apps/web/app/globals.css @@ -0,0 +1,931 @@ +:root { + --font-sans: "Avenir Next", "Segoe UI", "Helvetica Neue", sans-serif; + --font-serif: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif; + --bg: #f3efe8; + --bg-accent: rgba(68, 88, 112, 0.12); + --surface: rgba(255, 252, 248, 0.88); + --surface-strong: rgba(255, 255, 255, 0.94); + --surface-muted: rgba(246, 240, 232, 0.82); + --border: rgba(42, 52, 66, 0.12); + --border-strong: rgba(42, 52, 66, 0.18); + --text: #18202a; + --text-soft: #566172; + --text-muted: #707988; + --accent: #274b63; + --accent-soft: rgba(39, 75, 99, 0.09); + --success: #2c6e62; + --success-soft: rgba(44, 110, 98, 0.1); + --warning: #8e6220; + --warning-soft: rgba(142, 98, 32, 0.11); + --danger: #8d4440; + --danger-soft: rgba(141, 68, 64, 0.1); + --info: #365d7c; + --info-soft: rgba(54, 93, 124, 0.11); + --shadow-lg: 0 22px 70px rgba(32, 43, 56, 0.09); + --shadow-md: 0 14px 36px rgba(32, 43, 56, 0.06); + --radius-xl: 28px; + --radius-lg: 22px; + --radius-md: 16px; + --radius-sm: 12px; + --content-width: 1360px; +} + +* { + box-sizing: border-box; + min-width: 0; +} + +html { + background: + radial-gradient(circle at top left, rgba(211, 221, 232, 0.5), transparent 28%), + radial-gradient(circle at top right, rgba(233, 220, 205, 0.55), transparent 24%), + var(--bg); + color: var(--text); +} + +body { + margin: 0; + font-family: var(--font-sans); + color: var(--text); + min-height: 100vh; +} + +a { + color: inherit; + text-decoration: none; +} + +button, +input, +textarea, +select { + font: inherit; +} + +button { + cursor: pointer; +} + +img, +svg { + display: block; + max-width: 100%; +} + +code, +.mono { + font-family: + "SFMono-Regular", "SF Mono", "JetBrains Mono", "Roboto Mono", "Menlo", monospace; +} + +.shell-chrome { + position: relative; + min-height: 100vh; +} + +.shell-chrome::before { + content: ""; + position: fixed; + inset: 0; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.35), transparent 32%), + radial-gradient(circle at 15% 20%, rgba(39, 75, 99, 0.08), transparent 24%); + pointer-events: none; +} + +.shell { + position: relative; + z-index: 1; + max-width: var(--content-width); + margin: 0 auto; + padding: 24px; + display: grid; + grid-template-columns: 280px minmax(0, 1fr); + gap: 24px; +} + +.shell-sidebar { + position: sticky; + top: 24px; + align-self: start; + display: grid; + gap: 18px; + padding: 22px; + background: rgba(255, 252, 248, 0.72); + border: 1px solid var(--border); + border-radius: 30px; + box-shadow: var(--shadow-md); + backdrop-filter: blur(16px); +} + +.brand-mark { + display: inline-grid; + place-items: center; + width: 42px; + height: 42px; + border-radius: 14px; + background: linear-gradient(180deg, rgba(39, 75, 99, 0.16), rgba(39, 75, 99, 0.08)); + border: 1px solid rgba(39, 75, 99, 0.16); + color: var(--accent); + font-weight: 700; + letter-spacing: 0.08em; +} + +.brand-copy { + display: grid; + gap: 6px; +} + +.eyebrow { + margin: 0; + font-size: 0.7rem; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--text-muted); +} + +.brand-title { + margin: 0; + font-family: var(--font-serif); + font-size: 1.5rem; + font-weight: 600; + letter-spacing: -0.02em; +} + +.brand-description, +.muted-copy { + margin: 0; + color: var(--text-soft); + line-height: 1.6; +} + +.shell-nav, +.shell-nav--mobile { + display: grid; + gap: 10px; +} + +.shell-nav__item { + display: grid; + gap: 4px; + padding: 14px 16px; + border-radius: 16px; + border: 1px solid transparent; + transition: + border-color 140ms ease, + background-color 140ms ease, + transform 140ms ease; +} + +.shell-nav__item:hover, +.shell-nav__item:focus-visible { + border-color: var(--border); + background: rgba(255, 255, 255, 0.6); + transform: translateY(-1px); +} + +.shell-nav__item.is-active { + border-color: rgba(39, 75, 99, 0.18); + background: var(--accent-soft); +} + +.shell-nav__title { + font-size: 0.95rem; + font-weight: 600; +} + +.shell-nav__caption { + color: var(--text-soft); + font-size: 0.86rem; + line-height: 1.45; +} + +.shell-note { + padding: 16px; + border-radius: 18px; + background: rgba(244, 238, 230, 0.8); + border: 1px solid var(--border); +} + +.shell-note__title { + margin: 0 0 8px; + font-size: 0.88rem; + font-weight: 600; +} + +.shell-column { + display: grid; + gap: 22px; +} + +.shell-topbar { + display: grid; + gap: 18px; + padding: 22px 24px; + background: rgba(255, 252, 248, 0.72); + border: 1px solid var(--border); + border-radius: 30px; + box-shadow: var(--shadow-md); + backdrop-filter: blur(16px); +} + +.shell-topbar__row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.shell-topbar__title { + margin: 0; + font-family: var(--font-serif); + font-size: clamp(1.6rem, 3vw, 2rem); + letter-spacing: -0.03em; +} + +.topbar-status { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.subtle-chip { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 9px 12px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.74); + border: 1px solid var(--border); + color: var(--text-soft); + font-size: 0.82rem; + line-height: 1; + white-space: nowrap; +} + +.shell-main { + padding-bottom: 24px; +} + +.content-frame { + display: grid; + gap: 24px; +} + +.page-stack, +.stack { + display: grid; + gap: 24px; +} + +.page-header { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + justify-content: space-between; + gap: 18px 24px; +} + +.page-header__copy { + display: grid; + gap: 10px; + max-width: 860px; +} + +.page-header h1 { + margin: 0; + font-family: var(--font-serif); + font-size: clamp(2rem, 4vw, 3rem); + line-height: 1.05; + letter-spacing: -0.04em; +} + +.page-header p { + margin: 0; + color: var(--text-soft); + line-height: 1.7; + max-width: 74ch; +} + +.header-meta { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.content-grid, +.dashboard-grid { + display: grid; + gap: 24px; +} + +.content-grid--wide { + grid-template-columns: minmax(0, 1.55fr) minmax(300px, 0.9fr); + align-items: flex-start; +} + +.dashboard-grid--detail { + grid-template-columns: minmax(320px, 0.95fr) minmax(0, 1.25fr); + align-items: flex-start; +} + +.metric-grid, +.route-grid { + display: grid; + gap: 18px; +} + +.metric-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.route-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.section-card { + display: grid; + gap: 18px; + padding: 24px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-md); + backdrop-filter: blur(14px); +} + +.section-card--metric { + gap: 10px; + min-height: 100%; +} + +.section-card__header { + display: grid; + gap: 8px; +} + +.section-card__title { + margin: 0; + font-size: 1.1rem; + font-weight: 600; + letter-spacing: -0.02em; +} + +.section-card__description { + margin: 0; + color: var(--text-soft); + line-height: 1.6; +} + +.metric-value { + font-size: clamp(2rem, 5vw, 2.65rem); + font-weight: 600; + letter-spacing: -0.05em; +} + +.metric-label { + font-size: 0.94rem; + font-weight: 600; +} + +.metric-detail { + margin: 0; + color: var(--text-soft); + line-height: 1.6; +} + +.nav-card { + display: grid; + gap: 12px; + padding: 18px; + border-radius: 20px; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.56); + transition: + transform 140ms ease, + border-color 140ms ease, + box-shadow 140ms ease; +} + +.nav-card:hover, +.nav-card:focus-visible { + transform: translateY(-1px); + border-color: var(--border-strong); + box-shadow: 0 14px 32px rgba(32, 43, 56, 0.05); +} + +.nav-card__topline { + display: flex; + justify-content: space-between; + gap: 12px; + align-items: flex-start; +} + +.nav-card__topline h3 { + margin: 0; + font-size: 1rem; + letter-spacing: -0.02em; +} + +.nav-card p { + margin: 0; + color: var(--text-soft); + line-height: 1.6; +} + +.nav-card__cta { + color: var(--accent); + font-size: 0.9rem; + font-weight: 600; +} + +.bullet-list { + margin: 0; + padding-left: 1.2rem; + color: var(--text-soft); + display: grid; + gap: 12px; + line-height: 1.6; +} + +.key-value-grid { + display: grid; + gap: 14px; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.key-value-grid div { + display: grid; + gap: 6px; + padding: 14px 16px; + border-radius: 16px; + background: rgba(255, 255, 255, 0.6); + border: 1px solid rgba(42, 52, 66, 0.08); +} + +.key-value-grid dt { + color: var(--text-muted); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.12em; +} + +.key-value-grid dd { + margin: 0; + line-height: 1.55; + overflow-wrap: anywhere; +} + +.status-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 11px; + border-radius: 999px; + border: 1px solid transparent; + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.04em; + line-height: 1; + text-transform: uppercase; + white-space: nowrap; +} + +.status-badge--success { + color: var(--success); + background: var(--success-soft); + border-color: rgba(44, 110, 98, 0.18); +} + +.status-badge--warning { + color: var(--warning); + background: var(--warning-soft); + border-color: rgba(142, 98, 32, 0.18); +} + +.status-badge--danger { + color: var(--danger); + background: var(--danger-soft); + border-color: rgba(141, 68, 64, 0.18); +} + +.status-badge--info { + color: var(--info); + background: var(--info-soft); + border-color: rgba(54, 93, 124, 0.18); +} + +.status-badge--neutral { + color: var(--text-soft); + background: rgba(255, 255, 255, 0.72); + border-color: var(--border); +} + +.empty-state { + display: grid; + gap: 12px; + justify-items: start; + padding: 28px; + border-radius: 22px; + background: rgba(255, 255, 255, 0.56); + border: 1px dashed var(--border-strong); +} + +.empty-state__title { + margin: 0; + font-size: 1rem; + font-weight: 600; +} + +.empty-state__description { + margin: 0; + color: var(--text-soft); + line-height: 1.6; +} + +.button, +.button-secondary { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 12px 16px; + border-radius: 14px; + border: 1px solid transparent; + font-weight: 600; + line-height: 1; + transition: + transform 140ms ease, + background-color 140ms ease, + border-color 140ms ease; +} + +.button { + background: var(--accent); + color: #f7fafc; +} + +.button:hover, +.button:focus-visible, +.button-secondary:hover, +.button-secondary:focus-visible { + transform: translateY(-1px); +} + +.button:disabled { + opacity: 0.62; + cursor: not-allowed; + transform: none; +} + +.button-secondary { + background: rgba(255, 255, 255, 0.72); + border-color: var(--border); + color: var(--text); +} + +.composer-card { + display: grid; + gap: 24px; + padding: 24px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-md); + backdrop-filter: blur(14px); +} + +.composer-card__header, +.detail-stack, +.trace-panel, +.trace-panel__detail, +.list-panel { + display: grid; + gap: 16px; +} + +.governance-banner { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; + padding: 14px 16px; + background: rgba(39, 75, 99, 0.06); + border: 1px solid rgba(39, 75, 99, 0.12); + border-radius: 18px; + color: var(--text-soft); +} + +.governance-banner strong { + color: var(--text); +} + +.form-field { + display: grid; + gap: 10px; +} + +.form-field label { + font-size: 0.9rem; + font-weight: 600; +} + +.form-field textarea, +.form-field input { + width: 100%; + padding: 16px 18px; + background: rgba(255, 255, 255, 0.74); + border: 1px solid var(--border); + border-radius: 18px; + color: var(--text); + resize: vertical; +} + +.form-field textarea { + min-height: 168px; + line-height: 1.6; +} + +.field-hint { + margin: 0; + color: var(--text-muted); + font-size: 0.86rem; + line-height: 1.5; +} + +.composer-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + justify-content: space-between; +} + +.composer-status { + color: var(--text-soft); + font-size: 0.9rem; +} + +.history-list, +.list-rows, +.timeline-list, +.trace-events { + display: grid; + gap: 12px; +} + +.history-entry, +.list-row, +.timeline-item, +.trace-event { + padding: 16px 18px; + border-radius: 18px; + border: 1px solid rgba(42, 52, 66, 0.08); + background: rgba(255, 255, 255, 0.58); +} + +.history-entry { + display: grid; + gap: 14px; +} + +.history-entry__topline, +.list-row__topline, +.timeline-item__topline, +.trace-event__topline, +.detail-summary { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.history-entry__label, +.list-row__eyebrow, +.detail-summary__label { + color: var(--text-muted); + font-size: 0.8rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.history-entry p, +.list-row p, +.timeline-item p, +.trace-event p { + margin: 0; + color: var(--text-soft); + line-height: 1.6; + overflow-wrap: anywhere; +} + +.history-entry__trace, +.cluster { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.split-layout { + display: grid; + gap: 24px; + grid-template-columns: minmax(320px, 0.95fr) minmax(0, 1.25fr); +} + +.list-panel__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; +} + +.list-panel__header h2, +.trace-panel h2 { + margin: 0; + font-size: 1.05rem; + letter-spacing: -0.02em; +} + +.list-panel__header p, +.trace-panel__detail > p { + margin: 0; + color: var(--text-soft); + line-height: 1.6; +} + +.list-row { + display: grid; + gap: 12px; + transition: + transform 140ms ease, + border-color 140ms ease, + background-color 140ms ease; +} + +.list-row:hover, +.list-row:focus-visible { + transform: translateY(-1px); + border-color: var(--border-strong); +} + +.list-row.is-selected { + border-color: rgba(39, 75, 99, 0.2); + background: var(--accent-soft); +} + +.list-row__title { + margin: 0; + font-size: 0.98rem; + font-weight: 600; + letter-spacing: -0.02em; +} + +.list-row__meta, +.attribute-list, +.evidence-list { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.meta-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 10px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.68); + border: 1px solid rgba(42, 52, 66, 0.08); + color: var(--text-soft); + font-size: 0.82rem; +} + +.detail-grid { + display: grid; + gap: 18px; +} + +.detail-group { + display: grid; + gap: 12px; +} + +.detail-group h3 { + margin: 0; + font-size: 0.92rem; + letter-spacing: -0.01em; +} + +.attribute-item, +.evidence-chip { + padding: 9px 11px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.7); + border: 1px solid rgba(42, 52, 66, 0.08); + color: var(--text-soft); + font-size: 0.84rem; + overflow-wrap: anywhere; +} + +.reason-list { + margin: 0; + padding-left: 1.2rem; + color: var(--text-soft); + display: grid; + gap: 10px; + line-height: 1.6; +} + +.timeline-item { + display: grid; + gap: 12px; +} + +.timeline-item__summary { + display: grid; + gap: 8px; +} + +.trace-events { + margin: 0; + padding: 0; + list-style: none; +} + +.trace-event h4 { + margin: 0; + font-size: 0.95rem; + letter-spacing: -0.01em; +} + +.trace-summary { + display: grid; + gap: 14px; +} + +.responsive-note { + color: var(--text-muted); + font-size: 0.82rem; +} + +@media (max-width: 1120px) { + .shell { + grid-template-columns: 1fr; + } + + .shell-sidebar { + display: none; + } + + .content-grid--wide, + .dashboard-grid--detail, + .split-layout, + .metric-grid, + .route-grid { + grid-template-columns: 1fr; + } +} + +@media (min-width: 1121px) { + .shell-nav--mobile { + display: none; + } +} + +@media (max-width: 740px) { + .shell { + padding: 14px; + gap: 16px; + } + + .shell-topbar, + .section-card, + .composer-card { + padding: 20px; + border-radius: 24px; + } + + .shell-topbar__row, + .page-header, + .composer-actions, + .list-panel__header, + .history-entry__topline, + .list-row__topline, + .timeline-item__topline, + .trace-event__topline, + .detail-summary, + .nav-card__topline { + flex-direction: column; + align-items: flex-start; + } + + .shell-nav--mobile { + grid-auto-flow: column; + grid-auto-columns: minmax(150px, 1fr); + overflow-x: auto; + padding-bottom: 2px; + } + + .key-value-grid { + grid-template-columns: 1fr; + } +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index ed6cafd..4fcf704 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,10 +1,23 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +import { AppShell } from "../components/app-shell"; + +import "./globals.css"; + +export const metadata: Metadata = { + title: "AliceBot Operator Shell", + description: "Governed operator interface for requests, approvals, tasks, and explainability.", +}; + export default function RootLayout({ children, -}: Readonly<{ children: React.ReactNode }>) { +}: Readonly<{ children: ReactNode }>) { return ( - {children} + + {children} + ); } - diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 7a46a7a..1d99bb2 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,51 +1,145 @@ -const milestones = [ - "API foundation and migrations", - "Continuity event store", - "Web dashboard shell", - "Worker orchestration", +import Link from "next/link"; + +import { PageHeader } from "../components/page-header"; +import { SectionCard } from "../components/section-card"; +import { StatusBadge } from "../components/status-badge"; + +const summaryCards = [ + { + value: "5", + label: "Operator views", + detail: "Home, request composition, approvals, task inspection, and explainability are all exposed in one bounded shell.", + }, + { + value: "3", + label: "Governance seams", + detail: "Requests, approvals, and tool executions stay visible instead of being hidden behind a consumer chat wrapper.", + }, + { + value: "2", + label: "Data modes", + detail: "Pages can read live backend seams when configured and degrade to explicit fixtures when no API contract is present.", + }, + { + value: "100%", + label: "Scoped surface", + detail: "The shell stays within the sprint packet: no auth expansion, no connector breadth, and no backend contract changes.", + }, +]; + +const routeCards = [ + { + href: "/chat", + title: "Governed Requests", + description: "Compose bounded operator requests, review response history, and keep compilation and response traces visible.", + status: "active", + }, + { + href: "/approvals", + title: "Approval Inbox", + description: "Review pending approvals with tool, scope, routing, and rationale all contained in a stable inspector layout.", + status: "pending_approval", + }, + { + href: "/tasks", + title: "Task Inspection", + description: "Inspect task lifecycle state, related governed requests, and ordered task-step progress without leaving the shell.", + status: "approved", + }, + { + href: "/traces", + title: "Explain-Why Review", + description: "Trace context compilation and governed actions through a calm evidence-first review surface.", + status: "executed", + }, +]; + +const shellNotes = [ + "Stable navigation with obvious current location and restrained emphasis.", + "Cards and lists sized for readable density rather than dashboard clutter.", + "Responsive stacking that protects text containment on tablet and mobile widths.", ]; export default function HomePage() { return ( -
-
-

- AliceBot Foundation -

-

- Operational shell for the modular monolith -

-

- The web app is intentionally minimal in this sprint. It exists to prove repository - structure while continuity, migrations, and safety primitives land in the API layer. -

-
    - {milestones.map((item) => ( -
  • {item}
  • - ))} -
+
+ + Sprint 6A shell + Design-system aligned +
+ } + /> + +
+ {summaryCards.map((card) => ( + +
{card.value}
+
{card.label}
+

{card.detail}

+
+ ))}
-
+ +
+ +
+ {routeCards.map((route) => ( + +
+

{route.title}

+ +
+

{route.description}

+ Open view + + ))} +
+
+ +
+ +
    + {shellNotes.map((note) => ( +
  • {note}
  • + ))} +
+
+ + +
+
+
Request path
+
Explicitly labeled as governed and reviewable.
+
+
+
Consequential actions
+
Held behind approval and execution review states.
+
+
+
Explainability
+
Trace review sits beside operational work, not in a debug-only corner.
+
+
+
+
+
+ ); } - diff --git a/apps/web/app/tasks/page.tsx b/apps/web/app/tasks/page.tsx new file mode 100644 index 0000000..637aee5 --- /dev/null +++ b/apps/web/app/tasks/page.tsx @@ -0,0 +1,318 @@ +import { PageHeader } from "../../components/page-header"; +import { SectionCard } from "../../components/section-card"; +import { StatusBadge } from "../../components/status-badge"; +import { TaskList, type TaskItem } from "../../components/task-list"; +import { TaskStepList, type TaskStepItem } from "../../components/task-step-list"; + +const taskFixtures: TaskItem[] = [ + { + id: "task-201", + thread_id: "thread-magnesium", + tool_id: "tool-purchase", + status: "pending_approval", + request: { + thread_id: "thread-magnesium", + tool_id: "tool-purchase", + action: "place_order", + scope: "supplements", + domain_hint: "ecommerce", + risk_hint: "purchase", + attributes: { + merchant: "Thorne", + item: "Magnesium Bisglycinate", + }, + }, + tool: { + id: "tool-purchase", + tool_key: "merchant_proxy", + name: "Merchant Proxy", + description: "Proxy for governed ecommerce actions.", + version: "0.1.0", + metadata_version: "tool_metadata_v0", + active: true, + tags: ["commerce", "approval"], + action_hints: ["place_order"], + scope_hints: ["supplements"], + domain_hints: ["ecommerce"], + risk_hints: ["purchase"], + metadata: {}, + created_at: "2026-03-15T08:00:00Z", + }, + latest_approval_id: "approval-101", + latest_execution_id: null, + created_at: "2026-03-17T06:49:00Z", + updated_at: "2026-03-17T06:50:00Z", + }, + { + id: "task-182", + thread_id: "thread-vitamin-d", + tool_id: "tool-purchase", + status: "approved", + request: { + thread_id: "thread-vitamin-d", + tool_id: "tool-purchase", + action: "place_order", + scope: "supplements", + domain_hint: "ecommerce", + risk_hint: "purchase", + attributes: { + merchant: "Fullscript", + item: "Vitamin D3 + K2", + }, + }, + tool: { + id: "tool-purchase", + tool_key: "merchant_proxy", + name: "Merchant Proxy", + description: "Proxy for governed ecommerce actions.", + version: "0.1.0", + metadata_version: "tool_metadata_v0", + active: true, + tags: ["commerce", "approval"], + action_hints: ["place_order"], + scope_hints: ["supplements"], + domain_hints: ["ecommerce"], + risk_hints: ["purchase"], + metadata: {}, + created_at: "2026-03-14T09:15:00Z", + }, + latest_approval_id: "approval-100", + latest_execution_id: null, + created_at: "2026-03-16T14:00:00Z", + updated_at: "2026-03-16T14:22:00Z", + }, +]; + +const stepFixtures: Record = { + "task-201": [ + { + id: "step-20", + task_id: "task-201", + sequence_no: 1, + kind: "governed_request", + status: "created", + request: { + thread_id: "thread-magnesium", + tool_id: "tool-purchase", + action: "place_order", + scope: "supplements", + domain_hint: "ecommerce", + risk_hint: "purchase", + attributes: { + merchant: "Thorne", + item: "Magnesium Bisglycinate", + package: "90 capsules", + }, + }, + outcome: { + routing_decision: "require_approval", + approval_id: "approval-101", + approval_status: "pending", + execution_id: null, + execution_status: null, + blocked_reason: null, + }, + lineage: { + parent_step_id: null, + source_approval_id: null, + source_execution_id: null, + }, + trace: { + trace_id: "trace-step-20", + trace_kind: "approval_request", + }, + created_at: "2026-03-17T06:49:00Z", + updated_at: "2026-03-17T06:50:00Z", + }, + ], + "task-182": [ + { + id: "step-14", + task_id: "task-182", + sequence_no: 1, + kind: "governed_request", + status: "approved", + request: { + thread_id: "thread-vitamin-d", + tool_id: "tool-purchase", + action: "place_order", + scope: "supplements", + domain_hint: "ecommerce", + risk_hint: "purchase", + attributes: { + merchant: "Fullscript", + item: "Vitamin D3 + K2", + quantity: "1", + }, + }, + outcome: { + routing_decision: "require_approval", + approval_id: "approval-100", + approval_status: "approved", + execution_id: null, + execution_status: null, + blocked_reason: null, + }, + lineage: { + parent_step_id: null, + source_approval_id: null, + source_execution_id: null, + }, + trace: { + trace_id: "trace-step-14", + trace_kind: "approval_resolution", + }, + created_at: "2026-03-16T14:00:00Z", + updated_at: "2026-03-16T14:22:00Z", + }, + ], +}; + +function getApiConfig() { + return { + apiBaseUrl: + process.env.NEXT_PUBLIC_ALICEBOT_API_BASE_URL ?? process.env.ALICEBOT_API_BASE_URL ?? "", + userId: process.env.NEXT_PUBLIC_ALICEBOT_USER_ID ?? process.env.ALICEBOT_USER_ID ?? "", + }; +} + +async function loadTasks(): Promise<{ items: TaskItem[]; source: "live" | "fixture" }> { + const { apiBaseUrl, userId } = getApiConfig(); + if (!apiBaseUrl || !userId) { + return { items: taskFixtures, source: "fixture" }; + } + + try { + const response = await fetch( + `${apiBaseUrl.replace(/\/$/, "")}/v0/tasks?user_id=${encodeURIComponent(userId)}`, + { cache: "no-store" }, + ); + + if (!response.ok) { + throw new Error("task list request failed"); + } + + const payload = (await response.json()) as { items?: TaskItem[] }; + return { + items: payload.items ?? taskFixtures, + source: "live", + }; + } catch { + return { items: taskFixtures, source: "fixture" }; + } +} + +async function loadTaskSteps( + taskId: string, + source: "live" | "fixture", +): Promise<{ items: TaskStepItem[]; source: "live" | "fixture" }> { + if (source === "fixture") { + return { items: stepFixtures[taskId] ?? [], source: "fixture" }; + } + + const { apiBaseUrl, userId } = getApiConfig(); + try { + const response = await fetch( + `${apiBaseUrl.replace(/\/$/, "")}/v0/tasks/${taskId}/steps?user_id=${encodeURIComponent(userId)}`, + { cache: "no-store" }, + ); + + if (!response.ok) { + throw new Error("task step request failed"); + } + + const payload = (await response.json()) as { items?: TaskStepItem[] }; + return { + items: payload.items ?? stepFixtures[taskId] ?? [], + source: "live", + }; + } catch { + return { items: stepFixtures[taskId] ?? [], source: "fixture" }; + } +} + +type SearchParams = Promise>; + +export default async function TasksPage({ + searchParams, +}: { + searchParams?: SearchParams; +}) { + const params = (searchParams ? await searchParams : {}) as Record< + string, + string | string[] | undefined + >; + const requestedTaskId = typeof params.task === "string" ? params.task : undefined; + const { items, source } = await loadTasks(); + const selectedTask = items.find((item) => item.id === requestedTaskId) ?? items[0] ?? null; + const { items: steps, source: stepSource } = selectedTask + ? await loadTaskSteps(selectedTask.id, source) + : { items: [], source }; + + return ( +
+ + {source === "live" ? "Live API" : "Fixture-backed"} + {items.length} tasks +
+ } + /> + +
+ + +
+ + {selectedTask ? ( +
+
+ + + {selectedTask.request.action} / {selectedTask.request.scope} + +
+
+
+
Thread
+
{selectedTask.thread_id}
+
+
+
Latest approval
+
{selectedTask.latest_approval_id ?? "Not linked"}
+
+
+
Latest execution
+
{selectedTask.latest_execution_id ?? "Not executed"}
+
+
+
Data source
+
{stepSource === "live" ? "Live task-step API" : "Local fixture steps"}
+
+
+
+ ) : ( +

+ No task records are available in the current mode. +

+ )} +
+ + +
+
+ + ); +} diff --git a/apps/web/app/traces/page.tsx b/apps/web/app/traces/page.tsx new file mode 100644 index 0000000..5bf40fb --- /dev/null +++ b/apps/web/app/traces/page.tsx @@ -0,0 +1,173 @@ +import { PageHeader } from "../../components/page-header"; +import { SectionCard } from "../../components/section-card"; +import { TraceList, type TraceItem } from "../../components/trace-list"; + +const traceFixtures: TraceItem[] = [ + { + id: "trace-ctx-401", + kind: "context_compile", + status: "completed", + title: "Context compile for magnesium reorder guidance", + summary: "Compiled prior task state, admitted memories, and recent thread continuity before assistant response assembly.", + eventCount: 9, + createdAt: "2026-03-17T08:45:00Z", + source: "Context compiler", + scope: "thread-magnesium", + related: { + threadId: "thread-magnesium", + taskId: "task-201", + }, + evidence: [ + "Memory evidence admitted for supplement preference and merchant history.", + "Recent approval state included as part of the continuity pack.", + "Task-step lineage referenced before response generation.", + ], + events: [ + { + id: "event-1", + kind: "compiler.scope", + title: "Scope resolved", + detail: "Single-user thread scope and compile limits were established for the request.", + }, + { + id: "event-2", + kind: "memory.retrieve", + title: "Memory evidence attached", + detail: "Preference and purchase-history memories were ranked into the response context pack.", + }, + { + id: "event-3", + kind: "task.retrieve", + title: "Task lifecycle linked", + detail: "Open task and step state were included so the answer could acknowledge the approval dependency.", + }, + ], + }, + { + id: "trace-approval-101", + kind: "approval_request", + status: "requires_review", + title: "Approval request for supplement purchase", + summary: "Routing required user approval before the merchant proxy could execute the purchase request.", + eventCount: 6, + createdAt: "2026-03-17T06:50:00Z", + source: "Approval workflow", + scope: "supplements", + related: { + threadId: "thread-magnesium", + taskId: "task-201", + approvalId: "approval-101", + }, + evidence: [ + "Policy rule marked purchase actions as approval-gated.", + "Tool metadata matched the requested action and scope.", + "Task-step trace link points back to the original governed request.", + ], + events: [ + { + id: "event-4", + kind: "tool.route", + title: "Routing completed", + detail: "The merchant proxy was selected as the governing tool for the request.", + }, + { + id: "event-5", + kind: "approval.state", + title: "Approval opened", + detail: "Approval record persisted with pending resolution state and task-step linkage.", + }, + { + id: "event-6", + kind: "task.lifecycle", + title: "Task updated", + detail: "Task lifecycle moved into a pending approval state while retaining request provenance.", + }, + ], + }, + { + id: "trace-exec-311", + kind: "proxy_execution", + status: "completed", + title: "Governed execution for vitamin reorder", + summary: "Approved supplement purchase request executed through the proxy handler with task and trace linkage preserved.", + eventCount: 7, + createdAt: "2026-03-16T14:24:00Z", + source: "Proxy execution", + scope: "supplements", + related: { + threadId: "thread-vitamin-d", + taskId: "task-182", + approvalId: "approval-100", + executionId: "execution-311", + }, + evidence: [ + "Execution occurred only after approval resolution.", + "Handler output and trace references stayed attached to the governed action record.", + "Task and task-step lifecycle traces were appended alongside execution status.", + ], + events: [ + { + id: "event-7", + kind: "approval.check", + title: "Approval validated", + detail: "Execution preflight confirmed the approval was in an executable state.", + }, + { + id: "event-8", + kind: "budget.check", + title: "Budget check passed", + detail: "Execution budget constraints did not block the governed action.", + }, + { + id: "event-9", + kind: "execution.result", + title: "Handler completed", + detail: "Proxy output was recorded for the approved supplement reorder with a linked execution trace and task-step status update.", + }, + ], + }, +]; + +type SearchParams = Promise>; + +export default async function TracesPage({ + searchParams, +}: { + searchParams?: SearchParams; +}) { + const params = (searchParams ? await searchParams : {}) as Record< + string, + string | string[] | undefined + >; + const selectedId = typeof params.trace === "string" ? params.trace : undefined; + + return ( +
+ + Fixture-backed detail view + Existing backend concepts only +
+ } + /> + + + + +
    +
  • Which evidence types contributed to the outcome and whether they were appropriate.
  • +
  • How the lifecycle moved from request to approval or execution without losing provenance.
  • +
  • Whether the current trace surface needs deeper live-event wiring in a future sprint.
  • +
+
+ + ); +} diff --git a/apps/web/components/app-shell.tsx b/apps/web/components/app-shell.tsx new file mode 100644 index 0000000..f2e254f --- /dev/null +++ b/apps/web/components/app-shell.tsx @@ -0,0 +1,119 @@ +"use client"; + +import type { ReactNode } from "react"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +const navigation = [ + { + href: "/", + label: "Overview", + caption: "Shell landing and governed surface summary", + }, + { + href: "/chat", + label: "Requests", + caption: "Compose bounded operator requests", + }, + { + href: "/approvals", + label: "Approvals", + caption: "Review approval queue and inspector", + }, + { + href: "/tasks", + label: "Tasks", + caption: "Inspect lifecycle state and task steps", + }, + { + href: "/traces", + label: "Traces", + caption: "Explain-why and governed action review", + }, +]; + +function isActive(pathname: string, href: string) { + if (href === "/") { + return pathname === "/"; + } + + return pathname.startsWith(href); +} + +export function AppShell({ children }: { children: ReactNode }) { + const pathname = usePathname(); + + return ( +
+
+ + +
+
+
+
+

MVP Web Shell

+

Governed operator interface

+
+ +
+ Single-user v1 + Existing backend seams only +
+
+ + +
+ +
+
{children}
+
+
+
+
+ ); +} diff --git a/apps/web/components/approval-list.tsx b/apps/web/components/approval-list.tsx new file mode 100644 index 0000000..ec9eea9 --- /dev/null +++ b/apps/web/components/approval-list.tsx @@ -0,0 +1,212 @@ +import Link from "next/link"; + +import { EmptyState } from "./empty-state"; +import { SectionCard } from "./section-card"; +import { StatusBadge } from "./status-badge"; + +export type ApprovalItem = { + id: string; + thread_id: string; + task_step_id: string | null; + status: string; + request: { + thread_id: string; + tool_id: string; + action: string; + scope: string; + domain_hint: string | null; + risk_hint: string | null; + attributes: Record; + }; + tool: { + id: string; + tool_key: string; + name: string; + description: string; + version: string; + metadata_version: string; + active: boolean; + tags: string[]; + action_hints: string[]; + scope_hints: string[]; + domain_hints: string[]; + risk_hints: string[]; + metadata: Record; + created_at: string; + }; + routing: { + decision: string; + reasons: Array<{ + code: string; + source: string; + message: string; + tool_id: string | null; + policy_id: string | null; + consent_key: string | null; + }>; + trace: { + trace_id: string; + trace_event_count: number; + }; + }; + created_at: string; + resolution: { + resolved_at: string; + resolved_by_user_id: string; + } | null; +}; + +function formatDate(value: string) { + return new Intl.DateTimeFormat("en", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(new Date(value)); +} + +function formatAttributeValue(value: unknown) { + if (value == null) { + return "None"; + } + + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + return String(value); + } + + return JSON.stringify(value); +} + +export function ApprovalList({ + items, + selectedId, +}: { + items: ApprovalItem[]; + selectedId?: string; +}) { + if (items.length === 0) { + return ( + + + + ); + } + + const selected = items.find((item) => item.id === selectedId) ?? items[0]; + + return ( +
+ +
+
+

{items.length} total approvals

+
+
+ {items.map((item) => ( + +
+
+ {formatDate(item.created_at)} +

{item.tool.name}

+
+ +
+

+ {item.request.action} / {item.request.scope} +

+
+ Thread {item.thread_id} + {item.request.risk_hint ? Risk {item.request.risk_hint} : null} +
+ + ))} +
+
+
+ + +
+
+ + + {selected.request.action} / {selected.request.scope} + +
+ +
+
+
Thread
+
{selected.thread_id}
+
+
+
Task step
+
{selected.task_step_id ?? "Unlinked"}
+
+
+
Routing decision
+
{selected.routing.decision}
+
+
+
Trace
+
+ {selected.routing.trace.trace_id} · {selected.routing.trace.trace_event_count} events +
+
+
+ +
+

Request attributes

+
+ {Object.entries(selected.request.attributes).map(([key, value]) => ( + + {key}: {formatAttributeValue(value)} + + ))} +
+
+ +
+

Routing rationale

+
    + {selected.routing.reasons.map((reason) => ( +
  • + {reason.message} +
  • + ))} +
+
+ +
+

Resolution

+

+ {selected.resolution + ? `Resolved ${formatDate(selected.resolution.resolved_at)} by ${selected.resolution.resolved_by_user_id}.` + : "Still awaiting explicit operator resolution."} +

+
+
+
+
+ ); +} diff --git a/apps/web/components/empty-state.tsx b/apps/web/components/empty-state.tsx new file mode 100644 index 0000000..fc44acf --- /dev/null +++ b/apps/web/components/empty-state.tsx @@ -0,0 +1,22 @@ +import Link from "next/link"; + +type EmptyStateProps = { + title: string; + description: string; + actionHref?: string; + actionLabel?: string; +}; + +export function EmptyState({ title, description, actionHref, actionLabel }: EmptyStateProps) { + return ( +
+

{title}

+

{description}

+ {actionHref && actionLabel ? ( + + {actionLabel} + + ) : null} +
+ ); +} diff --git a/apps/web/components/page-header.tsx b/apps/web/components/page-header.tsx new file mode 100644 index 0000000..49fc524 --- /dev/null +++ b/apps/web/components/page-header.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from "react"; + +type PageHeaderProps = { + eyebrow?: string; + title: string; + description: string; + meta?: ReactNode; +}; + +export function PageHeader({ eyebrow, title, description, meta }: PageHeaderProps) { + return ( +
+
+ {eyebrow ?

{eyebrow}

: null} +

{title}

+

{description}

+
+ {meta ?
{meta}
: null} +
+ ); +} diff --git a/apps/web/components/request-composer.tsx b/apps/web/components/request-composer.tsx new file mode 100644 index 0000000..c1d0b97 --- /dev/null +++ b/apps/web/components/request-composer.tsx @@ -0,0 +1,230 @@ +"use client"; + +import type { FormEvent } from "react"; +import { useState, useTransition } from "react"; + +export type RequestHistoryEntry = { + id: string; + request: string; + response: string; + submittedAt: string; + source: "live" | "fixture"; + trace?: { + compileTraceId: string; + compileTraceEventCount: number; + responseTraceId: string; + responseTraceEventCount: number; + }; +}; + +type RequestComposerProps = { + initialEntries: RequestHistoryEntry[]; + apiBaseUrl?: string; + userId?: string; + threadId?: string; +}; + +type LiveResponsePayload = { + assistant: { + event_id: string; + text: string; + }; + trace: { + compile_trace_id: string; + compile_trace_event_count: number; + response_trace_id: string; + response_trace_event_count: number; + }; +}; + +function formatDate(value: string) { + return new Intl.DateTimeFormat("en", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(new Date(value)); +} + +function buildFixtureEntry(message: string): RequestHistoryEntry { + const excerpt = message.trim().slice(0, 120); + const requestLabel = excerpt.length > 0 ? excerpt : "Operator request"; + const nonce = Date.now().toString(36); + + return { + id: `fixture-${nonce}`, + request: requestLabel, + response: + `Prepared a governed response preview for "${requestLabel}". In live mode this surface returns assistant output together with compile and response trace references from the backend.`, + submittedAt: new Date().toISOString(), + source: "fixture", + trace: { + compileTraceId: `trace-ctx-${nonce}`, + compileTraceEventCount: 5, + responseTraceId: `trace-resp-${nonce}`, + responseTraceEventCount: 3, + }, + }; +} + +export function RequestComposer({ + initialEntries, + apiBaseUrl, + userId, + threadId, +}: RequestComposerProps) { + const [message, setMessage] = useState(""); + const [entries, setEntries] = useState(initialEntries); + const [statusText, setStatusText] = useState("Ready for a governed operator request."); + const [isPending, startTransition] = useTransition(); + + const liveModeReady = Boolean(apiBaseUrl && userId && threadId); + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + + const nextMessage = message.trim(); + if (!nextMessage) { + return; + } + + setStatusText(liveModeReady ? "Submitting request to the response endpoint..." : "Saving fixture-backed preview..."); + + if (!liveModeReady) { + const entry = buildFixtureEntry(nextMessage); + startTransition(() => { + setEntries((current) => [entry, ...current]); + setMessage(""); + setStatusText("Fixture response added. Configure the web API env vars to switch this view into live mode."); + }); + return; + } + + try { + const response = await fetch(`${apiBaseUrl?.replace(/\/$/, "")}/v0/responses`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + user_id: userId, + thread_id: threadId, + message: nextMessage, + max_sessions: 8, + max_events: 80, + max_memories: 20, + max_entities: 12, + max_entity_edges: 20, + }), + }); + + const payload = (await response.json()) as LiveResponsePayload | { detail?: string }; + if (!response.ok || !("assistant" in payload)) { + throw new Error("detail" in payload && payload.detail ? payload.detail : "Request failed"); + } + + const entry: RequestHistoryEntry = { + id: payload.assistant.event_id, + request: nextMessage, + response: payload.assistant.text, + submittedAt: new Date().toISOString(), + source: "live", + trace: { + compileTraceId: payload.trace.compile_trace_id, + compileTraceEventCount: payload.trace.compile_trace_event_count, + responseTraceId: payload.trace.response_trace_id, + responseTraceEventCount: payload.trace.response_trace_event_count, + }, + }; + + startTransition(() => { + setEntries((current) => [entry, ...current]); + setMessage(""); + setStatusText("Live response received and trace references recorded."); + }); + } catch (error) { + const detail = error instanceof Error ? error.message : "Request failed"; + setStatusText(`Unable to submit live request: ${detail}`); + } + } + + return ( +
+
+
+ {liveModeReady ? "Live operator mode" : "Fixture operator mode"} + + Requests stay explicitly governed and recent trace references remain attached to each response. + +
+ +
+ +

+ Keep requests bounded to existing backend concepts. This surface is optimized for clarity + and review rather than casual back-and-forth. +

+
+
+ +
+
+