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
16 changes: 8 additions & 8 deletions .github/workflows/web-e2e.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
name: Web E2E

# Manual-only for now. The backend's lifespan requires a reachable LLM
# provider (Ollama by default per config/config.yaml), which CI runners
# don't have. v2.1 will introduce an opt-in stub-provider config so this
# can run on every PR; until then, run Playwright locally against the
# Caddy-fronted backend (see docs/REACT_UI_PARITY.md "Verification"
# section).
on:
pull_request:
branches: [main]
paths:
- 'web/**'
- 'src/runtime/**'
- '.github/workflows/web-e2e.yml'
workflow_dispatch:

jobs:
Expand Down Expand Up @@ -64,10 +64,10 @@ jobs:
- name: Boot backend (serves SPA + API)
run: |
mkdir -p logs
nohup uv run uvicorn runtime.api:app --port 8000 > logs/uvicorn.log 2>&1 &
nohup uv run uvicorn runtime.api:get_app --factory --port 8000 > logs/uvicorn.log 2>&1 &
echo $! > .backend.pid
for i in $(seq 1 60); do
if curl -sf http://localhost:8000/api/v1/ui/hints >/dev/null; then
if curl -sf http://localhost:8000/health >/dev/null; then
echo "backend ready after ${i}s"
exit 0
fi
Expand Down
24 changes: 22 additions & 2 deletions dist/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1440,6 +1440,7 @@ async def _poll(self, registry):




# ----- imports for runtime/api_dedup.py -----
"""Dedup retraction HTTP routes.

Expand All @@ -1462,8 +1463,9 @@ async def _poll(self, registry):
"""


from typing import Any, Callable, Union

from fastapi import FastAPI, HTTPException
from fastapi import APIRouter, FastAPI, HTTPException


# ----- imports for runtime/api_session_full.py -----
Expand Down Expand Up @@ -16338,6 +16340,19 @@ async def ws_events(websocket: WebSocket, session_id: str) -> None:
# ==================================================================
add_recent_events_routes(api_v1)

# ==================================================================
# Dedup retraction: POST /api/v1/sessions/{id}/un-duplicate
# Operator-triggered correction when the dedup pipeline flipped a
# session to status='duplicate' incorrectly. The store rewrites
# status back to a runnable state in the same transaction as the
# audit row (see SessionStore.un_duplicate). Mounted on api_v1 so
# the route inherits the /api/v1 prefix and CORS/exception envelope.
# ==================================================================
register_dedup_routes(
api_v1,
store_provider=lambda: fastapi_app.state.orchestrator.store,
)

# Legacy /incidents/* and /investigate redirects to /api/v1/* equivalents.
# 308 preserves method + body so legacy POSTs (e.g. /incidents/{id}/resume)
# keep working transparently. Removed in v2.1.
Expand Down Expand Up @@ -16428,12 +16443,17 @@ class UnDuplicateResponse(BaseModel):


def register_dedup_routes(
app: FastAPI,
app: Union[FastAPI, APIRouter],
*,
store_provider: Callable[[], Any],
) -> None:
"""Register the un-duplicate route on ``app``.

Accepts either a full ``FastAPI`` instance (used by lightweight
test fixtures so the URL has no prefix) or an ``APIRouter`` so
``runtime.api.build_app`` can mount the route on the ``/api/v1``
router and inherit its prefix.

``store_provider`` is a no-arg callable that returns the live
``SessionStore``. We accept a callable (rather than the store
directly) so apps can defer construction until first request — the
Expand Down
24 changes: 22 additions & 2 deletions dist/apps/code-review.py
Original file line number Diff line number Diff line change
Expand Up @@ -1440,6 +1440,7 @@ async def _poll(self, registry):




# ----- imports for runtime/api_dedup.py -----
"""Dedup retraction HTTP routes.

