- Frontend scriptBuilder API client inventory now matches the backend schema: createSession accepts BOTH origin and ai_session_id (both required together for inline callers, both omitted for standalone). - 'If template -> unchanged' sharpened: render location is unchanged, but run stamping moves into the panel's new 'I ran this' action per the §5 apply lifecycle correction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
385 lines
29 KiB
Markdown
385 lines
29 KiB
Markdown
# FlowPilot Phase 9 — Tabbed Script Builder + NoTemplateDialog relocation
|
||
|
||
**Date:** 2026-04-23
|
||
**Branch target:** `feat/flowpilot-migration` (continuation of Phases 0–8)
|
||
**Depends on:** Phase 8 (ProposalBanner in chat region)
|
||
|
||
---
|
||
|
||
## Goal
|
||
|
||
Close the two remaining open items from the FlowPilot migration handoff:
|
||
|
||
1. **NoTemplateDialog narrow-lane bug** — today the dialog renders in the task lane (~340px) and its `grid-cols-1 sm:grid-cols-3` layout crushes the three option cards. When the AI proposes a fix with no drafted script, all three cards render disabled, producing a dead end.
|
||
2. **Tabbed Script Builder inside the chat** — give the engineer a way to draft the missing script without leaving the session (either by chatting with the AI or hand-writing in a code editor), then feed the draft back into the existing fix lifecycle.
|
||
|
||
Plus two Phase 8 cleanup items flagged during code review:
|
||
|
||
3. **`EscalateInterceptDialog` missing the Partial choice** — if a fix is in `applied_partial` when the engineer escalates, the intercept dialog's current three choices (worked / didn't work / never applied) don't match. Add a fourth choice for partial.
|
||
4. **`applied_at` semantics correction** — today Phase 8's `handleApplyFix` stamps `applied_at` on every banner Apply click, starting the Verifying timer even when the engineer is only opening a drafting/evaluation surface. Move the stamp to the actual run-action handlers (see §5).
|
||
|
||
This phase depends on Phase 8's `ProposalBanner` already being in the chat region — it reuses the same "chat-region-owns-Apply-flow" philosophy.
|
||
|
||
---
|
||
|
||
## Architectural decisions (settled during brainstorming)
|
||
|
||
| # | Decision | Rationale |
|
||
|---|---|---|
|
||
| 1 | When a fix has no `ai_drafted_script`, the banner's Apply button routes **directly** to the Script Builder tab (bypassing `NoTemplateDialog` entirely). | Banner is the single entry point for Apply. `NoTemplateDialog` stays narrowly scoped to evaluating a draft that actually exists. |
|
||
| 2 | Inside the Script Builder tab, the default experience is AI-driven — a new `ScriptBuilderTab` controller owns session lifecycle + submit, and *renders* `ScriptBuilderChat` (which stays purely presentational). A "✎ Write it myself" button in the tab's header toolbar swaps the controller's render into a Monaco editor. | AI is the common path. Persistence semantics belong on the controller, not the chat display component (`ScriptBuilderChat` already exposes `onSaveScript` as its seam — the controller wires that callback). |
|
||
| 3 | The manual editor uses **Monaco**, reusing the pattern from `frontend/src/components/tree-editor/code-mode/CodeModeEditor.tsx`. | Monaco is already a dependency (`@monaco-editor/react` + `monaco-editor`). No bundle cost, proven pattern. |
|
||
| 4 | The Script Builder tab is **always present while the fix is non-terminal** (no close affordance). An **indicator dot** on the tab signals in-progress draft state. | Matches Phase 8's `display: none` philosophy — engineers move freely between chat and draft without tracking a separate open/close state. |
|
||
| 5 | `NoTemplateDialog` (draft-exists case) moves from `TaskLane.bottomSlot` to the **chat region** (sibling of `ProposalBanner`, slides up above composer). | Script evaluation is an action surface, not a context surface — belongs with the other action surfaces. Chat region is wide enough for the three cards to actually fit side-by-side. |
|
||
| 6 | `EscalateInterceptDialog` gains a **fourth "Partial" choice** that writes `applied_partial` with a notes prompt. | Closes the gap flagged in Phase 8 final review. Minimal incremental cost since the dialog is already getting touched. |
|
||
| 7 | `applied_at` is stamped only when the engineer commits to an action that **runs or triggers** a script — not on banner Apply click. Opening a drafting/evaluation surface no longer starts the Verifying timer. | Prevents false "applied" state when the engineer is still authoring. Corrects a Phase 8 over-eager stamp that this phase would otherwise multiply across three surfaces. |
|
||
|
||
---
|
||
|
||
## Architecture
|
||
|
||
### 1. Chat region gets a tab strip
|
||
|
||
A two-tab strip at the top of the chat region:
|
||
|
||
```
|
||
┌──────────────────────────────────────┐
|
||
│ [Chat] [Script Builder ●] │
|
||
├──────────────────────────────────────┤
|
||
│ │
|
||
│ (content per active tab, │
|
||
│ via display:none toggling) │
|
||
│ │
|
||
└──────────────────────────────────────┘
|
||
```
|
||
|
||
- **When the strip renders:** only when an `activeFix` exists AND the fix is non-terminal AND (`fix.ai_drafted_script` is null AND `fix.script_template_id` is null) — i.e., the fix genuinely needs a script drafted. Otherwise the chat region shows without tabs.
|
||
- **Tab switching uses `display: none`**, not unmount. Chat scroll position, draft message, and Script Builder state all persist across switches.
|
||
- **Indicator dot** on the Script Builder tab fires when there's in-progress draft state: at least one AI message sent in the `ScriptBuilderChat`, or non-empty Monaco buffer. Clears when the draft is submitted.
|
||
- **Session switch** clears tab state via the existing `resetSessionDerivedState` helper.
|
||
|
||
### 2. Script Builder tab content
|
||
|
||
A new controller component `ScriptBuilderTab` owns the inline lifecycle:
|
||
- Creates / resumes a `script_builder_sessions` row with `origin='pilot_inline'` + `ai_session_id = <pilot session id>`.
|
||
- Manages AI-chat message state (via the existing script-builder message endpoints) and the Monaco editor buffer.
|
||
- On submit, fires `PATCH /ai-sessions/{sid}/suggested-fixes/{fid}/script`.
|
||
|
||
`ScriptBuilderChat` itself is **unchanged** — it stays a pure display component taking `messages`, `language`, `onViewScript`, `onSaveScript`, `isLoading`. The controller wires `onSaveScript` to its submit path instead of the template-creation path the standalone `/script-builder` page uses.
|
||
|
||
A header toolbar above the controller's render area hosts the mode toggle:
|
||
|
||
```
|
||
┌──────────────────────────────────────┐
|
||
│ Script Builder · Outlook fix │
|
||
│ [✎ Write myself]│
|
||
├──────────────────────────────────────┤
|
||
│ (mode-specific content) │
|
||
│ │
|
||
└──────────────────────────────────────┘
|
||
```
|
||
|
||
- Clicking **✎ Write myself** flips `scriptBuilderMode` to `'editor'` — the controller renders Monaco in place of `ScriptBuilderChat`, pre-loaded with a scaffold (fix description as a language-appropriate comment header + an empty body).
|
||
- A reciprocal **✨ Back to AI** button in editor mode returns to the chat.
|
||
- Switching modes **does not discard** work. The Monaco buffer and the script-builder session both persist across toggles. This matters when an engineer drafts with AI, switches to editor to tweak a line, then considers going back.
|
||
- Both modes share a single terminal action: the controller's **Submit → `PATCH /ai-sessions/{sid}/suggested-fixes/{fid}/script`**. On success the fix gains `ai_drafted_script`; the tab strip disappears (since the fix no longer needs a script) and the banner's Apply button now routes to `NoTemplateDialog` in the chat region.
|
||
- **Submit does NOT stamp `applied_at`.** A draft is not an application — see §5 Apply lifecycle below.
|
||
|
||
### 3. NoTemplateDialog relocation to chat region
|
||
|
||
- Removed from `TaskLane.bottomSlot`. Renders in the chat region, slide-up-above-composer (same mechanical placement as `ProposalBanner`).
|
||
- The three-card layout (`grid-cols-3` at the chat region's natural width) actually fits — no `grid-cols-1` regression needed.
|
||
- Opens when the engineer clicks Apply on the banner AND `fix.ai_drafted_script` is non-empty.
|
||
- Decision semantics unchanged (still `one_off` / `draft_template` / `build_template` with the same server-side effects) except for the moved apply stamp — see §5. Only the render location changes beyond that.
|
||
|
||
### 4. Banner Apply routing (updated)
|
||
|
||
Three mutually-exclusive outcomes based on the fix's shape:
|
||
|
||
```
|
||
handleApplyFix():
|
||
if fix.script_template_id:
|
||
open TemplateMatchPanel (unchanged — still renders in task lane for now)
|
||
elif fix.ai_drafted_script:
|
||
open NoTemplateDialog in chat region (new location, Chat tab)
|
||
else:
|
||
open Script Builder tab in chat region (new tab)
|
||
```
|
||
|
||
The NoTemplateDialog-in-chat-region path lives on the **Chat tab** (slides up above composer; the tab strip only renders for the no-draft case, so when NoTemplateDialog shows, the tab strip is not on screen). The Script Builder tab path is the opposite — tab strip renders, engineer is on the Script Builder tab.
|
||
|
||
`TemplateMatchPanel` stays in the task lane for this phase — it's a different surface with different interactions and it's not broken. Moving it is possible future work.
|
||
|
||
### 5. Apply lifecycle — `applied_at` semantics correction
|
||
|
||
**Problem.** Today (Phase 8) `handleApplyFix` calls `POST /apply` the moment the banner's Apply button is clicked, stamping `applied_at` regardless of what happens next. This starts the Verifying timer (nudge countdown, Resolve auto-success, Escalate intercept) even if the engineer is only opening a drafting surface and hasn't actually run anything yet. For the no-draft path introduced in this phase, that's clearly wrong — opening the Script Builder tab is the start of *authoring*, not the start of *verifying*.
|
||
|
||
**Rule.** `applied_at` is stamped **only when the engineer commits to an action that produces or triggers a run**, not when they open a surface:
|
||
|
||
| Banner Apply click → routes to... | Stamps `applied_at`? |
|
||
|---|---|
|
||
| `TemplateMatchPanel` (existing flow) | Only when the engineer clicks a new explicit **"✓ I ran this"** action inside the panel (see below) |
|
||
| `NoTemplateDialog` → `one_off` card ("Run now, no template") | **Yes** — the card click declares "I'm running this now" |
|
||
| `NoTemplateDialog` → `draft_template` card ("Run now, templatize after") | **Yes** — same declaration, the template proposal is a side effect |
|
||
| `NoTemplateDialog` → `build_template` card ("Just open the builder") | No — no run is declared; the engineer is going off to author a proper template |
|
||
| Script Builder tab → Submit | No — just produces a draft. Engineer then clicks Apply again, gets `NoTemplateDialog`, picks `one_off` or `draft_template` to declare the run |
|
||
|
||
**New explicit "I ran this" action in `TemplateMatchPanel`.** Today the panel has Generate, Copy, and Edit Parameters — none of which commit to running. Copying doesn't imply running; the engineer can walk away. This phase adds a distinct primary button (accent-colored, below Copy) labeled **"✓ I ran this"** or **"Mark as applied"**. Click → calls `applyFix` → fix transitions to Verifying. Until clicked, the fix stays in `proposed`.
|
||
|
||
**Implementation.**
|
||
- Remove `sessionSuggestedFixesApi.applyFix(...)` call from `handleApplyFix`. Move it to the three run-declaring call sites: `NoTemplateDialog`'s `handleScriptDecision('one_off' | 'draft_template')` paths AND the new `TemplateMatchPanel` "I ran this" button. The `applyFix` endpoint itself (from Phase 8 Issue #2) stays unchanged — only its call sites move.
|
||
- Until `applied_at` is stamped, the fix remains in `proposed`. `bannerMode` computation already returns `'proposed'` when `applied_at` is null, so the banner naturally stays on Proposed state through the entire drafting phase.
|
||
- **Phase 8 consequence.** This is a semantic revision of Phase 8, not just Phase 9 behavior. Tests must assert: opening `TemplateMatchPanel` does NOT stamp `applied_at`; clicking "I ran this" DOES; `NoTemplateDialog` `one_off` AND `draft_template` both DO; `build_template` does NOT.
|
||
|
||
### 6. EscalateInterceptDialog partial choice
|
||
|
||
Adds a fourth button to the existing popover:
|
||
|
||
| Existing choices | New choice |
|
||
|---|---|
|
||
| The fix didn't work | (existing) |
|
||
| It worked — escalating for another reason | (existing) |
|
||
| Never actually applied it | (existing) |
|
||
| **I applied some of it — partial** | **NEW** |
|
||
|
||
- When clicked: prompts for partial notes (same pattern as the banner's Partial path — `window.prompt` for now, matching Phase 8's interim), then calls `patchOutcome('applied_partial', notes)`.
|
||
- `handleInterceptChoice` gains an `applied_partial` branch. The `InterceptChoice` type already includes `'applied_partial'` via `FixOutcome | 'never_applied'`, so no type changes needed.
|
||
- When a fix enters the dialog already in `applied_partial` state, the fourth button is hidden (can't transition partial → partial with different semantics). The "didn't work" button remains available to progress to `applied_failed`.
|
||
|
||
---
|
||
|
||
## Data model
|
||
|
||
### New migration
|
||
|
||
`script_builder_sessions` **already has** `ai_session_id` (FK → `ai_sessions.id`, nullable, `ON DELETE SET NULL`) with the comment "Link to FlowPilot session if launched from there." The existing column is the link we need — no new FK is added. The migration introduces only the `origin` discriminator plus a uniqueness guard for inline sessions:
|
||
|
||
```sql
|
||
ALTER TABLE script_builder_sessions
|
||
ADD COLUMN origin VARCHAR(20) NOT NULL DEFAULT 'standalone';
|
||
|
||
ALTER TABLE script_builder_sessions
|
||
ADD CONSTRAINT ck_script_builder_sessions_origin
|
||
CHECK (origin IN ('standalone', 'pilot_inline'));
|
||
|
||
-- Invariant: pilot_inline rows must be linked to a pilot session.
|
||
-- Standalone rows may or may not be linked (legacy back-channel).
|
||
ALTER TABLE script_builder_sessions
|
||
ADD CONSTRAINT ck_script_builder_sessions_origin_ai_session
|
||
CHECK (origin <> 'pilot_inline' OR ai_session_id IS NOT NULL);
|
||
|
||
-- Uniqueness: at most one pilot_inline session per (user, pilot session).
|
||
-- Required to back the get-or-create semantics on the endpoint and prevent
|
||
-- duplicate scratch rows on remount. Partial index scoped to pilot_inline
|
||
-- so standalone rows are unaffected.
|
||
CREATE UNIQUE INDEX ux_script_builder_sessions_pilot_inline
|
||
ON script_builder_sessions (user_id, ai_session_id)
|
||
WHERE origin = 'pilot_inline';
|
||
```
|
||
|
||
`origin = 'standalone'` → existing `/script-builder` page usage (existing rows backfill to this default). `origin = 'pilot_inline'` → new Script Builder tab; `ai_session_id` is populated at row creation.
|
||
|
||
`origin` earns its keep as an explicit discriminator for:
|
||
- Filtering (`list_sessions` / `count_user_sessions` exclude `pilot_inline` by default — see §Data model filter changes below).
|
||
- Future split-quota billing (decided to count as one billable session for now, but tagged for analytics).
|
||
|
||
### Data-model filter changes — `script_builder_sessions` list + count
|
||
|
||
Inline sessions would otherwise pollute the standalone `/script-builder` dashboard and count against the per-user 5-session cap enforced by the `POST /script-builder/sessions` endpoint. Required changes:
|
||
|
||
- `script_builder_service.list_sessions(user_id)` → default scope `origin = 'standalone'`. Callers that genuinely want all rows (e.g., an admin dashboard in a future phase) can pass an explicit `include_inline=True` flag, but no current caller needs it.
|
||
- `script_builder_service.count_user_sessions(user_id)` → same scope.
|
||
- Both changes covered by tests:
|
||
- 5 `pilot_inline` sessions should still leave the engineer free to create 5 standalone sessions (no cap interaction).
|
||
- `list_sessions` returns only `standalone` rows.
|
||
|
||
### New backend endpoint
|
||
|
||
```
|
||
PATCH /api/v1/ai-sessions/{session_id}/suggested-fixes/{fix_id}/script
|
||
```
|
||
|
||
Request:
|
||
```json
|
||
{
|
||
"ai_drafted_script": "string (required, 1..50_000 chars)",
|
||
"ai_drafted_parameters": { /* optional JSONB */ }
|
||
}
|
||
```
|
||
|
||
Behavior:
|
||
- Auth: `require_engineer_or_admin` + `_load_session_or_404`.
|
||
- 404 if fix not found on that session.
|
||
- 409 if fix is in a terminal status (`applied_success`, `applied_failed`, `dismissed`) — a drafted script can't be attached after the fix is done.
|
||
- Sets `fix.ai_drafted_script` + `fix.ai_drafted_parameters`.
|
||
- **Does NOT stamp `fix.applied_at`.** A draft is not an application — see §5 above.
|
||
- **Bumps `ai_sessions.state_version`** — the fix just transitioned from "needs drafting" to "has draft", which affects Resolve/Escalate preview regeneration.
|
||
- Returns `SessionSuggestedFixResponse`.
|
||
|
||
### ScriptBuilderTab controller (frontend) — no changes to `ScriptBuilderChat`
|
||
|
||
`ScriptBuilderChat` (`frontend/src/components/script-builder/ScriptBuilderChat.tsx`) is a presentational component taking `messages`, `language`, `onViewScript`, `onSaveScript`, `isLoading`. **It does not need a `mode` prop** — adding persistence semantics to a display component would be wrong.
|
||
|
||
Instead, introduce a new controller component `frontend/src/components/pilot/ScriptBuilderTab.tsx` that owns the inline lifecycle:
|
||
|
||
- On mount: **get-or-create** the single inline `script_builder_sessions` row for `(current user, current pilot session)` via the existing `POST /script-builder/sessions` endpoint, passing `origin: 'pilot_inline'` and the current pilot session id for `ai_session_id`. The endpoint becomes idempotent for `origin='pilot_inline'` — if a row exists for that `(user_id, ai_session_id)` pair, it's returned; otherwise created. The partial unique index on the DB backs the invariant independent of endpoint code. Remounting (tab hide/show, page refresh) resumes the same session — no duplicates, no lost draft continuity.
|
||
- Holds local state for the AI message list, the Monaco buffer, and `scriptBuilderMode`.
|
||
- Renders `ScriptBuilderChat` in AI mode with `onSaveScript` wired to the inline submit path (PATCH /script), NOT the standalone template-creation path.
|
||
- Renders Monaco (via existing `CodeModeEditor` pattern) in `'editor'` mode with its own Save button that triggers the same submit.
|
||
- Emits an `onScriptDrafted` event to `AssistantChatPage` on success so the page can `setActiveFix(updated)`, hide the tab strip, and return the engineer to Chat tab.
|
||
|
||
The standalone `/script-builder` page retains its current behavior unchanged — it continues to create `script_templates` rows on submit. The split happens cleanly at the controller layer, not inside `ScriptBuilderChat`.
|
||
|
||
### `POST /script-builder/sessions` — changes for inline origin
|
||
|
||
The existing endpoint is extended in three ways:
|
||
|
||
1. **Accepts `origin`** in the request body (`Literal['standalone', 'pilot_inline']`, default `'standalone'`). Legacy callers unchanged.
|
||
2. **Authorization on `ai_session_id`.** When `origin='pilot_inline'` is passed AND `ai_session_id` is provided, the handler MUST verify the referenced `ai_sessions` row is owned by the current user (or within their account — whichever guard `_load_session_or_404(db, ai_session_id, current_user)` already enforces for the pilot endpoints). Without this check, a caller could attach an inline scratch session to an arbitrary pilot session. The check fires before any row lookup or creation.
|
||
3. **Idempotent for `origin='pilot_inline'`.** If a row with `(user_id = current, ai_session_id = provided, origin = 'pilot_inline')` already exists, the handler returns that row (200) instead of creating a new one (201). The unique partial index enforces at-most-one at the DB layer; a race between two concurrent POSTs surfaces as an integrity error that the handler catches and re-reads.
|
||
|
||
For `origin='standalone'`, behavior is unchanged — always creates, still subject to the 5-session cap.
|
||
|
||
The 5-session cap applies only to `standalone` rows (see §Data-model filter changes). Inline sessions are out of that accounting entirely.
|
||
|
||
---
|
||
|
||
## State
|
||
|
||
### Frontend state (AssistantChatPage)
|
||
|
||
New local state on the page:
|
||
- `chatTab: 'chat' | 'script_builder'` — which tab is visible. Defaults to `'chat'`.
|
||
- `scriptBuilderHasProgress: boolean` — drives the indicator dot. Set by `ScriptBuilderTab` via an `onProgressChange` callback.
|
||
|
||
Reset in `resetSessionDerivedState`: both back to defaults.
|
||
|
||
`scriptBuilderMode` ('ai' | 'editor') lives **inside `ScriptBuilderTab`**, not on the page — the parent never needs to drive the AI/editor toggle. The controller resets it naturally via unmount/remount when the page switches sessions.
|
||
|
||
Banner's Apply handler (`handleApplyFix`) updated:
|
||
- If no script + no template → set `chatTab = 'script_builder'` (and show tab strip).
|
||
- If drafted script → open NoTemplateDialog in the chat region (new state or existing `scriptPanelOpen` reused).
|
||
- If template → open `TemplateMatchPanel` in the task lane (render location unchanged); run stamping happens via the new "I ran this" action inside the panel (see §5), not on Apply click.
|
||
|
||
### Tab strip visibility
|
||
|
||
The tab strip is derived, not state:
|
||
```ts
|
||
const showTabStrip =
|
||
activeFix != null &&
|
||
activeFix.status !== 'dismissed' &&
|
||
activeFix.status !== 'applied_success' &&
|
||
activeFix.status !== 'applied_failed' &&
|
||
!activeFix.script_template_id &&
|
||
!activeFix.ai_drafted_script
|
||
```
|
||
|
||
When the strip hides (e.g., after script is drafted), `chatTab` resets to `'chat'` to avoid stuck state.
|
||
|
||
### Tab switching guard
|
||
|
||
The existing `currentChatRef` pattern (Async-select-load-apply guard) applies: when the engineer switches chats, any in-flight tab-derived state is discarded.
|
||
|
||
---
|
||
|
||
## Out of scope
|
||
|
||
- **NoTemplateDialog grid fix.** Moved to the chat region (wide enough), so the `grid-cols-1 sm:grid-cols-3` layout now works as intended. No grid edit required.
|
||
- **`window.prompt` replacement** for partial-notes / failure-reason capture. Still the Phase 8 interim pattern; replacement is deferred to a later design debt pass.
|
||
- **TemplateMatchPanel relocation** to the chat region. Different surface, different interactions, not broken today. Possible future work.
|
||
- **Dedicated "clear AI outcome proposal" button in the UI.** Already covered by Phase 8 Issue #3 fix (DELETE endpoint + clear-on-outcome-write).
|
||
- **Task lane bottom-slot audit.** With NoTemplateDialog removed from the slot, it may be empty on most sessions. Keep the slot API stable; any cleanup is out of scope.
|
||
|
||
---
|
||
|
||
## Tests
|
||
|
||
### Backend
|
||
|
||
- **Migration:** forward + downgrade reversibility; existing rows backfill to `origin='standalone'`; the `origin='pilot_inline' ⇒ ai_session_id IS NOT NULL` invariant is enforced by the check constraint.
|
||
- **PATCH /script endpoint** (new test file `test_fix_script_endpoint.py`):
|
||
- happy path — 200, `ai_drafted_script` set, `state_version` bumped, `applied_at` untouched.
|
||
- 404 on wrong session.
|
||
- 409 on terminal status.
|
||
- 400 on empty body.
|
||
- **list/count filter changes** (extend `test_script_builder.py` or nearby):
|
||
- 5 `pilot_inline` sessions + subsequent `standalone` session creation succeeds (does not hit the 5-cap).
|
||
- `list_sessions` returns only `standalone` rows by default.
|
||
- **Apply lifecycle correction** (extend `test_fix_outcome_endpoint.py`):
|
||
- Banner Apply click that routes to a drafting/evaluation surface does NOT stamp `applied_at`.
|
||
- `one_off` decision from `NoTemplateDialog` DOES stamp `applied_at`.
|
||
- `draft_template` decision from `NoTemplateDialog` DOES stamp `applied_at` (it still runs the script).
|
||
- `build_template` decision from `NoTemplateDialog` does NOT stamp (no run).
|
||
- `TemplateMatchPanel` "I ran this" action DOES stamp `applied_at`; Generate / Copy alone do NOT.
|
||
- **Script Builder session create — inline semantics** (extend `test_script_builder.py` or equivalent):
|
||
- First `POST /script-builder/sessions` with `origin='pilot_inline', ai_session_id=X` creates and returns a row.
|
||
- Second `POST` with the same `(ai_session_id, user)` returns the SAME row (no duplicate created); DB row count confirms.
|
||
- `POST` with `origin='pilot_inline'` and `ai_session_id` pointing at another user's pilot session is rejected (403/404).
|
||
- Race: two concurrent `POST`s for the same `(user, ai_session_id)` resolve to the same row id (one winner, one returns the existing).
|
||
|
||
### Frontend
|
||
|
||
Manual verification (no component test harness in this codebase per CLAUDE.md):
|
||
- No-draft fix → Apply click opens Script Builder tab.
|
||
- AI path: chat with AI, submit, tab disappears, NoTemplateDialog becomes eligible.
|
||
- Manual path: ✎ Write myself → Monaco loads with scaffold → edit → submit → tab disappears.
|
||
- Drafted fix → Apply click opens NoTemplateDialog in chat region (three cards side-by-side).
|
||
- Tab indicator dot appears on first AI message / non-empty Monaco buffer; clears on submit.
|
||
- Session switch with open Script Builder tab → tab/mode state resets.
|
||
- EscalateInterceptDialog partial choice → applied_partial written with notes.
|
||
|
||
### Build discipline
|
||
|
||
- `tsc -b` clean
|
||
- `npm run build` clean
|
||
- `docker exec resolutionflow_backend pytest` — all pre-existing suites still pass, no regression from the new endpoint.
|
||
|
||
---
|
||
|
||
## Files to touch (rough inventory)
|
||
|
||
**Backend — new:**
|
||
- `backend/alembic/versions/<hash>_script_builder_origin.py`
|
||
- `backend/tests/test_fix_script_endpoint.py`
|
||
|
||
**Backend — modified:**
|
||
- `backend/app/models/script_builder_session.py` — add `origin` column only (`ai_session_id` already exists).
|
||
- `backend/app/schemas/session_suggested_fix.py` — add `SessionSuggestedFixScriptRequest`.
|
||
- `backend/app/schemas/script_builder.py` — extend `ScriptBuilderCreateRequest` with two new optional fields: `origin: Literal['standalone', 'pilot_inline'] = 'standalone'` and `ai_session_id: UUID | None = None`. Handler-side validation: when `origin='pilot_inline'`, `ai_session_id` is required (not null) AND must pass the current-user ownership check. Legacy callers pass neither and continue to create standalone sessions as before.
|
||
- `backend/app/api/endpoints/session_suggested_fixes.py` — add PATCH /script endpoint. Move the existing `applied_at` stamp out of the apply path and into `handleScriptDecision('one_off' | 'draft_template')` plus `TemplateMatchPanel`'s new "I ran this" handler (server side: no change to `/apply`; callers shift instead).
|
||
- `backend/app/api/endpoints/script_builder.py` — accept `origin` on session creation; enforce the `pilot_inline ⇒ ai_session_id` invariant at the handler level.
|
||
- `backend/app/services/script_builder_service.py` — persist `origin`; `list_sessions` + `count_user_sessions` filter to `origin='standalone'` by default.
|
||
- `backend/app/models/session_suggested_fix.py` — unchanged (schema already has `ai_drafted_script`).
|
||
|
||
**Frontend — new:**
|
||
- `frontend/src/components/pilot/ChatTabStrip.tsx` — renders the `[Chat] [Script Builder ●]` strip.
|
||
- `frontend/src/components/pilot/ScriptBuilderTab.tsx` — controller that owns session lifecycle, AI message state, Monaco buffer, mode toggle, and submit. Renders `ScriptBuilderChat` in AI mode and Monaco in editor mode.
|
||
- `frontend/src/components/pilot/NoTemplateDialogInline.tsx` (or reuse existing `NoTemplateDialog` with a new wrapper for chat-region styling).
|
||
|
||
**Frontend — modified:**
|
||
- `frontend/src/api/sessionSuggestedFixes.ts` — add `patchScript(sessionId, fixId, body, parameters)` method.
|
||
- `frontend/src/api/scriptBuilder.ts` (or equivalent) — `createSession` accepts optional `origin` and `ai_session_id` arguments (both required together when the caller is `ScriptBuilderTab`; both omitted for the legacy standalone caller).
|
||
- `frontend/src/components/script-builder/ScriptBuilderChat.tsx` — **unchanged**. Stays a pure display component.
|
||
- `frontend/src/pages/ScriptBuilderPage.tsx` — **unchanged on the session-creation path** (defaults to `origin='standalone'`).
|
||
- `frontend/src/pages/AssistantChatPage.tsx` — wire tab strip, mount `ScriptBuilderTab`, banner Apply routing (no `applied_at` stamp on click), NoTemplateDialog chat-region render. Move the `sessionSuggestedFixesApi.applyFix(...)` call from `handleApplyFix` to `handleScriptDecision('one_off' | 'draft_template')` and `TemplateMatchPanel`'s new "I ran this" handler.
|
||
- `frontend/src/components/pilot/EscalateInterceptDialog.tsx` — add fourth choice.
|
||
- `frontend/src/components/pilot/TaskLane.tsx` — remove `bottomSlot` usage of NoTemplateDialog (leave prop API stable).
|
||
|
||
**Frontend — deleted:**
|
||
- None (existing components get refactored, not deleted).
|
||
|
||
---
|
||
|
||
## Rollout
|
||
|
||
- Single branch, merged as part of the in-flight `feat/flowpilot-migration` PR (same as Phase 8).
|
||
- No feature flag — the new surface is strictly additive to the banner's Apply flow; old behavior for drafted-script fixes is preserved (just renders in a different location).
|
||
|
||
---
|
||
|
||
## Open deferrals (acknowledged, not in this phase)
|
||
|
||
- `window.prompt` → inline input migration for partial notes / failure reasons.
|
||
- Anti-parrot compliance check for the inline `ScriptBuilderTab` flow — verify it reuses the existing script-builder AI system prompt (no new prompt content introduced; the controller only changes what `onSaveScript` does, not what the AI sees).
|
||
- Telemetry events for tab opens / AI→editor toggles / script submissions from tab — add in the Phase 9 implementation plan if we want them.
|