Expand All @@ -1462,8 +1463,9 @@ async def _poll(self, registry):
"""


from typing import Any, Callable, Union

from fastapi import FastAPI, HTTPException
from fastapi import APIRouter, FastAPI, HTTPException


# ----- imports for runtime/api_session_full.py -----
Expand Down Expand Up @@ -16391,6 +16393,19 @@ async def ws_events(websocket: WebSocket, session_id: str) -> None:
# ==================================================================
add_recent_events_routes(api_v1)

# ==================================================================
# Dedup retraction: POST /api/v1/sessions/{id}/un-duplicate
# Operator-triggered correction when the dedup pipeline flipped a
# session to status='duplicate' incorrectly. The store rewrites
# status back to a runnable state in the same transaction as the
# audit row (see SessionStore.un_duplicate). Mounted on api_v1 so
# the route inherits the /api/v1 prefix and CORS/exception envelope.
# ==================================================================
register_dedup_routes(
api_v1,
store_provider=lambda: fastapi_app.state.orchestrator.store,
)

# Legacy /incidents/* and /investigate redirects to /api/v1/* equivalents.
# 308 preserves method + body so legacy POSTs (e.g. /incidents/{id}/resume)
# keep working transparently. Removed in v2.1.
Expand Down Expand Up @@ -16481,12 +16496,17 @@ class UnDuplicateResponse(BaseModel):


def register_dedup_routes(
app: FastAPI,
app: Union[FastAPI, APIRouter],
*,
store_provider: Callable[[], Any],
) -> None:
"""Register the un-duplicate route on ``app``.

Accepts either a full ``FastAPI`` instance (used by lightweight
test fixtures so the URL has no prefix) or an ``APIRouter`` so
``runtime.api.build_app`` can mount the route on the ``/api/v1``
router and inherit its prefix.

``store_provider`` is a no-arg callable that returns the live
``SessionStore``. We accept a callable (rather than the store
directly) so apps can defer construction until first request — the
Expand Down
24 changes: 22 additions & 2 deletions dist/apps/incident-management.py
Original file line number Diff line number Diff line change
Expand Up @@ -1440,6 +1440,7 @@ async def _poll(self, registry):




# ----- imports for runtime/api_dedup.py -----
"""Dedup retraction HTTP routes.

Expand All @@ -1462,8 +1463,9 @@ async def _poll(self, registry):
"""


from typing import Any, Callable, Union

from fastapi import FastAPI, HTTPException
from fastapi import APIRouter, FastAPI, HTTPException


# ----- imports for runtime/api_session_full.py -----
Expand Down Expand Up @@ -16403,6 +16405,19 @@ async def ws_events(websocket: WebSocket, session_id: str) -> None:
# ==================================================================
add_recent_events_routes(api_v1)

# ==================================================================
# Dedup retraction: POST /api/v1/sessions/{id}/un-duplicate
# Operator-triggered correction when the dedup pipeline flipped a
# session to status='duplicate' incorrectly. The store rewrites
# status back to a runnable state in the same transaction as the
# audit row (see SessionStore.un_duplicate). Mounted on api_v1 so
# the route inherits the /api/v1 prefix and CORS/exception envelope.
# ==================================================================
register_dedup_routes(
api_v1,
store_provider=lambda: fastapi_app.state.orchestrator.store,
)

# Legacy /incidents/* and /investigate redirects to /api/v1/* equivalents.
# 308 preserves method + body so legacy POSTs (e.g. /incidents/{id}/resume)
# keep working transparently. Removed in v2.1.
Expand Down Expand Up @@ -16493,12 +16508,17 @@ class UnDuplicateResponse(BaseModel):


def register_dedup_routes(
app: FastAPI,
app: Union[FastAPI, APIRouter],
*,
store_provider: Callable[[], Any],
) -> None:
"""Register the un-duplicate route on ``app``.

Accepts either a full ``FastAPI`` instance (used by lightweight
test fixtures so the URL has no prefix) or an ``APIRouter`` so
``runtime.api.build_app`` can mount the route on the ``/api/v1``
router and inherit its prefix.

``store_provider`` is a no-arg callable that returns the live
``SessionStore``. We accept a callable (rather than the store
directly) so apps can defer construction until first request — the
Expand Down
38 changes: 24 additions & 14 deletions docs/REACT_UI_PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,53 +7,63 @@ can be removed in v2.1.
Streamlit module: `src/runtime/ui.py`.
React app: `web/src/`.

**v2.0.0-rc2 changelog (this revision):** the rc1 matrix overclaimed
that the app-overlay views were wired — they were not. rc2 actually
wires them (`useAppViews` + `<SelectedPanel>` "App-specific views"
section), plus closes the retry pipeline (preview + POST) and the
un-duplicate flow (route mount + `<UnDuplicateModal>`). See PR
`feat/v2-rc2-close-endpoint-gaps`.

## Coverage matrix

| Streamlit feature (function) | React equivalent | Status | Notes |
|---|---|---|---|
| Sidebar — session list (`render_sidebar`, `_render_session_row`) | `<SessionsRail>` + `<MonitorRail>` "Other Sessions" panel | **full** | Same data source (GET /api/v1/sessions); React adds keyboard-free selection + per-session badges |
| Sidebar — active in-flight row (`_render_active_row`) | `<SessionsRail>` row with `data-active="true"` styling | **full** | React shows breathing dot via `asr-pulse` |
| Investigate form (top-level form in `main`) | `<NewSessionModal>` | **full** | POST /api/v1/sessions with `{query, environment, submitter}`. Same envelope; modal vs. inline |
| Session header / metadata (`_render_top_badges`, `_render_metrics`) | `<CanvasHead>` (eyebrow + title + meta row) | **full** | React adds active pulse + STOP / RETRY buttons inline |
| Session header / metadata (`_render_top_badges`, `_render_metrics`) | `<CanvasHead>` (eyebrow + title + meta row) | **full** | React adds active pulse + STOP / RETRY / UN-DUPLICATE buttons inline |
| Findings block (`_render_findings_block`) | `<SessionCanvas>` → `<Transcript>` → `<Turn>` body summary | **full** | Editorial layout vs. KV block; same source `session.findings` |
| Resolution block (`_render_resolution_block`) | `<Transcript>` terminal turn + `<CanvasHead>` status pill | **full** | React shows status as `RESOLVED` pill rather than a separate section |
| Hypothesis trail (`_render_hypothesis_trail_block`) | `<SelectedPanel>` when a tool call surfaces hypotheses; embedded in turn meta | **partial** | React surfaces hypothesis-shaped data via SelectedPanel but lacks the dedicated "Trail" view. Defer to v2.1; the underlying tool-call audit is unchanged. |
| Pending approvals (`_render_pending_approvals_block`) | `<HITLBand>` inline + `<ApprovalsQueuePanel>` cross-session list | **full** | React drives the same POST /api/v1/sessions/{sid}/approvals/{tcid} endpoint |
| Approve action | `<HITLBand>` Approve button → direct apiFetch | **full** | rationale=null path |
| Approve with rationale | `<HITLBand>` Approve-with-rationale → `<ApproveRationaleModal>` | **full** | Includes uiHints.approval_rationale_templates chip row (Task 52) |
| Reject action | `<HITLBand>` Reject → `<ConfirmModal>` destructive | **full** | Same endpoint with `decision: 'reject'` (Task 53) |
| Stop session | `<CanvasHead>` Stop button → `<ConfirmModal>` destructive | **full** | DELETE /api/v1/sessions/{sid} (Task 53) |
| Retry decision (`_render_retry_block`, `_preview_retry_decision_sync`) | `<CanvasHead>` Retry button (visible when status='error') | **partial** | Button calls `refresh()`. v2.1 will surface the retry preview JSON in a side modal |
| Approve with rationale | `<HITLBand>` Approve-with-rationale → `<ApproveRationaleModal>` | **full** | Includes uiHints.approval_rationale_templates chip row |
| Reject action | `<HITLBand>` Reject → `<ConfirmModal>` destructive | **full** | Same endpoint with `decision: 'reject'` |
| Stop session | `<CanvasHead>` Stop button → `<ConfirmModal>` destructive | **full** | DELETE /api/v1/sessions/{sid} |
| Retry decision (`_render_retry_block`, `_preview_retry_decision_sync`) | `<CanvasHead>` Retry button → `<ConfirmModal>` with preview reason → POST /sessions/{sid}/retry | **full (rc2)** | rc2: `useRetryPreview` drives enabled state + reason tooltip; confirm modal shows the preview reason; POST consumes the SSE body and refreshes the bootstrap bundle |
| **Un-duplicate (rc2 new)** | `<CanvasHead>` Un-duplicate button (status==='duplicate') → `<UnDuplicateModal>` | **full (rc2)** | rc2: backend `register_dedup_routes` now wired in `build_app`; UI POSTs `{retracted_by, note}` to /sessions/{sid}/un-duplicate and refreshes |
| Intervention block (`_render_intervention_block`) | `<HITLBand>` (question rendering, args dump, risk badge) | **full** | React renders policy + risk + waited-seconds + confidence in the same band |
| Tool calls log (`_render_tool_calls_block`) | `<Transcript>` per-turn `<ToolCallCard>` list + `<SelectedPanel>` detail | **full** | React adds click-to-select via `useSetSelected` |
| Agents accordion (`_render_agents_accordion`) | `<FlowStrip>` top-of-canvas pipeline overview | **partial** | FlowStrip shows agents-as-nodes with status; the detailed system_prompt_excerpt is in `<SelectedPanel>` when an agent is selected. Defer the "full prompt expander" to v2.1. |
| Tools by category (`_render_tools_by_category`) | `<ToolsPanel>` monitor | **full** | Same GET /api/v1/tools; React groups by category in collapsible monitor |
| Lessons learned | `<LessonsPanel>` monitor (per-session) | **full** | GET /api/v1/sessions/{sid}/lessons; React polls once via react-query |
| Health (`_make_repository` health gating) | `<HealthPanel>` monitor + Topbar `<HealthDot>` | **full** | 30s poll of /health |
| Cross-session activity feed | `<OtherSessionsPanel>` monitor | **full** | Powered by SSE GET /api/v1/sessions/recent/events |
| App-specific UI views (Approach C overlays) | `<SelectedPanel>` "App-specific views →" links | **full** | GET /api/v1/apps/{app}/ui-views |
| App-specific UI views (Approach C overlays) | `<SelectedPanel>` "App-specific views →" links via `useAppViews` | **full (rc2)** | rc1 docs claimed this; rc1 code did not deliver. rc2 actually wires `GET /api/v1/apps/{app}/ui-views` and renders matching links per selection (`always`, `agent:NAME`, `tool:NAME` filters) |
| Run metadata + global status bar | `<Statusbar>` | **full** | sse event count + vm_seq + connection state + versions |
| Mobile / responsive | `<MobileShell>` + `<TabletShell>` (Tasks 57-61) | **full** | Streamlit has no mobile story; React: <768 mobile, 768-1199 tablet, >=1200 desktop |
| Mobile / responsive | `<MobileShell>` + `<TabletShell>` | **full** | Streamlit has no mobile story; React: <768 mobile, 768-1199 tablet, >=1200 desktop |
| Keyboard shortcuts | — | **deferred** | Locked decision: no keyboard shortcuts in v2.0 (see in-flight notes); v2.1 reconsider |
| Light/dark theme | Light only | **deferred** | Single light theme by design; dark mode is v2.1 |

## Verdict

- 21 features at **full** parity.
- 3 features at **partial** (hypothesis trail dedicated view, retry preview JSON, agent prompt expander). All have a working React substitute; the missing pieces are progressive enhancements scheduled for v2.1.
- 2 features intentionally **deferred** (keyboard shortcuts, dark theme).
- **22 features at full parity** (rc2 promoted retry + app-overlay; un-duplicate added as new row, also full).
- **2 features at partial** (hypothesis trail dedicated view, agent prompt expander). Both have a working React substitute; the missing pieces are progressive enhancements scheduled for v2.1.
- **2 features intentionally deferred** (keyboard shortcuts, dark theme).

The React UI clears the v2.0.0-rc1 ship gate and rc2 sweep closes the endpoint-coverage gaps surfaced by the post-merge audit (rc1 → rc2 promotion proposal: cut a `v2.0.0-rc2` tag after this PR merges).

## Latent items (not in the parity matrix because Streamlit doesn't have them either)

The React UI clears the v2.0.0-rc1 ship gate. Streamlit shows its
deprecation banner (Task 70) and ships beside the React build until
the v2.0.0 GA release.
- `POST /sessions/{id}/resume` for non-tool HITL (free-text input prompts). Today's HITL flow only resumes via the approvals POST; UI will need an additional code path when free-text HITL ships.

## Open ticket parking lot (v2.1)

- Hypothesis trail dedicated panel
- Retry preview JSON in a side modal
- Agent system_prompt expander accessible from the FlowStrip
- Keyboard shortcuts: `?` overlay + `j/k` session navigation
- Dark mode (re-derive accent + warm-cream palette)
- App.tsx + SessionCanvas double `useSessionFull` subscription
- `<Select>` Radix upgrade (currently native)
- SessionId type rename (incident_management still emits `INC-`)
- Multi-app per-deploy: today `appName` is informational (single app per deploy); v2.1 should switch to per-app filtering of `ui-views` endpoints
14 changes: 14 additions & 0 deletions src/runtime/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from starlette.exceptions import HTTPException as StarletteHTTPException

from runtime.api_apps_overlay import add_apps_overlay_routes
from runtime.api_dedup import register_dedup_routes
from runtime.api_recent_events import add_recent_events_routes
from runtime.api_session_full import add_session_full_routes
from runtime.api_static import mount_static_assets
Expand Down Expand Up @@ -986,6 +987,19 @@ async def ws_events(websocket: WebSocket, session_id: str) -> None:
# ==================================================================
add_recent_events_routes(api_v1)

# ==================================================================
# Dedup retraction: POST /api/v1/sessions/{id}/un-duplicate
# Operator-triggered correction when the dedup pipeline flipped a
# session to status='duplicate' incorrectly. The store rewrites
# status back to a runnable state in the same transaction as the
# audit row (see SessionStore.un_duplicate). Mounted on api_v1 so
# the route inherits the /api/v1 prefix and CORS/exception envelope.
# ==================================================================
register_dedup_routes(
api_v1,
store_provider=lambda: fastapi_app.state.orchestrator.store,
)

# Legacy /incidents/* and /investigate redirects to /api/v1/* equivalents.
# 308 preserves method + body so legacy POSTs (e.g. /incidents/{id}/resume)
# keep working transparently. Removed in v2.1.
Expand Down
11 changes: 8 additions & 3 deletions src/runtime/api_dedup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
"""
from __future__ import annotations

from typing import Any, Callable
from typing import Any, Callable, Union

from fastapi import FastAPI, HTTPException
from fastapi import APIRouter, FastAPI, HTTPException
from pydantic import BaseModel, Field


Expand Down Expand Up @@ -49,12 +49,17 @@


def register_dedup_routes(
app: FastAPI,
app: Union[FastAPI, APIRouter],

Check warning on line 52 in src/runtime/api_dedup.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a union type expression for this type hint.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_asr&issues=AZ4wvpweBO_6kV5uQ_cn&open=AZ4wvpweBO_6kV5uQ_cn&pullRequest=39
*,
store_provider: Callable[[], Any],
) -> None:
"""Register the un-duplicate route on ``app``.

Accepts either a full ``FastAPI`` instance (used by lightweight
test fixtures so the URL has no prefix) or an ``APIRouter`` so
``runtime.api.build_app`` can mount the route on the ``/api/v1``
router and inherit its prefix.

``store_provider`` is a no-arg callable that returns the live
``SessionStore``. We accept a callable (rather than the store
directly) so apps can defer construction until first request — the
Expand Down
Loading
Loading