From ecd73936468587b169dbec1dfd67483211b2bb4d Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Fri, 27 Mar 2026 21:48:06 +0000
Subject: [PATCH 01/49] feat: persist task lane across submits and session
reloads
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Task lane questions/actions are now saved to a pending_task_lane JSONB
column on ai_sessions, restoring them on session switch or page reload.
Partial submit no longer force-clears the lane — the AI response
controls what stays. Also removes redundant "New Session" button from
the sidebar (dashboard already provides this).
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../versions/fc01_add_pending_task_lane.py | 30 +++++++++++++++++++
backend/app/models/ai_session.py | 4 +++
backend/app/schemas/ai_session.py | 1 +
backend/app/services/unified_chat_service.py | 18 +++++++++++
frontend/src/components/layout/Sidebar.tsx | 25 +---------------
frontend/src/hooks/useFlowPilotSession.ts | 1 +
frontend/src/pages/AssistantChatPage.tsx | 17 +++++++----
frontend/src/types/ai-session.ts | 1 +
8 files changed, 67 insertions(+), 30 deletions(-)
create mode 100644 backend/alembic/versions/fc01_add_pending_task_lane.py
diff --git a/backend/alembic/versions/fc01_add_pending_task_lane.py b/backend/alembic/versions/fc01_add_pending_task_lane.py
new file mode 100644
index 00000000..4a579c38
--- /dev/null
+++ b/backend/alembic/versions/fc01_add_pending_task_lane.py
@@ -0,0 +1,30 @@
+"""add pending_task_lane to ai_sessions
+
+Revision ID: fc01a1b2c3d4
+Revises: fb1481317ff6
+Create Date: 2026-03-27
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects.postgresql import JSONB
+
+revision = "fc01a1b2c3d4"
+down_revision = "fb1481317ff6"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ op.add_column(
+ "ai_sessions",
+ sa.Column(
+ "pending_task_lane",
+ JSONB,
+ nullable=True,
+ comment="Current task lane state: {questions: [...], actions: [...]}",
+ ),
+ )
+
+
+def downgrade() -> None:
+ op.drop_column("ai_sessions", "pending_task_lane")
diff --git a/backend/app/models/ai_session.py b/backend/app/models/ai_session.py
index 0c10d9db..8bf1684a 100644
--- a/backend/app/models/ai_session.py
+++ b/backend/app/models/ai_session.py
@@ -209,6 +209,10 @@ class AISession(Base):
JSONB, nullable=False, default=list,
comment="Full LLM message history for context continuity",
)
+ pending_task_lane: Mapped[Optional[dict[str, Any]]] = mapped_column(
+ JSONB, nullable=True,
+ comment="Current task lane state: {questions: [...], actions: [...]}",
+ )
# ── Branching ──
is_branching: Mapped[bool] = mapped_column(
diff --git a/backend/app/schemas/ai_session.py b/backend/app/schemas/ai_session.py
index 4037f38a..0039653e 100644
--- a/backend/app/schemas/ai_session.py
+++ b/backend/app/schemas/ai_session.py
@@ -228,6 +228,7 @@ class AISessionDetail(AISessionSummary):
ticket_data: dict[str, Any] | None = None
steps: list[AISessionStepResponse] = []
conversation_messages: list[dict[str, Any]] = [] # Chat sessions store messages here
+ pending_task_lane: dict[str, Any] | None = None
is_branching: bool = False
active_branch_id: str | None = None
diff --git a/backend/app/services/unified_chat_service.py b/backend/app/services/unified_chat_service.py
index 494eb01f..44236e66 100644
--- a/backend/app/services/unified_chat_service.py
+++ b/backend/app/services/unified_chat_service.py
@@ -286,6 +286,15 @@ async def send_chat_message(
except Exception:
logger.exception("Failed to create fork within branch for session %s", session.id)
+ # Persist task lane state on session
+ if branch_questions_data or branch_actions_data:
+ session.pending_task_lane = {
+ "questions": branch_questions_data or [],
+ "actions": branch_actions_data or [],
+ }
+ else:
+ session.pending_task_lane = None
+
suggested_flows = extract_suggested_flows(
await rag_search(query=message, account_id=account_id, db=db, limit=8)
)
@@ -393,6 +402,15 @@ async def send_chat_message(
logger.exception("Failed to create fork for session %s", session_id)
# Fork failed but chat message still sent — don't break the response
+ # Persist task lane state on session
+ if questions_data or actions_data:
+ session.pending_task_lane = {
+ "questions": questions_data or [],
+ "actions": actions_data or [],
+ }
+ else:
+ session.pending_task_lane = None
+
suggested_flows = extract_suggested_flows(rag_results)
return display_content, suggested_flows, session, fork_metadata, actions_data, questions_data
diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx
index 3c3b825a..2b2fa664 100644
--- a/frontend/src/components/layout/Sidebar.tsx
+++ b/frontend/src/components/layout/Sidebar.tsx
@@ -5,7 +5,7 @@ import {
LayoutGrid, Clock, AlertTriangle, GitBranch, Code2, Wand2,
ListChecks, Download, BarChart3,
Settings, Pin, PinOff,
- Plus, History, FileText,
+ History, FileText,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
@@ -363,17 +363,6 @@ export function Sidebar() {
style={{ background: 'var(--color-bg-sidebar)', borderRight: '1px solid var(--color-border-default)' }}
onWheel={handleWheel}
>
- {/* New Session button */}
-
-
{/* Pinned sidebar content */}
{sections.map((section, si) => (
@@ -420,18 +409,6 @@ export function Sidebar() {
style={{ background: 'var(--color-bg-sidebar)', borderRight: '1px solid var(--color-border-default)', width: '72px' }}
onWheel={handleWheel}
>
- {/* New Session button */}
-
-
{/* Nav items */}
{railGroups.map((item, i) => renderRailItem(item, `rail-${i}`))}
diff --git a/frontend/src/hooks/useFlowPilotSession.ts b/frontend/src/hooks/useFlowPilotSession.ts
index 8b0f661c..6034cbb0 100644
--- a/frontend/src/hooks/useFlowPilotSession.ts
+++ b/frontend/src/hooks/useFlowPilotSession.ts
@@ -92,6 +92,7 @@ export function useFlowPilotSession(): UseFlowPilotSession {
ticket_data: null,
steps: [firstStep],
conversation_messages: [],
+ pending_task_lane: null,
is_branching: false,
active_branch_id: null,
})
diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx
index fad0b11d..db060d0d 100644
--- a/frontend/src/pages/AssistantChatPage.tsx
+++ b/frontend/src/pages/AssistantChatPage.tsx
@@ -160,7 +160,7 @@ export default function AssistantChatPage() {
const selectChat = useCallback(async (chatId: string) => {
setActiveChatId(chatId)
- // Clear TaskLane when switching chats
+ // Clear TaskLane when switching chats — will restore from backend if available
setShowTaskLane(false)
setActiveQuestions([])
setActiveActions([])
@@ -172,6 +172,16 @@ export default function AssistantChatPage() {
content: m.content,
}))
)
+ // Restore task lane from persisted state
+ if (detail.pending_task_lane) {
+ const q = detail.pending_task_lane.questions || []
+ const a = detail.pending_task_lane.actions || []
+ if (q.length > 0 || a.length > 0) {
+ setActiveQuestions(q)
+ setActiveActions(a)
+ setShowTaskLane(true)
+ }
+ }
} catch {
setMessages([])
}
@@ -288,11 +298,6 @@ export default function AssistantChatPage() {
}
const userMessage = parts.join('\n\n')
- // Close the task lane
- setShowTaskLane(false)
- setActiveQuestions([])
- setActiveActions([])
-
setMessages(prev => [...prev, { role: 'user', content: userMessage }])
setLoading(true)
diff --git a/frontend/src/types/ai-session.ts b/frontend/src/types/ai-session.ts
index 446f3149..e1abab7f 100644
--- a/frontend/src/types/ai-session.ts
+++ b/frontend/src/types/ai-session.ts
@@ -195,6 +195,7 @@ export interface AISessionDetail extends AISessionSummary {
ticket_data: Record
| null
steps: AISessionStepResponse[]
conversation_messages: Array<{ role: string; content: string }>
+ pending_task_lane: { questions: QuestionItem[]; actions: ActionItem[] } | null
is_branching: boolean
active_branch_id: string | null
}
From a285b6cdc21864dda25f6b5740ab15038e1ec741 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 02:12:34 +0000
Subject: [PATCH 02/49] fix: rebase migration onto correct alembic head
The pending_task_lane migration was branching from fb1481317ff6 which
already had a child (4f4137ce79e5). Fixes multiple-heads error.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
backend/alembic/versions/fc01_add_pending_task_lane.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/alembic/versions/fc01_add_pending_task_lane.py b/backend/alembic/versions/fc01_add_pending_task_lane.py
index 4a579c38..ad3996da 100644
--- a/backend/alembic/versions/fc01_add_pending_task_lane.py
+++ b/backend/alembic/versions/fc01_add_pending_task_lane.py
@@ -9,7 +9,7 @@ import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB
revision = "fc01a1b2c3d4"
-down_revision = "fb1481317ff6"
+down_revision = "4f4137ce79e5"
branch_labels = None
depends_on = None
From 3bb3712d741069c27bc6a89dc56944b612762a60 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 02:17:51 +0000
Subject: [PATCH 03/49] chore: trigger fresh Railway build (clear Docker cache)
From d07e622aef35e1b7b5e6b9d5859f940bec2160f2 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 02:20:18 +0000
Subject: [PATCH 04/49] fix: rebase migration onto actual head (067)
Co-Authored-By: Claude Opus 4.6 (1M context)
---
backend/alembic/versions/fc01_add_pending_task_lane.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/alembic/versions/fc01_add_pending_task_lane.py b/backend/alembic/versions/fc01_add_pending_task_lane.py
index ad3996da..632de9b4 100644
--- a/backend/alembic/versions/fc01_add_pending_task_lane.py
+++ b/backend/alembic/versions/fc01_add_pending_task_lane.py
@@ -9,7 +9,7 @@ import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB
revision = "fc01a1b2c3d4"
-down_revision = "4f4137ce79e5"
+down_revision = "067"
branch_labels = None
depends_on = None
From e8dfc84717b2a3d0b38cb3fef23fd52316b7a167 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 02:23:56 +0000
Subject: [PATCH 05/49] fix: rename migration to sequential 068, document
convention
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Renamed fc01_add_pending_task_lane → 068_add_pending_task_lane with
revision ID "068" and down_revision "067". Added migration naming
convention to CLAUDE.md to prevent future hex-hash migrations.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
CLAUDE.md | 6 +++++-
...dd_pending_task_lane.py => 068_add_pending_task_lane.py} | 6 +++---
2 files changed, 8 insertions(+), 4 deletions(-)
rename backend/alembic/versions/{fc01_add_pending_task_lane.py => 068_add_pending_task_lane.py} (88%)
diff --git a/CLAUDE.md b/CLAUDE.md
index 21a34431..d87335e8 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -211,6 +211,10 @@ cd frontend && npm run build
# Database migrations
cd backend && alembic upgrade head
alembic revision --autogenerate -m "Description" --rev-id=NNN # NNN = next sequential number
+# IMPORTANT: Migrations use sequential 3-digit IDs (001, 002, ..., 068, 069).
+# Check the latest: ls backend/alembic/versions/ | grep -E '^\d{3}_' | sort | tail -1
+# The revision ID and filename prefix MUST match (e.g., revision="068", file=068_description.py).
+# down_revision MUST point to the previous sequential number. Never use hex hash IDs for new migrations.
# Access PostgreSQL
docker exec -it patherly_postgres psql -U postgres -d patherly
@@ -415,7 +419,7 @@ gh run view --json jobs --jq '.jobs[] | {name: .name, conclusion: .conclusi
- **New endpoint:** Create in `endpoints/` → add to `router.py` → schema in `schemas/` → tests → frontend API client
- **New page:** Create in `pages/` → add route in `router.tsx` → nav link in `AppLayout.tsx`
- **New public route (no auth):** Add at top level in `router.tsx` alongside `/login`, `/register` — NOT inside the `ProtectedRoute`/`AppLayout` children.
-- **Schema change:** Update model → `alembic revision --autogenerate -m "desc"` → review → `alembic upgrade head`
+- **Schema change:** Update model → `alembic revision --autogenerate -m "desc" --rev-id=NNN` (NNN = next sequential number, e.g., 068 → 069) → review → `alembic upgrade head`
- **New frontend API module:** Types in `types/` → export from `types/index.ts` → client in `api/` → export from `api/index.ts`
---
diff --git a/backend/alembic/versions/fc01_add_pending_task_lane.py b/backend/alembic/versions/068_add_pending_task_lane.py
similarity index 88%
rename from backend/alembic/versions/fc01_add_pending_task_lane.py
rename to backend/alembic/versions/068_add_pending_task_lane.py
index 632de9b4..a8da1977 100644
--- a/backend/alembic/versions/fc01_add_pending_task_lane.py
+++ b/backend/alembic/versions/068_add_pending_task_lane.py
@@ -1,14 +1,14 @@
"""add pending_task_lane to ai_sessions
-Revision ID: fc01a1b2c3d4
-Revises: fb1481317ff6
+Revision ID: 068
+Revises: 067
Create Date: 2026-03-27
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB
-revision = "fc01a1b2c3d4"
+revision = "068"
down_revision = "067"
branch_labels = None
depends_on = None
From 6268aa3520aae2c1f582650c6893aab15e9f68f4 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 03:05:58 +0000
Subject: [PATCH 06/49] fix: task lane no longer self-hides on submit
TaskLane had `if (submitted) return null` which immediately hid the
component after submit, before the AI could respond. Now the parent
controls visibility: the lane stays visible during the AI call, then
either updates with new tasks or clears when the AI sends none.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/components/assistant/TaskLane.tsx | 8 +++-----
frontend/src/pages/AssistantChatPage.tsx | 7 ++++++-
2 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/frontend/src/components/assistant/TaskLane.tsx b/frontend/src/components/assistant/TaskLane.tsx
index 80314e3b..e9972ca6 100644
--- a/frontend/src/components/assistant/TaskLane.tsx
+++ b/frontend/src/components/assistant/TaskLane.tsx
@@ -50,7 +50,6 @@ export function TaskLane({ questions, actions, onSubmit, onClose, loading }: Tas
})),
])
const [submitting, setSubmitting] = useState(false)
- const [submitted, setSubmitted] = useState(false)
const [showRunAll, setShowRunAll] = useState(false)
const [showPreview, setShowPreview] = useState(false)
@@ -112,7 +111,6 @@ export function TaskLane({ questions, actions, onSubmit, onClose, loading }: Tas
type: 'action', label: a.label, command: a.command, description: a.description, state: 'pending', value: '',
})),
])
- setSubmitted(false)
}, [questions, actions])
const updateTask = (idx: number, updates: Partial) => {
@@ -164,12 +162,12 @@ export function TaskLane({ questions, actions, onSubmit, onClose, loading }: Tas
const handleSubmit = () => {
setSubmitting(true)
onSubmit(tasks)
- setSubmitted(true)
+ // Don't self-hide — parent controls visibility via showTaskLane.
+ // The AI response will either send updated tasks (replacing these)
+ // or send none (parent hides the lane).
setSubmitting(false)
}
- if (submitted) return null
-
return (
0
const hasActions = response.actions && response.actions.length > 0
if (hasQuestions || hasActions) {
setActiveQuestions(response.questions || [])
setActiveActions(response.actions || [])
setShowTaskLane(true)
+ } else {
+ // AI sent no new tasks — clear the lane
+ setShowTaskLane(false)
+ setActiveQuestions([])
+ setActiveActions([])
}
} catch {
setMessages(prev => [
From e9f96474e0814a844ea6d4805f129cc63601538b Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 04:21:42 +0000
Subject: [PATCH 07/49] feat: persist task lane state across reloads and
session switches
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
TaskLane now saves user's in-progress answers (typed text, checked
items) to sessionStorage keyed by session ID. On reload or session
switch, the full task lane state restores — including partial work.
- TaskLane: saves tasks array to sessionStorage on every change,
restores from sessionStorage on mount
- AssistantChatPage: saves task lane metadata (visibility, questions,
actions, chatId) to sessionStorage, restores on mount
- Closing the task lane clears its sessionStorage entry
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../src/components/assistant/TaskLane.tsx | 52 +++++++++++++++----
frontend/src/pages/AssistantChatPage.tsx | 44 ++++++++++++++--
2 files changed, 82 insertions(+), 14 deletions(-)
diff --git a/frontend/src/components/assistant/TaskLane.tsx b/frontend/src/components/assistant/TaskLane.tsx
index e9972ca6..ddd60d79 100644
--- a/frontend/src/components/assistant/TaskLane.tsx
+++ b/frontend/src/components/assistant/TaskLane.tsx
@@ -33,22 +33,51 @@ type TaskResponse = QuestionResponse | ActionResponse
interface TaskLaneProps {
questions: QuestionItem[]
actions: ActionItem[]
+ sessionId?: string | null
onSubmit: (responses: TaskResponse[]) => void
onClose: () => void
loading?: boolean
}
+// ── Storage helpers ──
+
+const TASK_LANE_STORAGE_KEY = 'rf-tasklane-state'
+
+function saveTaskState(sessionId: string, tasks: TaskResponse[]) {
+ try {
+ sessionStorage.setItem(`${TASK_LANE_STORAGE_KEY}:${sessionId}`, JSON.stringify(tasks))
+ } catch { /* quota exceeded — ignore */ }
+}
+
+function loadTaskState(sessionId: string): TaskResponse[] | null {
+ try {
+ const stored = sessionStorage.getItem(`${TASK_LANE_STORAGE_KEY}:${sessionId}`)
+ return stored ? JSON.parse(stored) : null
+ } catch { return null }
+}
+
+export function clearTaskState(sessionId: string) {
+ try { sessionStorage.removeItem(`${TASK_LANE_STORAGE_KEY}:${sessionId}`) } catch { /* ignore */ }
+}
+
// ── Component ──
-export function TaskLane({ questions, actions, onSubmit, onClose, loading }: TaskLaneProps) {
- const [tasks, setTasks] = useState(() => [
- ...questions.map((q): QuestionResponse => ({
- type: 'question', text: q.text, context: q.context, state: 'pending', value: '',
- })),
- ...actions.map((a): ActionResponse => ({
- type: 'action', label: a.label, command: a.command, description: a.description, state: 'pending', value: '',
- })),
- ])
+export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loading }: TaskLaneProps) {
+ const [tasks, setTasks] = useState(() => {
+ // Try to restore saved state for this session (preserves user's in-progress answers)
+ if (sessionId) {
+ const saved = loadTaskState(sessionId)
+ if (saved && saved.length > 0) return saved
+ }
+ return [
+ ...questions.map((q): QuestionResponse => ({
+ type: 'question', text: q.text, context: q.context, state: 'pending', value: '',
+ })),
+ ...actions.map((a): ActionResponse => ({
+ type: 'action', label: a.label, command: a.command, description: a.description, state: 'pending', value: '',
+ })),
+ ]
+ })
const [submitting, setSubmitting] = useState(false)
const [showRunAll, setShowRunAll] = useState(false)
const [showPreview, setShowPreview] = useState(false)
@@ -100,6 +129,11 @@ export function TaskLane({ questions, actions, onSubmit, onClose, loading }: Tas
}
}, [handleMouseMove, handleMouseUp])
+ // Save task state to sessionStorage on every change
+ useEffect(() => {
+ if (sessionId) saveTaskState(sessionId, tasks)
+ }, [sessionId, tasks])
+
// Reset when new tasks come in from AI response
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: syncs derived state from prop changes
diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx
index 61b665d0..18b7d218 100644
--- a/frontend/src/pages/AssistantChatPage.tsx
+++ b/frontend/src/pages/AssistantChatPage.tsx
@@ -12,7 +12,7 @@ import { analytics } from '@/lib/analytics'
import { toast } from '@/lib/toast'
import { ChatSidebar, ChatSidebarCollapsedBar } from '@/components/assistant/ChatSidebar'
import { ChatMessage } from '@/components/assistant/ChatMessage'
-import { TaskLane } from '@/components/assistant/TaskLane'
+import { TaskLane, clearTaskState } from '@/components/assistant/TaskLane'
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
import type { ChatListItem, ConclusionOutcome } from '@/types/assistant-chat'
import type { SuggestedFlow } from '@/types/copilot'
@@ -42,9 +42,27 @@ export default function AssistantChatPage() {
const [logContent, setLogContent] = useState('')
const [pendingUploads, setPendingUploads] = useState([])
const [isDragOver, setIsDragOver] = useState(false)
- const [activeQuestions, setActiveQuestions] = useState([])
- const [activeActions, setActiveActions] = useState([])
- const [showTaskLane, setShowTaskLane] = useState(false)
+ const [activeQuestions, setActiveQuestions] = useState(() => {
+ try {
+ const saved = sessionStorage.getItem('rf-tasklane-meta')
+ if (saved) { const d = JSON.parse(saved); return d.questions || [] }
+ } catch { /* ignore */ }
+ return []
+ })
+ const [activeActions, setActiveActions] = useState(() => {
+ try {
+ const saved = sessionStorage.getItem('rf-tasklane-meta')
+ if (saved) { const d = JSON.parse(saved); return d.actions || [] }
+ } catch { /* ignore */ }
+ return []
+ })
+ const [showTaskLane, setShowTaskLane] = useState(() => {
+ try {
+ const saved = sessionStorage.getItem('rf-tasklane-meta')
+ if (saved) { const d = JSON.parse(saved); return d.show === true && d.chatId === (urlSessionId || null) }
+ } catch { /* ignore */ }
+ return false
+ })
const [sidebarCollapsed, setSidebarCollapsed] = useState(() =>
localStorage.getItem('rf-chat-sidebar-collapsed') === 'true'
)
@@ -137,6 +155,18 @@ export default function AssistantChatPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
+ // Persist task lane metadata to sessionStorage
+ useEffect(() => {
+ try {
+ sessionStorage.setItem('rf-tasklane-meta', JSON.stringify({
+ show: showTaskLane,
+ chatId: activeChatId,
+ questions: activeQuestions,
+ actions: activeActions,
+ }))
+ } catch { /* ignore */ }
+ }, [showTaskLane, activeChatId, activeQuestions, activeActions])
+
// Auto-scroll
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
@@ -737,8 +767,12 @@ export default function AssistantChatPage() {
setShowTaskLane(false)}
+ onClose={() => {
+ setShowTaskLane(false)
+ if (activeChatId) clearTaskState(activeChatId)
+ }}
loading={loading}
/>
)}
From 1e78977e4fc7f7967bf0a115bcf9da2bfc25db93 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 05:39:09 +0000
Subject: [PATCH 08/49] feat: restore active chat session on reload
Save activeChatId to sessionStorage so reloading /assistant restores
the last conversation. Messages load from backend conversation_messages,
task lane restores from sessionStorage (with partial answers intact).
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/pages/AssistantChatPage.tsx | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx
index 18b7d218..3dcd4c27 100644
--- a/frontend/src/pages/AssistantChatPage.tsx
+++ b/frontend/src/pages/AssistantChatPage.tsx
@@ -31,7 +31,10 @@ export default function AssistantChatPage() {
const navigate = useNavigate()
const { sessionId: urlSessionId } = useParams<{ sessionId?: string }>()
const [chats, setChats] = useState([])
- const [activeChatId, setActiveChatId] = useState(urlSessionId || null)
+ const [activeChatId, setActiveChatId] = useState(() => {
+ if (urlSessionId) return urlSessionId
+ try { return sessionStorage.getItem('rf-active-chat-id') } catch { return null }
+ })
const [messages, setMessages] = useState([])
const [input, setInput] = useState('')
const [loading, setLoading] = useState(false)
@@ -77,6 +80,14 @@ export default function AssistantChatPage() {
const dragCounterRef = useRef(0)
const prefillHandledRef = useRef(false)
+ // Persist active chat ID to sessionStorage
+ useEffect(() => {
+ try {
+ if (activeChatId) sessionStorage.setItem('rf-active-chat-id', activeChatId)
+ else sessionStorage.removeItem('rf-active-chat-id')
+ } catch { /* ignore */ }
+ }, [activeChatId])
+
// Load chat list from ai_sessions
useEffect(() => {
loadChats()
@@ -89,6 +100,13 @@ export default function AssistantChatPage() {
}
}, [urlSessionId]) // eslint-disable-line react-hooks/exhaustive-deps
+ // Restore session from sessionStorage on mount (when URL has no session ID)
+ useEffect(() => {
+ if (!urlSessionId && activeChatId) {
+ selectChat(activeChatId)
+ }
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
+
// Handle prefill from command palette / dashboard handoff
useEffect(() => {
const state = location.state as { prefill?: string; uploadIds?: string[] } | null
From 977e5a8ddb25a1ec5988c4cd6cc458a258950e71 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 13:03:06 +0000
Subject: [PATCH 09/49] fix: task lane restore checks activeChatId, not
urlSessionId
The showTaskLane initializer was comparing the saved chatId against
urlSessionId (null on /assistant), so it never matched. Now compares
against activeChatId which is already restored from sessionStorage.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/pages/AssistantChatPage.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx
index 3dcd4c27..45ce031b 100644
--- a/frontend/src/pages/AssistantChatPage.tsx
+++ b/frontend/src/pages/AssistantChatPage.tsx
@@ -48,21 +48,21 @@ export default function AssistantChatPage() {
const [activeQuestions, setActiveQuestions] = useState(() => {
try {
const saved = sessionStorage.getItem('rf-tasklane-meta')
- if (saved) { const d = JSON.parse(saved); return d.questions || [] }
+ if (saved) { const d = JSON.parse(saved); if (d.chatId === activeChatId) return d.questions || [] }
} catch { /* ignore */ }
return []
})
const [activeActions, setActiveActions] = useState(() => {
try {
const saved = sessionStorage.getItem('rf-tasklane-meta')
- if (saved) { const d = JSON.parse(saved); return d.actions || [] }
+ if (saved) { const d = JSON.parse(saved); if (d.chatId === activeChatId) return d.actions || [] }
} catch { /* ignore */ }
return []
})
const [showTaskLane, setShowTaskLane] = useState(() => {
try {
const saved = sessionStorage.getItem('rf-tasklane-meta')
- if (saved) { const d = JSON.parse(saved); return d.show === true && d.chatId === (urlSessionId || null) }
+ if (saved) { const d = JSON.parse(saved); return d.show === true && d.chatId === activeChatId }
} catch { /* ignore */ }
return false
})
From 80af408f2d51a30d416cfc391659d08248c7af8b Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 19:11:13 +0000
Subject: [PATCH 10/49] feat: persist task lane user responses to backend
Add PUT /ai-sessions/{id}/task-lane endpoint that saves the full task
lane state (AI questions/actions + user's in-progress responses) to
the pending_task_lane JSONB column. TaskLane debounce-saves to the
backend every 2s after changes. On session load, user responses are
restored from the backend into sessionStorage so TaskLane picks them
up on mount. Users can now close the browser, come back later, and
find their task lane exactly where they left it.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
backend/app/api/endpoints/ai_sessions.py | 28 +++++++++++++++++++
backend/app/schemas/ai_session.py | 10 +++++++
frontend/src/api/aiSessions.ts | 8 ++++++
.../src/components/assistant/TaskLane.tsx | 19 +++++++++++--
frontend/src/pages/AssistantChatPage.tsx | 7 +++++
5 files changed, 69 insertions(+), 3 deletions(-)
diff --git a/backend/app/api/endpoints/ai_sessions.py b/backend/app/api/endpoints/ai_sessions.py
index 722f54b7..b5420787 100644
--- a/backend/app/api/endpoints/ai_sessions.py
+++ b/backend/app/api/endpoints/ai_sessions.py
@@ -48,6 +48,7 @@ from app.schemas.ai_session import (
ChatSessionCreateResponse,
ChatMessageRequest,
ChatMessageResponse,
+ SaveTaskLaneRequest,
)
from app.services import flowpilot_engine
from app.services import unified_chat_service
@@ -497,6 +498,33 @@ async def pause_session(
await db.commit()
+# ── Save Task Lane ──
+
+@router.put("/{session_id}/task-lane", status_code=204)
+@limiter.limit("30/minute")
+async def save_task_lane(
+ request: Request,
+ session_id: UUID,
+ body: SaveTaskLaneRequest,
+ current_user: Annotated[User, Depends(get_current_active_user)],
+ db: Annotated[AsyncSession, Depends(get_db)],
+ _: None = Depends(require_engineer_or_admin),
+):
+ """Save the current task lane state including user's in-progress responses."""
+ session = await db.get(AISession, session_id)
+ if not session:
+ raise HTTPException(status_code=404, detail="Session not found")
+ if session.user_id != current_user.id:
+ raise HTTPException(status_code=403, detail="Not your session")
+
+ session.pending_task_lane = {
+ "questions": [q.model_dump() for q in body.questions],
+ "actions": [a.model_dump() for a in body.actions],
+ "responses": body.responses,
+ }
+ await db.commit()
+
+
# ── Resume ──
@router.post("/{session_id}/resume", status_code=204)
diff --git a/backend/app/schemas/ai_session.py b/backend/app/schemas/ai_session.py
index 0039653e..23bfd652 100644
--- a/backend/app/schemas/ai_session.py
+++ b/backend/app/schemas/ai_session.py
@@ -287,6 +287,16 @@ class ChatMessageResponse(BaseModel):
questions: list[QuestionItem] | None = None
+class SaveTaskLaneRequest(BaseModel):
+ """Save the full task lane state (AI items + user responses)."""
+ questions: list[QuestionItem] = []
+ actions: list[ActionItem] = []
+ responses: list[dict[str, Any]] = Field(
+ default_factory=list,
+ description="User's in-progress task responses with state/value",
+ )
+
+
class AISessionSearchResult(BaseModel):
"""Lightweight session result for Command Palette / autocomplete."""
id: UUID
diff --git a/frontend/src/api/aiSessions.ts b/frontend/src/api/aiSessions.ts
index 39259c1b..08aae5bc 100644
--- a/frontend/src/api/aiSessions.ts
+++ b/frontend/src/api/aiSessions.ts
@@ -110,6 +110,14 @@ export const aiSessionsApi = {
return response.data
},
+ async saveTaskLane(sessionId: string, data: {
+ questions: Array<{ text: string; context?: string }>;
+ actions: Array<{ label: string; command?: string | null; description?: string }>;
+ responses: Array>;
+ }): Promise {
+ await apiClient.put(`/ai-sessions/${sessionId}/task-lane`, data)
+ },
+
async pauseSession(sessionId: string): Promise {
await apiClient.post(`/ai-sessions/${sessionId}/pause`)
},
diff --git a/frontend/src/components/assistant/TaskLane.tsx b/frontend/src/components/assistant/TaskLane.tsx
index ddd60d79..791eba74 100644
--- a/frontend/src/components/assistant/TaskLane.tsx
+++ b/frontend/src/components/assistant/TaskLane.tsx
@@ -5,6 +5,7 @@ import {
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast'
+import { aiSessionsApi } from '@/api/aiSessions'
import type { ActionItem, QuestionItem } from '@/types/ai-session'
// ── Types ──
@@ -129,10 +130,22 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
}
}, [handleMouseMove, handleMouseUp])
- // Save task state to sessionStorage on every change
+ // Save task state to sessionStorage on every change + debounce to backend
+ const saveTimerRef = useRef | null>(null)
useEffect(() => {
- if (sessionId) saveTaskState(sessionId, tasks)
- }, [sessionId, tasks])
+ if (!sessionId) return
+ saveTaskState(sessionId, tasks)
+ // Debounce save to backend (2s after last change)
+ if (saveTimerRef.current) clearTimeout(saveTimerRef.current)
+ saveTimerRef.current = setTimeout(() => {
+ aiSessionsApi.saveTaskLane(sessionId, {
+ questions: questions.map(q => ({ text: q.text, context: q.context })),
+ actions: actions.map(a => ({ label: a.label, command: a.command, description: a.description })),
+ responses: tasks as unknown as Array>,
+ }).catch(() => { /* silent — best-effort save */ })
+ }, 2000)
+ return () => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current) }
+ }, [sessionId, tasks]) // eslint-disable-line react-hooks/exhaustive-deps
// Reset when new tasks come in from AI response
useEffect(() => {
diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx
index 45ce031b..3a95f680 100644
--- a/frontend/src/pages/AssistantChatPage.tsx
+++ b/frontend/src/pages/AssistantChatPage.tsx
@@ -228,6 +228,13 @@ export default function AssistantChatPage() {
setActiveQuestions(q)
setActiveActions(a)
setShowTaskLane(true)
+ // Pre-load user's saved responses into sessionStorage so TaskLane restores them
+ const responses = (detail.pending_task_lane as Record).responses as unknown[] | undefined
+ if (responses && responses.length > 0) {
+ try {
+ sessionStorage.setItem(`rf-tasklane-state:${chatId}`, JSON.stringify(responses))
+ } catch { /* ignore */ }
+ }
}
}
} catch {
From 4fa26149e617e3857d04570b8e32b0e20b8467ce Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 19:16:06 +0000
Subject: [PATCH 11/49] fix: add payload size limits to task lane save endpoint
- Max 50 questions, 50 actions, 100 responses (Pydantic max_length)
- Max 256KB total serialized payload (prevents DB bloat)
- Existing guards: JWT auth, role check, ownership check, rate limit
Co-Authored-By: Claude Opus 4.6 (1M context)
---
backend/app/api/endpoints/ai_sessions.py | 9 ++++++++-
backend/app/schemas/ai_session.py | 5 +++--
2 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/backend/app/api/endpoints/ai_sessions.py b/backend/app/api/endpoints/ai_sessions.py
index b5420787..4ffd7097 100644
--- a/backend/app/api/endpoints/ai_sessions.py
+++ b/backend/app/api/endpoints/ai_sessions.py
@@ -517,11 +517,18 @@ async def save_task_lane(
if session.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Not your session")
- session.pending_task_lane = {
+ payload = {
"questions": [q.model_dump() for q in body.questions],
"actions": [a.model_dump() for a in body.actions],
"responses": body.responses,
}
+
+ # Guard against oversized payloads (max 256KB serialized)
+ import json
+ if len(json.dumps(payload)) > 256 * 1024:
+ raise HTTPException(status_code=413, detail="Task lane payload too large")
+
+ session.pending_task_lane = payload
await db.commit()
diff --git a/backend/app/schemas/ai_session.py b/backend/app/schemas/ai_session.py
index 23bfd652..fd37fde6 100644
--- a/backend/app/schemas/ai_session.py
+++ b/backend/app/schemas/ai_session.py
@@ -289,10 +289,11 @@ class ChatMessageResponse(BaseModel):
class SaveTaskLaneRequest(BaseModel):
"""Save the full task lane state (AI items + user responses)."""
- questions: list[QuestionItem] = []
- actions: list[ActionItem] = []
+ questions: list[QuestionItem] = Field(default_factory=list, max_length=50)
+ actions: list[ActionItem] = Field(default_factory=list, max_length=50)
responses: list[dict[str, Any]] = Field(
default_factory=list,
+ max_length=100,
description="User's in-progress task responses with state/value",
)
From 96602a66769c04a7fc5e5fbe0baff684211d56cb Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 21:44:04 +0000
Subject: [PATCH 12/49] fix: return pending_task_lane in session detail API
response + debug logging
_build_session_detail was omitting pending_task_lane, is_branching, and
active_branch_id from the GET /ai-sessions/{id} response. The fields
existed on the schema and model but were never passed in the manual
constructor, so task lane state could never be restored on navigation.
Also adds console logging to AssistantChatPage selectChat flow to
diagnose message restoration, and fixes ScriptTemplateEditor stepper
dismiss firing during programmatic script_body updates.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
backend/app/api/endpoints/ai_sessions.py | 3 +++
.../components/script-editor/ScriptTemplateEditor.tsx | 10 +++++++---
frontend/src/pages/AssistantChatPage.tsx | 7 ++++++-
3 files changed, 16 insertions(+), 4 deletions(-)
diff --git a/backend/app/api/endpoints/ai_sessions.py b/backend/app/api/endpoints/ai_sessions.py
index 4ffd7097..537737e6 100644
--- a/backend/app/api/endpoints/ai_sessions.py
+++ b/backend/app/api/endpoints/ai_sessions.py
@@ -117,6 +117,9 @@ def _build_session_detail(session: AISession) -> AISessionDetail:
resolved_at=session.resolved_at,
steps=step_responses,
conversation_messages=session.conversation_messages or [],
+ pending_task_lane=session.pending_task_lane,
+ is_branching=getattr(session, 'is_branching', False),
+ active_branch_id=str(session.active_branch_id) if getattr(session, 'active_branch_id', None) else None,
)
diff --git a/frontend/src/components/script-editor/ScriptTemplateEditor.tsx b/frontend/src/components/script-editor/ScriptTemplateEditor.tsx
index 0b3a8671..d0e3228e 100644
--- a/frontend/src/components/script-editor/ScriptTemplateEditor.tsx
+++ b/frontend/src/components/script-editor/ScriptTemplateEditor.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react'
+import { useState, useEffect, useRef } from 'react'
import { ArrowLeft, Loader2, Save, Scan, Trash2 } from 'lucide-react'
import { Input } from '@/components/ui/Input'
import { Textarea } from '@/components/ui/Textarea'
@@ -64,16 +64,19 @@ export function ScriptTemplateEditor({ templateId, onBack, onSaved }: Props) {
const [detectedCandidates, setDetectedCandidates] = useState([])
const [showStepper, setShowStepper] = useState(false)
const [detectionSummary, setDetectionSummary] = useState(null)
+ const acceptingCandidateRef = useRef(false)
const { canShareScriptTemplate } = usePermissions()
- // Dismiss stepper if user edits the script body during detection
+ // Dismiss stepper if user manually edits the script body during detection
+ // (but NOT when handleAcceptCandidate programmatically updates script_body)
const scriptBodyRef = form.script_body
useEffect(() => {
- if (showStepper) {
+ if (showStepper && !acceptingCandidateRef.current) {
setShowStepper(false)
setDetectedCandidates([])
}
+ acceptingCandidateRef.current = false
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scriptBodyRef])
@@ -263,6 +266,7 @@ export function ScriptTemplateEditor({ templateId, onBack, onSaved }: Props) {
sensitive: overrides.sensitive,
}
+ acceptingCandidateRef.current = true
setForm(f => ({
...f,
script_body: updatedScript,
diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx
index 3a95f680..30e20a2b 100644
--- a/frontend/src/pages/AssistantChatPage.tsx
+++ b/frontend/src/pages/AssistantChatPage.tsx
@@ -102,7 +102,9 @@ export default function AssistantChatPage() {
// Restore session from sessionStorage on mount (when URL has no session ID)
useEffect(() => {
+ console.log('[AssistantChat] Mount restore check — urlSessionId:', urlSessionId, 'activeChatId:', activeChatId)
if (!urlSessionId && activeChatId) {
+ console.log('[AssistantChat] Calling selectChat to restore:', activeChatId)
selectChat(activeChatId)
}
}, []) // eslint-disable-line react-hooks/exhaustive-deps
@@ -207,6 +209,7 @@ export default function AssistantChatPage() {
}
const selectChat = useCallback(async (chatId: string) => {
+ console.log('[AssistantChat] selectChat called with:', chatId)
setActiveChatId(chatId)
// Clear TaskLane when switching chats — will restore from backend if available
setShowTaskLane(false)
@@ -214,6 +217,7 @@ export default function AssistantChatPage() {
setActiveActions([])
try {
const detail = await aiSessionsApi.getSession(chatId)
+ console.log('[AssistantChat] getSession response — messages:', detail.conversation_messages?.length, 'pending_task_lane:', !!detail.pending_task_lane)
setMessages(
(detail.conversation_messages || []).map(m => ({
role: m.role as 'user' | 'assistant',
@@ -237,7 +241,8 @@ export default function AssistantChatPage() {
}
}
}
- } catch {
+ } catch (err) {
+ console.error('[AssistantChat] Failed to load chat session:', err)
setMessages([])
}
}, [])
From 0483b6f0f5516995745ee1694ee9dd38fd175047 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 22:18:17 +0000
Subject: [PATCH 13/49] fix: prevent Monaco onChange echo from dismissing
parameter stepper
ScriptBodyEditor's onChange fired when the value prop changed externally
(from handleAcceptCandidate inserting placeholders), creating a feedback
loop that reset acceptingCandidateRef before the second useEffect cycle.
Guard onChange to only propagate when the value actually differs from the
current prop.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../components/script-editor/ScriptBodyEditor.tsx | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/frontend/src/components/script-editor/ScriptBodyEditor.tsx b/frontend/src/components/script-editor/ScriptBodyEditor.tsx
index c4c505be..03a651ba 100644
--- a/frontend/src/components/script-editor/ScriptBodyEditor.tsx
+++ b/frontend/src/components/script-editor/ScriptBodyEditor.tsx
@@ -1,4 +1,4 @@
-import { useCallback } from 'react'
+import { useCallback, useRef } from 'react'
import Editor, { type BeforeMount } from '@monaco-editor/react'
import { resolutionFlowTheme, THEME_ID } from '@/components/tree-editor/code-mode/resolutionFlowTheme'
import { Spinner } from '@/components/common/Spinner'
@@ -10,11 +10,22 @@ interface Props {
}
export function ScriptBodyEditor({ value, onChange, disabled }: Props) {
+ const lastValueRef = useRef(value)
+ lastValueRef.current = value
+
const handleBeforeMount: BeforeMount = useCallback((monaco) => {
// Register our dark theme if not already defined
monaco.editor.defineTheme(THEME_ID, resolutionFlowTheme)
}, [])
+ const handleChange = useCallback((v: string | undefined) => {
+ const next = v ?? ''
+ // Only propagate user-initiated edits, not echoes from external value prop changes
+ if (next !== lastValueRef.current) {
+ onChange(next)
+ }
+ }, [onChange])
+
return (
onChange(v ?? '')}
+ onChange={handleChange}
beforeMount={handleBeforeMount}
loading={
From af2a41830ce6a5a96f062817be7538d4cf8f6236 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 22:33:47 +0000
Subject: [PATCH 14/49] docs: add spec and implementation plan for task lane
minimize + resolve streaming
Co-Authored-By: Claude Opus 4.6 (1M context)
---
...3-28-tasklane-minimize-and-resolve-docs.md | 1066 +++++++++++++++++
...sklane-minimize-and-resolve-docs-design.md | 158 +++
2 files changed, 1224 insertions(+)
create mode 100644 docs/superpowers/plans/2026-03-28-tasklane-minimize-and-resolve-docs.md
create mode 100644 docs/superpowers/specs/2026-03-28-tasklane-minimize-and-resolve-docs-design.md
diff --git a/docs/superpowers/plans/2026-03-28-tasklane-minimize-and-resolve-docs.md b/docs/superpowers/plans/2026-03-28-tasklane-minimize-and-resolve-docs.md
new file mode 100644
index 00000000..42a7d66c
--- /dev/null
+++ b/docs/superpowers/plans/2026-03-28-tasklane-minimize-and-resolve-docs.md
@@ -0,0 +1,1066 @@
+# Task Lane Minimize/Reopen + Resolve Documentation Streaming — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Make the task lane collapsible (not destructive) with a reopen pill, and replace the blocking resolve flow with instant resolve + streamed ticket notes generation.
+
+**Architecture:** Feature 1 is a frontend-only change to TaskLane close behavior + a new pill button. Feature 2 splits the resolve endpoint into a fast status-only call and a new SSE streaming endpoint for AI-generated ticket notes, with prompt caching and singleton client reuse on the backend.
+
+**Tech Stack:** React, TypeScript, Lucide icons, FastAPI, Anthropic SDK (streaming + prompt caching), SSE via `StreamingResponse`
+
+---
+
+### Task 1: Task Lane — Replace X with PanelRightClose Icon
+
+**Files:**
+- Modify: `frontend/src/components/assistant/TaskLane.tsx:2,250-252`
+
+- [ ] **Step 1: Update the import to include PanelRightClose**
+
+Replace line 2 import:
+
+```tsx
+import {
+ Copy, Check, SkipForward, Terminal, ChevronDown, ChevronUp,
+ Send, Clipboard, Loader2, PanelRightClose, MessageCircleQuestion, Eye,
+} from 'lucide-react'
+```
+
+(Replace `X` with `PanelRightClose` in the import list.)
+
+- [ ] **Step 2: Replace the X icon in the close button**
+
+Replace lines 250-252:
+
+```tsx
+
+
+
+```
+
+- [ ] **Step 3: Verify the icon renders correctly**
+
+Run the frontend dev server, open an assistant chat, trigger a task lane, and confirm the close button shows a panel-collapse icon instead of an X.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add frontend/src/components/assistant/TaskLane.tsx
+git commit -m "feat: replace task lane X with PanelRightClose icon"
+```
+
+---
+
+### Task 2: Task Lane — Stop Destroying State on Close
+
+**Files:**
+- Modify: `frontend/src/pages/AssistantChatPage.tsx:797-800`
+
+- [ ] **Step 1: Remove clearTaskState from the onClose handler**
+
+Find the TaskLane onClose handler (around line 797-800):
+
+```tsx
+ onClose={() => {
+ setShowTaskLane(false)
+ if (activeChatId) clearTaskState(activeChatId)
+ }}
+```
+
+Replace with:
+
+```tsx
+ onClose={() => {
+ setShowTaskLane(false)
+ }}
+```
+
+This keeps task state in sessionStorage and backend `pending_task_lane` so it can be restored.
+
+- [ ] **Step 2: Verify state persists after closing**
+
+1. Open assistant chat, trigger task lane with questions/actions
+2. Type partial answers into the task lane
+3. Click the PanelRightClose button
+4. Task lane disappears but answers should persist in sessionStorage
+
+Verify: `sessionStorage.getItem('rf-tasklane-meta')` still has `questions` and `actions` with the chatId.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add frontend/src/pages/AssistantChatPage.tsx
+git commit -m "feat: task lane close preserves state instead of clearing"
+```
+
+---
+
+### Task 3: Task Lane — Add Reopen Pill to Input Toolbar
+
+**Files:**
+- Modify: `frontend/src/pages/AssistantChatPage.tsx:1-3,738-756`
+
+- [ ] **Step 1: Add ListChecks to the Lucide import**
+
+Find the Lucide import at the top of `AssistantChatPage.tsx` (line 3):
+
+```tsx
+import { Sparkles, Send, Loader2, Flag, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus } from 'lucide-react'
+```
+
+Add `ListChecks`:
+
+```tsx
+import { Sparkles, Send, Loader2, Flag, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus, ListChecks } from 'lucide-react'
+```
+
+- [ ] **Step 2: Add the Tasks pill button to the input toolbar**
+
+Find the input toolbar's left button group (around line 738-756). After the Conclude button block (the `{messages.length >= 2 && (` block that ends around line 755), add the Tasks pill:
+
+```tsx
+ {!showTaskLane && (activeQuestions.length > 0 || activeActions.length > 0) && (
+ setShowTaskLane(true)}
+ className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-accent-text hover:text-foreground hover:bg-accent-dim transition-colors"
+ title="Show task panel"
+ >
+
+ Tasks ({activeQuestions.length + activeActions.length})
+
+ )}
+```
+
+- [ ] **Step 3: Verify the full collapse/reopen flow**
+
+1. Open assistant chat, trigger task lane
+2. Click PanelRightClose — task lane collapses
+3. Confirm "Tasks (N)" pill appears in the input toolbar
+4. Click the pill — task lane reopens with previous state intact
+5. Confirm pill disappears when task lane is open
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add frontend/src/pages/AssistantChatPage.tsx
+git commit -m "feat: add Tasks pill to reopen collapsed task lane"
+```
+
+---
+
+### Task 4: Backend — Singleton AsyncAnthropic Client
+
+**Files:**
+- Modify: `backend/app/core/ai_provider.py:168-210`
+
+- [ ] **Step 1: Add a module-level singleton for the Anthropic client**
+
+In `backend/app/core/ai_provider.py`, add a module-level cache dict before the `AnthropicProvider` class (before line 168):
+
+```python
+# Singleton client cache — avoids creating new HTTP connections per call
+_anthropic_clients: dict[str, "anthropic.AsyncAnthropic"] = {}
+
+
+def _get_anthropic_client(api_key: str, timeout: int = 45) -> "anthropic.AsyncAnthropic":
+ """Return a cached AsyncAnthropic client, creating one if needed."""
+ import anthropic
+
+ cache_key = f"{api_key[:8]}:{timeout}"
+ if cache_key not in _anthropic_clients:
+ _anthropic_clients[cache_key] = anthropic.AsyncAnthropic(
+ api_key=api_key,
+ timeout=timeout,
+ max_retries=1,
+ )
+ return _anthropic_clients[cache_key]
+```
+
+- [ ] **Step 2: Update AnthropicProvider to use the singleton**
+
+Replace the `generate_json` method's client creation (lines 182-188):
+
+```python
+ async def generate_json(
+ self,
+ system_prompt: str,
+ messages: list[dict[str, str]],
+ max_tokens: int = 4096,
+ ) -> tuple[str, int, int]:
+ client = _get_anthropic_client(self._api_key, self._timeout)
+
+ response = await client.messages.create(
+ model=self._model,
+ max_tokens=max_tokens,
+ system=system_prompt,
+ messages=messages,
+ )
+
+ text = response.content[0].text
+ input_tokens = response.usage.input_tokens
+ output_tokens = response.usage.output_tokens
+
+ return text, input_tokens, output_tokens
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add backend/app/core/ai_provider.py
+git commit -m "perf: singleton AsyncAnthropic client to avoid per-call connection setup"
+```
+
+---
+
+### Task 5: Backend — Add generate_text_stream to AIProvider
+
+**Files:**
+- Modify: `backend/app/core/ai_provider.py:16-55,168-210`
+
+- [ ] **Step 1: Add the abstract method to the AIProvider base class**
+
+After the `generate_text` abstract method (after line 55), add:
+
+```python
+ async def generate_text_stream(
+ self,
+ system_prompt: str,
+ messages: list[dict[str, str]],
+ max_tokens: int = 4096,
+ ) -> "AsyncIterator[str]":
+ """Stream a text response token by token.
+
+ Args:
+ system_prompt: System-level instruction for the model.
+ messages: List of message dicts with "role" and "content" keys.
+ max_tokens: Maximum output tokens.
+
+ Yields:
+ Text chunks as they are generated.
+ """
+ raise NotImplementedError("Streaming not supported for this provider")
+ # Make this an async generator to satisfy type checker
+ yield "" # pragma: no cover
+```
+
+Also add the import at the top of the file (line 1 area):
+
+```python
+from collections.abc import AsyncIterator
+```
+
+- [ ] **Step 2: Implement streaming in AnthropicProvider**
+
+Add the streaming method to `AnthropicProvider` after `generate_text` (after line 210):
+
+```python
+ async def generate_text_stream(
+ self,
+ system_prompt: str,
+ messages: list[dict[str, str]],
+ max_tokens: int = 4096,
+ ) -> AsyncIterator[str]:
+ client = _get_anthropic_client(self._api_key, self._timeout)
+
+ async with client.messages.stream(
+ model=self._model,
+ max_tokens=max_tokens,
+ system=system_prompt,
+ messages=messages,
+ ) as stream:
+ async for text in stream.text_stream:
+ yield text
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add backend/app/core/ai_provider.py
+git commit -m "feat: add generate_text_stream to AnthropicProvider for SSE support"
+```
+
+---
+
+### Task 6: Backend — Add Streaming Ticket Notes Generator
+
+**Files:**
+- Modify: `backend/app/services/flowpilot_engine.py` (after `generate_status_update`, around line 1012)
+
+- [ ] **Step 1: Add the streaming ticket notes function**
+
+After the `generate_status_update` function (after line 1011), add:
+
+```python
+async def stream_ticket_notes(
+ session_id: UUID,
+ user_id: UUID,
+ db: AsyncSession,
+) -> AsyncIterator[str]:
+ """Stream AI-generated structured ticket notes for a resolved session.
+
+ Yields text chunks suitable for SSE streaming. Uses prompt caching
+ on the system prompt for fast repeat calls.
+ """
+ session = await _load_session(session_id, user_id, db)
+
+ # Build conversation summary from messages (chat sessions)
+ # or steps (guided sessions)
+ messages = session.conversation_messages or []
+ if messages:
+ recent = messages[-20:] # Last 20 messages for richer context
+ convo_text = "\n".join(
+ f"{'Engineer' if m['role'] == 'user' else 'AI Assistant'}: {m['content'][:500]}"
+ for m in recent
+ if isinstance(m, dict) and "role" in m and "content" in m
+ )
+ else:
+ # Fall back to steps for guided sessions
+ steps_summary = []
+ for step in sorted(session.steps, key=lambda s: s.step_order):
+ content = step.content or {}
+ text = content.get("text", "")
+ response = step.free_text_input or step.selected_option or ("Skipped" if step.was_skipped else None)
+ entry = f"Step {step.step_order + 1}: {text}"
+ if response:
+ entry += f"\n Engineer response: {response}"
+ steps_summary.append(entry)
+ convo_text = "\n".join(steps_summary) if steps_summary else "No session data."
+
+ # Calculate time spent
+ now = datetime.now(timezone.utc)
+ ref_time = session.resolved_at or now
+ delta = ref_time - session.created_at
+ total_minutes = int(delta.total_seconds() / 60)
+ time_display = f"{total_minutes} minutes" if total_minutes < 60 else f"{total_minutes // 60}h {total_minutes % 60}m"
+
+ system_prompt = """You are generating internal ticket notes for an MSP engineer's PSA system.
+
+Generate EXACTLY these four markdown sections, in this order:
+
+## Problem Summary
+Summarize what the engineer reported and the initial symptoms. 1-3 sentences.
+
+## Steps Taken
+List the key diagnostic steps, commands run, checks performed, and findings. Use bullet points.
+
+## Resolution
+What fixed the issue or what the final action was. Be specific and technical.
+
+## Next Steps
+Any follow-up items, monitoring to watch, or preventive measures. Write "None" if not applicable.
+
+Rules:
+- Be technical, concise, and factual
+- Use markdown formatting (headers, bullet lists, bold for emphasis)
+- Include specific technical details (commands, settings, error messages) where available
+- Do NOT include greetings, sign-offs, or pleasantries
+- Do NOT wrap output in code fences
+- Output ONLY the four sections above, nothing else"""
+
+ user_message_parts = [
+ f"Session status: {session.status}",
+ f"Time spent: {time_display}",
+ f"Problem summary: {session.problem_summary or 'Not specified'}",
+ ]
+ if session.problem_domain:
+ user_message_parts.append(f"Problem domain: {session.problem_domain}")
+ if session.resolution_summary:
+ user_message_parts.append(f"Resolution notes: {session.resolution_summary}")
+ user_message_parts.append(f"\nSession conversation:\n{convo_text}")
+
+ user_message = "\n".join(user_message_parts)
+
+ provider = get_ai_provider(settings.get_model_for_action("quick_action"))
+
+ # Use streaming if provider supports it (Anthropic), otherwise fall back
+ try:
+ async for chunk in provider.generate_text_stream(
+ system_prompt=system_prompt,
+ messages=[{"role": "user", "content": user_message}],
+ max_tokens=1500,
+ ):
+ yield chunk
+ except NotImplementedError:
+ # Fallback for non-streaming providers (Gemini)
+ text, _, _ = await provider.generate_text(
+ system_prompt=system_prompt,
+ messages=[{"role": "user", "content": user_message}],
+ max_tokens=1500,
+ )
+ yield text
+```
+
+Also add the import at the top of the file if not already present:
+
+```python
+from collections.abc import AsyncIterator
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add backend/app/services/flowpilot_engine.py
+git commit -m "feat: add stream_ticket_notes generator for SSE doc streaming"
+```
+
+---
+
+### Task 7: Backend — Add SSE Streaming Endpoint
+
+**Files:**
+- Modify: `backend/app/api/endpoints/ai_sessions.py` (after `get_documentation`, around line 925)
+
+- [ ] **Step 1: Add the SSE streaming endpoint**
+
+After the `get_documentation` endpoint (after line 924), add:
+
+```python
+@router.get("/{session_id}/documentation/stream")
+@limiter.limit("20/minute")
+async def stream_documentation(
+ request: Request,
+ session_id: UUID,
+ current_user: Annotated[User, Depends(get_current_active_user)],
+ db: Annotated[AsyncSession, Depends(get_db)],
+):
+ """Stream AI-generated ticket notes as Server-Sent Events."""
+ from starlette.responses import StreamingResponse
+
+ # Verify session ownership
+ result = await db.execute(
+ select(AISession).where(AISession.id == session_id)
+ )
+ session = result.scalar_one_or_none()
+ if not session:
+ raise HTTPException(status_code=404, detail="Session not found")
+ if session.user_id != current_user.id:
+ raise HTTPException(status_code=403, detail="Not authorized")
+
+ async def event_generator():
+ try:
+ async for chunk in flowpilot_engine.stream_ticket_notes(
+ session_id=session_id,
+ user_id=current_user.id,
+ db=db,
+ ):
+ # SSE format: data: \n\n
+ yield f"data: {chunk}\n\n"
+ yield "data: [DONE]\n\n"
+ except Exception as e:
+ logger.exception("SSE stream error for session %s: %s", session_id, e)
+ yield f"data: [ERROR] {str(e)}\n\n"
+
+ return StreamingResponse(
+ event_generator(),
+ media_type="text/event-stream",
+ headers={
+ "Cache-Control": "no-cache",
+ "Connection": "keep-alive",
+ "X-Accel-Buffering": "no", # Disable nginx buffering
+ },
+ )
+```
+
+Also add the `AISession` import at the top if not already there (it should be — verify with the existing imports around line 30-55):
+
+```python
+from app.models.ai_session import AISession
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add backend/app/api/endpoints/ai_sessions.py
+git commit -m "feat: add SSE endpoint for streaming ticket notes generation"
+```
+
+---
+
+### Task 8: Backend — Make resolve_session Non-Blocking
+
+**Files:**
+- Modify: `backend/app/api/endpoints/ai_sessions.py:413-446`
+- Modify: `backend/app/schemas/ai_session.py:135-142`
+
+- [ ] **Step 1: Make documentation optional in SessionCloseResponse**
+
+In `backend/app/schemas/ai_session.py`, change line 139:
+
+```python
+class SessionCloseResponse(BaseModel):
+ """Response after resolving or escalating."""
+ session_id: UUID
+ status: str
+ documentation: SessionDocumentation | None = None
+ psa_push_status: str = "no_psa" # sent | pending_retry | no_psa | failed
+ psa_push_error: str | None = None
+ member_mapping_warning: str | None = None
+```
+
+(Change `documentation: SessionDocumentation` to `documentation: SessionDocumentation | None = None`)
+
+- [ ] **Step 2: Update the frontend type to match**
+
+In `frontend/src/types/ai-session.ts`, change line 128:
+
+```typescript
+export interface SessionCloseResponse {
+ session_id: string
+ status: string
+ documentation: SessionDocumentation | null
+ psa_push_status: string
+ psa_push_error: string | null
+ member_mapping_warning: string | null
+}
+```
+
+(Change `documentation: SessionDocumentation` to `documentation: SessionDocumentation | null`)
+
+- [ ] **Step 3: Update the resolve endpoint to not generate docs inline**
+
+The `resolve_session` endpoint (lines 413-446) currently calls `flowpilot_engine.resolve_session()` which generates `_generate_documentation(session)` inline. The resolve_session in flowpilot_engine already sets status + saves summary + returns docs — we just need the endpoint to stop depending on docs for its response.
+
+In `backend/app/services/flowpilot_engine.py`, the `resolve_session` function (line 497) already returns `SessionCloseResponse` with `documentation=documentation`. Since we made `documentation` optional, the existing flow still works but is no longer blocking on an LLM call (it never was — `_generate_documentation` is pure Python and fast). The slow part was `generate_session_embedding` (Voyage AI embedding call) and `_push_to_psa`.
+
+The actual fix: move the embedding generation to be fire-and-forget in the endpoint. In `backend/app/api/endpoints/ai_sessions.py`, replace the resolve_session endpoint (lines 413-446):
+
+```python
+@router.post("/{session_id}/resolve", response_model=SessionCloseResponse)
+@limiter.limit("15/minute")
+async def resolve_session(
+ request: Request,
+ session_id: UUID,
+ data: ResolveSessionRequest,
+ current_user: Annotated[User, Depends(get_current_active_user)],
+ db: Annotated[AsyncSession, Depends(get_db)],
+ _: None = Depends(require_engineer_or_admin),
+):
+ """Resolve a session. Returns immediately; use /documentation/stream for ticket notes."""
+ try:
+ result = await flowpilot_engine.resolve_session(
+ session_id=session_id,
+ request=data,
+ user_id=current_user.id,
+ db=db,
+ )
+ except ValueError as e:
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
+ except PermissionError as e:
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e))
+
+ await db.commit()
+
+ # Fire-and-forget: resolution outputs + embedding (don't block the response)
+ import asyncio
+
+ async def _post_resolve_tasks():
+ try:
+ from app.services.resolution_output_generator import ResolutionOutputGenerator
+ gen = ResolutionOutputGenerator(db)
+ await gen.generate_all(session_id)
+ except Exception:
+ logger.exception(f"Failed to generate resolution outputs for session {session_id}")
+
+ asyncio.create_task(_post_resolve_tasks())
+
+ return result
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add backend/app/api/endpoints/ai_sessions.py backend/app/schemas/ai_session.py frontend/src/types/ai-session.ts
+git commit -m "feat: make resolve endpoint non-blocking, documentation optional"
+```
+
+---
+
+### Task 9: Frontend — Add streamDocumentation to API Client
+
+**Files:**
+- Modify: `frontend/src/api/aiSessions.ts:95-100`
+
+- [ ] **Step 1: Add the streaming API method**
+
+After the `getDocumentation` method (around line 100), add:
+
+```typescript
+ async streamDocumentation(
+ sessionId: string,
+ onChunk: (text: string) => void,
+ onDone: () => void,
+ onError: (error: string) => void,
+ ): Promise {
+ const token = localStorage.getItem('access_token')
+ const baseUrl = import.meta.env.VITE_API_URL || ''
+
+ try {
+ const response = await fetch(
+ `${baseUrl}/api/ai-sessions/${sessionId}/documentation/stream`,
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ )
+
+ if (!response.ok) {
+ onError(`HTTP ${response.status}`)
+ return
+ }
+
+ const reader = response.body?.getReader()
+ if (!reader) {
+ onError('No response body')
+ return
+ }
+
+ const decoder = new TextDecoder()
+ let buffer = ''
+
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) break
+
+ buffer += decoder.decode(value, { stream: true })
+ const lines = buffer.split('\n')
+ buffer = lines.pop() || ''
+
+ for (const line of lines) {
+ if (line.startsWith('data: ')) {
+ const data = line.slice(6)
+ if (data === '[DONE]') {
+ onDone()
+ return
+ }
+ if (data.startsWith('[ERROR]')) {
+ onError(data.slice(8))
+ return
+ }
+ onChunk(data)
+ }
+ }
+ }
+ // Stream ended without [DONE]
+ onDone()
+ } catch (err) {
+ onError(err instanceof Error ? err.message : 'Stream failed')
+ }
+ },
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add frontend/src/api/aiSessions.ts
+git commit -m "feat: add streamDocumentation SSE client for ticket notes"
+```
+
+---
+
+### Task 10: Frontend — Rewrite ConcludeSessionModal for Two-Phase Flow
+
+**Files:**
+- Modify: `frontend/src/components/assistant/ConcludeSessionModal.tsx`
+- Modify: `frontend/src/pages/AssistantChatPage.tsx:396-414`
+
+- [ ] **Step 1: Update the ConcludeSessionModal props and state**
+
+The modal's `onConclude` prop currently returns `Promise` (the summary text). We need to change it to return `Promise<{ sessionId: string }>` so the modal can initiate streaming itself.
+
+Update the interface and imports at the top of `ConcludeSessionModal.tsx`:
+
+```tsx
+import { useState, useEffect, useRef } from 'react'
+import {
+ X,
+ CheckCircle2,
+ ArrowUpRight,
+ Pause,
+ Loader2,
+ Copy,
+ Check,
+ RefreshCw,
+ ClipboardList,
+ Sparkles,
+ AlertTriangle,
+} from 'lucide-react'
+import { cn } from '@/lib/utils'
+import { MarkdownContent } from '@/components/ui/MarkdownContent'
+import { aiSessionsApi } from '@/api/aiSessions'
+import { toast } from '@/lib/toast'
+
+type ConclusionOutcome = 'resolved' | 'escalated' | 'paused'
+
+interface ConcludeSessionModalProps {
+ isOpen: boolean
+ onClose: () => void
+ onConclude: (outcome: ConclusionOutcome, notes: string) => Promise
+ onResumeNew: (summary: string) => void
+ chatTitle: string
+ sessionId: string | null
+}
+```
+
+(Added `sessionId` prop, `useRef` import, `AlertTriangle` icon, `aiSessionsApi` import, `toast` import.)
+
+- [ ] **Step 2: Rewrite the component state and handleGenerate**
+
+Replace the state declarations and `handleGenerate` function (lines 66-106):
+
+```tsx
+export function ConcludeSessionModal({
+ isOpen,
+ onClose,
+ onConclude,
+ onResumeNew,
+ chatTitle,
+ sessionId,
+}: ConcludeSessionModalProps) {
+ const [step, setStep] = useState('select-outcome')
+ const [outcome, setOutcome] = useState(null)
+ const [notes, setNotes] = useState('')
+ const [summary, setSummary] = useState('')
+ const [generating, setGenerating] = useState(false)
+ const [streaming, setStreaming] = useState(false)
+ const [streamError, setStreamError] = useState(null)
+ const [copied, setCopied] = useState(false)
+ const [error, setError] = useState(null)
+ const summaryRef = useRef('')
+
+ // Reset state when modal opens
+ useEffect(() => {
+ if (isOpen) {
+ setStep('select-outcome')
+ setOutcome(null)
+ setNotes('')
+ setSummary('')
+ summaryRef.current = ''
+ setGenerating(false)
+ setStreaming(false)
+ setStreamError(null)
+ setCopied(false)
+ setError(null)
+ }
+ }, [isOpen])
+
+ const handleOutcomeSelect = (selected: ConclusionOutcome) => {
+ setOutcome(selected)
+ setStep('add-notes')
+ }
+
+ const handleGenerate = async () => {
+ if (!outcome) return
+ setGenerating(true)
+ setError(null)
+
+ try {
+ // Phase 1: Resolve/escalate/pause the session (fast)
+ await onConclude(outcome, notes)
+
+ // Phase 2: Transition to summary step immediately
+ setStep('summary')
+ setGenerating(false)
+
+ // For resolved sessions, stream ticket notes
+ if (outcome === 'resolved' && sessionId) {
+ setStreaming(true)
+ setStreamError(null)
+ summaryRef.current = ''
+
+ aiSessionsApi.streamDocumentation(
+ sessionId,
+ (chunk) => {
+ summaryRef.current += chunk
+ setSummary(summaryRef.current)
+ },
+ () => {
+ setStreaming(false)
+ },
+ (err) => {
+ setStreaming(false)
+ setStreamError(err)
+ // Try non-streaming fallback
+ aiSessionsApi.getDocumentation(sessionId).then((doc) => {
+ const fallback = `## Problem Summary\n${doc.problem_summary}\n\n## Steps Taken\n${doc.diagnostic_steps.map(s => `- ${s.description}`).join('\n')}\n\n## Resolution\n${doc.resolution_summary || 'See conversation'}\n\n## Next Steps\nNone`
+ setSummary(fallback)
+ setStreamError(null)
+ }).catch(() => {
+ // Final fallback — just show what we have
+ if (!summaryRef.current) {
+ setSummary('Documentation generation failed. You can copy the conversation from the chat.')
+ }
+ })
+ },
+ )
+ } else if (outcome === 'escalated') {
+ setSummary('Session escalated. Ticket notes will be generated when the session is resolved.')
+ } else {
+ setSummary('Session paused. Progress saved — you can resume anytime.')
+ }
+ } catch {
+ setError('Failed to conclude session. Please try again.')
+ setGenerating(false)
+ }
+ }
+```
+
+- [ ] **Step 3: Update the summary step rendering**
+
+Replace the summary step content (lines 298-325) with:
+
+```tsx
+ {/* Step 3: Summary */}
+ {step === 'summary' && (
+
+ {/* Outcome badge */}
+ {selectedOutcome && (
+
+
+ {selectedOutcome.label}
+
+ )}
+
+ {/* Generated ticket notes */}
+
+
+
+
+ Ticket Notes
+
+ {streaming && (
+
+ )}
+
+
+ {/* Streaming content or skeleton */}
+ {summary ? (
+
+
+
+ ) : streaming ? (
+
+ ) : streamError ? (
+
+ ) : null}
+
+
+ )}
+```
+
+- [ ] **Step 4: Update the footer for the summary step**
+
+Replace the summary step footer (lines 373-416) with:
+
+```tsx
+ {step === 'summary' && (
+ <>
+
+ {outcome === 'paused' && (
+
+
+ Resume in New Chat
+
+ )}
+
+
+ {summary && !streaming && (
+
+ {copied ? (
+ <>
+
+ Copied!
+ >
+ ) : (
+ <>
+
+ Copy to Clipboard
+ >
+ )}
+
+ )}
+
+ Done
+
+
+ >
+ )}
+```
+
+- [ ] **Step 5: Update handleConclude in AssistantChatPage**
+
+In `frontend/src/pages/AssistantChatPage.tsx`, update `handleConclude` (lines 396-414) to return immediately without waiting for documentation:
+
+```tsx
+ const handleConclude = async (outcome: ConclusionOutcome, _notes: string): Promise => {
+ if (!activeChatId) throw new Error('No active chat')
+
+ if (outcome === 'resolved') {
+ await aiSessionsApi.resolveSession(activeChatId, {
+ resolution_summary: _notes || 'Resolved via assistant chat',
+ })
+ return activeChatId
+ } else if (outcome === 'escalated') {
+ await aiSessionsApi.escalateSession(activeChatId, {
+ escalation_reason: _notes || 'Escalated from assistant chat',
+ })
+ return activeChatId
+ } else {
+ await aiSessionsApi.pauseSession(activeChatId)
+ return activeChatId
+ }
+ }
+```
+
+- [ ] **Step 6: Pass sessionId to ConcludeSessionModal**
+
+Find where `ConcludeSessionModal` is rendered (around line 812-818) and add the `sessionId` prop:
+
+```tsx
+ setShowConclude(false)}
+ onConclude={handleConclude}
+ onResumeNew={handleResumeNew}
+ chatTitle={chats.find(c => c.id === activeChatId)?.title ?? 'Chat'}
+ sessionId={activeChatId}
+ />
+```
+
+- [ ] **Step 7: Verify the full flow**
+
+1. Open assistant chat, have a conversation
+2. Click Conclude → Resolved → add optional notes → Generate Summary
+3. Modal should transition to summary step IMMEDIATELY
+4. Skeleton loading should appear, then ticket notes stream in progressively
+5. When streaming completes, "Copy to Clipboard" button appears
+6. Click copy → toast "Ticket notes copied" → clipboard has the markdown
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add frontend/src/components/assistant/ConcludeSessionModal.tsx frontend/src/pages/AssistantChatPage.tsx
+git commit -m "feat: two-phase resolve with streaming ticket notes generation"
+```
+
+---
+
+### Task 11: Frontend Build Verification
+
+**Files:** None (verification only)
+
+- [ ] **Step 1: Run the frontend build**
+
+```bash
+export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && nvm use 20
+cd frontend && npm run build
+```
+
+Expected: Build succeeds with no TypeScript errors. If OOM occurs (VPS memory constraint), run `npx tsc --noEmit` instead as a lighter check.
+
+- [ ] **Step 2: Fix any TypeScript errors**
+
+If errors appear, fix unused imports, missing props, or type mismatches. Common issues:
+- `ConcludeSessionModal` now requires `sessionId` prop — verify all usages pass it
+- `SessionCloseResponse.documentation` is now optional — verify `.documentation?.` optional chaining everywhere it's accessed
+- Unused `X` import in TaskLane after replacing with `PanelRightClose`
+
+- [ ] **Step 3: Commit fixes if needed**
+
+```bash
+git add -A
+git commit -m "fix: resolve TypeScript errors from task lane and resolve docs changes"
+```
+
+---
+
+### Task 12: Remove Debug Logging from AssistantChatPage
+
+**Files:**
+- Modify: `frontend/src/pages/AssistantChatPage.tsx`
+
+- [ ] **Step 1: Remove the console.log statements added during debugging**
+
+Remove these debug lines:
+- `console.log('[AssistantChat] Mount restore check — ...')` (around line 105)
+- `console.log('[AssistantChat] Calling selectChat to restore:', ...)` (around line 107)
+- `console.log('[AssistantChat] selectChat called with:', ...)` (around line 212)
+- `console.log('[AssistantChat] getSession response — ...')` (around line 220)
+- `console.error('[AssistantChat] Failed to load chat session:', err)` (around line 243)
+
+Keep the `catch (err)` form (not bare `catch`) but remove the `console.error` line. Revert to bare `catch`:
+
+```tsx
+ } catch {
+ setMessages([])
+ }
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add frontend/src/pages/AssistantChatPage.tsx
+git commit -m "chore: remove debug logging from AssistantChatPage"
+```
+
+---
+
+### Task 13: Final Integration Test
+
+**Files:** None (manual testing)
+
+- [ ] **Step 1: Test task lane minimize/reopen flow**
+
+1. Start or continue a chat that has task lane items
+2. Click PanelRightClose — panel collapses, "Tasks (N)" pill appears in toolbar
+3. Click pill — panel reopens with previous state (including partial answers)
+4. Navigate away from assistant page and back — task lane restores from backend
+5. Close and reopen — state persists
+
+- [ ] **Step 2: Test resolve with streaming ticket notes**
+
+1. Have a multi-message conversation
+2. Click Conclude → Resolved → optional notes → Generate Summary
+3. Modal transitions instantly to summary step with skeleton
+4. Ticket notes stream in with four sections: Problem Summary, Steps Taken, Resolution, Next Steps
+5. Copy button appears after streaming completes
+6. Click Copy → toast confirms → paste into text editor to verify markdown
+
+- [ ] **Step 3: Test fallback behavior**
+
+1. If streaming fails (e.g., AI key missing), verify fallback to non-streaming doc generation
+2. If both fail, verify "Documentation generation failed" message appears
+
+- [ ] **Step 4: Push to PR**
+
+```bash
+git push
+```
diff --git a/docs/superpowers/specs/2026-03-28-tasklane-minimize-and-resolve-docs-design.md b/docs/superpowers/specs/2026-03-28-tasklane-minimize-and-resolve-docs-design.md
new file mode 100644
index 00000000..ac1e5b09
--- /dev/null
+++ b/docs/superpowers/specs/2026-03-28-tasklane-minimize-and-resolve-docs-design.md
@@ -0,0 +1,158 @@
+# Task Lane Minimize/Reopen + Resolve Documentation Streaming
+
+**Date:** 2026-03-28
+**Status:** Approved
+
+---
+
+## Problem
+
+Two UX issues in the Assistant Chat page:
+
+1. **Task Lane close destroys state.** The X button calls `clearTaskState()` and hides the panel. There's no way to bring it back without getting a new AI response with markers. Engineers lose in-progress task responses.
+
+2. **Conclude → Resolved hangs.** The resolve flow blocks on an LLM call to generate documentation. The modal shows "Generating..." for 2-5s+ with no progressive feedback. The generated output needs to be structured ticket notes engineers can copy into their PSA.
+
+---
+
+## Feature 1: Task Lane Minimize/Reopen
+
+### Close Button Change
+
+- Replace the X icon (`X` from Lucide) with `PanelRightClose` to signal "collapse" not "destroy."
+- On click: set `showTaskLane = false`. Do NOT call `clearTaskState(sessionId)`.
+- Task state (questions, actions, user responses) remains in sessionStorage and backend `pending_task_lane`.
+
+### Reopen Pill on Input Toolbar
+
+- New pill button in the chat input toolbar row, alongside Attach / Paste Logs / Conclude.
+- **Visibility condition:** `(activeQuestions.length > 0 || activeActions.length > 0) && !showTaskLane`
+- **Label:** `Tasks (N)` where N = `activeQuestions.length + activeActions.length`
+- **Icon:** `ListChecks` from Lucide
+- **Action:** `setShowTaskLane(true)`
+- **Style:** Same ghost button style as Attach/Paste Logs — muted text, hover highlight.
+
+### Files Changed
+
+| File | Change |
+|------|--------|
+| `frontend/src/components/assistant/TaskLane.tsx` | Replace `X` icon with `PanelRightClose` in header |
+| `frontend/src/pages/AssistantChatPage.tsx` | Remove `clearTaskState()` from onClose handler; add Tasks pill to input toolbar |
+
+---
+
+## Feature 2: Resolve with Streaming Documentation
+
+### Two-Phase Resolve Flow
+
+**Phase 1 — Instant resolve:**
+
+1. User clicks Resolve in `ConcludeSessionModal`, enters optional notes, confirms.
+2. Frontend calls `POST /ai-sessions/{id}/resolve` with `{ resolution_summary }`.
+3. Backend sets session status to `resolved`, saves summary, returns immediately. No LLM call on this path.
+4. Modal transitions to the summary step instantly, showing a skeleton loading state for "Ticket Notes."
+
+**Phase 2 — Streamed doc generation:**
+
+1. Immediately after phase 1, frontend opens an SSE connection to `GET /ai-sessions/{id}/documentation/stream`.
+2. Backend streams the structured ticket notes as they generate, token by token.
+3. Frontend renders chunks progressively into the summary step using `MarkdownContent`.
+4. When stream completes, show a "Copy to Clipboard" button.
+
+### Ticket Notes Format
+
+The LLM generates four structured sections:
+
+```markdown
+## Problem Summary
+[What the engineer reported / intake context]
+
+## Steps Taken
+[Key diagnostic steps and findings from conversation]
+
+## Resolution
+[What fixed it / final action taken]
+
+## Next Steps
+[Follow-up items, if any — or "None"]
+```
+
+### Fallback Behavior
+
+- If SSE stream fails or times out (30s): fall back to non-streaming `GET /ai-sessions/{id}/documentation`.
+- If that also fails: show "Documentation unavailable" with a "Copy Conversation" button that formats the raw `conversation_messages` into a basic structured summary (no LLM, pure template).
+
+### Copy to Clipboard
+
+- Prominent button below the rendered ticket notes.
+- Copies the full markdown text.
+- Toast confirmation: "Ticket notes copied."
+
+### AI Optimizations
+
+**1. Streaming (SSE endpoint):**
+- New endpoint: `GET /ai-sessions/{id}/documentation/stream`
+- Returns `text/event-stream` via FastAPI `StreamingResponse`.
+- Uses Anthropic's `client.messages.stream()` for token-level streaming.
+- Time-to-first-byte drops from ~2-5s to ~200ms.
+
+**2. Prompt caching:**
+- Apply `cache_control: {"type": "ephemeral"}` to the system prompt and conversation context prefix in the documentation generation call.
+- Pattern already exists in `assistant_chat_service.py` (`_call_anthropic_cached`).
+- Repeat calls for the same session (regenerate, different doc types) hit cache — up to 85% faster, 90% cheaper on input tokens.
+
+**3. Client reuse:**
+- Singleton `AsyncAnthropic` client instead of creating a new one per call.
+- Eliminates connection setup overhead.
+
+**Model tier:** Haiku (already configured as `quick_action` tier) — fastest model, appropriate for summarization.
+
+### Backend Changes
+
+| File | Change |
+|------|--------|
+| `backend/app/api/endpoints/ai_sessions.py` | Modify `resolve_session` to only set status + save summary (remove the blocking `get_session_documentation` call); add new SSE streaming endpoint for doc generation |
+| `backend/app/services/flowpilot_engine.py` | Add `stream_session_documentation()` generator function using `client.messages.stream()` |
+| `backend/app/core/ai_provider.py` | Add `generate_text_stream()` method returning async iterator; singleton client |
+| `backend/app/services/flowpilot_engine.py` | Add prompt caching to `_build_status_update_prompt` call path |
+
+### Frontend Changes
+
+| File | Change |
+|------|--------|
+| `frontend/src/components/assistant/ConcludeSessionModal.tsx` | Two-phase flow: instant resolve → streaming doc render with skeleton → copy button |
+| `frontend/src/api/aiSessions.ts` | Add `streamDocumentation(sessionId)` using fetch + ReadableStream |
+| `frontend/src/pages/AssistantChatPage.tsx` | Update `handleConclude` to not await documentation |
+
+### ConcludeSessionModal Summary Step Layout
+
+```
+┌─────────────────────────────────────┐
+│ Session Resolved │
+│ │
+│ ┌─ Ticket Notes ─────────────────┐ │
+│ │ ## Problem Summary │ │
+│ │ [streaming text...] │ │
+│ │ │ │
+│ │ ## Steps Taken │ │
+│ │ [streaming text...] │ │
+│ │ │ │
+│ │ ## Resolution │ │
+│ │ [streaming text...] │ │
+│ │ │ │
+│ │ ## Next Steps │ │
+│ │ [streaming text...] │ │
+│ └────────────────────────────────┘ │
+│ │
+│ [ Copy to Clipboard ] [ Done ] │
+└─────────────────────────────────────┘
+```
+
+---
+
+## Out of Scope
+
+- Client-facing update generation (future feature, different audience prompt)
+- Direct PSA posting (requires ConnectWise integration to be wired to sessions)
+- Task lane drag-to-reorder
+- Task lane persistence across browser tabs (sessionStorage is per-tab by design)
From c6772c6607fb384cfeee2d52639be846c0eec241 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 23:02:07 +0000
Subject: [PATCH 15/49] perf: singleton AsyncAnthropic client to avoid per-call
connection setup
Co-Authored-By: Claude Opus 4.6 (1M context)
---
backend/app/core/ai_provider.py | 26 +++++++++++++++++++-------
1 file changed, 19 insertions(+), 7 deletions(-)
diff --git a/backend/app/core/ai_provider.py b/backend/app/core/ai_provider.py
index 4e38cceb..7453ec9c 100644
--- a/backend/app/core/ai_provider.py
+++ b/backend/app/core/ai_provider.py
@@ -165,6 +165,24 @@ class GeminiProvider(AIProvider):
return text, input_tokens, output_tokens
+# Singleton client cache — avoids creating new HTTP connections per call
+_anthropic_clients: dict[str, "anthropic.AsyncAnthropic"] = {}
+
+
+def _get_anthropic_client(api_key: str, timeout: int = 45) -> "anthropic.AsyncAnthropic":
+ """Return a cached AsyncAnthropic client, creating one if needed."""
+ import anthropic
+
+ cache_key = f"{api_key[:8]}:{timeout}"
+ if cache_key not in _anthropic_clients:
+ _anthropic_clients[cache_key] = anthropic.AsyncAnthropic(
+ api_key=api_key,
+ timeout=timeout,
+ max_retries=1,
+ )
+ return _anthropic_clients[cache_key]
+
+
class AnthropicProvider(AIProvider):
"""Anthropic Claude provider using the anthropic SDK."""
@@ -179,13 +197,7 @@ class AnthropicProvider(AIProvider):
messages: list[dict[str, str]],
max_tokens: int = 4096,
) -> tuple[str, int, int]:
- import anthropic
-
- client = anthropic.AsyncAnthropic(
- api_key=self._api_key,
- timeout=self._timeout,
- max_retries=1,
- )
+ client = _get_anthropic_client(self._api_key, self._timeout)
response = await client.messages.create(
model=self._model,
From c47b8d26e555b04b5fc32bd1815c6dd1082da597 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 23:02:14 +0000
Subject: [PATCH 16/49] feat: replace task lane X with PanelRightClose icon
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/components/assistant/TaskLane.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/frontend/src/components/assistant/TaskLane.tsx b/frontend/src/components/assistant/TaskLane.tsx
index 791eba74..e361b66e 100644
--- a/frontend/src/components/assistant/TaskLane.tsx
+++ b/frontend/src/components/assistant/TaskLane.tsx
@@ -1,7 +1,7 @@
import { useState, useEffect, useRef, useCallback } from 'react'
import {
Copy, Check, SkipForward, Terminal, ChevronDown, ChevronUp,
- Send, Clipboard, Loader2, X, MessageCircleQuestion, Eye,
+ Send, Clipboard, Loader2, PanelRightClose, MessageCircleQuestion, Eye,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast'
@@ -247,8 +247,8 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
{allHandled ? '✓ Ready' : `${doneCount}/${totalCount}`}
-
-
+
+
From ca686c0901f8591e8ca1e0ea2a4e70f13745a4ed Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 23:02:20 +0000
Subject: [PATCH 17/49] feat: task lane close preserves state instead of
clearing
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/pages/AssistantChatPage.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx
index 30e20a2b..abb1b39d 100644
--- a/frontend/src/pages/AssistantChatPage.tsx
+++ b/frontend/src/pages/AssistantChatPage.tsx
@@ -801,7 +801,6 @@ export default function AssistantChatPage() {
onSubmit={handleTaskSubmit}
onClose={() => {
setShowTaskLane(false)
- if (activeChatId) clearTaskState(activeChatId)
}}
loading={loading}
/>
From 2f3781bfc22a89b8a967a3c1a31b41c634f4ace2 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 23:02:35 +0000
Subject: [PATCH 18/49] feat: add generate_text_stream to AnthropicProvider for
SSE support
Co-Authored-By: Claude Opus 4.6 (1M context)
---
backend/app/core/ai_provider.py | 38 +++++++++++++++++++++++++++++++++
1 file changed, 38 insertions(+)
diff --git a/backend/app/core/ai_provider.py b/backend/app/core/ai_provider.py
index 7453ec9c..84d4c33b 100644
--- a/backend/app/core/ai_provider.py
+++ b/backend/app/core/ai_provider.py
@@ -7,6 +7,7 @@ backends for JSON generation used by the AI Flow Builder.
import logging
from abc import ABC, abstractmethod
+from collections.abc import AsyncIterator
from app.core.config import settings
@@ -54,6 +55,26 @@ class AIProvider(ABC):
"""
...
+ async def generate_text_stream(
+ self,
+ system_prompt: str,
+ messages: list[dict[str, str]],
+ max_tokens: int = 4096,
+ ) -> "AsyncIterator[str]":
+ """Stream a text response token by token.
+
+ Args:
+ system_prompt: System-level instruction for the model.
+ messages: List of message dicts with "role" and "content" keys.
+ max_tokens: Maximum output tokens.
+
+ Yields:
+ Text chunks as they are generated.
+ """
+ raise NotImplementedError("Streaming not supported for this provider")
+ # Make this an async generator to satisfy type checker
+ yield "" # pragma: no cover
+
class GeminiProvider(AIProvider):
"""Google Gemini provider using the google-genai SDK."""
@@ -221,6 +242,23 @@ class AnthropicProvider(AIProvider):
# Anthropic doesn't differentiate between JSON and text mode
return await self.generate_json(system_prompt, messages, max_tokens)
+ async def generate_text_stream(
+ self,
+ system_prompt: str,
+ messages: list[dict[str, str]],
+ max_tokens: int = 4096,
+ ) -> AsyncIterator[str]:
+ client = _get_anthropic_client(self._api_key, self._timeout)
+
+ async with client.messages.stream(
+ model=self._model,
+ max_tokens=max_tokens,
+ system=system_prompt,
+ messages=messages,
+ ) as stream:
+ async for text in stream.text_stream:
+ yield text
+
def get_ai_provider(model: str | None = None) -> AIProvider:
"""Factory that returns the configured AI provider.
From bb4743568b8f743ca84ed21e87a11b13f66fd329 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 23:02:44 +0000
Subject: [PATCH 19/49] feat: add Tasks pill to reopen collapsed task lane
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/pages/AssistantChatPage.tsx | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx
index abb1b39d..759f3627 100644
--- a/frontend/src/pages/AssistantChatPage.tsx
+++ b/frontend/src/pages/AssistantChatPage.tsx
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef, useCallback } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
-import { Sparkles, Send, Loader2, Flag, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus } from 'lucide-react'
+import { Sparkles, Send, Loader2, Flag, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus, ListChecks } from 'lucide-react'
import { cn } from '@/lib/utils'
import { uploadsApi } from '@/api/uploads'
import type { PendingUpload } from '@/types/upload'
@@ -12,7 +12,7 @@ import { analytics } from '@/lib/analytics'
import { toast } from '@/lib/toast'
import { ChatSidebar, ChatSidebarCollapsedBar } from '@/components/assistant/ChatSidebar'
import { ChatMessage } from '@/components/assistant/ChatMessage'
-import { TaskLane, clearTaskState } from '@/components/assistant/TaskLane'
+import { TaskLane } from '@/components/assistant/TaskLane'
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
import type { ChatListItem, ConclusionOutcome } from '@/types/assistant-chat'
import type { SuggestedFlow } from '@/types/copilot'
@@ -758,6 +758,17 @@ export default function AssistantChatPage() {
Conclude
)}
+ {!showTaskLane && (activeQuestions.length > 0 || activeActions.length > 0) && (
+ setShowTaskLane(true)}
+ className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-accent-text hover:text-foreground hover:bg-accent-dim transition-colors"
+ title="Show task panel"
+ >
+
+ Tasks ({activeQuestions.length + activeActions.length})
+
+ )}
Date: Sat, 28 Mar 2026 23:04:07 +0000
Subject: [PATCH 20/49] feat: add stream_ticket_notes generator for SSE doc
streaming
Co-Authored-By: Claude Opus 4.6 (1M context)
---
backend/app/services/flowpilot_engine.py | 99 ++++++++++++++++++++++++
1 file changed, 99 insertions(+)
diff --git a/backend/app/services/flowpilot_engine.py b/backend/app/services/flowpilot_engine.py
index 2f6f40ff..7ed8b7e1 100644
--- a/backend/app/services/flowpilot_engine.py
+++ b/backend/app/services/flowpilot_engine.py
@@ -8,6 +8,7 @@ import json
import logging
import uuid
from datetime import datetime, timezone
+from collections.abc import AsyncIterator
from typing import Any, Optional
from uuid import UUID
@@ -1011,6 +1012,104 @@ async def generate_status_update(
)
+async def stream_ticket_notes(
+ session_id: UUID,
+ user_id: UUID,
+ db: AsyncSession,
+) -> AsyncIterator[str]:
+ """Stream AI-generated structured ticket notes for a resolved session.
+
+ Yields text chunks suitable for SSE streaming.
+ """
+ session = await _load_session(session_id, user_id, db)
+
+ # Build conversation summary from messages (chat sessions)
+ # or steps (guided sessions)
+ messages = session.conversation_messages or []
+ if messages:
+ recent = messages[-20:] # Last 20 messages for richer context
+ convo_text = "\n".join(
+ f"{'Engineer' if m['role'] == 'user' else 'AI Assistant'}: {m['content'][:500]}"
+ for m in recent
+ if isinstance(m, dict) and "role" in m and "content" in m
+ )
+ else:
+ # Fall back to steps for guided sessions
+ steps_summary = []
+ for step in sorted(session.steps, key=lambda s: s.step_order):
+ content = step.content or {}
+ text = content.get("text", "")
+ response = step.free_text_input or step.selected_option or ("Skipped" if step.was_skipped else None)
+ entry = f"Step {step.step_order + 1}: {text}"
+ if response:
+ entry += f"\n Engineer response: {response}"
+ steps_summary.append(entry)
+ convo_text = "\n".join(steps_summary) if steps_summary else "No session data."
+
+ # Calculate time spent
+ now = datetime.now(timezone.utc)
+ ref_time = session.resolved_at or now
+ delta = ref_time - session.created_at
+ total_minutes = int(delta.total_seconds() / 60)
+ time_display = f"{total_minutes} minutes" if total_minutes < 60 else f"{total_minutes // 60}h {total_minutes % 60}m"
+
+ system_prompt = """You are generating internal ticket notes for an MSP engineer's PSA system.
+
+Generate EXACTLY these four markdown sections, in this order:
+
+## Problem Summary
+Summarize what the engineer reported and the initial symptoms. 1-3 sentences.
+
+## Steps Taken
+List the key diagnostic steps, commands run, checks performed, and findings. Use bullet points.
+
+## Resolution
+What fixed the issue or what the final action was. Be specific and technical.
+
+## Next Steps
+Any follow-up items, monitoring to watch, or preventive measures. Write "None" if not applicable.
+
+Rules:
+- Be technical, concise, and factual
+- Use markdown formatting (headers, bullet lists, bold for emphasis)
+- Include specific technical details (commands, settings, error messages) where available
+- Do NOT include greetings, sign-offs, or pleasantries
+- Do NOT wrap output in code fences
+- Output ONLY the four sections above, nothing else"""
+
+ user_message_parts = [
+ f"Session status: {session.status}",
+ f"Time spent: {time_display}",
+ f"Problem summary: {session.problem_summary or 'Not specified'}",
+ ]
+ if session.problem_domain:
+ user_message_parts.append(f"Problem domain: {session.problem_domain}")
+ if session.resolution_summary:
+ user_message_parts.append(f"Resolution notes: {session.resolution_summary}")
+ user_message_parts.append(f"\nSession conversation:\n{convo_text}")
+
+ user_message = "\n".join(user_message_parts)
+
+ provider = get_ai_provider(settings.get_model_for_action("quick_action"))
+
+ # Use streaming if provider supports it (Anthropic), otherwise fall back
+ try:
+ async for chunk in provider.generate_text_stream(
+ system_prompt=system_prompt,
+ messages=[{"role": "user", "content": user_message}],
+ max_tokens=1500,
+ ):
+ yield chunk
+ except NotImplementedError:
+ # Fallback for non-streaming providers (Gemini)
+ text, _, _ = await provider.generate_text(
+ system_prompt=system_prompt,
+ messages=[{"role": "user", "content": user_message}],
+ max_tokens=1500,
+ )
+ yield text
+
+
def _build_status_update_prompt(
audience: str,
length: str,
From d456b1156e102e1e95b1027d4931ea5a8ac3fe75 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 23:04:14 +0000
Subject: [PATCH 21/49] feat: add SSE endpoint for streaming ticket notes
generation
Co-Authored-By: Claude Opus 4.6 (1M context)
---
backend/app/api/endpoints/ai_sessions.py | 46 ++++++++++++++++++++++++
1 file changed, 46 insertions(+)
diff --git a/backend/app/api/endpoints/ai_sessions.py b/backend/app/api/endpoints/ai_sessions.py
index 537737e6..9e6fc943 100644
--- a/backend/app/api/endpoints/ai_sessions.py
+++ b/backend/app/api/endpoints/ai_sessions.py
@@ -924,6 +924,52 @@ async def get_documentation(
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e))
+@router.get("/{session_id}/documentation/stream")
+@limiter.limit("20/minute")
+async def stream_documentation(
+ request: Request,
+ session_id: UUID,
+ current_user: Annotated[User, Depends(get_current_active_user)],
+ db: Annotated[AsyncSession, Depends(get_db)],
+):
+ """Stream AI-generated ticket notes as Server-Sent Events."""
+ from starlette.responses import StreamingResponse
+
+ # Verify session ownership
+ result = await db.execute(
+ select(AISession).where(AISession.id == session_id)
+ )
+ session = result.scalar_one_or_none()
+ if not session:
+ raise HTTPException(status_code=404, detail="Session not found")
+ if session.user_id != current_user.id:
+ raise HTTPException(status_code=403, detail="Not authorized")
+
+ async def event_generator():
+ try:
+ async for chunk in flowpilot_engine.stream_ticket_notes(
+ session_id=session_id,
+ user_id=current_user.id,
+ db=db,
+ ):
+ # SSE format: data: \n\n
+ yield f"data: {chunk}\n\n"
+ yield "data: [DONE]\n\n"
+ except Exception as e:
+ logger.exception("SSE stream error for session %s: %s", session_id, e)
+ yield f"data: [ERROR] {str(e)}\n\n"
+
+ return StreamingResponse(
+ event_generator(),
+ media_type="text/event-stream",
+ headers={
+ "Cache-Control": "no-cache",
+ "Connection": "keep-alive",
+ "X-Accel-Buffering": "no", # Disable nginx buffering
+ },
+ )
+
+
# ── Status Update ──
@router.post("/{session_id}/status-update", response_model=StatusUpdateResponse)
From 7a5d56494b0cdb0d7a8f41af309bbb49b098771a Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 23:04:38 +0000
Subject: [PATCH 22/49] feat: make resolve endpoint non-blocking, documentation
optional
Co-Authored-By: Claude Opus 4.6 (1M context)
---
backend/app/api/endpoints/ai_sessions.py | 25 ++++++++++++++----------
backend/app/schemas/ai_session.py | 2 +-
frontend/src/types/ai-session.ts | 2 +-
3 files changed, 17 insertions(+), 12 deletions(-)
diff --git a/backend/app/api/endpoints/ai_sessions.py b/backend/app/api/endpoints/ai_sessions.py
index 9e6fc943..38ca0286 100644
--- a/backend/app/api/endpoints/ai_sessions.py
+++ b/backend/app/api/endpoints/ai_sessions.py
@@ -420,7 +420,7 @@ async def resolve_session(
db: Annotated[AsyncSession, Depends(get_db)],
_: None = Depends(require_engineer_or_admin),
):
- """Resolve a FlowPilot session and generate documentation."""
+ """Resolve a session. Returns immediately; use /documentation/stream for ticket notes."""
try:
result = await flowpilot_engine.resolve_session(
session_id=session_id,
@@ -433,16 +433,21 @@ async def resolve_session(
except PermissionError as e:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e))
- # Generate resolution outputs (branching feature)
- try:
- from app.services.resolution_output_generator import ResolutionOutputGenerator
- gen = ResolutionOutputGenerator(db)
- await gen.generate_all(session_id)
- except Exception:
- logger.exception(f"Failed to generate resolution outputs for session {session_id}")
- # Non-blocking — resolve still succeeds even if output generation fails
-
await db.commit()
+
+ # Fire-and-forget: resolution outputs (don't block the response)
+ import asyncio
+
+ async def _post_resolve_tasks():
+ try:
+ from app.services.resolution_output_generator import ResolutionOutputGenerator
+ gen = ResolutionOutputGenerator(db)
+ await gen.generate_all(session_id)
+ except Exception:
+ logger.exception(f"Failed to generate resolution outputs for session {session_id}")
+
+ asyncio.create_task(_post_resolve_tasks())
+
return result
diff --git a/backend/app/schemas/ai_session.py b/backend/app/schemas/ai_session.py
index fd37fde6..b66afbbf 100644
--- a/backend/app/schemas/ai_session.py
+++ b/backend/app/schemas/ai_session.py
@@ -136,7 +136,7 @@ class SessionCloseResponse(BaseModel):
"""Response after resolving or escalating."""
session_id: UUID
status: str
- documentation: SessionDocumentation
+ documentation: SessionDocumentation | None = None
psa_push_status: str = "no_psa" # sent | pending_retry | no_psa | failed
psa_push_error: str | None = None
member_mapping_warning: str | None = None
diff --git a/frontend/src/types/ai-session.ts b/frontend/src/types/ai-session.ts
index e1abab7f..751d4345 100644
--- a/frontend/src/types/ai-session.ts
+++ b/frontend/src/types/ai-session.ts
@@ -125,7 +125,7 @@ export interface SessionDocumentation {
export interface SessionCloseResponse {
session_id: string
status: string
- documentation: SessionDocumentation
+ documentation: SessionDocumentation | null
psa_push_status: string // "sent" | "pending_retry" | "no_psa" | "failed"
psa_push_error: string | null
member_mapping_warning: string | null
From bcab8158ab6cb0dbd77ea5891688ea736941c109 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 23:05:20 +0000
Subject: [PATCH 23/49] feat: add streamDocumentation SSE client for ticket
notes
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/api/aiSessions.ts | 63 ++++++++++++++++++++++++++++++++++
1 file changed, 63 insertions(+)
diff --git a/frontend/src/api/aiSessions.ts b/frontend/src/api/aiSessions.ts
index 08aae5bc..2877bbb7 100644
--- a/frontend/src/api/aiSessions.ts
+++ b/frontend/src/api/aiSessions.ts
@@ -99,6 +99,69 @@ export const aiSessionsApi = {
return response.data
},
+ async streamDocumentation(
+ sessionId: string,
+ onChunk: (text: string) => void,
+ onDone: () => void,
+ onError: (error: string) => void,
+ ): Promise {
+ const token = localStorage.getItem('access_token')
+ const baseUrl = import.meta.env.VITE_API_URL || ''
+
+ try {
+ const response = await fetch(
+ `${baseUrl}/api/ai-sessions/${sessionId}/documentation/stream`,
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ )
+
+ if (!response.ok) {
+ onError(`HTTP ${response.status}`)
+ return
+ }
+
+ const reader = response.body?.getReader()
+ if (!reader) {
+ onError('No response body')
+ return
+ }
+
+ const decoder = new TextDecoder()
+ let buffer = ''
+
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) break
+
+ buffer += decoder.decode(value, { stream: true })
+ const lines = buffer.split('\n')
+ buffer = lines.pop() || ''
+
+ for (const line of lines) {
+ if (line.startsWith('data: ')) {
+ const data = line.slice(6)
+ if (data === '[DONE]') {
+ onDone()
+ return
+ }
+ if (data.startsWith('[ERROR]')) {
+ onError(data.slice(8))
+ return
+ }
+ onChunk(data)
+ }
+ }
+ }
+ // Stream ended without [DONE]
+ onDone()
+ } catch (err) {
+ onError(err instanceof Error ? err.message : 'Stream failed')
+ }
+ },
+
async rateSession(sessionId: string, data: { rating: number; feedback?: string }): Promise {
await apiClient.post(`/ai-sessions/${sessionId}/rate`, data)
},
From 5e04aad16f461acde3b8cf437c6dac907bb1186c Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 23:08:25 +0000
Subject: [PATCH 24/49] feat: two-phase resolve with streaming ticket notes
generation
ConcludeSessionModal now resolves instantly (Phase 1) then streams
ticket notes via SSE (Phase 2), with skeleton loading and fallback.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../assistant/ConcludeSessionModal.tsx | 133 ++++++++++++++----
frontend/src/pages/AssistantChatPage.tsx | 13 +-
2 files changed, 108 insertions(+), 38 deletions(-)
diff --git a/frontend/src/components/assistant/ConcludeSessionModal.tsx b/frontend/src/components/assistant/ConcludeSessionModal.tsx
index f874cab3..c32e71cd 100644
--- a/frontend/src/components/assistant/ConcludeSessionModal.tsx
+++ b/frontend/src/components/assistant/ConcludeSessionModal.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react'
+import { useState, useEffect, useRef } from 'react'
import {
X,
CheckCircle2,
@@ -10,9 +10,11 @@ import {
RefreshCw,
ClipboardList,
Sparkles,
+ AlertTriangle,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { MarkdownContent } from '@/components/ui/MarkdownContent'
+import { aiSessionsApi } from '@/api/aiSessions'
type ConclusionOutcome = 'resolved' | 'escalated' | 'paused'
@@ -22,6 +24,7 @@ interface ConcludeSessionModalProps {
onConclude: (outcome: ConclusionOutcome, notes: string) => Promise
onResumeNew: (summary: string) => void
chatTitle: string
+ sessionId: string | null
}
const OUTCOMES: { value: ConclusionOutcome; label: string; description: string; icon: typeof CheckCircle2; color: string; bg: string; border: string }[] = [
@@ -62,6 +65,7 @@ export function ConcludeSessionModal({
onConclude,
onResumeNew,
chatTitle,
+ sessionId,
}: ConcludeSessionModalProps) {
const [step, setStep] = useState('select-outcome')
const [outcome, setOutcome] = useState(null)
@@ -70,6 +74,9 @@ export function ConcludeSessionModal({
const [generating, setGenerating] = useState(false)
const [copied, setCopied] = useState(false)
const [error, setError] = useState(null)
+ const [streaming, setStreaming] = useState(false)
+ const [streamError, setStreamError] = useState(null)
+ const summaryRef = useRef('')
// Reset state when modal opens
useEffect(() => {
@@ -81,6 +88,9 @@ export function ConcludeSessionModal({
setGenerating(false)
setCopied(false)
setError(null)
+ setStreaming(false)
+ setStreamError(null)
+ summaryRef.current = ''
}
}, [isOpen])
@@ -95,12 +105,50 @@ export function ConcludeSessionModal({
setError(null)
try {
- const result = await onConclude(outcome, notes)
- setSummary(result)
+ // Phase 1: Resolve/escalate/pause the session (fast)
+ await onConclude(outcome, notes)
+
+ // Phase 2: Transition to summary step immediately
setStep('summary')
+ setGenerating(false)
+
+ // For resolved sessions, stream ticket notes
+ if (outcome === 'resolved' && sessionId) {
+ setStreaming(true)
+ setStreamError(null)
+ summaryRef.current = ''
+
+ aiSessionsApi.streamDocumentation(
+ sessionId,
+ (chunk) => {
+ summaryRef.current += chunk
+ setSummary(summaryRef.current)
+ },
+ () => {
+ setStreaming(false)
+ },
+ (err) => {
+ setStreaming(false)
+ setStreamError(err)
+ // Try non-streaming fallback
+ aiSessionsApi.getDocumentation(sessionId).then((doc) => {
+ const fallback = `## Problem Summary\n${doc.problem_summary}\n\n## Steps Taken\n${doc.diagnostic_steps.map(s => `- ${s.description}`).join('\n')}\n\n## Resolution\n${doc.resolution_summary || 'See conversation'}\n\n## Next Steps\nNone`
+ setSummary(fallback)
+ setStreamError(null)
+ }).catch(() => {
+ if (!summaryRef.current) {
+ setSummary('Documentation generation failed. You can copy the conversation from the chat.')
+ }
+ })
+ },
+ )
+ } else if (outcome === 'escalated') {
+ setSummary('Session escalated. Ticket notes will be generated when the session is resolved.')
+ } else {
+ setSummary('Session paused. Progress saved — you can resume anytime.')
+ }
} catch {
- setError('Failed to generate summary. Please try again.')
- } finally {
+ setError('Failed to conclude session. Please try again.')
setGenerating(false)
}
}
@@ -306,7 +354,7 @@ export function ConcludeSessionModal({
)}
- {/* Generated summary */}
+ {/* Generated ticket notes */}
- Generated Ticket Notes
+ Ticket Notes
+ {streaming && (
+
+ )}
-
-
-
+
+ {/* Streaming content or skeleton */}
+ {summary ? (
+
+
+
+ ) : streaming ? (
+
+ ) : streamError ? (
+
+ ) : null}
)}
@@ -384,27 +453,29 @@ export function ConcludeSessionModal({
)}
-
- {copied ? (
- <>
-
- Copied!
- >
- ) : (
- <>
-
- Copy to Clipboard
- >
- )}
-
+ {summary && !streaming && (
+
+ {copied ? (
+ <>
+
+ Copied!
+ >
+ ) : (
+ <>
+
+ Copy to Clipboard
+ >
+ )}
+
+ )}
=> {
if (!activeChatId) throw new Error('No active chat')
- // Map conclusion outcomes to ai_sessions actions
if (outcome === 'resolved') {
- const result = await aiSessionsApi.resolveSession(activeChatId, {
+ await aiSessionsApi.resolveSession(activeChatId, {
resolution_summary: _notes || 'Resolved via assistant chat',
})
- return result.documentation?.problem_summary || 'Session resolved'
+ return activeChatId
} else if (outcome === 'escalated') {
- const result = await aiSessionsApi.escalateSession(activeChatId, {
+ await aiSessionsApi.escalateSession(activeChatId, {
escalation_reason: _notes || 'Escalated from assistant chat',
})
- return result.documentation?.problem_summary || 'Session escalated'
+ return activeChatId
} else {
- // paused
await aiSessionsApi.pauseSession(activeChatId)
- return 'Session paused'
+ return activeChatId
}
}
@@ -830,6 +828,7 @@ export default function AssistantChatPage() {
onConclude={handleConclude}
onResumeNew={handleResumeNew}
chatTitle={chats.find(c => c.id === activeChatId)?.title ?? 'Chat'}
+ sessionId={activeChatId}
/>
>
From 8c7a863f5ff8563b9aff3b0b6ef72aa12f044056 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 23:09:26 +0000
Subject: [PATCH 25/49] chore: remove debug logging from AssistantChatPage
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/pages/AssistantChatPage.tsx | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx
index d280c0cf..b7afcb7f 100644
--- a/frontend/src/pages/AssistantChatPage.tsx
+++ b/frontend/src/pages/AssistantChatPage.tsx
@@ -102,9 +102,7 @@ export default function AssistantChatPage() {
// Restore session from sessionStorage on mount (when URL has no session ID)
useEffect(() => {
- console.log('[AssistantChat] Mount restore check — urlSessionId:', urlSessionId, 'activeChatId:', activeChatId)
if (!urlSessionId && activeChatId) {
- console.log('[AssistantChat] Calling selectChat to restore:', activeChatId)
selectChat(activeChatId)
}
}, []) // eslint-disable-line react-hooks/exhaustive-deps
@@ -209,7 +207,6 @@ export default function AssistantChatPage() {
}
const selectChat = useCallback(async (chatId: string) => {
- console.log('[AssistantChat] selectChat called with:', chatId)
setActiveChatId(chatId)
// Clear TaskLane when switching chats — will restore from backend if available
setShowTaskLane(false)
@@ -217,7 +214,6 @@ export default function AssistantChatPage() {
setActiveActions([])
try {
const detail = await aiSessionsApi.getSession(chatId)
- console.log('[AssistantChat] getSession response — messages:', detail.conversation_messages?.length, 'pending_task_lane:', !!detail.pending_task_lane)
setMessages(
(detail.conversation_messages || []).map(m => ({
role: m.role as 'user' | 'assistant',
@@ -241,8 +237,7 @@ export default function AssistantChatPage() {
}
}
}
- } catch (err) {
- console.error('[AssistantChat] Failed to load chat session:', err)
+ } catch {
setMessages([])
}
}, [])
From 564d88e90f93ad37ef56df773f0988ee1f0a95ad Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sat, 28 Mar 2026 23:25:08 +0000
Subject: [PATCH 26/49] fix: update useFlowPilotSession return types for
optional documentation
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
SessionCloseResponse.documentation is now nullable — update resolve/escalate
return types to match.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/hooks/useFlowPilotSession.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/frontend/src/hooks/useFlowPilotSession.ts b/frontend/src/hooks/useFlowPilotSession.ts
index 6034cbb0..873b3846 100644
--- a/frontend/src/hooks/useFlowPilotSession.ts
+++ b/frontend/src/hooks/useFlowPilotSession.ts
@@ -27,8 +27,8 @@ export interface UseFlowPilotSession {
// Actions
startSession: (intake: AISessionCreateRequest) => Promise
respondToStep: (response: StepResponseRequest) => Promise
- resolveSession: (data: ResolveSessionRequest) => Promise
- escalateSession: (data: EscalateSessionRequest) => Promise
+ resolveSession: (data: ResolveSessionRequest) => Promise
+ escalateSession: (data: EscalateSessionRequest) => Promise
pauseSession: () => Promise
resumeOwnSession: () => Promise
abandonSession: () => Promise
@@ -134,7 +134,7 @@ export function useFlowPilotSession(): UseFlowPilotSession {
}
}, [session])
- const resolveSession = useCallback(async (data: ResolveSessionRequest): Promise => {
+ const resolveSession = useCallback(async (data: ResolveSessionRequest): Promise => {
if (!session) throw new Error('No active session')
setIsProcessing(true)
try {
@@ -156,7 +156,7 @@ export function useFlowPilotSession(): UseFlowPilotSession {
}
}, [session])
- const escalateSession = useCallback(async (data: EscalateSessionRequest): Promise => {
+ const escalateSession = useCallback(async (data: EscalateSessionRequest): Promise => {
if (!session) throw new Error('No active session')
setIsProcessing(true)
try {
From c3a53f2f30f65f1cd6799c3856aba892b3bfc3d0 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 05:35:08 +0000
Subject: [PATCH 27/49] docs: add parameterize-and-save design spec
Design for fixing AI-generated scripts saving to library without
parameters, adding parameter detection/review to the save flow,
and a "New from Script" paste entry point on the library page.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
...2026-03-29-parameterize-and-save-design.md | 165 ++++++++++++++++++
1 file changed, 165 insertions(+)
create mode 100644 docs/superpowers/specs/2026-03-29-parameterize-and-save-design.md
diff --git a/docs/superpowers/specs/2026-03-29-parameterize-and-save-design.md b/docs/superpowers/specs/2026-03-29-parameterize-and-save-design.md
new file mode 100644
index 00000000..a78b1867
--- /dev/null
+++ b/docs/superpowers/specs/2026-03-29-parameterize-and-save-design.md
@@ -0,0 +1,165 @@
+# Parameterize & Save — Script Library Integration
+
+> **Date:** 2026-03-29
+> **Status:** Approved
+> **Scope:** Fix AI-generated scripts saving to library without parameters; add parameter detection/review to save flow; add "New from Script" paste entry point
+
+---
+
+## Problem
+
+When the AI Script Builder generates a script and the user saves it to the Script Library:
+
+1. **`parameters_schema` is hardcoded to `{"parameters": []}`** in `save_to_library()` — no parameter detection runs
+2. **The script body uses raw PowerShell `param()` syntax**, not the `{{ key }}` template placeholders that `ScriptTemplateEngine.render()` expects
+
+The result: saved templates have no parameters and can't be rendered with user-provided values. The template engine has nothing to substitute.
+
+The frontend already has working parameter detection (`scriptParameterDetector.ts`) and a review UI (`ParameterDetectorStepper`), but they're only wired into `ScriptTemplateEditor` — not the save-to-library flow.
+
+## Solution
+
+A shared `ParameterizeAndSavePanel` component that:
+
+1. Shows the script with live preview of `{{ }}` replacements
+2. Auto-runs parameter detection and lets the user review/accept/skip each candidate
+3. Collects minimal metadata (name, description, category, share toggle)
+4. Sends the rewritten script body + built parameters schema to the backend
+
+Used in two entry points:
+- **AI Script Builder** — replaces the current `SaveToLibraryDialog`
+- **Script Library page** — "New from Script" button for pasting raw scripts
+
+> **Note:** Parameter detection currently supports PowerShell only. Bash and Python scripts will show "No parameters detected" and save as-is. Detection for those languages is planned for a future iteration.
+
+---
+
+## Design
+
+### `ParameterizeAndSavePanel` Component
+
+A slide-in panel from the right, ~480px wide, semi-transparent scrim behind it. Close via X button or scrim click.
+
+**Layout (top to bottom):**
+
+1. **Script Preview** — read-only code block showing the script body. As parameters are accepted via the stepper, the preview updates live to show `{{ key }}` replacements highlighted in amber.
+
+2. **Parameter Detection Zone** — auto-runs `detectParameterCandidates()` when the panel opens.
+ - If candidates found: renders the existing `ParameterDetectorStepper` inline
+ - If no candidates found: shows "No parameters detected — script will be saved as-is"
+
+3. **Metadata Fields** — name (required), description, category dropdown, share-with-team toggle. Same fields as the current `SaveToLibraryDialog`.
+
+4. **Save Button** — sends rewritten script body + parameters schema + metadata to the backend.
+
+**Two modes controlled by props:**
+
+- **`script` mode** (from AI builder): script body + language provided, skips straight to preview + detection
+- **`paste` mode** (from library page): shows a textarea + language picker at the top of the panel, above the preview area. Once pasted and confirmed, textarea collapses into the read-only preview and detection runs.
+
+**State:** Local React state (not Zustand). Tracks:
+- `workingScript`: the in-progress script body, mutated as candidates are accepted
+- `parametersSchema`: accumulated `ScriptParameter[]` array
+- `metadata`: name, description, categoryId, shareWithTeam
+- `mode`: paste vs script (derived from props)
+
+### Entry Point 1: AI Script Builder
+
+`ScriptBuilderPage` currently opens `SaveToLibraryDialog`. Replace with `ParameterizeAndSavePanel` in `script` mode.
+
+**Props:**
+- `scriptBody`: `session.latest_script`
+- `language`: `session.language`
+- `defaultName`: suggested filename minus extension
+- `onSave`: calls `scriptBuilderApi.saveToLibrary()` with enriched payload
+- `onClose`: closes the panel
+
+### Entry Point 2: Script Library Page
+
+Add a "New from Script" button on `ScriptLibraryPage` (near the "Manage Templates" link). Opens `ParameterizeAndSavePanel` in `paste` mode.
+
+**Props:**
+- `scriptBody`: `undefined` (triggers paste mode)
+- `language`: `undefined` (user picks in the panel)
+- `onSave`: calls `scriptsApi.createTemplate()` directly
+- `onClose`: closes the panel
+
+### Backend Changes
+
+**`SaveToLibraryRequest` schema** (`backend/app/schemas/script_builder.py`):
+
+Add two optional fields:
+```python
+script_body: str | None = None # Rewritten script with {{ }} placeholders
+parameters_schema: dict | None = None # Built parameter schema from frontend
+```
+
+**`save_to_library()` service** (`backend/app/services/script_builder_service.py`):
+
+Add `script_body: str | None = None` and `parameters_schema: dict | None = None` to the function signature. Use provided values instead of hardcoding:
+```python
+template = ScriptTemplate(
+ ...
+ script_body=script_body or session.latest_script,
+ parameters_schema=parameters_schema or {"parameters": []},
+ ...
+)
+```
+
+**`save_to_library` endpoint** (`backend/app/api/endpoints/script_builder.py`):
+
+Pass `data.script_body` and `data.parameters_schema` from the request through to the service function.
+
+**No new endpoints needed.** The library paste flow uses the existing `POST /scripts/templates` which already accepts `script_body` + `parameters_schema`.
+
+### Script Rewriting Logic
+
+The `ParameterizeAndSavePanel` reuses the same approach as `ScriptTemplateEditor.handleAcceptCandidate()`:
+
+When a candidate is accepted:
+1. Find the matched line in the working script
+2. Replace the default value portion with `'{{ key }}'`
+3. Add the parameter to the accumulated schema
+
+This happens in the panel's local state. The stepper emits accept/skip events; the panel handles the rewriting.
+
+---
+
+## File Changes
+
+### New Files
+- `frontend/src/components/scripts/ParameterizeAndSavePanel.tsx` — shared panel component
+
+### Modified Files
+- `frontend/src/pages/ScriptBuilderPage.tsx` — replace `SaveToLibraryDialog` with `ParameterizeAndSavePanel` (script mode)
+- `frontend/src/pages/ScriptLibraryPage.tsx` — add "New from Script" button, render `ParameterizeAndSavePanel` (paste mode)
+- `backend/app/schemas/script_builder.py` — add `script_body` and `parameters_schema` to `SaveToLibraryRequest`
+- `backend/app/services/script_builder_service.py` — use provided values in `save_to_library()`
+- `backend/app/api/endpoints/script_builder.py` — pass new fields through
+
+### Deleted Files
+- `frontend/src/components/script-builder/SaveToLibraryDialog.tsx` — replaced entirely
+
+### Unchanged (reused as-is)
+- `frontend/src/components/script-editor/ParameterDetectorStepper.tsx`
+- `frontend/src/lib/scriptParameterDetector.ts`
+- `frontend/src/components/script-editor/ScriptTemplateEditor.tsx`
+- `POST /scripts/templates` endpoint
+
+---
+
+## Scope Boundaries
+
+**In scope:**
+- `ParameterizeAndSavePanel` component with script/paste modes
+- Parameter detection + stepper review in the save flow
+- Script body rewriting with `{{ }}` placeholders
+- Backend accepting enriched save payload
+- "New from Script" button on library page
+- PowerShell parameter detection only
+
+**Out of scope:**
+- Bash/Python parameter detection (future iteration)
+- Changes to `ScriptTemplateEditor` or `ParameterSchemaBuilder`
+- Changes to `ScriptTemplateEngine` rendering logic
+- New API endpoints
From 60e1384a2c7788bd9f460b85284f7b6b9351a00d Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 05:46:40 +0000
Subject: [PATCH 28/49] docs: add parameterize-and-save implementation plan
7-task plan covering backend schema updates, ParameterizeAndSavePanel
component, ScriptBuilderPage integration, ScriptLibraryPage "New from
Script" entry point, and SaveToLibraryDialog deletion.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../plans/2026-03-29-parameterize-and-save.md | 937 ++++++++++++++++++
1 file changed, 937 insertions(+)
create mode 100644 docs/superpowers/plans/2026-03-29-parameterize-and-save.md
diff --git a/docs/superpowers/plans/2026-03-29-parameterize-and-save.md b/docs/superpowers/plans/2026-03-29-parameterize-and-save.md
new file mode 100644
index 00000000..23d36095
--- /dev/null
+++ b/docs/superpowers/plans/2026-03-29-parameterize-and-save.md
@@ -0,0 +1,937 @@
+# Parameterize & Save Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Fix AI-generated scripts saving to library without parameters by adding parameter detection, user review, and template rewriting to the save flow — plus a "New from Script" paste entry point on the library page.
+
+**Architecture:** A new `ParameterizeAndSavePanel` slide-in panel component handles two modes: `script` (pre-populated from AI builder) and `paste` (user pastes raw script). It auto-runs `detectParameterCandidates()`, renders `ParameterDetectorStepper` for review, rewrites the script body with `{{ key }}` placeholders, and sends enriched payload to the backend. Backend `save_to_library()` accepts the provided `script_body` + `parameters_schema` instead of hardcoding empty values.
+
+**Tech Stack:** React 19, TypeScript, Tailwind CSS v4, FastAPI, Pydantic v2, SQLAlchemy 2.0
+
+**Spec:** `docs/superpowers/specs/2026-03-29-parameterize-and-save-design.md`
+
+---
+
+### Task 1: Backend — Update `SaveToLibraryRequest` schema and `save_to_library()` service
+
+**Files:**
+- Modify: `backend/app/schemas/script_builder.py:79-84` (SaveToLibraryRequest)
+- Modify: `backend/app/services/script_builder_service.py:317-377` (save_to_library function)
+- Modify: `backend/app/api/endpoints/script_builder.py:156-188` (save_to_library endpoint)
+
+- [ ] **Step 1: Update `SaveToLibraryRequest` schema**
+
+In `backend/app/schemas/script_builder.py`, add two optional fields to `SaveToLibraryRequest`:
+
+```python
+class SaveToLibraryRequest(BaseModel):
+ """Request to save a generated script to the Script Library."""
+ name: str = Field(min_length=1, max_length=200)
+ description: str | None = None
+ category_id: UUID | None = None
+ share_with_team: bool = False
+ script_body: str | None = None
+ parameters_schema: dict | None = None
+```
+
+- [ ] **Step 2: Update `save_to_library()` service function signature and body**
+
+In `backend/app/services/script_builder_service.py`, add two new parameters to `save_to_library()` and use them in the `ScriptTemplate` constructor:
+
+```python
+async def save_to_library(
+ db: AsyncSession,
+ session: ScriptBuilderSession,
+ name: str,
+ description: str | None,
+ category_id: UUID | None,
+ share_with_team: bool,
+ user_id: UUID,
+ team_id: UUID | None,
+ script_body: str | None = None,
+ parameters_schema: dict | None = None,
+) -> "ScriptTemplate":
+```
+
+And in the `ScriptTemplate(...)` constructor inside the same function, change the two hardcoded lines:
+
+```python
+ template = ScriptTemplate(
+ id=uuid_mod.uuid4(),
+ category_id=resolved_category_id,
+ created_by=user_id,
+ team_id=team_id if share_with_team else None,
+ name=name,
+ slug=slug,
+ description=description,
+ script_body=script_body or session.latest_script,
+ parameters_schema=parameters_schema or {"parameters": []},
+ default_values={},
+ validation_rules={},
+ tags=[session.language, "ai-generated"],
+ complexity="intermediate",
+ is_verified=False,
+ is_active=True,
+ version=1,
+ usage_count=0,
+ )
+```
+
+- [ ] **Step 3: Update the endpoint to pass new fields through**
+
+In `backend/app/api/endpoints/script_builder.py`, update the `save_to_library` endpoint to pass the new fields to the service:
+
+```python
+ template = await script_builder_service.save_to_library(
+ db=db,
+ session=session,
+ name=data.name,
+ description=data.description,
+ category_id=data.category_id,
+ share_with_team=data.share_with_team,
+ user_id=current_user.id,
+ team_id=current_user.team_id,
+ script_body=data.script_body,
+ parameters_schema=data.parameters_schema,
+ )
+```
+
+- [ ] **Step 4: Verify backend still starts cleanly**
+
+Run: `cd /home/coder/resolutionflow/backend && source venv/bin/activate && python -c "from app.schemas.script_builder import SaveToLibraryRequest; print('OK')"`
+
+Expected: `OK`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add backend/app/schemas/script_builder.py backend/app/services/script_builder_service.py backend/app/api/endpoints/script_builder.py
+git commit -m "feat: accept script_body and parameters_schema in save-to-library flow
+
+Previously save_to_library() hardcoded parameters_schema to empty and
+always used session.latest_script. Now accepts optional overrides from
+the frontend for parameterized script bodies.
+
+Co-Authored-By: Claude Opus 4.6 (1M context) "
+```
+
+---
+
+### Task 2: Frontend — Update `SaveToLibraryRequest` type
+
+**Files:**
+- Modify: `frontend/src/types/script-builder.ts:45-50` (SaveToLibraryRequest interface)
+
+- [ ] **Step 1: Add new fields to the TypeScript interface**
+
+In `frontend/src/types/script-builder.ts`, update `SaveToLibraryRequest`:
+
+```typescript
+export interface SaveToLibraryRequest {
+ name: string
+ description?: string
+ category_id?: string
+ share_with_team?: boolean
+ script_body?: string
+ parameters_schema?: { parameters: ScriptParameter[] }
+}
+```
+
+Add the import for `ScriptParameter` at the top of the file:
+
+```typescript
+import type { ScriptParameter } from './scripts'
+```
+
+- [ ] **Step 2: Verify types compile**
+
+Run: `cd /home/coder/resolutionflow/frontend && npx tsc --noEmit --pretty 2>&1 | head -30`
+
+Expected: No errors related to `SaveToLibraryRequest` or `ScriptParameter`.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add frontend/src/types/script-builder.ts
+git commit -m "feat: add script_body and parameters_schema to SaveToLibraryRequest type
+
+Co-Authored-By: Claude Opus 4.6 (1M context) "
+```
+
+---
+
+### Task 3: Frontend — Create `ParameterizeAndSavePanel` component
+
+**Files:**
+- Create: `frontend/src/components/scripts/ParameterizeAndSavePanel.tsx`
+
+This is the core new component. It handles two modes (`script` and `paste`), auto-runs parameter detection, embeds the existing `ParameterDetectorStepper`, rewrites the script body with `{{ key }}` placeholders, collects metadata, and calls the save handler.
+
+- [ ] **Step 1: Create the component file**
+
+Create `frontend/src/components/scripts/ParameterizeAndSavePanel.tsx`:
+
+```tsx
+import { useState, useEffect, useRef, useCallback } from 'react'
+import { X, Loader2, FileCode, AlertCircle } from 'lucide-react'
+import { cn } from '@/lib/utils'
+import { scriptsApi } from '@/api'
+import { detectParameterCandidates } from '@/lib/scriptParameterDetector'
+import { ParameterDetectorStepper } from '@/components/script-editor/ParameterDetectorStepper'
+import type {
+ ScriptCategoryResponse,
+ ScriptParameter,
+ ScriptParametersSchema,
+ ParameterCandidate,
+} from '@/types'
+
+interface ParameterizeAndSavePanelProps {
+ /** Pre-populated script body (script mode). Undefined triggers paste mode. */
+ scriptBody?: string
+ /** Script language. Undefined shows language picker in paste mode. */
+ language?: string
+ /** Default name for the template. */
+ defaultName?: string
+ /** Default description for the template. */
+ defaultDescription?: string
+ /** Called with the final enriched payload when user saves. */
+ onSave: (payload: {
+ name: string
+ description: string | undefined
+ category_id: string | undefined
+ share_with_team: boolean
+ script_body: string
+ parameters_schema: ScriptParametersSchema
+ }) => Promise
+ /** Called when the panel is closed without saving. */
+ onClose: () => void
+}
+
+const LANGUAGES = [
+ { value: 'powershell', label: 'PowerShell' },
+ { value: 'bash', label: 'Bash' },
+ { value: 'python', label: 'Python' },
+]
+
+export function ParameterizeAndSavePanel({
+ scriptBody,
+ language: initialLanguage,
+ defaultName = '',
+ defaultDescription = '',
+ onSave,
+ onClose,
+}: ParameterizeAndSavePanelProps) {
+ // Mode: script (body provided) vs paste (user pastes)
+ const isPasteMode = scriptBody === undefined
+
+ // Paste mode state
+ const [pastedScript, setPastedScript] = useState('')
+ const [selectedLanguage, setSelectedLanguage] = useState(initialLanguage || 'powershell')
+ const [scriptConfirmed, setScriptConfirmed] = useState(false)
+
+ // Working state — the script body being rewritten as params are accepted
+ const effectiveScript = isPasteMode ? pastedScript : scriptBody!
+ const [workingScript, setWorkingScript] = useState(effectiveScript)
+ const [parameters, setParameters] = useState([])
+
+ // Detection state
+ const [candidates, setCandidates] = useState([])
+ const [detectionRan, setDetectionRan] = useState(false)
+ const [showStepper, setShowStepper] = useState(false)
+ const [detectionSummary, setDetectionSummary] = useState(null)
+
+ // Metadata state
+ const [name, setName] = useState(defaultName)
+ const [description, setDescription] = useState(defaultDescription)
+ const [categoryId, setCategoryId] = useState('')
+ const [shareWithTeam, setShareWithTeam] = useState(false)
+ const [categories, setCategories] = useState([])
+
+ // Save state
+ const [isSaving, setIsSaving] = useState(false)
+ const [error, setError] = useState(null)
+
+ const panelRef = useRef(null)
+
+ // Load categories on mount
+ useEffect(() => {
+ scriptsApi.getCategories().then(setCategories).catch(() => {})
+ }, [])
+
+ // Auto-run detection when script is ready (script mode: on mount, paste mode: after confirm)
+ const runDetection = useCallback((script: string) => {
+ const detected = detectParameterCandidates(script)
+ setCandidates(detected)
+ setDetectionRan(true)
+ if (detected.length > 0) {
+ setShowStepper(true)
+ } else {
+ setDetectionSummary('No parameters detected — script will be saved as-is. Parameter detection currently supports PowerShell only.')
+ }
+ }, [])
+
+ // Script mode: run detection on mount
+ useEffect(() => {
+ if (!isPasteMode && effectiveScript) {
+ setWorkingScript(effectiveScript)
+ runDetection(effectiveScript)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ // Paste mode: run detection after script is confirmed
+ useEffect(() => {
+ if (isPasteMode && scriptConfirmed && pastedScript) {
+ setWorkingScript(pastedScript)
+ runDetection(pastedScript)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [scriptConfirmed])
+
+ // Escape key closes panel
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') onClose()
+ }
+ document.addEventListener('keydown', handleKeyDown)
+ return () => document.removeEventListener('keydown', handleKeyDown)
+ }, [onClose])
+
+ const handleConfirmPaste = () => {
+ if (!pastedScript.trim()) return
+ setScriptConfirmed(true)
+ }
+
+ const handleAcceptCandidate = (
+ candidate: ParameterCandidate,
+ overrides: {
+ key: string
+ label: string
+ type: ScriptParameter['type']
+ sensitive: boolean
+ required: boolean
+ defaultValue: string | boolean | number | null
+ }
+ ) => {
+ // Rewrite the script body — replace the default value with {{ key }} placeholder
+ let updatedScript = workingScript
+ if (candidate.source === 'param_block') {
+ const defaultMatch = candidate.matchedLine.match(/=\s*(.+?)(?:\s*,?\s*$)/)
+ if (defaultMatch) {
+ updatedScript = updatedScript.replace(
+ candidate.matchedLine,
+ candidate.matchedLine.replace(defaultMatch[1], `'{{${overrides.key}}}'`)
+ )
+ }
+ } else {
+ const assignMatch = candidate.matchedLine.match(/=\s*(.+)$/)
+ if (assignMatch) {
+ updatedScript = updatedScript.replace(
+ candidate.matchedLine,
+ candidate.matchedLine.replace(assignMatch[1], `'{{${overrides.key}}}'`)
+ )
+ }
+ }
+ setWorkingScript(updatedScript)
+
+ // Add parameter to accumulated schema
+ const newParam: ScriptParameter = {
+ key: overrides.key,
+ label: overrides.label,
+ type: overrides.type,
+ required: overrides.required,
+ placeholder: null,
+ group: null,
+ order: parameters.length + 1,
+ help_text: null,
+ options: null,
+ default: overrides.defaultValue,
+ validation: null,
+ sensitive: overrides.sensitive,
+ }
+ setParameters(prev => [...prev, newParam])
+ }
+
+ const handleSkipCandidate = () => {
+ // Stepper advances internally — nothing to do here
+ }
+
+ const handleDetectionFinish = (acceptedCount: number, totalCount: number) => {
+ setShowStepper(false)
+ setCandidates([])
+ setDetectionSummary(
+ acceptedCount === 0
+ ? 'No parameters were added. Script will be saved as-is.'
+ : `Added ${acceptedCount} of ${totalCount} detected parameter${totalCount !== 1 ? 's' : ''}.`
+ )
+ }
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!name.trim()) return
+
+ setIsSaving(true)
+ setError(null)
+
+ try {
+ await onSave({
+ name: name.trim(),
+ description: description.trim() || undefined,
+ category_id: categoryId || undefined,
+ share_with_team: shareWithTeam,
+ script_body: workingScript,
+ parameters_schema: { parameters },
+ })
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to save script. Please try again.')
+ } finally {
+ setIsSaving(false)
+ }
+ }
+
+ // Determine if save button should be enabled
+ const canSave = name.trim().length > 0
+ && !isSaving
+ && !showStepper
+ && (isPasteMode ? scriptConfirmed : true)
+
+ return (
+ <>
+ {/* Scrim */}
+
+
+ {/* Panel */}
+
+ {/* Header */}
+
+
+
+
+ {isPasteMode ? 'Import Script to Library' : 'Save to Library'}
+
+
+
+
+
+
+
+ {/* Scrollable content */}
+
+
+ {/* Paste mode: script input area (before confirmation) */}
+ {isPasteMode && !scriptConfirmed && (
+
+
+ Paste Your Script
+
+
+ {LANGUAGES.map((lang) => (
+ setSelectedLanguage(lang.value)}
+ className={cn(
+ 'px-3 py-1.5 rounded-md text-xs font-medium transition-all',
+ selectedLanguage === lang.value
+ ? 'bg-primary text-white'
+ : 'text-muted-foreground hover:text-foreground bg-elevated'
+ )}
+ >
+ {lang.label}
+
+ ))}
+
+
+ )}
+
+ {/* Script preview (visible after paste confirm, or always in script mode) */}
+ {(!isPasteMode || scriptConfirmed) && (
+
+
+ Script Preview
+
+
+
+ {workingScript.split(/({{.*?}})/).map((part, i) =>
+ /^{{.*}}$/.test(part)
+ ? {part}
+ : {part}
+ )}
+
+
+
+ )}
+
+ {/* Parameter detection zone */}
+ {detectionRan && !showStepper && detectionSummary && (
+
+ )}
+
+ {showStepper && candidates.length > 0 && (
+
+
+ Detected Parameters
+
+ p.key)}
+ onAccept={handleAcceptCandidate}
+ onSkip={handleSkipCandidate}
+ onFinish={handleDetectionFinish}
+ />
+
+ )}
+
+ {/* Accepted parameters summary */}
+ {parameters.length > 0 && !showStepper && (
+
+
+ Parameters ({parameters.length})
+
+
+ {parameters.map((p) => (
+
+
+ {`{{${p.key}}}`}
+ {p.label}
+
+
+ {p.type}{p.sensitive ? ' · sensitive' : ''}{p.required ? '' : ' · optional'}
+
+
+ ))}
+
+
+ )}
+
+ {/* Metadata form — shown after detection is done (or immediately if no candidates) */}
+ {detectionRan && !showStepper && (
+
+ )}
+
+
+ >
+ )
+}
+```
+
+- [ ] **Step 2: Verify the component compiles**
+
+Run: `cd /home/coder/resolutionflow/frontend && npx tsc --noEmit --pretty 2>&1 | head -30`
+
+Expected: No errors in `ParameterizeAndSavePanel.tsx`.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add frontend/src/components/scripts/ParameterizeAndSavePanel.tsx
+git commit -m "feat: add ParameterizeAndSavePanel component
+
+Slide-in panel for saving scripts to library with parameter detection,
+stepper review, template rewriting, and metadata collection. Supports
+both script mode (from AI builder) and paste mode (raw script import).
+
+Co-Authored-By: Claude Opus 4.6 (1M context) "
+```
+
+---
+
+### Task 4: Frontend — Wire `ParameterizeAndSavePanel` into `ScriptBuilderPage`
+
+**Files:**
+- Modify: `frontend/src/pages/ScriptBuilderPage.tsx`
+
+Replace the `SaveToLibraryDialog` import and usage with `ParameterizeAndSavePanel`.
+
+- [ ] **Step 1: Update imports**
+
+In `frontend/src/pages/ScriptBuilderPage.tsx`, replace the `SaveToLibraryDialog` import:
+
+```typescript
+// REMOVE this line:
+import { SaveToLibraryDialog } from '@/components/script-builder/SaveToLibraryDialog'
+
+// ADD this line:
+import { ParameterizeAndSavePanel } from '@/components/scripts/ParameterizeAndSavePanel'
+```
+
+Also add `scriptBuilderApi` to the existing import if not already there (it is already imported on line 8):
+
+```typescript
+import { scriptBuilderApi } from '@/api'
+```
+
+- [ ] **Step 2: Add the save handler function**
+
+Inside the `ScriptBuilderPage` component, replace `handleSaved`:
+
+```typescript
+ const handleSaved = async (payload: {
+ name: string
+ description: string | undefined
+ category_id: string | undefined
+ share_with_team: boolean
+ script_body: string
+ parameters_schema: { parameters: import('@/types').ScriptParameter[] }
+ }) => {
+ if (!session) return
+ await scriptBuilderApi.saveToLibrary(session.id, {
+ name: payload.name,
+ description: payload.description,
+ category_id: payload.category_id,
+ share_with_team: payload.share_with_team,
+ script_body: payload.script_body,
+ parameters_schema: payload.parameters_schema,
+ })
+ setShowSaveDialog(false)
+ }
+```
+
+- [ ] **Step 3: Replace the dialog JSX with the panel**
+
+Replace the `SaveToLibraryDialog` JSX block (lines 194-200) with:
+
+```tsx
+ {/* Save panel */}
+ {showSaveDialog && session && session.latest_script && (
+ setShowSaveDialog(false)}
+ />
+ )}
+```
+
+- [ ] **Step 4: Verify it compiles**
+
+Run: `cd /home/coder/resolutionflow/frontend && npx tsc --noEmit --pretty 2>&1 | head -30`
+
+Expected: No type errors.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add frontend/src/pages/ScriptBuilderPage.tsx
+git commit -m "feat: wire ParameterizeAndSavePanel into ScriptBuilderPage
+
+Replace SaveToLibraryDialog with the new panel that includes parameter
+detection, review, and template rewriting before saving to library.
+
+Co-Authored-By: Claude Opus 4.6 (1M context) "
+```
+
+---
+
+### Task 5: Frontend — Add "New from Script" button to `ScriptLibraryPage`
+
+**Files:**
+- Modify: `frontend/src/pages/ScriptLibraryPage.tsx`
+
+- [ ] **Step 1: Add imports**
+
+Add these imports to the top of `ScriptLibraryPage.tsx`:
+
+```typescript
+import { FileUp } from 'lucide-react'
+import { ParameterizeAndSavePanel } from '@/components/scripts/ParameterizeAndSavePanel'
+import { scriptsApi } from '@/api'
+import type { ScriptParameter } from '@/types'
+```
+
+Update the existing lucide import to include `FileUp` alongside `Terminal`, `Settings`, `Wand2`:
+
+```typescript
+import { Terminal, Settings, Wand2, FileUp } from 'lucide-react'
+```
+
+- [ ] **Step 2: Add state and handler for the import panel**
+
+Inside `ScriptLibraryPage`, add state and a save handler:
+
+```typescript
+ const [showImportPanel, setShowImportPanel] = useState(false)
+
+ const handleImportSave = async (payload: {
+ name: string
+ description: string | undefined
+ category_id: string | undefined
+ share_with_team: boolean
+ script_body: string
+ parameters_schema: { parameters: ScriptParameter[] }
+ }) => {
+ // createTemplate requires category_id — if user didn't pick one,
+ // fall back to the first available category
+ const categoryId = payload.category_id || categories[0]?.id
+ if (!categoryId) {
+ throw new Error('No categories available. Please create a category first.')
+ }
+ await scriptsApi.createTemplate({
+ category_id: categoryId,
+ name: payload.name,
+ description: payload.description,
+ script_body: payload.script_body,
+ parameters_schema: payload.parameters_schema,
+ })
+ setShowImportPanel(false)
+ // Reload templates to show the newly created one
+ const filters = activeTab === 'mine' ? { mine: true } : { shared: true }
+ loadTemplates(filters)
+ }
+```
+
+Note: `categories` needs to be read from the store. Add this line alongside the other store selectors:
+
+```typescript
+ const categories = useScriptGeneratorStore(s => s.categories)
+```
+
+- [ ] **Step 3: Add the "New from Script" button**
+
+In the page header section, next to the "Manage Templates" link, add:
+
+```tsx
+ {isEngineer && (
+
+
+
+ Manage Templates
+
+ setShowImportPanel(true)}
+ className="inline-flex items-center gap-1.5 text-xs text-primary bg-accent-dim hover:bg-primary/15 px-2.5 py-1 rounded-full transition-colors"
+ >
+
+ New from Script
+
+
+ )}
+```
+
+This replaces the existing `{isEngineer && (...)}` block that only had the Manage Templates link.
+
+- [ ] **Step 4: Add the panel JSX**
+
+At the end of the component return, before the closing ``, add:
+
+```tsx
+ {/* Import script panel */}
+ {showImportPanel && (
+ setShowImportPanel(false)}
+ />
+ )}
+```
+
+- [ ] **Step 5: Verify it compiles**
+
+Run: `cd /home/coder/resolutionflow/frontend && npx tsc --noEmit --pretty 2>&1 | head -30`
+
+Expected: No type errors.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add frontend/src/pages/ScriptLibraryPage.tsx
+git commit -m "feat: add 'New from Script' button to ScriptLibraryPage
+
+Opens the ParameterizeAndSavePanel in paste mode, letting users import
+raw scripts with parameter detection and review before saving to library.
+
+Co-Authored-By: Claude Opus 4.6 (1M context) "
+```
+
+---
+
+### Task 6: Frontend — Delete `SaveToLibraryDialog` and clean up imports
+
+**Files:**
+- Delete: `frontend/src/components/script-builder/SaveToLibraryDialog.tsx`
+
+- [ ] **Step 1: Verify no other files import `SaveToLibraryDialog`**
+
+Run: `grep -r "SaveToLibraryDialog" frontend/src/ --include="*.ts" --include="*.tsx" | grep -v "node_modules"`
+
+Expected: No results (ScriptBuilderPage was updated in Task 4).
+
+- [ ] **Step 2: Delete the file**
+
+```bash
+rm frontend/src/components/script-builder/SaveToLibraryDialog.tsx
+```
+
+- [ ] **Step 3: Run full build check**
+
+Run: `cd /home/coder/resolutionflow/frontend && npm run build 2>&1 | tail -20`
+
+Expected: Build succeeds with no errors. This is the strictest check (`tsc -b` enforces `noUnusedLocals`).
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add -u frontend/src/components/script-builder/SaveToLibraryDialog.tsx
+git commit -m "chore: delete SaveToLibraryDialog, replaced by ParameterizeAndSavePanel
+
+Co-Authored-By: Claude Opus 4.6 (1M context) "
+```
+
+---
+
+### Task 7: Full build verification and final commit
+
+**Files:** None (verification only)
+
+- [ ] **Step 1: Run frontend build**
+
+Run: `cd /home/coder/resolutionflow/frontend && npm run build 2>&1 | tail -20`
+
+Expected: Build succeeds. This catches any `noUnusedLocals`/`noUnusedParameters` errors the earlier `tsc --noEmit` might miss.
+
+- [ ] **Step 2: Verify backend imports are clean**
+
+Run: `cd /home/coder/resolutionflow/backend && source venv/bin/activate && python -c "from app.api.endpoints.script_builder import router; print('OK')"`
+
+Expected: `OK`
+
+- [ ] **Step 3: Verify git status is clean**
+
+Run: `git status`
+
+Expected: On branch `feat/task-lane-persistence`, working tree clean (all changes committed in Tasks 1-6).
From 63d470455fa20455308d8df4eb227bed493ec692 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 05:48:53 +0000
Subject: [PATCH 29/49] feat: accept script_body and parameters_schema in
save-to-library flow
Previously save_to_library() hardcoded parameters_schema to empty and
always used session.latest_script. Now accepts optional overrides from
the frontend for parameterized script bodies.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
backend/app/api/endpoints/script_builder.py | 2 ++
backend/app/schemas/script_builder.py | 2 ++
backend/app/services/script_builder_service.py | 6 ++++--
3 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/backend/app/api/endpoints/script_builder.py b/backend/app/api/endpoints/script_builder.py
index 1ac5851c..b328477c 100644
--- a/backend/app/api/endpoints/script_builder.py
+++ b/backend/app/api/endpoints/script_builder.py
@@ -179,6 +179,8 @@ async def save_to_library(
share_with_team=data.share_with_team,
user_id=current_user.id,
team_id=current_user.team_id,
+ script_body=data.script_body,
+ parameters_schema=data.parameters_schema,
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
diff --git a/backend/app/schemas/script_builder.py b/backend/app/schemas/script_builder.py
index 893d16be..1a7add15 100644
--- a/backend/app/schemas/script_builder.py
+++ b/backend/app/schemas/script_builder.py
@@ -82,3 +82,5 @@ class SaveToLibraryRequest(BaseModel):
description: str | None = None
category_id: UUID | None = None
share_with_team: bool = False
+ script_body: str | None = None
+ parameters_schema: dict | None = None
diff --git a/backend/app/services/script_builder_service.py b/backend/app/services/script_builder_service.py
index 3fdeb3ea..b483f39e 100644
--- a/backend/app/services/script_builder_service.py
+++ b/backend/app/services/script_builder_service.py
@@ -323,6 +323,8 @@ async def save_to_library(
share_with_team: bool,
user_id: UUID,
team_id: UUID | None,
+ script_body: str | None = None,
+ parameters_schema: dict | None = None,
) -> "ScriptTemplate":
"""Save the latest generated script to the Script Library as a ScriptTemplate."""
import uuid as uuid_mod
@@ -361,8 +363,8 @@ async def save_to_library(
name=name,
slug=slug,
description=description,
- script_body=session.latest_script,
- parameters_schema={"parameters": []},
+ script_body=script_body or session.latest_script,
+ parameters_schema=parameters_schema or {"parameters": []},
default_values={},
validation_rules={},
tags=[session.language, "ai-generated"],
From 503402386a3f8e70b6d709a37676527f073e35a6 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 05:49:32 +0000
Subject: [PATCH 30/49] feat: add script_body and parameters_schema to
SaveToLibraryRequest type
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/types/script-builder.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/frontend/src/types/script-builder.ts b/frontend/src/types/script-builder.ts
index cfa8165e..16d72c5e 100644
--- a/frontend/src/types/script-builder.ts
+++ b/frontend/src/types/script-builder.ts
@@ -1,3 +1,5 @@
+import type { ScriptParameter } from './scripts'
+
export interface ScriptBuilderSessionSummary {
id: string
language: string
@@ -47,4 +49,6 @@ export interface SaveToLibraryRequest {
description?: string
category_id?: string
share_with_team?: boolean
+ script_body?: string
+ parameters_schema?: { parameters: ScriptParameter[] }
}
From e6edf34485a43180637a97986fa53088c7e1344f Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 05:52:17 +0000
Subject: [PATCH 31/49] feat: add ParameterizeAndSavePanel component
Slide-in panel for saving scripts to library with parameter detection,
stepper review, template rewriting, and metadata collection. Supports
both script mode (from AI builder) and paste mode (raw script import).
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../scripts/ParameterizeAndSavePanel.tsx | 472 ++++++++++++++++++
1 file changed, 472 insertions(+)
create mode 100644 frontend/src/components/scripts/ParameterizeAndSavePanel.tsx
diff --git a/frontend/src/components/scripts/ParameterizeAndSavePanel.tsx b/frontend/src/components/scripts/ParameterizeAndSavePanel.tsx
new file mode 100644
index 00000000..95be3cfb
--- /dev/null
+++ b/frontend/src/components/scripts/ParameterizeAndSavePanel.tsx
@@ -0,0 +1,472 @@
+import { useState, useEffect, useRef, useCallback } from 'react'
+import { X, Loader2, FileCode, AlertCircle } from 'lucide-react'
+import { cn } from '@/lib/utils'
+import { scriptsApi } from '@/api'
+import { detectParameterCandidates } from '@/lib/scriptParameterDetector'
+import { ParameterDetectorStepper } from '@/components/script-editor/ParameterDetectorStepper'
+import type {
+ ScriptCategoryResponse,
+ ScriptParameter,
+ ScriptParametersSchema,
+ ParameterCandidate,
+} from '@/types'
+
+interface ParameterizeAndSavePanelProps {
+ /** Pre-populated script body (script mode). Undefined triggers paste mode. */
+ scriptBody?: string
+ /** Script language. Undefined shows language picker in paste mode. */
+ language?: string
+ /** Default name for the template. */
+ defaultName?: string
+ /** Default description for the template. */
+ defaultDescription?: string
+ /** Called with the final enriched payload when user saves. */
+ onSave: (payload: {
+ name: string
+ description: string | undefined
+ category_id: string | undefined
+ share_with_team: boolean
+ script_body: string
+ parameters_schema: ScriptParametersSchema
+ }) => Promise
+ /** Called when the panel is closed without saving. */
+ onClose: () => void
+}
+
+const LANGUAGES = [
+ { value: 'powershell', label: 'PowerShell' },
+ { value: 'bash', label: 'Bash' },
+ { value: 'python', label: 'Python' },
+]
+
+export function ParameterizeAndSavePanel({
+ scriptBody,
+ language: initialLanguage,
+ defaultName = '',
+ defaultDescription = '',
+ onSave,
+ onClose,
+}: ParameterizeAndSavePanelProps) {
+ // Mode: script (body provided) vs paste (user pastes)
+ const isPasteMode = scriptBody === undefined
+
+ // Paste mode state
+ const [pastedScript, setPastedScript] = useState('')
+ const [selectedLanguage, setSelectedLanguage] = useState(initialLanguage || 'powershell')
+ const [scriptConfirmed, setScriptConfirmed] = useState(false)
+
+ // Working state — the script body being rewritten as params are accepted
+ const effectiveScript = isPasteMode ? pastedScript : scriptBody!
+ const [workingScript, setWorkingScript] = useState(effectiveScript)
+ const [parameters, setParameters] = useState([])
+
+ // Detection state
+ const [candidates, setCandidates] = useState([])
+ const [detectionRan, setDetectionRan] = useState(false)
+ const [showStepper, setShowStepper] = useState(false)
+ const [detectionSummary, setDetectionSummary] = useState(null)
+
+ // Metadata state
+ const [name, setName] = useState(defaultName)
+ const [description, setDescription] = useState(defaultDescription)
+ const [categoryId, setCategoryId] = useState('')
+ const [shareWithTeam, setShareWithTeam] = useState(false)
+ const [categories, setCategories] = useState([])
+
+ // Save state
+ const [isSaving, setIsSaving] = useState(false)
+ const [error, setError] = useState(null)
+
+ const panelRef = useRef(null)
+
+ // Load categories on mount
+ useEffect(() => {
+ scriptsApi.getCategories().then(setCategories).catch(() => {})
+ }, [])
+
+ // Auto-run detection when script is ready (script mode: on mount, paste mode: after confirm)
+ const runDetection = useCallback((script: string) => {
+ const detected = detectParameterCandidates(script)
+ setCandidates(detected)
+ setDetectionRan(true)
+ if (detected.length > 0) {
+ setShowStepper(true)
+ } else {
+ setDetectionSummary('No parameters detected — script will be saved as-is. Parameter detection currently supports PowerShell only.')
+ }
+ }, [])
+
+ // Script mode: run detection on mount
+ useEffect(() => {
+ if (!isPasteMode && effectiveScript) {
+ setWorkingScript(effectiveScript)
+ runDetection(effectiveScript)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ // Paste mode: run detection after script is confirmed
+ useEffect(() => {
+ if (isPasteMode && scriptConfirmed && pastedScript) {
+ setWorkingScript(pastedScript)
+ runDetection(pastedScript)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [scriptConfirmed])
+
+ // Escape key closes panel
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') onClose()
+ }
+ document.addEventListener('keydown', handleKeyDown)
+ return () => document.removeEventListener('keydown', handleKeyDown)
+ }, [onClose])
+
+ const handleConfirmPaste = () => {
+ if (!pastedScript.trim()) return
+ setScriptConfirmed(true)
+ }
+
+ const handleAcceptCandidate = (
+ candidate: ParameterCandidate,
+ overrides: {
+ key: string
+ label: string
+ type: ScriptParameter['type']
+ sensitive: boolean
+ required: boolean
+ defaultValue: string | boolean | number | null
+ }
+ ) => {
+ // Rewrite the script body — replace the default value with {{ key }} placeholder
+ let updatedScript = workingScript
+ if (candidate.source === 'param_block') {
+ const defaultMatch = candidate.matchedLine.match(/=\s*(.+?)(?:\s*,?\s*$)/)
+ if (defaultMatch) {
+ updatedScript = updatedScript.replace(
+ candidate.matchedLine,
+ candidate.matchedLine.replace(defaultMatch[1], `'{{${overrides.key}}}'`)
+ )
+ }
+ } else {
+ const assignMatch = candidate.matchedLine.match(/=\s*(.+)$/)
+ if (assignMatch) {
+ updatedScript = updatedScript.replace(
+ candidate.matchedLine,
+ candidate.matchedLine.replace(assignMatch[1], `'{{${overrides.key}}}'`)
+ )
+ }
+ }
+ setWorkingScript(updatedScript)
+
+ // Add parameter to accumulated schema
+ const newParam: ScriptParameter = {
+ key: overrides.key,
+ label: overrides.label,
+ type: overrides.type,
+ required: overrides.required,
+ placeholder: null,
+ group: null,
+ order: parameters.length + 1,
+ help_text: null,
+ options: null,
+ default: overrides.defaultValue,
+ validation: null,
+ sensitive: overrides.sensitive,
+ }
+ setParameters(prev => [...prev, newParam])
+ }
+
+ const handleSkipCandidate = () => {
+ // Stepper advances internally — nothing to do here
+ }
+
+ const handleDetectionFinish = (acceptedCount: number, totalCount: number) => {
+ setShowStepper(false)
+ setCandidates([])
+ setDetectionSummary(
+ acceptedCount === 0
+ ? 'No parameters were added. Script will be saved as-is.'
+ : `Added ${acceptedCount} of ${totalCount} detected parameter${totalCount !== 1 ? 's' : ''}.`
+ )
+ }
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!name.trim()) return
+
+ setIsSaving(true)
+ setError(null)
+
+ try {
+ await onSave({
+ name: name.trim(),
+ description: description.trim() || undefined,
+ category_id: categoryId || undefined,
+ share_with_team: shareWithTeam,
+ script_body: workingScript,
+ parameters_schema: { parameters },
+ })
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to save script. Please try again.')
+ } finally {
+ setIsSaving(false)
+ }
+ }
+
+ // Determine if save button should be enabled
+ const canSave = name.trim().length > 0
+ && !isSaving
+ && !showStepper
+ && (isPasteMode ? scriptConfirmed : true)
+
+ return (
+ <>
+ {/* Scrim */}
+
+
+ {/* Panel */}
+
+ {/* Header */}
+
+
+
+
+ {isPasteMode ? 'Import Script to Library' : 'Save to Library'}
+
+
+
+
+
+
+
+ {/* Scrollable content */}
+
+
+ {/* Paste mode: script input area (before confirmation) */}
+ {isPasteMode && !scriptConfirmed && (
+
+
+ Paste Your Script
+
+
+ {LANGUAGES.map((lang) => (
+ setSelectedLanguage(lang.value)}
+ className={cn(
+ 'px-3 py-1.5 rounded-md text-xs font-medium transition-all',
+ selectedLanguage === lang.value
+ ? 'bg-primary text-white'
+ : 'text-muted-foreground hover:text-foreground bg-elevated'
+ )}
+ >
+ {lang.label}
+
+ ))}
+
+ setPastedScript(e.target.value)}
+ rows={12}
+ className={cn(
+ 'w-full rounded-lg px-3 py-2 text-sm font-mono resize-none',
+ 'border border-default bg-card text-foreground placeholder:text-muted-foreground',
+ 'focus:outline-none focus:border-primary/30 transition-colors'
+ )}
+ placeholder="Paste your PowerShell, Bash, or Python script here..."
+ autoFocus
+ />
+
+ Detect Parameters
+
+
+ )}
+
+ {/* Script preview (visible after paste confirm, or always in script mode) */}
+ {(!isPasteMode || scriptConfirmed) && (
+
+
+ Script Preview
+
+
+
+ {workingScript.split(/({{.*?}})/).map((part, i) =>
+ /^{{.*}}$/.test(part)
+ ? {part}
+ : {part}
+ )}
+
+
+
+ )}
+
+ {/* Parameter detection zone */}
+ {detectionRan && !showStepper && detectionSummary && (
+
+ )}
+
+ {showStepper && candidates.length > 0 && (
+
+
+ Detected Parameters
+
+ p.key)}
+ onAccept={handleAcceptCandidate}
+ onSkip={handleSkipCandidate}
+ onFinish={handleDetectionFinish}
+ />
+
+ )}
+
+ {/* Accepted parameters summary */}
+ {parameters.length > 0 && !showStepper && (
+
+
+ Parameters ({parameters.length})
+
+
+ {parameters.map((p) => (
+
+
+ {`{{${p.key}}}`}
+ {p.label}
+
+
+ {p.type}{p.sensitive ? ' · sensitive' : ''}{p.required ? '' : ' · optional'}
+
+
+ ))}
+
+
+ )}
+
+ {/* Metadata form — shown after detection is done (or immediately if no candidates) */}
+ {detectionRan && !showStepper && (
+
+
+ Template Details
+
+
+ {/* Name */}
+
+
+ Name *
+
+ setName(e.target.value)}
+ required
+ className={cn(
+ 'w-full rounded-lg px-3 py-2 text-sm',
+ 'border border-default bg-card text-foreground placeholder:text-muted-foreground',
+ 'focus:outline-none focus:border-primary/30 transition-colors'
+ )}
+ placeholder="Script name"
+ />
+
+
+ {/* Description */}
+
+
+ Description
+
+ setDescription(e.target.value)}
+ rows={3}
+ className={cn(
+ 'w-full rounded-lg px-3 py-2 text-sm resize-none',
+ 'border border-default bg-card text-foreground placeholder:text-muted-foreground',
+ 'focus:outline-none focus:border-primary/30 transition-colors'
+ )}
+ placeholder="What does this script do?"
+ />
+
+
+ {/* Category */}
+
+
+ Category
+
+ setCategoryId(e.target.value)}
+ className={cn(
+ 'w-full rounded-lg px-3 py-2 text-sm',
+ 'border border-default bg-card text-foreground',
+ 'focus:outline-none focus:border-primary/30 transition-colors'
+ )}
+ >
+ No category
+ {categories.map((cat) => (
+ {cat.name}
+ ))}
+
+
+
+ {/* Share with team */}
+
+ setShareWithTeam(e.target.checked)}
+ className="w-4 h-4 rounded border-border bg-card text-orange-500 focus:ring-orange-500/20"
+ />
+ Share with team
+
+
+ {/* Error */}
+ {error && (
+ {error}
+ )}
+
+ {/* Save button */}
+
+ {isSaving && }
+ {isSaving ? 'Saving...' : 'Save to Library'}
+
+
+ )}
+
+
+ >
+ )
+}
From da71cd6ca4258715bcaddd4a34053107c7d397b9 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 05:53:49 +0000
Subject: [PATCH 32/49] feat: wire ParameterizeAndSavePanel into
ScriptBuilderPage
Replace SaveToLibraryDialog with the new panel that includes parameter
detection, review, and template rewriting before saving to library.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/pages/ScriptBuilderPage.tsx | 31 ++++++++++++++++++------
1 file changed, 24 insertions(+), 7 deletions(-)
diff --git a/frontend/src/pages/ScriptBuilderPage.tsx b/frontend/src/pages/ScriptBuilderPage.tsx
index ccc48ded..31ae7aff 100644
--- a/frontend/src/pages/ScriptBuilderPage.tsx
+++ b/frontend/src/pages/ScriptBuilderPage.tsx
@@ -6,7 +6,7 @@ import { scriptBuilderApi } from '@/api'
import { ScriptBuilderChat } from '@/components/script-builder/ScriptBuilderChat'
import { ScriptBuilderInput } from '@/components/script-builder/ScriptBuilderInput'
import { ScriptPreviewModal } from '@/components/script-builder/ScriptPreviewModal'
-import { SaveToLibraryDialog } from '@/components/script-builder/SaveToLibraryDialog'
+import { ParameterizeAndSavePanel } from '@/components/scripts/ParameterizeAndSavePanel'
import type { ScriptBuilderSessionDetail, ScriptBuilderMessage } from '@/types'
const LANGUAGES = [
@@ -112,7 +112,23 @@ export default function ScriptBuilderPage() {
setShowSaveDialog(true)
}
- const handleSaved = () => {
+ const handleSaved = async (payload: {
+ name: string
+ description: string | undefined
+ category_id: string | undefined
+ share_with_team: boolean
+ script_body: string
+ parameters_schema: { parameters: import('@/types').ScriptParameter[] }
+ }) => {
+ if (!session) return
+ await scriptBuilderApi.saveToLibrary(session.id, {
+ name: payload.name,
+ description: payload.description,
+ category_id: payload.category_id,
+ share_with_team: payload.share_with_team,
+ script_body: payload.script_body,
+ parameters_schema: payload.parameters_schema,
+ })
setShowSaveDialog(false)
}
@@ -190,13 +206,14 @@ export default function ScriptBuilderPage() {
/>
)}
- {/* Save dialog */}
- {showSaveDialog && session && (
- setShowSaveDialog(false)}
- onSaved={handleSaved}
/>
)}
From 00e4a16ab56718fe9385a2eca98ebb8dc97f56b3 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 05:54:48 +0000
Subject: [PATCH 33/49] feat: add 'New from Script' button to ScriptLibraryPage
Opens the ParameterizeAndSavePanel in paste mode, letting users import
raw scripts with parameter detection and review before saving to library.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/pages/ScriptLibraryPage.tsx | 64 +++++++++++++++++++++---
1 file changed, 56 insertions(+), 8 deletions(-)
diff --git a/frontend/src/pages/ScriptLibraryPage.tsx b/frontend/src/pages/ScriptLibraryPage.tsx
index 3ba882a3..c550957e 100644
--- a/frontend/src/pages/ScriptLibraryPage.tsx
+++ b/frontend/src/pages/ScriptLibraryPage.tsx
@@ -1,12 +1,15 @@
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
-import { Terminal, Settings, Wand2 } from 'lucide-react'
+import { Terminal, Settings, Wand2, FileUp } from 'lucide-react'
import { useScriptGeneratorStore } from '@/store/scriptGeneratorStore'
import { usePermissions } from '@/hooks/usePermissions'
import { ScriptFilterBar } from '@/components/scripts/ScriptFilterBar'
import { ScriptTemplateList } from '@/components/scripts/ScriptTemplateList'
import { ScriptConfigurePane } from '@/components/scripts/ScriptConfigurePane'
import { ScriptPreview } from '@/components/scripts/ScriptPreview'
+import { ParameterizeAndSavePanel } from '@/components/scripts/ParameterizeAndSavePanel'
+import { scriptsApi } from '@/api'
+import type { ScriptParameter } from '@/types'
type LibraryTab = 'mine' | 'team'
@@ -23,8 +26,35 @@ export default function ScriptLibraryPage() {
const clearOutput = useScriptGeneratorStore(s => s.clearOutput)
const selectedTemplate = useScriptGeneratorStore(s => s.selectedTemplate)
+ const categories = useScriptGeneratorStore(s => s.categories)
+
const { isEngineer } = usePermissions()
const canGenerate = isEngineer
+ const [showImportPanel, setShowImportPanel] = useState(false)
+
+ const handleImportSave = async (payload: {
+ name: string
+ description: string | undefined
+ category_id: string | undefined
+ share_with_team: boolean
+ script_body: string
+ parameters_schema: { parameters: ScriptParameter[] }
+ }) => {
+ const categoryId = payload.category_id || categories[0]?.id
+ if (!categoryId) {
+ throw new Error('No categories available. Please create a category first.')
+ }
+ await scriptsApi.createTemplate({
+ category_id: categoryId,
+ name: payload.name,
+ description: payload.description,
+ script_body: payload.script_body,
+ parameters_schema: payload.parameters_schema,
+ })
+ setShowImportPanel(false)
+ const filters = activeTab === 'mine' ? { mine: true } : { shared: true }
+ loadTemplates(filters)
+ }
useEffect(() => {
loadCategories().then(() => {
@@ -70,13 +100,23 @@ export default function ScriptLibraryPage() {
Browse PowerShell templates, fill in parameters, and generate ready-to-run scripts.
{isEngineer && (
-
-
- Manage Templates
-
+
+
+
+ Manage Templates
+
+ setShowImportPanel(true)}
+ className="inline-flex items-center gap-1.5 text-xs text-primary bg-accent-dim hover:bg-primary/15 px-2.5 py-1 rounded-full transition-colors"
+ >
+
+ New from Script
+
+
)}
)}
+
+ {/* Import script panel */}
+ {showImportPanel && (
+ setShowImportPanel(false)}
+ />
+ )}
)
}
From 0d89597fc0be9b916f8e918052b34b56f69241a0 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 05:58:39 +0000
Subject: [PATCH 34/49] chore: delete SaveToLibraryDialog, replaced by
ParameterizeAndSavePanel
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../script-builder/SaveToLibraryDialog.tsx | 186 ------------------
1 file changed, 186 deletions(-)
delete mode 100644 frontend/src/components/script-builder/SaveToLibraryDialog.tsx
diff --git a/frontend/src/components/script-builder/SaveToLibraryDialog.tsx b/frontend/src/components/script-builder/SaveToLibraryDialog.tsx
deleted file mode 100644
index b3be2145..00000000
--- a/frontend/src/components/script-builder/SaveToLibraryDialog.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import { useState, useEffect } from 'react'
-import { X, Loader2 } from 'lucide-react'
-import { cn } from '@/lib/utils'
-import { scriptBuilderApi, scriptsApi } from '@/api'
-import type { ScriptCategoryResponse } from '@/types'
-
-interface SaveToLibraryDialogProps {
- sessionId: string
- defaultName: string
- defaultDescription?: string
- onClose: () => void
- onSaved: () => void
-}
-
-export function SaveToLibraryDialog({
- sessionId,
- defaultName,
- defaultDescription,
- onClose,
- onSaved,
-}: SaveToLibraryDialogProps) {
- const [name, setName] = useState(defaultName)
- const [description, setDescription] = useState(defaultDescription || '')
- const [categoryId, setCategoryId] = useState('')
- const [shareWithTeam, setShareWithTeam] = useState(false)
- const [categories, setCategories] = useState([])
- const [isSaving, setIsSaving] = useState(false)
- const [error, setError] = useState(null)
-
- useEffect(() => {
- scriptsApi.getCategories().then(setCategories).catch(() => {})
- }, [])
-
- useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key === 'Escape') onClose()
- }
- document.addEventListener('keydown', handleKeyDown)
- return () => document.removeEventListener('keydown', handleKeyDown)
- }, [onClose])
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- if (!name.trim()) return
-
- setIsSaving(true)
- setError(null)
-
- try {
- await scriptBuilderApi.saveToLibrary(sessionId, {
- name: name.trim(),
- description: description.trim() || undefined,
- category_id: categoryId || undefined,
- share_with_team: shareWithTeam,
- })
- onSaved()
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Failed to save script. Please try again.')
- } finally {
- setIsSaving(false)
- }
- }
-
- return (
- { if (e.target === e.currentTarget) onClose() }}
- >
-
- {/* Header */}
-
-
Save to Library
-
-
-
-
-
- {/* Form */}
-
- {/* Name */}
-
-
- Name *
-
- setName(e.target.value)}
- required
- className={cn(
- "w-full rounded-lg px-3 py-2 text-sm",
- "border border-border bg-card text-foreground placeholder:text-muted-foreground",
- "focus:outline-none focus:border-[rgba(249,115,22,0.3)] transition-colors"
- )}
- placeholder="Script name"
- />
-
-
- {/* Description */}
-
-
- Description
-
- setDescription(e.target.value)}
- rows={3}
- className={cn(
- "w-full rounded-lg px-3 py-2 text-sm resize-none",
- "border border-border bg-card text-foreground placeholder:text-muted-foreground",
- "focus:outline-none focus:border-[rgba(249,115,22,0.3)] transition-colors"
- )}
- placeholder="What does this script do?"
- />
-
-
- {/* Category */}
-
-
- Category
-
- setCategoryId(e.target.value)}
- className={cn(
- "w-full rounded-lg px-3 py-2 text-sm",
- "border border-border bg-card text-foreground",
- "focus:outline-none focus:border-[rgba(249,115,22,0.3)] transition-colors"
- )}
- >
- No category
- {categories.map((cat) => (
- {cat.name}
- ))}
-
-
-
- {/* Share with team */}
-
- setShareWithTeam(e.target.checked)}
- className="w-4 h-4 rounded border-border bg-card text-orange-500 focus:ring-orange-500/20"
- />
- Share with team
-
-
- {/* Error */}
- {error && (
- {error}
- )}
-
- {/* Actions */}
-
-
- Cancel
-
-
- {isSaving && }
- {isSaving ? 'Saving...' : 'Save to Library'}
-
-
-
-
-
- )
-}
From 2c8aca395138d0cee9a7b0df2deb3507c15c0c11 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 06:03:34 +0000
Subject: [PATCH 35/49] feat: add status update generation to assistant chat
Wire StatusUpdateModal into AssistantChatPage with "Update" button in
the chat toolbar. Enhance ConcludeSessionModal pause/escalate outcomes
to offer ticket notes, client update, or email draft generation instead
of static messages.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../assistant/ConcludeSessionModal.tsx | 176 ++++++++++++++----
frontend/src/pages/AssistantChatPage.tsx | 30 ++-
2 files changed, 165 insertions(+), 41 deletions(-)
diff --git a/frontend/src/components/assistant/ConcludeSessionModal.tsx b/frontend/src/components/assistant/ConcludeSessionModal.tsx
index c32e71cd..3f7609a0 100644
--- a/frontend/src/components/assistant/ConcludeSessionModal.tsx
+++ b/frontend/src/components/assistant/ConcludeSessionModal.tsx
@@ -11,6 +11,9 @@ import {
ClipboardList,
Sparkles,
AlertTriangle,
+ FileText,
+ User,
+ Mail,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { MarkdownContent } from '@/components/ui/MarkdownContent'
@@ -76,6 +79,7 @@ export function ConcludeSessionModal({
const [error, setError] = useState(null)
const [streaming, setStreaming] = useState(false)
const [streamError, setStreamError] = useState(null)
+ const [generatingUpdate, setGeneratingUpdate] = useState(false)
const summaryRef = useRef('')
// Reset state when modal opens
@@ -90,6 +94,7 @@ export function ConcludeSessionModal({
setError(null)
setStreaming(false)
setStreamError(null)
+ setGeneratingUpdate(false)
summaryRef.current = ''
}
}, [isOpen])
@@ -142,10 +147,9 @@ export function ConcludeSessionModal({
})
},
)
- } else if (outcome === 'escalated') {
- setSummary('Session escalated. Ticket notes will be generated when the session is resolved.')
} else {
- setSummary('Session paused. Progress saved — you can resume anytime.')
+ // For paused/escalated: don't set summary yet — show status update options
+ setSummary('')
}
} catch {
setError('Failed to conclude session. Please try again.')
@@ -176,6 +180,25 @@ export function ConcludeSessionModal({
onClose()
}
+ const handleGenerateStatusUpdate = async (audience: 'ticket_notes' | 'client_update' | 'email_draft') => {
+ if (!sessionId) return
+ setGeneratingUpdate(true)
+ try {
+ const context = outcome === 'escalated' ? 'escalation' : 'status'
+ const result = await aiSessionsApi.generateStatusUpdate(sessionId, {
+ audience,
+ length: 'detailed',
+ context,
+ })
+ setSummary(result.content)
+ setCopied(false)
+ } catch {
+ setSummary('Failed to generate status update. You can copy the conversation from the chat.')
+ } finally {
+ setGeneratingUpdate(false)
+ }
+ }
+
if (!isOpen) return null
const selectedOutcome = OUTCOMES.find(o => o.value === outcome)
@@ -354,42 +377,115 @@ export function ConcludeSessionModal({
)}
- {/* Generated ticket notes */}
-
-
-
-
- Ticket Notes
-
- {streaming && (
-
- )}
-
+ {/* Resolved: streamed ticket notes */}
+ {outcome === 'resolved' && (
+
+
+
+
+ Ticket Notes
+
+ {streaming && (
+
+ )}
+
- {/* Streaming content or skeleton */}
- {summary ? (
+ {summary ? (
+
+
+
+ ) : streaming ? (
+
+ ) : streamError ? (
+
+ ) : null}
+
+ )}
+
+ {/* Paused/Escalated: status update options */}
+ {(outcome === 'paused' || outcome === 'escalated') && !summary && !generatingUpdate && (
+
+
+ {outcome === 'paused'
+ ? 'Session paused. Generate a status update to share progress.'
+ : 'Session escalated. Generate an update to document the handoff.'}
+
+
+
handleGenerateStatusUpdate('ticket_notes')}
+ className="flex w-full items-center gap-3 rounded-lg px-4 py-3 text-left transition-colors hover:bg-[var(--color-bg-elevated)]"
+ style={{ border: '1px solid var(--color-border-default)' }}
+ >
+
+
+
Ticket Notes
+
Technical, for your PSA
+
+
+
handleGenerateStatusUpdate('client_update')}
+ className="flex w-full items-center gap-3 rounded-lg px-4 py-3 text-left transition-colors hover:bg-[var(--color-bg-elevated)]"
+ style={{ border: '1px solid var(--color-border-default)' }}
+ >
+
+
+
Client Update
+
Professional, non-technical
+
+
+
handleGenerateStatusUpdate('email_draft')}
+ className="flex w-full items-center gap-3 rounded-lg px-4 py-3 text-left transition-colors hover:bg-[var(--color-bg-elevated)]"
+ style={{ border: '1px solid var(--color-border-default)' }}
+ >
+
+
+
Email Draft
+
Full email with subject line
+
+
+
+
+ )}
+
+ {/* Paused/Escalated: generating spinner */}
+ {(outcome === 'paused' || outcome === 'escalated') && generatingUpdate && (
+
+
+
Generating status update...
+
+ )}
+
+ {/* Paused/Escalated: generated result */}
+ {(outcome === 'paused' || outcome === 'escalated') && summary && !generatingUpdate && (
+
+
+
+
+ Status Update
+
+
- ) : streaming ? (
-
- ) : streamError ? (
-
- ) : null}
-
+
+ )}
)}
@@ -451,9 +547,17 @@ export function ConcludeSessionModal({
Resume in New Chat
)}
+ {(outcome === 'paused' || outcome === 'escalated') && summary && !generatingUpdate && (
+ { setSummary(''); setCopied(false) }}
+ className="flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium text-muted-foreground hover:text-foreground bg-input border border-border hover:border-border-hover transition-all"
+ >
+ Switch Format
+
+ )}
- {summary && !streaming && (
+ {summary && !streaming && !generatingUpdate && (
)}
{messages.length >= 2 && (
- setShowConclude(true)} disabled={loading} className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:text-amber-400 hover:bg-amber-400/10 transition-colors disabled:opacity-40" title="Conclude session">
-
- Conclude
-
+ <>
+ setShowStatusUpdate(true)} disabled={loading} className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:text-orange-400 hover:bg-orange-500/10 transition-colors disabled:opacity-40" title="Share status update">
+
+ Update
+
+ setShowConclude(true)} disabled={loading} className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:text-amber-400 hover:bg-amber-400/10 transition-colors disabled:opacity-40" title="Conclude session">
+
+ Conclude
+
+ >
)}
{!showTaskLane && (activeQuestions.length > 0 || activeActions.length > 0) && (
c.id === activeChatId)?.title ?? 'Chat'}
sessionId={activeChatId}
/>
+
+ {/* Status Update Modal */}
+ {activeChatId && (
+ setShowStatusUpdate(false)}
+ onGenerate={(audience, length, context) =>
+ aiSessionsApi.generateStatusUpdate(activeChatId, { audience, length, context })
+ }
+ context="status"
+ />
+ )}
>
)
From d6d1002172794c008d187438835135b4618ccf77 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 06:35:11 +0000
Subject: [PATCH 36/49] fix: add status_update to step_type CHECK constraint
The generate_status_update service inserted AISessionStep with
step_type='status_update' which violated the DB CHECK constraint,
causing a 500 error. Also fix incorrect field name confidence_score
(should be confidence_at_step) and remove nonexistent confidence_tier.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../069_add_status_update_step_type.py | 31 +++++++++++++++++++
backend/app/models/ai_session_step.py | 2 +-
backend/app/services/flowpilot_engine.py | 3 +-
3 files changed, 33 insertions(+), 3 deletions(-)
create mode 100644 backend/alembic/versions/069_add_status_update_step_type.py
diff --git a/backend/alembic/versions/069_add_status_update_step_type.py b/backend/alembic/versions/069_add_status_update_step_type.py
new file mode 100644
index 00000000..382cef9e
--- /dev/null
+++ b/backend/alembic/versions/069_add_status_update_step_type.py
@@ -0,0 +1,31 @@
+"""add status_update to ai_session_steps step_type constraint
+
+Revision ID: 069
+Revises: 068
+Create Date: 2026-03-29
+"""
+from alembic import op
+
+
+revision = "069"
+down_revision = "068"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ op.drop_constraint("ck_ai_session_steps_step_type", "ai_session_steps", type_="check")
+ op.create_check_constraint(
+ "ck_ai_session_steps_step_type", "ai_session_steps",
+ "step_type IN ('question', 'action', 'script_generation', 'verification', "
+ "'info_request', 'note', 'intake_analysis', 'fork', 'status_update')",
+ )
+
+
+def downgrade() -> None:
+ op.drop_constraint("ck_ai_session_steps_step_type", "ai_session_steps", type_="check")
+ op.create_check_constraint(
+ "ck_ai_session_steps_step_type", "ai_session_steps",
+ "step_type IN ('question', 'action', 'script_generation', 'verification', "
+ "'info_request', 'note', 'intake_analysis', 'fork')",
+ )
diff --git a/backend/app/models/ai_session_step.py b/backend/app/models/ai_session_step.py
index ac08da72..1642632b 100644
--- a/backend/app/models/ai_session_step.py
+++ b/backend/app/models/ai_session_step.py
@@ -36,7 +36,7 @@ class AISessionStep(Base):
__table_args__ = (
CheckConstraint(
"step_type IN ('question', 'action', 'script_generation', 'verification', "
- "'info_request', 'note', 'intake_analysis', 'fork')",
+ "'info_request', 'note', 'intake_analysis', 'fork', 'status_update')",
name="ck_ai_session_steps_step_type",
),
)
diff --git a/backend/app/services/flowpilot_engine.py b/backend/app/services/flowpilot_engine.py
index 7ed8b7e1..3a9a6f78 100644
--- a/backend/app/services/flowpilot_engine.py
+++ b/backend/app/services/flowpilot_engine.py
@@ -992,8 +992,7 @@ async def generate_status_update(
"generated_content": raw_response.strip(),
"client_name": client_name,
},
- confidence_score=1.0,
- confidence_tier="high",
+ confidence_at_step=1.0,
)
db.add(step)
session.step_count += 1
From d6877bb55ec6f06ed09114f3b370730cd7d37351 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 07:21:44 +0000
Subject: [PATCH 37/49] fix: correct streamDocumentation URL path to include
/v1 prefix
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The SSE stream URL was /api/ai-sessions/ but the backend mounts all
routes under /api/v1/. This caused a 404, falling through to a
non-streaming fallback that returned empty documentation for chat
sessions. This is why "Conclude → Resolved" never showed ticket notes.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/api/aiSessions.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/api/aiSessions.ts b/frontend/src/api/aiSessions.ts
index 2877bbb7..59d82e24 100644
--- a/frontend/src/api/aiSessions.ts
+++ b/frontend/src/api/aiSessions.ts
@@ -110,7 +110,7 @@ export const aiSessionsApi = {
try {
const response = await fetch(
- `${baseUrl}/api/ai-sessions/${sessionId}/documentation/stream`,
+ `${baseUrl}/api/v1/ai-sessions/${sessionId}/documentation/stream`,
{
headers: {
Authorization: `Bearer ${token}`,
From a705bd58f9b4d41da795d376911c4f9a9f870ed5 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 07:28:35 +0000
Subject: [PATCH 38/49] fix: clear stale task lane when starting session from
dashboard prefill
The sendPrefill flow (dashboard handoff) did not clear activeQuestions/
activeActions before creating the new session. The task lane initializer
loaded stale data from sessionStorage keyed to the previous session ID,
showing old tasks while the new session was processing.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/pages/AssistantChatPage.tsx | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx
index 38fadb56..dc6fcb7b 100644
--- a/frontend/src/pages/AssistantChatPage.tsx
+++ b/frontend/src/pages/AssistantChatPage.tsx
@@ -120,6 +120,11 @@ export default function AssistantChatPage() {
navigate(location.pathname, { replace: true, state: {} })
const sendPrefill = async () => {
+ // Clear stale task lane from previous session
+ setShowTaskLane(false)
+ setActiveQuestions([])
+ setActiveActions([])
+
try {
const session = await aiSessionsApi.createChatSession({
intake_type: 'free_text',
From bc6afbc90a2158ce00e29f6c3b171315fc117e55 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 07:32:14 +0000
Subject: [PATCH 39/49] fix: use status update API as fallback for resolve
ticket notes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The non-streaming fallback used getDocumentation which relies on
session.steps — empty for chat sessions, producing only the bare
resolution_summary text. Switch fallback to generateStatusUpdate
which reads conversation_messages and generates proper context-aware
ticket notes for both chat and guided sessions.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../src/components/assistant/ConcludeSessionModal.tsx | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/frontend/src/components/assistant/ConcludeSessionModal.tsx b/frontend/src/components/assistant/ConcludeSessionModal.tsx
index 3f7609a0..7fa87ad3 100644
--- a/frontend/src/components/assistant/ConcludeSessionModal.tsx
+++ b/frontend/src/components/assistant/ConcludeSessionModal.tsx
@@ -135,10 +135,13 @@ export function ConcludeSessionModal({
(err) => {
setStreaming(false)
setStreamError(err)
- // Try non-streaming fallback
- aiSessionsApi.getDocumentation(sessionId).then((doc) => {
- const fallback = `## Problem Summary\n${doc.problem_summary}\n\n## Steps Taken\n${doc.diagnostic_steps.map(s => `- ${s.description}`).join('\n')}\n\n## Resolution\n${doc.resolution_summary || 'See conversation'}\n\n## Next Steps\nNone`
- setSummary(fallback)
+ // Fallback: use status update API which works with conversation context
+ aiSessionsApi.generateStatusUpdate(sessionId, {
+ audience: 'ticket_notes',
+ length: 'detailed',
+ context: 'resolution',
+ }).then((result) => {
+ setSummary(result.content)
setStreamError(null)
}).catch(() => {
if (!summaryRef.current) {
From d5122123c2ce08f6651e051868c230ea6275d7bc Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 12:53:16 +0000
Subject: [PATCH 40/49] fix: preserve task lane answers across page reload and
browser close
Two issues fixed:
1. TaskLane useEffect on [questions, actions] was resetting all tasks
to pending with empty values, wiping restored user answers. Now
checks sessionStorage for saved state before resetting.
2. selectChat was setting activeQuestions/activeActions before writing
responses to sessionStorage, causing a race where TaskLane mounted
with new props but empty sessionStorage. Now writes responses to
sessionStorage first so TaskLane can restore them on prop change.
The backend saveTaskLane debounce (2s) persists responses to the DB,
and selectChat restores them via pending_task_lane.responses. This
chain now survives browser close.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/components/assistant/TaskLane.tsx | 12 ++++++++++--
frontend/src/pages/AssistantChatPage.tsx | 9 +++++----
2 files changed, 15 insertions(+), 6 deletions(-)
diff --git a/frontend/src/components/assistant/TaskLane.tsx b/frontend/src/components/assistant/TaskLane.tsx
index e361b66e..152890fc 100644
--- a/frontend/src/components/assistant/TaskLane.tsx
+++ b/frontend/src/components/assistant/TaskLane.tsx
@@ -147,8 +147,16 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
return () => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current) }
}, [sessionId, tasks]) // eslint-disable-line react-hooks/exhaustive-deps
- // Reset when new tasks come in from AI response
+ // Reset when new tasks come in from AI response — but preserve saved state
useEffect(() => {
+ if (sessionId) {
+ const saved = loadTaskState(sessionId)
+ if (saved && saved.length > 0) {
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: syncs derived state from prop changes
+ setTasks(saved)
+ return
+ }
+ }
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: syncs derived state from prop changes
setTasks([
...questions.map((q): QuestionResponse => ({
@@ -158,7 +166,7 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
type: 'action', label: a.label, command: a.command, description: a.description, state: 'pending', value: '',
})),
])
- }, [questions, actions])
+ }, [questions, actions]) // eslint-disable-line react-hooks/exhaustive-deps
const updateTask = (idx: number, updates: Partial) => {
setTasks(prev => prev.map((t, i) => i === idx ? { ...t, ...updates } as TaskResponse : t))
diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx
index dc6fcb7b..ad6d41c1 100644
--- a/frontend/src/pages/AssistantChatPage.tsx
+++ b/frontend/src/pages/AssistantChatPage.tsx
@@ -232,16 +232,17 @@ export default function AssistantChatPage() {
const q = detail.pending_task_lane.questions || []
const a = detail.pending_task_lane.actions || []
if (q.length > 0 || a.length > 0) {
- setActiveQuestions(q)
- setActiveActions(a)
- setShowTaskLane(true)
- // Pre-load user's saved responses into sessionStorage so TaskLane restores them
+ // Pre-load user's saved responses into sessionStorage BEFORE setting props
+ // so TaskLane can restore them on mount/prop-change
const responses = (detail.pending_task_lane as Record).responses as unknown[] | undefined
if (responses && responses.length > 0) {
try {
sessionStorage.setItem(`rf-tasklane-state:${chatId}`, JSON.stringify(responses))
} catch { /* ignore */ }
}
+ setActiveQuestions(q)
+ setActiveActions(a)
+ setShowTaskLane(true)
}
}
} catch {
From eafc3752a046490899cac27ad99c99c5d961d6e9 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 15:45:41 +0000
Subject: [PATCH 41/49] =?UTF-8?q?feat:=20update=20design=20system=20v5?=
=?UTF-8?q?=E2=86=92v6,=20accent=20blue=20replaces=20ember=20orange?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Accent: #f97316 (orange) → #60a5fa/#2563eb (electric blue)
- Info: new cyan (#67e8f9/#0891b2) since blue took the accent slot
- Warning: #eab308 (yellow) → #fbbf24/#d97706 (amber reclaimed)
- Surfaces: deeper charcoal range for better layer separation
- Full light mode semantic color variants specified
- Based on competitive research: no MSP tool uses this blue register
Co-Authored-By: Claude Opus 4.6 (1M context)
---
CLAUDE.md | 4 +-
DESIGN-SYSTEM.md | 111 +++++++++++++++++++++++++++--------------------
2 files changed, 67 insertions(+), 48 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index d87335e8..25af73c8 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -21,7 +21,7 @@
- **Design system:** [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) — THE source of truth for all design decisions
- **Design aesthetic:** Flat, high-contrast dark theme (Sentry/PostHog-inspired). No glass morphism, no gradients on surfaces, no ambient effects. Light mode planned.
-- **Accent color:** Ember orange (#f97316 / #ea580c). Used sparingly — ≤5% of the UI. Warning is yellow (#eab308), not amber, to stay distinct from accent.
+- **Accent color:** Electric blue (#60a5fa dark / #2563eb light). Used sparingly — ≤5% of the UI. Warning is amber (#fbbf24), info is cyan (#67e8f9).
- **Fonts:** IBM Plex Sans (`font-sans`, body), Bricolage Grotesque (`font-heading`, headings), JetBrains Mono (`font-mono`, code) — loaded via Google Fonts
- **Logo:** 30px gradient square (ember orange) + "ResolutionFlow" in Bricolage Grotesque 700
- **Layout:** Icon rail sidebar (72px default) with hover flyout panels. Pinnable to full 260px sidebar. See [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md)
@@ -342,7 +342,7 @@ gh run view --json jobs --jq '.jobs[] | {name: .name, conclusion: .conclusi
**87. FlowPilot must ask GUI vs script preference:** When a task can be done via GUI or script (e.g., creating AD users), FlowPilot must ask the engineer which approach they prefer BEFORE suggesting either. Never assume the user wants a script. See `FLOWPILOT_SYSTEM_PROMPT` rules in `flowpilot_engine.py`.
-**88. Charcoal palette — sidebar-darkest approach:** Sidebar `#10121a`, page `#1a1c23`, cards `#22252e`, borders `#2e3240`. This gives more contrast range than true-dark (`#0c0d10`). All colors via CSS variables in `index.css` `@theme` block. Accent is ember orange (#f97316), not cyan.
+**88. Charcoal palette — sidebar-darkest approach:** Sidebar `#0e1016`, page `#16181f`, cards `#1e2028`, borders `#2a2e3a`. This gives more contrast range than true-dark. All colors via CSS variables in `index.css` `@theme` block. Accent is electric blue (#60a5fa), not orange or cyan.
**92. `tsc -b` in Dockerfile is stricter than `npx tsc --noEmit`:** The production build (`tsc -b && vite build`) enforces `noUnusedLocals` and `noUnusedParameters` as hard errors. After any refactor that moves logic between components or removes features, trace every import and destructured prop to remove orphans. IDE warnings (yellow squiggles) flag these — check them before pushing.
diff --git a/DESIGN-SYSTEM.md b/DESIGN-SYSTEM.md
index e7808b01..04d0fe58 100644
--- a/DESIGN-SYSTEM.md
+++ b/DESIGN-SYSTEM.md
@@ -1,16 +1,16 @@
-# ResolutionFlow Design System v5
+# ResolutionFlow Design System v6
> **Status:** ACTIVE — This document is the single source of truth for all frontend design decisions.
-> **Supersedes:** All previous design system docs including `DESIGN_SYSTEM_GUIDE.md`, `UI-DESIGN-SYSTEM.md`, `REBRAND-IMPLEMENTATION-GUIDE.md`, and any `COMPONENT_EXAMPLES.md` files. Also supersedes the v4 cyan accent system.
-> **Last Updated:** March 24, 2026
+> **Supersedes:** All previous design system docs including v5 (ember orange accent), `DESIGN_SYSTEM_GUIDE.md`, `UI-DESIGN-SYSTEM.md`, `REBRAND-IMPLEMENTATION-GUIDE.md`, and any `COMPONENT_EXAMPLES.md` files.
+> **Last Updated:** March 29, 2026
---
## Design Philosophy
-ResolutionFlow uses a **flat, high-contrast dark theme** inspired by Sentry and PostHog. The aesthetic is premium, clean, and minimal — no glass morphism, no backdrop blur, no ambient orbs, no gradient backgrounds on surfaces. The accent color (ember orange) appears in ≤5% of the UI. Warm accent on cold charcoal surfaces creates high contrast and distinctive personality — orange conveys urgency and action, fitting for a troubleshooting tool. Every design decision prioritizes **readability over decoration**.
+ResolutionFlow uses a **flat, high-contrast dark theme** inspired by Sentry and PostHog. The aesthetic is premium, clean, and minimal — no glass morphism, no backdrop blur, no ambient orbs, no gradient backgrounds on surfaces. The accent color (electric blue) appears in ≤5% of the UI. Cool blue accent on deep charcoal surfaces creates high contrast and technical confidence — blue conveys trust, precision, and reliability, fitting for a troubleshooting tool that MSP engineers depend on during outages. Every design decision prioritizes **readability over decoration**.
-**Light mode** is a planned addition (dark/light toggle). Design all components with CSS variables so theming is a variable swap, not a rewrite.
+**Dual-mode design:** Dark mode is the default. Light mode is fully specified — all colors use CSS custom properties so theming is a variable swap, not a rewrite. Semantic colors shift to darker variants (Tailwind 400→600) in light mode to maintain WCAG AA contrast.
---
@@ -18,16 +18,17 @@ ResolutionFlow uses a **flat, high-contrast dark theme** inspired by Sentry and
All colors are defined as CSS custom properties in `index.css` inside the `@theme` block (Tailwind v4) or `:root` / `.dark` blocks (Tailwind v3).
-### Dark Mode (Default) — Charcoal Palette
+### Dark Mode (Default) — Deep Charcoal Palette
```
-Page background: #1a1c23
-Sidebar background: #10121a
-Card background: #22252e
-Card hover: #282b35
-Input background: #282b35
-Code background: #14161e
-Elevated surface: #2e3140
+Page background: #16181f
+Sidebar background: #0e1016
+Card background: #1e2028
+Card hover: #252830
+Input background: #252830
+Code background: #12141a
+Elevated surface: #2a2d38
+Surface raised: #303442
Text primary: #e2e5eb
Text heading: #f0f2f5
@@ -35,25 +36,26 @@ Text secondary: #848b9b
Text muted: #4f5666
Text rail label: #e2e5eb
-Border default: #2e3240
+Border default: #2a2e3a
Border hover: #3d4252
-Accent (ember): #f97316
-Accent hover: #ea580c
-Accent dim (10%): rgba(249,115,22,0.10)
-Accent text: #fdba74
+Accent (blue): #60a5fa
+Accent hover: #3b82f6
+Accent dim (10%): rgba(96,165,250,0.10)
+Accent text: #93c5fd
+Accent glow (15%): rgba(96,165,250,0.15)
Success: #34d399
Success dim: rgba(52,211,153,0.10)
-Warning: #eab308
-Warning dim: rgba(234,179,8,0.10)
+Warning (amber): #fbbf24
+Warning dim: rgba(251,191,36,0.10)
Danger: #f87171
Danger dim: rgba(248,113,113,0.10)
+Info (cyan): #67e8f9
+Info dim: rgba(103,232,249,0.10)
```
-> **Note:** Warning shifted from amber (#fbbf24) to yellow (#eab308) to maintain clear separation from the orange accent.
-
-### Light Mode (Planned)
+### Light Mode
```
Page background: #f3f4f7
@@ -63,6 +65,7 @@ Card hover: #f8f9fb
Input background: #eef0f4
Code background: #f5f6f9
Elevated surface: #e8eaef
+Surface raised: #dde0e7
Text primary: #1a1d24
Text heading: #0d0f13
@@ -72,9 +75,20 @@ Text muted: #8b92a1
Border default: #dde0e7
Border hover: #c5c9d3
-Accent: #ea580c
-Accent dim: rgba(234,88,12,0.07)
-Accent text: #c2410c
+Accent: #2563eb
+Accent hover: #1d4ed8
+Accent dim: rgba(37,99,235,0.07)
+Accent text: #1d4ed8
+Accent glow: rgba(37,99,235,0.10)
+
+Success: #059669
+Success dim: rgba(5,150,105,0.07)
+Warning: #d97706
+Warning dim: rgba(217,119,6,0.07)
+Danger: #dc2626
+Danger dim: rgba(220,38,38,0.07)
+Info: #0891b2
+Info dim: rgba(8,145,178,0.07)
```
### What NOT To Use
@@ -83,8 +97,7 @@ Accent text: #c2410c
- No gradient backgrounds on cards or surfaces
- No ambient orbs or floating glow elements
- No `bg-white/[0.04]` opacity-based backgrounds
-- No purple gradient accent (`#818cf8 → #a78bfa`) — this is deprecated
-- No cyan accent (`#22d3ee` / `#06b6d4` / `#67e8f9`) — replaced by ember orange in v5
+- No ember orange accent (`#f97316` / `#ea580c` / `#fdba74`) — replaced by blue in v6
- No `text-gradient-brand` utility — replaced by solid `accent-text` color
- No `glass-card`, `glass-stat`, `glass-card-glow` CSS utilities
@@ -173,7 +186,7 @@ The default navigation is a narrow icon rail (72px) with:
```
Background: bg-card
Border: 1px solid border-default
-Border-left: 3px solid [varies by position - accent, success, warning, accent]
+Border-left: 3px solid [varies by position - accent, success, warning, info]
Border-radius: 8px
Padding: 18px 16px
```
@@ -218,10 +231,11 @@ Border-radius: 20px (pill)
| Type | Background | Text Color |
|------|-----------|------------|
-| Info/Accent | accent-dim | accent-text |
+| Accent | accent-dim | accent-text |
| Success | success-dim | success |
| Warning | warning-dim | warning |
| Danger | danger-dim | danger |
+| Info | info-dim | info |
### Form Inputs
@@ -239,14 +253,14 @@ Placeholder: text-muted
**Primary:**
```
-Background: accent (#f97316)
+Background: accent (#60a5fa dark / #2563eb light)
Color: #fff
Border: none
Border-radius: 5px
Padding: 9px 16px
Font: 13px, 550 weight
-Hover: accent-hover (#ea580c), box-shadow 0 2px 12px rgba(249,115,22,0.25), translateY(-1px)
-Active: translateY(0), box-shadow 0 0 4px rgba(249,115,22,0.15)
+Hover: accent-hover (#3b82f6 dark / #1d4ed8 light), box-shadow 0 2px 12px accent-glow, translateY(-1px)
+Active: translateY(0), box-shadow 0 0 4px accent-glow
```
**Ghost:**
@@ -260,7 +274,7 @@ Hover: bg-elevated, text-primary, border-hover
### Code Blocks
```
-Background: bg-code (#0e1017)
+Background: bg-code (#12141a)
Border: 1px solid border-default
Border-radius: 8px
Padding: 18px 20px
@@ -275,7 +289,7 @@ Font: JetBrains Mono, 12px, line-height 1.7
| Keyword | #c792ea |
| Function/Cmdlet | #82aaff |
| String | #c3e88d |
-| Variable | #89ddff |
+| Variable | #93c5fd (accent-text) |
| Parameter | #8c93a4 |
| Number | #f78c6c |
@@ -293,7 +307,7 @@ Color: accent-text
## Logo
-- **Mark:** 30-32px square, border-radius 8px, `linear-gradient(135deg, #ea580c, #f97316)`, white lightning bolt SVG
+- **Mark:** 30-32px square, border-radius 8px, `linear-gradient(135deg, #3b82f6, #60a5fa)`, white lightning bolt SVG
- **Wordmark:** "ResolutionFlow" in Bricolage Grotesque, 16-17px, weight 700, text-heading color
- **Combined:** Mark + wordmark horizontally, 10px gap
@@ -329,21 +343,21 @@ Shadows communicate **interaction state**, not decoration. On dark backgrounds,
**Resting state:** No shadows. Elements are flat with 1px borders.
-**Elevation on dark backgrounds (the principle):** Instead of shadow = darker, elevation = lighter. A "raised" element gets a brighter surface color (`bg-elevated` / `#2e3140`) and optionally a very faint orange glow. This creates perceived depth through contrast.
+**Elevation on dark backgrounds (the principle):** Instead of shadow = darker, elevation = lighter. A "raised" element gets a brighter surface color (`bg-elevated` / `#2a2d38` or `bg-raised` / `#303442`) and optionally a very faint blue glow. This creates perceived depth through contrast.
**Hover state (buttons):** Lift effect with accent glow.
-- Primary button hover: `0 2px 12px rgba(249,115,22,0.25)` + `translateY(-1px)` — orange glow
+- Primary button hover: `0 2px 12px rgba(96,165,250,0.25)` + `translateY(-1px)` — blue glow
- Ghost button hover: brighter border (`border-hover`) + `translateY(-1px)`, no shadow
- Active/click: glow fades, element "presses down" to `translateY(0)`
**Active/selected state (tabs, toggles):** Elevated surface + faint accent glow.
-- Active tab: `bg-elevated` + `box-shadow: 0 1px 4px rgba(249,115,22,0.08)` — class: `tab-active-shadow`
+- Active tab: `bg-elevated` + `box-shadow: 0 1px 4px rgba(96,165,250,0.08)` — class: `tab-active-shadow`
**Card hover lift (optional):** For clickable cards.
-- Hover: brighter border + `0 2px 8px rgba(249,115,22,0.06)` + `translateY(-2px)` — class: `card-lift`
+- Hover: brighter border + `0 2px 8px rgba(96,165,250,0.06)` + `translateY(-2px)` — class: `card-lift`
**Overlays:** Flyouts, dropdowns, modals get stronger shadows (they overlay lighter content).
@@ -356,7 +370,7 @@ Shadows communicate **interaction state**, not decoration. On dark backgrounds,
- No `rgba(0,0,0,...)` shadows on resting elements (invisible on dark bg)
- No permanent decorative shadows
- No heavy glow effects — accent glow should be barely perceptible (≤ 0.1 opacity)
-- No cyan glow effects — all accent glows use orange rgba(249,115,22,...)
+- No orange glow effects — all accent glows use blue rgba(96,165,250,...)
---
@@ -407,7 +421,8 @@ These files contain outdated design information and should be ignored:
- `REBRAND-IMPLEMENTATION-GUIDE.md` — Old purple rebrand from Patherly
- `COMPONENT_EXAMPLES.md` — Old monochrome component patterns
- Any file referencing `glass-card`, `glass-stat`, `bg-gradient-brand`, or `text-gradient-brand`
-- Any code using cyan accent values (`#22d3ee`, `#06b6d4`, `#67e8f9`, `rgba(34,211,238,...)`) — migrate to ember orange
+- Any code using ember orange accent values (`#f97316`, `#ea580c`, `#fdba74`, `rgba(249,115,22,...)`) — migrate to blue accent
+- Any code using old cyan accent values (`#22d3ee`, `#06b6d4`) for accent purposes — cyan is now the info color only
---
@@ -415,7 +430,11 @@ These files contain outdated design information and should be ignored:
| Date | Decision | Rationale |
|------|----------|-----------|
-| 2026-03-24 | Accent color changed from cyan (#22d3ee) to ember orange (#f97316) | Cyan caused contrast issues, felt generic "tech SaaS". Orange provides warmth against cold charcoal, conveys urgency fitting for troubleshooting, and is distinctive in the MSP tool space. |
-| 2026-03-24 | Warning color shifted from amber (#fbbf24) to yellow (#eab308) | Orange accent would clash with amber warning. Yellow provides clear semantic separation from the orange accent. |
-| 2026-03-24 | Light mode accent set to #ea580c (orange-600) | Darker orange variant ensures proper contrast on white/light surfaces. |
-| 2026-03-24 | Synced DESIGN-SYSTEM.md to actual charcoal palette | Doc was behind — still showed pre-charcoal values (#0c0d10 page, #14161d card). Updated to match index.css (#1a1c23 page, #22252e card). |
+| 2026-03-24 | Accent color changed from cyan (#22d3ee) to ember orange (#f97316) | Cyan caused contrast issues, felt generic "tech SaaS". Orange provides warmth against cold charcoal. |
+| 2026-03-24 | Warning color shifted from amber (#fbbf24) to yellow (#eab308) | Orange accent would clash with amber warning. |
+| 2026-03-24 | Synced DESIGN-SYSTEM.md to actual charcoal palette | Doc was behind — updated to match index.css. |
+| 2026-03-29 | Accent changed from ember orange (#f97316) to electric blue (#60a5fa/#2563eb) | Orange did not test well with users — read as "budget/cautionary." Blue conveys trust and technical confidence. Electric blue is brighter than the corporate blues used by NinjaOne/ConnectWise/Datto, maintaining differentiation. Works on both dark and light backgrounds. |
+| 2026-03-29 | Info color set to cyan (#67e8f9/#0891b2) | Blue accent freed the info slot. Cyan is visually distinct from accent blue (cooler/lighter) and intuitive for informational contexts. |
+| 2026-03-29 | Warning reclaimed as amber (#fbbf24/#d97706) | Yellow (#eab308) was only needed to avoid clashing with orange accent. With blue accent, amber is the natural warning color again. |
+| 2026-03-29 | Surfaces deepened for better layer separation | Page: #1a1c23→#16181f, Card: #22252e→#1e2028, Sidebar: #10121a→#0e1016. Wider range gives 4+ clear visual depth layers instead of 2. Added surface-raised (#303442) for hover/active states. |
+| 2026-03-29 | Full light mode semantic colors specified | All semantic colors get light-mode variants at Tailwind 600 level (dark uses 400). Ensures WCAG AA contrast on white/light surfaces. |
From 5c9772f66af8057121ec86f7cc9a5c1c471407cb Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 15:53:19 +0000
Subject: [PATCH 42/49] =?UTF-8?q?docs:=20add=20color=20migration=20v5?=
=?UTF-8?q?=E2=86=92v6=20spec?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Maps every color token from old values to new, identifies all migration
layers (CSS vars, hardcoded hex, Tailwind classes, config).
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../2026-03-29-color-migration-v6-design.md | 111 ++++++++++++++++++
1 file changed, 111 insertions(+)
create mode 100644 docs/superpowers/specs/2026-03-29-color-migration-v6-design.md
diff --git a/docs/superpowers/specs/2026-03-29-color-migration-v6-design.md b/docs/superpowers/specs/2026-03-29-color-migration-v6-design.md
new file mode 100644
index 00000000..dbff4cba
--- /dev/null
+++ b/docs/superpowers/specs/2026-03-29-color-migration-v6-design.md
@@ -0,0 +1,111 @@
+# Color Migration v5→v6 — Design Spec
+
+> **Date:** 2026-03-29
+> **Status:** Approved
+> **Scope:** Migrate all color values from design system v5 (ember orange accent) to v6 (electric blue accent, cyan info, amber warning, deeper surfaces)
+
+---
+
+## Problem
+
+DESIGN-SYSTEM.md was updated to v6 with new color values, but the actual CSS variables and hardcoded color references in the codebase still use v5 values. The UI still renders with ember orange accent, old surface depths, yellow warnings, and no info color.
+
+## Color Mapping
+
+### Accent (orange → blue)
+
+| Token | v5 (old) | v6 dark | v6 light |
+|-------|----------|---------|----------|
+| accent | #f97316 | #60a5fa | #2563eb |
+| accent-hover | #ea580c | #3b82f6 | #1d4ed8 |
+| accent-dim | rgba(249,115,22,0.10) | rgba(96,165,250,0.10) | rgba(37,99,235,0.07) |
+| accent-text | #fdba74 | #93c5fd | #1d4ed8 |
+| accent-glow | — | rgba(96,165,250,0.15) | rgba(37,99,235,0.10) |
+
+### Surfaces (deeper charcoal)
+
+| Token | v5 (old) | v6 |
+|-------|----------|-----|
+| page | #1a1c23 | #16181f |
+| sidebar | #10121a | #0e1016 |
+| card | #22252e | #1e2028 |
+| card-hover | #282b35 | #252830 |
+| input | #282b35 | #252830 |
+| code | #14161e | #12141a |
+| elevated | #2e3140 | #2a2d38 |
+| surface-raised (NEW) | — | #303442 |
+| border-default | #2e3240 | #2a2e3a |
+
+### Warning (yellow → amber)
+
+| Token | v5 (old) | v6 dark | v6 light |
+|-------|----------|---------|----------|
+| warning | #eab308 | #fbbf24 | #d97706 |
+| warning-dim | rgba(234,179,8,0.10) | rgba(251,191,36,0.10) | rgba(217,119,6,0.07) |
+
+### Info (NEW — cyan)
+
+| Token | v6 dark | v6 light |
+|-------|---------|----------|
+| info | #67e8f9 | #0891b2 |
+| info-dim | rgba(103,232,249,0.10) | rgba(8,145,178,0.07) |
+
+### Semantic (light mode variants added)
+
+| Token | v6 dark (unchanged) | v6 light (new) |
+|-------|---------------------|----------------|
+| success | #34d399 | #059669 |
+| success-dim | rgba(52,211,153,0.10) | rgba(5,150,105,0.07) |
+| danger | #f87171 | #dc2626 |
+| danger-dim | rgba(248,113,113,0.10) | rgba(220,38,38,0.07) |
+
+## Migration Layers
+
+### Layer 1: CSS Custom Properties (`index.css`)
+
+The `@theme` block in `frontend/src/index.css` defines all design tokens. Updating these cascades to ~80% of the codebase automatically via Tailwind utility classes (`bg-accent`, `text-primary`, `border-default`, etc.).
+
+**Changes:** Swap all color values per the mapping above. Add `info` and `info-dim` tokens. Add `surface-raised` token. Add light mode accent/semantic variants.
+
+### Layer 2: Hardcoded Hex Values
+
+Files that bypass CSS variables with inline hex colors:
+- `frontend/src/components/common/BrandLogo.tsx` — logo gradient (`#ea580c`, `#f97316`)
+- `frontend/src/pages/LandingPage.tsx` — inline styles with `#f97316`
+- `frontend/src/constants/categoryColors.ts` — color array with `#ea580c`, `#f97316`
+- `frontend/src/styles/landing.css` (if exists) — orange rgba values in shadows/glows
+- Any component with `rgba(249,115,22,...)` inline
+
+### Layer 3: Tailwind Color Classes
+
+~21 files using `orange-500`, `orange-600`, `orange-400` Tailwind classes. Replace with `blue-400`, `blue-500`, `blue-600` equivalents.
+
+### Layer 4: Configuration
+
+- `frontend/index.html` — `meta theme-color` value
+- Any PWA manifest colors
+
+## File Changes
+
+### Primary
+- `frontend/src/index.css` — CSS custom properties (the big one)
+- `frontend/index.html` — meta theme-color
+
+### Hardcoded Colors
+- `frontend/src/components/common/BrandLogo.tsx` — logo gradient
+- `frontend/src/pages/LandingPage.tsx` — inline styles
+- `frontend/src/constants/categoryColors.ts` — color array
+- Any landing.css / component CSS files with hardcoded orange
+
+### Tailwind Classes (grep for orange-400/500/600)
+- ~21 component files — mechanical find-and-replace
+
+### Warning Yellow
+- Any files with hardcoded `#eab308` — replace with `#fbbf24`
+
+## Out of Scope
+
+- Typography changes (none in v6)
+- Layout/spacing changes (none in v6)
+- Component structure changes (none in v6)
+- Adding the light mode toggle UI (separate feature)
From 98d50ff31a6f25f8cdc5523351d6609051a67629 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 16:13:56 +0000
Subject: [PATCH 43/49] docs: add color migration v6 implementation plan
7-task plan: CSS custom properties, rgba replacements, Tailwind class
migration, hardcoded hex values, landing page, config, and verification.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../plans/2026-03-29-color-migration-v6.md | 461 ++++++++++++++++++
1 file changed, 461 insertions(+)
create mode 100644 docs/superpowers/plans/2026-03-29-color-migration-v6.md
diff --git a/docs/superpowers/plans/2026-03-29-color-migration-v6.md b/docs/superpowers/plans/2026-03-29-color-migration-v6.md
new file mode 100644
index 00000000..ff2e4db8
--- /dev/null
+++ b/docs/superpowers/plans/2026-03-29-color-migration-v6.md
@@ -0,0 +1,461 @@
+# Color Migration v5→v6 Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Migrate all color values in the frontend from design system v5 (ember orange accent, shallow surfaces, yellow warning) to v6 (electric blue accent, deeper charcoal surfaces, amber warning, cyan info).
+
+**Architecture:** Layer 1 updates CSS custom properties in `index.css` (cascades to ~80% of usage). Layer 2 does find-and-replace on hardcoded `rgba(249,115,22,...)` focus border patterns across ~40 component files. Layer 3 replaces `orange-*` Tailwind classes. Layer 4 updates hardcoded hex values in specific files (BrandLogo, EmptyStateIllustrations, landing page, etc.). Each layer commits independently.
+
+**Tech Stack:** React 19, Tailwind CSS v4, CSS custom properties
+
+**Spec:** `docs/superpowers/specs/2026-03-29-color-migration-v6-design.md`
+
+---
+
+### Task 1: Update CSS Custom Properties in `index.css`
+
+This is the highest-impact change — updating the `@theme` block cascades to every component using Tailwind utility classes.
+
+**Files:**
+- Modify: `frontend/src/index.css`
+
+- [ ] **Step 1: Update surface colors in the @theme block**
+
+In `frontend/src/index.css`, replace lines 11-18 (the surface color block) with:
+
+```css
+ /* ── Surface colors (Deep Charcoal — sidebar darkest) ─ */
+ --color-bg-page: #16181f;
+ --color-bg-sidebar: #0e1016;
+ --color-bg-card: #1e2028;
+ --color-bg-card-hover: #252830;
+ --color-bg-input: #252830;
+ --color-bg-code: #12141a;
+ --color-bg-elevated: #2a2d38;
+ --color-bg-raised: #303442;
+```
+
+- [ ] **Step 2: Update border colors**
+
+Replace lines 28-29 (border colors) with:
+
+```css
+ --color-border-default: #2a2e3a;
+ --color-border-hover: #3d4252;
+```
+
+- [ ] **Step 3: Update accent colors**
+
+Replace lines 31-35 (accent block) with:
+
+```css
+ /* ── Accent (electric blue) ───────────────────────── */
+ --color-accent: #60a5fa;
+ --color-accent-hover: #3b82f6;
+ --color-accent-dim: rgba(96,165,250,0.10);
+ --color-accent-text: #93c5fd;
+```
+
+- [ ] **Step 4: Update semantic colors (warning + add info)**
+
+Replace lines 37-43 (semantic block) with:
+
+```css
+ /* ── Semantic colors ───────────────────────────── */
+ --color-success: #34d399;
+ --color-success-dim: rgba(52,211,153,0.10);
+ --color-warning: #fbbf24;
+ --color-warning-dim: rgba(251,191,36,0.10);
+ --color-danger: #f87171;
+ --color-danger-dim: rgba(248,113,113,0.10);
+ --color-info: #67e8f9;
+ --color-info-dim: rgba(103,232,249,0.10);
+```
+
+- [ ] **Step 5: Update Tailwind semantic mappings**
+
+Replace lines 46-64 (Tailwind mappings) with:
+
+```css
+ /* ── Tailwind semantic mappings ─────────────────── */
+ --color-background: #16181f;
+ --color-foreground: #e2e5eb;
+ --color-card: #1e2028;
+ --color-card-foreground: #e2e5eb;
+ --color-popover: #1e2028;
+ --color-popover-foreground: #e2e5eb;
+ --color-primary: #60a5fa;
+ --color-primary-foreground: #ffffff;
+ --color-secondary: #2a2d38;
+ --color-secondary-foreground: #e2e5eb;
+ --color-muted: #2a2d38;
+ --color-muted-foreground: #848b9b;
+ --color-accent-tw: #2a2d38;
+ --color-accent-foreground: #e2e5eb;
+ --color-destructive: #f87171;
+ --color-destructive-foreground: #ffffff;
+ --color-border: #2a2e3a;
+ --color-input: #252830;
+ --color-ring: #60a5fa;
+```
+
+- [ ] **Step 6: Update orange rgba in utility classes**
+
+Replace the `btn-primary-v4` hover/active box-shadow values (lines 194-200):
+
+```css
+ &:hover {
+ filter: brightness(1.1);
+ box-shadow: 0 2px 10px rgba(96, 165, 250, 0.2);
+ transform: translateY(-1px);
+ }
+ &:active {
+ box-shadow: 0 0 4px rgba(96, 165, 250, 0.1);
+ transform: translateY(0);
+ }
+```
+
+Replace `tab-active-shadow` (line 229):
+
+```css
+ box-shadow: 0 1px 4px rgba(96, 165, 250, 0.08);
+```
+
+Replace `card-lift` hover box-shadow (line 237):
+
+```css
+ box-shadow: 0 2px 8px rgba(96, 165, 250, 0.06);
+```
+
+- [ ] **Step 7: Verify it compiles**
+
+Run: `cd /home/coder/resolutionflow/frontend && npx tsc --noEmit --pretty 2>&1 | head -10`
+
+Expected: No errors.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add frontend/src/index.css
+git commit -m "feat: migrate index.css to design system v6 color tokens
+
+Accent: orange (#f97316) → blue (#60a5fa)
+Surfaces: deeper charcoal range (#16181f page, #1e2028 card)
+Warning: yellow (#eab308) → amber (#fbbf24)
+Info: new cyan (#67e8f9) token added
+All Tailwind semantic mappings updated to match.
+
+Co-Authored-By: Claude Opus 4.6 (1M context) "
+```
+
+---
+
+### Task 2: Replace hardcoded `rgba(249,115,22,...)` focus borders across all components
+
+~40 files use `focus:border-[rgba(249,115,22,0.3)]` or similar patterns for input focus states. This is a mechanical find-and-replace.
+
+**Files:** All files listed in the grep output for `rgba(249,115,22` — approximately 40 component and page files.
+
+- [ ] **Step 1: Replace all `rgba(249,115,22,` with `rgba(96,165,250,` across the codebase**
+
+Run this command to do the replacement:
+
+```bash
+cd /home/coder/resolutionflow/frontend/src
+grep -rl 'rgba(249,115,22' --include='*.tsx' --include='*.ts' --include='*.css' | while read f; do
+ sed -i 's/rgba(249,115,22/rgba(96,165,250/g' "$f"
+done
+```
+
+This replaces ALL instances: `rgba(249,115,22,0.3)` → `rgba(96,165,250,0.3)`, `rgba(249,115,22,0.10)` → `rgba(96,165,250,0.10)`, etc. The opacity values stay the same.
+
+Note: `index.css` was already updated in Task 1 so this is a no-op for that file's `--color-accent-dim` line.
+
+- [ ] **Step 2: Also replace `rgba(249, 115, 22` (with spaces)**
+
+Some files may use spaced format:
+
+```bash
+grep -rl 'rgba(249, 115, 22' --include='*.tsx' --include='*.ts' --include='*.css' | while read f; do
+ sed -i 's/rgba(249, 115, 22/rgba(96, 165, 250/g' "$f"
+done
+```
+
+- [ ] **Step 3: Verify it compiles**
+
+Run: `cd /home/coder/resolutionflow/frontend && npx tsc --noEmit --pretty 2>&1 | head -10`
+
+Expected: No errors.
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd /home/coder/resolutionflow
+git add -A frontend/src/
+git commit -m "feat: replace all hardcoded orange rgba with blue rgba
+
+Mechanical find-and-replace: rgba(249,115,22,...) → rgba(96,165,250,...)
+across ~40 component and page files. Focus borders, hover glows, and
+dim backgrounds all now use the blue accent color.
+
+Co-Authored-By: Claude Opus 4.6 (1M context) "
+```
+
+---
+
+### Task 3: Replace `orange-*` Tailwind classes
+
+~21 files use `orange-400`, `orange-500`, `orange-600` Tailwind color classes. Replace with blue equivalents.
+
+**Files:** All files with `orange-[3456]00` class references.
+
+- [ ] **Step 1: Replace orange Tailwind classes with blue equivalents**
+
+The mapping:
+- `orange-300` → `blue-300`
+- `orange-400` → `blue-400`
+- `orange-500` → `blue-500`
+- `orange-600` → `blue-600`
+
+Run:
+
+```bash
+cd /home/coder/resolutionflow/frontend/src
+grep -rl 'orange-' --include='*.tsx' --include='*.ts' | while read f; do
+ sed -i 's/orange-300/blue-300/g; s/orange-400/blue-400/g; s/orange-500/blue-500/g; s/orange-600/blue-600/g' "$f"
+done
+```
+
+- [ ] **Step 2: Verify it compiles**
+
+Run: `cd /home/coder/resolutionflow/frontend && npx tsc --noEmit --pretty 2>&1 | head -10`
+
+Expected: No errors.
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd /home/coder/resolutionflow
+git add -A frontend/src/
+git commit -m "feat: replace orange-* Tailwind classes with blue-* equivalents
+
+orange-400→blue-400, orange-500→blue-500, orange-600→blue-600
+across ~21 component files.
+
+Co-Authored-By: Claude Opus 4.6 (1M context) "
+```
+
+---
+
+### Task 4: Update hardcoded hex values in specific files
+
+Several files have hardcoded `#f97316`, `#ea580c`, `#fdba74` hex values that don't go through CSS variables.
+
+**Files:**
+- Modify: `frontend/src/components/common/BrandLogo.tsx`
+- Modify: `frontend/src/components/common/EmptyStateIllustrations.tsx`
+- Modify: `frontend/src/constants/categoryColors.ts`
+- Modify: `frontend/src/styles/landing.css`
+- Modify: `frontend/src/pages/LandingPage.tsx`
+- Modify: `frontend/src/assets/brand/icon.svg`
+- Modify: `frontend/src/assets/brand/logo-horizontal.svg`
+- Modify: `frontend/src/assets/brand/logo-with-tagline.svg`
+- Modify: Any remaining files from the grep output for `#f97316|#ea580c|#fdba74`
+
+- [ ] **Step 1: Update BrandLogo.tsx gradient**
+
+In `frontend/src/components/common/BrandLogo.tsx`, replace the gradient on line 22:
+
+```tsx
+ background: 'linear-gradient(135deg, #3b82f6, #60a5fa)',
+```
+
+Also update the JSDoc comment on line 9:
+
+```tsx
+ * Brand logo mark: gradient blue square with rounded corners
+```
+
+- [ ] **Step 2: Replace all hardcoded orange hex values across the codebase**
+
+Run:
+
+```bash
+cd /home/coder/resolutionflow/frontend/src
+# #f97316 → #60a5fa (accent)
+grep -rl '#f97316' --include='*.tsx' --include='*.ts' --include='*.css' --include='*.svg' | while read f; do
+ sed -i 's/#f97316/#60a5fa/g' "$f"
+done
+
+# #ea580c → #3b82f6 (accent-hover)
+grep -rl '#ea580c' --include='*.tsx' --include='*.ts' --include='*.css' --include='*.svg' | while read f; do
+ sed -i 's/#ea580c/#3b82f6/g' "$f"
+done
+
+# #fdba74 → #93c5fd (accent-text)
+grep -rl '#fdba74' --include='*.tsx' --include='*.ts' --include='*.css' --include='*.svg' | while read f; do
+ sed -i 's/#fdba74/#93c5fd/g' "$f"
+done
+```
+
+- [ ] **Step 3: Update warning yellow to amber**
+
+```bash
+cd /home/coder/resolutionflow/frontend/src
+# #eab308 → #fbbf24
+grep -rl '#eab308' --include='*.tsx' --include='*.ts' --include='*.css' | while read f; do
+ sed -i 's/#eab308/#fbbf24/g' "$f"
+done
+
+# rgba(234,179,8 → rgba(251,191,36
+grep -rl 'rgba(234,179,8' --include='*.tsx' --include='*.ts' --include='*.css' | while read f; do
+ sed -i 's/rgba(234,179,8/rgba(251,191,36/g' "$f"
+done
+```
+
+- [ ] **Step 4: Fix categoryColors.ts duplicates**
+
+After the sed replacements, `frontend/src/constants/categoryColors.ts` will have `#3b82f6` (from `#ea580c`) and `#60a5fa` (from `#f97316`) — but the array already had `#3b82f6` at position 0. Fix the file to have 10 unique colors:
+
+```typescript
+export const CATEGORY_COLORS = [
+ '#3b82f6', // blue
+ '#22c55e', // green
+ '#f59e0b', // amber
+ '#ef4444', // red
+ '#8b5cf6', // violet
+ '#0891b2', // cyan
+ '#ec4899', // pink
+ '#60a5fa', // sky blue
+ '#14b8a6', // teal
+ '#6366f1', // indigo
+] as const
+```
+
+This replaces the old `#ea580c` (deep orange) slot with `#0891b2` (cyan) and keeps `#60a5fa` (sky blue) in the old `#f97316` slot.
+
+- [ ] **Step 5: Verify it compiles**
+
+Run: `cd /home/coder/resolutionflow/frontend && npx tsc --noEmit --pretty 2>&1 | head -10`
+
+Expected: No errors.
+
+- [ ] **Step 6: Commit**
+
+```bash
+cd /home/coder/resolutionflow
+git add -A frontend/src/
+git commit -m "feat: replace hardcoded orange hex values with blue equivalents
+
+BrandLogo gradient, EmptyStateIllustrations SVGs, categoryColors,
+landing page, brand SVG assets, and all remaining files with
+hardcoded #f97316/#ea580c/#fdba74 values.
+Also migrates warning #eab308 → #fbbf24 (amber).
+
+Co-Authored-By: Claude Opus 4.6 (1M context) "
+```
+
+---
+
+### Task 5: Update landing.css and LandingPage.tsx
+
+The landing page has its own CSS file and inline styles with orange references.
+
+**Files:**
+- Modify: `frontend/src/styles/landing.css`
+- Modify: `frontend/src/pages/LandingPage.tsx`
+
+- [ ] **Step 1: Read landing.css and fix any remaining orange references**
+
+Read `frontend/src/styles/landing.css` and replace any remaining:
+- `#f97316` → `#60a5fa`
+- `#ea580c` → `#3b82f6`
+- `rgba(249,115,22,...)` → `rgba(96,165,250,...)`
+- Any old surface colors (`#1a1c23` → `#16181f`, `#22252e` → `#1e2028`, etc.)
+
+These should already be handled by Task 2 and Task 4's sed commands, but verify and fix any that were missed (e.g., spaced hex values, different formatting).
+
+- [ ] **Step 2: Read LandingPage.tsx and fix any remaining orange references**
+
+Read `frontend/src/pages/LandingPage.tsx` and verify all orange references were replaced by the sed commands in Tasks 2 and 4. Fix any remaining manually.
+
+- [ ] **Step 3: Verify it compiles**
+
+Run: `cd /home/coder/resolutionflow/frontend && npx tsc --noEmit --pretty 2>&1 | head -10`
+
+Expected: No errors.
+
+- [ ] **Step 4: Commit (if changes were needed)**
+
+```bash
+cd /home/coder/resolutionflow
+git add frontend/src/styles/landing.css frontend/src/pages/LandingPage.tsx
+git commit -m "fix: clean up remaining orange references in landing page
+
+Co-Authored-By: Claude Opus 4.6 (1M context) "
+```
+
+---
+
+### Task 6: Update index.html and configuration files
+
+**Files:**
+- Modify: `frontend/index.html`
+
+- [ ] **Step 1: Update meta theme-color**
+
+In `frontend/index.html`, line 17, replace:
+
+```html
+
+```
+
+with:
+
+```html
+
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+cd /home/coder/resolutionflow
+git add frontend/index.html
+git commit -m "fix: update meta theme-color to new sidebar color (#0e1016)
+
+Co-Authored-By: Claude Opus 4.6 (1M context) "
+```
+
+---
+
+### Task 7: Full build verification and sweep for stragglers
+
+**Files:** None (verification only)
+
+- [ ] **Step 1: Run full frontend build**
+
+Run: `cd /home/coder/resolutionflow/frontend && NODE_OPTIONS="--max-old-space-size=4096" npm run build 2>&1 | tail -20`
+
+Expected: Build succeeds with no errors.
+
+- [ ] **Step 2: Search for any remaining orange references**
+
+Run:
+
+```bash
+cd /home/coder/resolutionflow/frontend/src
+echo "=== Remaining #f97316 ===" && grep -r '#f97316' --include='*.tsx' --include='*.ts' --include='*.css' || echo "NONE"
+echo "=== Remaining #ea580c ===" && grep -r '#ea580c' --include='*.tsx' --include='*.ts' --include='*.css' || echo "NONE"
+echo "=== Remaining #fdba74 ===" && grep -r '#fdba74' --include='*.tsx' --include='*.ts' --include='*.css' || echo "NONE"
+echo "=== Remaining rgba(249,115,22 ===" && grep -r 'rgba(249,115,22' --include='*.tsx' --include='*.ts' --include='*.css' || echo "NONE"
+echo "=== Remaining orange- classes ===" && grep -r 'orange-[3456]00' --include='*.tsx' --include='*.ts' || echo "NONE"
+echo "=== Remaining #eab308 ===" && grep -r '#eab308' --include='*.tsx' --include='*.ts' --include='*.css' || echo "NONE"
+```
+
+Expected: All say "NONE". If any remain, fix them and commit.
+
+- [ ] **Step 3: Verify git status is clean**
+
+Run: `git status`
+
+Expected: Working tree clean (all changes committed).
From 3e5270825312f5120076b26b1c00209947e41195 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 16:19:21 +0000
Subject: [PATCH 44/49] feat: migrate index.css to design system v6 color
tokens
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Accent: orange (#f97316) → blue (#60a5fa)
Surfaces: deeper charcoal range (#16181f page, #1e2028 card)
Warning: yellow (#eab308) → amber (#fbbf24)
Info: new cyan (#67e8f9) token added
All Tailwind semantic mappings updated to match.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/index.css | 63 ++++++++++++++++++++++--------------------
1 file changed, 33 insertions(+), 30 deletions(-)
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 06749d8b..2fb1f923 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -8,14 +8,15 @@
@import '@xyflow/react/dist/style.css';
@theme {
- /* ── Surface colors (Charcoal — sidebar darkest) ─ */
- --color-bg-page: #1a1c23;
- --color-bg-sidebar: #10121a;
- --color-bg-card: #22252e;
- --color-bg-card-hover: #282b35;
- --color-bg-input: #282b35;
- --color-bg-code: #14161e;
- --color-bg-elevated: #2e3140;
+ /* ── Surface colors (Deep Charcoal — sidebar darkest) ─ */
+ --color-bg-page: #16181f;
+ --color-bg-sidebar: #0e1016;
+ --color-bg-card: #1e2028;
+ --color-bg-card-hover: #252830;
+ --color-bg-input: #252830;
+ --color-bg-code: #12141a;
+ --color-bg-elevated: #2a2d38;
+ --color-bg-raised: #303442;
/* ── Text colors ───────────────────────────────── */
--color-text-heading: #f0f2f5;
@@ -25,43 +26,45 @@
--color-text-rail-label: #e2e5eb;
/* ── Border colors ─────────────────────────────── */
- --color-border-default: #2e3240;
+ --color-border-default: #2a2e3a;
--color-border-hover: #3d4252;
- /* ── Accent (ember orange) ─────────────────────── */
- --color-accent: #f97316;
- --color-accent-hover: #ea580c;
- --color-accent-dim: rgba(249,115,22,0.10);
- --color-accent-text: #fdba74;
+ /* ── Accent (electric blue) ───────────────────────── */
+ --color-accent: #60a5fa;
+ --color-accent-hover: #3b82f6;
+ --color-accent-dim: rgba(96,165,250,0.10);
+ --color-accent-text: #93c5fd;
/* ── Semantic colors ───────────────────────────── */
--color-success: #34d399;
--color-success-dim: rgba(52,211,153,0.10);
- --color-warning: #eab308;
- --color-warning-dim: rgba(234,179,8,0.10);
+ --color-warning: #fbbf24;
+ --color-warning-dim: rgba(251,191,36,0.10);
--color-danger: #f87171;
--color-danger-dim: rgba(248,113,113,0.10);
+ --color-info: #67e8f9;
+ --color-info-dim: rgba(103,232,249,0.10);
/* ── Tailwind semantic mappings ─────────────────── */
- --color-background: #1a1c23;
+ --color-background: #16181f;
--color-foreground: #e2e5eb;
- --color-card: #22252e;
+ --color-card: #1e2028;
--color-card-foreground: #e2e5eb;
- --color-popover: #22252e;
+ --color-popover: #1e2028;
--color-popover-foreground: #e2e5eb;
- --color-primary: #f97316;
+ --color-primary: #60a5fa;
--color-primary-foreground: #ffffff;
- --color-secondary: #2e3140;
+ --color-secondary: #2a2d38;
--color-secondary-foreground: #e2e5eb;
- --color-muted: #2e3140;
+ --color-muted: #2a2d38;
--color-muted-foreground: #848b9b;
- --color-accent-tw: #2e3140;
+ --color-accent-tw: #2a2d38;
--color-accent-foreground: #e2e5eb;
--color-destructive: #f87171;
--color-destructive-foreground: #ffffff;
- --color-border: #2e3240;
- --color-input: #282b35;
- --color-ring: #f97316;
+ --color-border: #2a2e3a;
+ --color-input: #252830;
+ --color-ring: #60a5fa;
/* ── Radii ─────────────────────────────────────── */
--radius-sm: 5px;
@@ -191,11 +194,11 @@
transition: filter 150ms ease, box-shadow 150ms ease, transform 150ms ease;
&:hover {
filter: brightness(1.1);
- box-shadow: 0 2px 10px rgba(249, 115, 22, 0.2);
+ box-shadow: 0 2px 10px rgba(96, 165, 250, 0.2);
transform: translateY(-1px);
}
&:active {
- box-shadow: 0 0 4px rgba(249, 115, 22, 0.1);
+ box-shadow: 0 0 4px rgba(96, 165, 250, 0.1);
transform: translateY(0);
}
}
@@ -226,7 +229,7 @@
/* Tab/toggle group: active item gets elevated surface + faint accent glow */
@utility tab-active-shadow {
background: var(--color-bg-elevated);
- box-shadow: 0 1px 4px rgba(249, 115, 22, 0.08);
+ box-shadow: 0 1px 4px rgba(96, 165, 250, 0.08);
}
/* Card hover lift — brighter border + subtle accent glow */
@@ -234,7 +237,7 @@
transition: box-shadow 200ms ease, transform 200ms ease, border-color 200ms ease;
&:hover {
border-color: var(--color-border-hover);
- box-shadow: 0 2px 8px rgba(249, 115, 22, 0.06);
+ box-shadow: 0 2px 8px rgba(96, 165, 250, 0.06);
transform: translateY(-2px);
}
&:active {
From 5ff9a9d75ea683ad1cd66685b734cfaad5ae1d07 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 16:20:13 +0000
Subject: [PATCH 45/49] feat: replace all hardcoded orange rgba with blue rgba
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Mechanical find-and-replace: rgba(249,115,22,...) → rgba(96,165,250,...)
across ~40 component and page files.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../components/analytics/PsaMetricsPanel.tsx | 2 +-
.../common/EmptyStateIllustrations.tsx | 44 +++++++++----------
.../src/components/common/RichTextInput.tsx | 2 +-
.../components/dashboard/RecentActivity.tsx | 4 +-
.../dashboard/StartSessionInput.tsx | 4 +-
.../flowpilot/FlowPilotActionBar.tsx | 2 +-
.../components/flowpilot/FlowPilotIntake.tsx | 4 +-
.../flowpilot/FlowPilotMessageBar.tsx | 4 +-
.../components/flowpilot/FlowPilotOptions.tsx | 2 +-
.../flowpilot/InSessionScriptGenerator.tsx | 2 +-
.../components/flowpilot/ProposalDetail.tsx | 2 +-
.../components/flowpilot/SessionBriefing.tsx | 2 +-
frontend/src/components/layout/Sidebar.tsx | 2 +-
.../procedural/InlineVariablePrompt.tsx | 6 +--
.../procedural/PrepareSessionModal.tsx | 12 ++---
.../script-builder/ScriptBuilderChat.tsx | 6 +--
.../script-builder/ScriptBuilderInput.tsx | 2 +-
.../script-editor/ParameterCard.tsx | 2 +-
.../ParameterDetectorStepper.tsx | 2 +-
.../script-editor/ParameterSchemaBuilder.tsx | 2 +-
.../script-editor/ScriptTemplateEditor.tsx | 4 +-
.../script-editor/ScriptTemplateListView.tsx | 2 +-
.../scripts/ScriptParameterField.tsx | 2 +-
.../session/AddSupportingDataModal.tsx | 4 +-
.../components/session/TicketContextPanel.tsx | 2 +-
.../components/settings/BrandingSettings.tsx | 2 +-
frontend/src/pages/AssistantChatPage.tsx | 4 +-
frontend/src/pages/FlowPilotSessionPage.tsx | 2 +-
frontend/src/pages/PublicTemplatesPage.tsx | 4 +-
frontend/src/pages/SessionHistoryPage.tsx | 12 ++---
frontend/src/pages/SurveyPage.tsx | 14 +++---
.../pages/account/BrandingSettingsPage.tsx | 6 +--
.../src/pages/account/IntegrationsPage.tsx | 4 +-
.../src/pages/admin/GalleryManagementPage.tsx | 4 +-
.../src/pages/admin/SurveyResponsesPage.tsx | 4 +-
frontend/src/styles/landing.css | 22 +++++-----
36 files changed, 100 insertions(+), 100 deletions(-)
diff --git a/frontend/src/components/analytics/PsaMetricsPanel.tsx b/frontend/src/components/analytics/PsaMetricsPanel.tsx
index 02884907..5347d223 100644
--- a/frontend/src/components/analytics/PsaMetricsPanel.tsx
+++ b/frontend/src/components/analytics/PsaMetricsPanel.tsx
@@ -95,7 +95,7 @@ export default function PsaMetricsPanel({ data }: PsaMetricsPanelProps) {
{/* Root node */}
-
+
{/* Branches */}
{/* Left child */}
-
+
{/* Right child */}
-
+
{/* Leaf branches */}
-
-
+
+
)
}
@@ -29,10 +29,10 @@ export function AnalyticsIllustration() {
return (
{/* Bars */}
-
-
-
-
+
+
+
+
{/* Baseline */}
@@ -43,15 +43,15 @@ export function SessionIllustration() {
return (
{/* Card 1 */}
-
+
{/* Card 2 */}
-
+
{/* Card 3 */}
-
+
@@ -62,11 +62,11 @@ export function IntegrationIllustration() {
return (
{/* Left box */}
-
+
{/* Right box */}
-
+
{/* Dashed arrows */}
@@ -83,13 +83,13 @@ export function StepLibraryIllustration() {
return (
{/* List items */}
-
+
-
+
-
+
-
+
)
@@ -99,7 +99,7 @@ export function ScriptIllustration() {
return (
{/* Terminal window */}
-
+
{/* Title bar */}
@@ -117,11 +117,11 @@ export function ShareIllustration() {
return (
{/* Center node */}
-
+
{/* Top-right node */}
-
+
{/* Bottom-right node */}
-
+
{/* Connecting lines */}
diff --git a/frontend/src/components/common/RichTextInput.tsx b/frontend/src/components/common/RichTextInput.tsx
index 5c390ff9..4357a4da 100644
--- a/frontend/src/components/common/RichTextInput.tsx
+++ b/frontend/src/components/common/RichTextInput.tsx
@@ -234,7 +234,7 @@ export function RichTextInput({
disabled={disabled}
className={cn(
'w-full bg-card border border-border rounded-xl p-3 text-sm text-foreground placeholder:text-muted-foreground',
- 'focus:border-[rgba(249,115,22,0.3)] focus:outline-none resize-none transition-colors',
+ 'focus:border-[rgba(96,165,250,0.3)] focus:outline-none resize-none transition-colors',
isDragOver && 'border-primary/50 bg-primary/5',
disabled && 'opacity-50 cursor-not-allowed'
)}
diff --git a/frontend/src/components/dashboard/RecentActivity.tsx b/frontend/src/components/dashboard/RecentActivity.tsx
index 35f392a5..023d3b73 100644
--- a/frontend/src/components/dashboard/RecentActivity.tsx
+++ b/frontend/src/components/dashboard/RecentActivity.tsx
@@ -16,9 +16,9 @@ interface RecentActivityProps {
const DEFAULT_ACTIVITIES: ActivityItem[] = [
{ id: '1', icon: Play, iconColor: '#34d399', iconBg: 'rgba(52, 211, 153, 0.1)', description: 'Started VPN Connectivity Triage session', timestamp: '2 min ago' },
- { id: '2', icon: CheckCircle, iconColor: '#ea580c', iconBg: 'rgba(249, 115, 22, 0.1)', description: 'Completed M365 License Provisioning', timestamp: '15 min ago' },
+ { id: '2', icon: CheckCircle, iconColor: '#ea580c', iconBg: 'rgba(96, 165, 250, 0.1)', description: 'Completed M365 License Provisioning', timestamp: '15 min ago' },
{ id: '3', icon: Edit, iconColor: '#eab308', iconBg: 'rgba(234, 179, 8, 0.1)', description: 'Updated Printer Troubleshooting flow', timestamp: '1 hr ago' },
- { id: '4', icon: GitBranch, iconColor: '#ea580c', iconBg: 'rgba(249, 115, 22, 0.1)', description: 'Created new DNS Resolution flow', timestamp: '3 hr ago' },
+ { id: '4', icon: GitBranch, iconColor: '#ea580c', iconBg: 'rgba(96, 165, 250, 0.1)', description: 'Created new DNS Resolution flow', timestamp: '3 hr ago' },
{ id: '5', icon: FileText, iconColor: '#8891a0', iconBg: 'rgba(136, 145, 160, 0.1)', description: 'Exported session report #TK-4821', timestamp: 'Yesterday' },
]
diff --git a/frontend/src/components/dashboard/StartSessionInput.tsx b/frontend/src/components/dashboard/StartSessionInput.tsx
index 9d17f167..e710cfa7 100644
--- a/frontend/src/components/dashboard/StartSessionInput.tsx
+++ b/frontend/src/components/dashboard/StartSessionInput.tsx
@@ -200,7 +200,7 @@ export function StartSessionInput() {
{/* Drag overlay */}
{isDragOver && (
@@ -278,7 +278,7 @@ export function StartSessionInput() {
onChange={(e) => setLogContent(e.target.value)}
placeholder="Paste event viewer logs, error messages, PowerShell output..."
rows={4}
- className="w-full resize-none rounded-lg border border-border bg-background p-3 font-mono text-xs text-foreground placeholder:text-muted-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-none"
+ className="w-full resize-none rounded-lg border border-border bg-background p-3 font-mono text-xs text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none"
/>
)}
diff --git a/frontend/src/components/flowpilot/FlowPilotActionBar.tsx b/frontend/src/components/flowpilot/FlowPilotActionBar.tsx
index a6aeb24e..30fc9994 100644
--- a/frontend/src/components/flowpilot/FlowPilotActionBar.tsx
+++ b/frontend/src/components/flowpilot/FlowPilotActionBar.tsx
@@ -152,7 +152,7 @@ export function FlowPilotActionBar({
value={resolutionSummary}
onChange={(e) => setResolutionSummary(e.target.value)}
placeholder="What resolved the issue?"
- className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-none resize-none"
+ className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none resize-none"
rows={4}
autoFocus
/>
diff --git a/frontend/src/components/flowpilot/FlowPilotIntake.tsx b/frontend/src/components/flowpilot/FlowPilotIntake.tsx
index 916c0c3e..a0ff603c 100644
--- a/frontend/src/components/flowpilot/FlowPilotIntake.tsx
+++ b/frontend/src/components/flowpilot/FlowPilotIntake.tsx
@@ -166,7 +166,7 @@ export function FlowPilotIntake({ onSubmit, isLoading, defaultProblem }: FlowPil
value={additionalContext}
onChange={(e) => setAdditionalContext(e.target.value)}
placeholder="Add extra context (optional) — e.g. 'User called back and said it's also affecting their second monitor'"
- className="mt-3 w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-none resize-none"
+ className="mt-3 w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none resize-none"
rows={3}
/>
@@ -229,7 +229,7 @@ export function FlowPilotIntake({ onSubmit, isLoading, defaultProblem }: FlowPil
value={logContent}
onChange={(e) => setLogContent(e.target.value)}
placeholder="Paste log output, error messages, or Event Viewer entries here..."
- className="w-full rounded-lg border border-border bg-card px-4 py-3 font-mono text-xs text-foreground placeholder:text-muted-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-none resize-none"
+ className="w-full rounded-lg border border-border bg-card px-4 py-3 font-mono text-xs text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none resize-none"
rows={6}
/>
)}
diff --git a/frontend/src/components/flowpilot/FlowPilotMessageBar.tsx b/frontend/src/components/flowpilot/FlowPilotMessageBar.tsx
index 9654684e..88794982 100644
--- a/frontend/src/components/flowpilot/FlowPilotMessageBar.tsx
+++ b/frontend/src/components/flowpilot/FlowPilotMessageBar.tsx
@@ -194,7 +194,7 @@ export function FlowPilotMessageBar({ onRespond, disabled = false, isProcessing
? 'border-border/50 opacity-50'
: isDragOver
? 'border-primary/50 bg-primary/5'
- : 'border-border focus-within:border-[rgba(249,115,22,0.3)] focus-within:ring-1 focus-within:ring-primary/20'
+ : 'border-border focus-within:border-[rgba(96,165,250,0.3)] focus-within:ring-1 focus-within:ring-primary/20'
)}
style={{ background: 'var(--color-bg-card)' }}
>
@@ -275,7 +275,7 @@ export function FlowPilotMessageBar({ onRespond, disabled = false, isProcessing
onChange={(e) => setLogContent(e.target.value)}
placeholder="Paste event viewer logs, error messages, PowerShell output..."
rows={3}
- className="w-full resize-none rounded-lg border border-border bg-background p-2 font-mono text-xs text-foreground placeholder:text-muted-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-none"
+ className="w-full resize-none rounded-lg border border-border bg-background p-2 font-mono text-xs text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none"
/>
)}
diff --git a/frontend/src/components/flowpilot/FlowPilotOptions.tsx b/frontend/src/components/flowpilot/FlowPilotOptions.tsx
index fac0e046..d5dd8adc 100644
--- a/frontend/src/components/flowpilot/FlowPilotOptions.tsx
+++ b/frontend/src/components/flowpilot/FlowPilotOptions.tsx
@@ -29,7 +29,7 @@ export function FlowPilotOptions({ options, onSelect, disabled }: FlowPilotOptio
disabled={disabled}
className={cn(
'group relative rounded-xl border p-3 sm:p-4 text-left transition-all min-h-[44px]',
- 'hover:border-[rgba(249,115,22,0.3)] hover:shadow-[0_0_20px_rgba(249,115,22,0.08)]',
+ 'hover:border-[rgba(96,165,250,0.3)] hover:shadow-[0_0_20px_rgba(96,165,250,0.08)]',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40',
isSelected
? 'border-primary/40 bg-accent-dim'
diff --git a/frontend/src/components/flowpilot/InSessionScriptGenerator.tsx b/frontend/src/components/flowpilot/InSessionScriptGenerator.tsx
index 678fa6ac..1ea639d6 100644
--- a/frontend/src/components/flowpilot/InSessionScriptGenerator.tsx
+++ b/frontend/src/components/flowpilot/InSessionScriptGenerator.tsx
@@ -96,7 +96,7 @@ export function InSessionScriptGenerator({
setParams(prev => ({ ...prev, [key]: e.target.value }))}
- className="w-full rounded-lg border border-border bg-card px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-none"
+ className="w-full rounded-lg border border-border bg-card px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none"
/>
))}
diff --git a/frontend/src/components/flowpilot/ProposalDetail.tsx b/frontend/src/components/flowpilot/ProposalDetail.tsx
index 69d700ca..9b343e86 100644
--- a/frontend/src/components/flowpilot/ProposalDetail.tsx
+++ b/frontend/src/components/flowpilot/ProposalDetail.tsx
@@ -187,7 +187,7 @@ export function ProposalDetail({ proposal, onReview }: ProposalDetailProps) {
value={reviewNotes}
onChange={(e) => setReviewNotes(e.target.value)}
placeholder="Reviewer notes (optional)"
- className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-none"
+ className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none"
/>
{/* Action buttons */}
diff --git a/frontend/src/components/flowpilot/SessionBriefing.tsx b/frontend/src/components/flowpilot/SessionBriefing.tsx
index e6b60fe4..553712ab 100644
--- a/frontend/src/components/flowpilot/SessionBriefing.tsx
+++ b/frontend/src/components/flowpilot/SessionBriefing.tsx
@@ -145,7 +145,7 @@ export function SessionBriefing({
value={freshContext}
onChange={(e) => setFreshContext(e.target.value)}
placeholder="What additional information do you have, or what would you like to investigate first?"
- className="w-full rounded-lg border border-border bg-card px-4 py-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-none resize-none"
+ className="w-full rounded-lg border border-border bg-card px-4 py-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none resize-none"
rows={3}
autoFocus
/>
diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx
index 2b2fa664..1a1a7e7a 100644
--- a/frontend/src/components/layout/Sidebar.tsx
+++ b/frontend/src/components/layout/Sidebar.tsx
@@ -288,7 +288,7 @@ export function Sidebar() {
'group relative flex items-center gap-3 rounded-lg px-3 py-2 text-[0.8125rem] font-medium transition-all duration-150',
active
? isParentDimmed
- ? 'bg-[rgba(249,115,22,0.05)] text-foreground/70'
+ ? 'bg-[rgba(96,165,250,0.05)] text-foreground/70'
: 'bg-accent-dim text-foreground'
: 'text-muted-foreground hover:bg-input hover:text-foreground'
)}
diff --git a/frontend/src/components/procedural/InlineVariablePrompt.tsx b/frontend/src/components/procedural/InlineVariablePrompt.tsx
index bdee434c..0a264cd9 100644
--- a/frontend/src/components/procedural/InlineVariablePrompt.tsx
+++ b/frontend/src/components/procedural/InlineVariablePrompt.tsx
@@ -60,7 +60,7 @@ export function InlineVariablePrompt({
className={cn(
'inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-sm font-medium transition-all',
'border-orange-500/40 bg-orange-500/5 text-orange-400',
- 'hover:border-orange-400/60 hover:bg-orange-500/10 hover:shadow-[0_0_12px_rgba(249,115,22,0.15)]',
+ 'hover:border-orange-400/60 hover:bg-orange-500/10 hover:shadow-[0_0_12px_rgba(96,165,250,0.15)]',
'disabled:opacity-50 disabled:cursor-not-allowed',
isRequired && 'ring-1 ring-orange-500/20'
)}
@@ -88,7 +88,7 @@ export function InlineVariablePrompt({
onBlur={() => {
if (!value) setIsEditing(false)
}}
- className="rounded-md border border-orange-500/40 bg-orange-500/5 px-2.5 py-1 text-sm text-foreground shadow-[0_0_12px_rgba(249,115,22,0.15)] focus:border-orange-400 focus:outline-hidden focus:ring-1 focus:ring-orange-400/30"
+ className="rounded-md border border-orange-500/40 bg-orange-500/5 px-2.5 py-1 text-sm text-foreground shadow-[0_0_12px_rgba(96,165,250,0.15)] focus:border-orange-400 focus:outline-hidden focus:ring-1 focus:ring-orange-400/30"
>
{placeholder}
{options.map((opt) => (
@@ -117,7 +117,7 @@ export function InlineVariablePrompt({
onBlur={handleSubmit}
onKeyDown={handleKeyDown}
placeholder={placeholder}
- className="w-48 rounded-md border border-orange-500/40 bg-orange-500/5 px-2.5 py-1 text-sm text-foreground shadow-[0_0_12px_rgba(249,115,22,0.15)] placeholder:text-muted-foreground focus:border-orange-400 focus:outline-hidden focus:ring-1 focus:ring-orange-400/30"
+ className="w-48 rounded-md border border-orange-500/40 bg-orange-500/5 px-2.5 py-1 text-sm text-foreground shadow-[0_0_12px_rgba(96,165,250,0.15)] placeholder:text-muted-foreground focus:border-orange-400 focus:outline-hidden focus:ring-1 focus:ring-orange-400/30"
/>
{helpText && (
diff --git a/frontend/src/components/procedural/PrepareSessionModal.tsx b/frontend/src/components/procedural/PrepareSessionModal.tsx
index bd7caf24..b09bb373 100644
--- a/frontend/src/components/procedural/PrepareSessionModal.tsx
+++ b/frontend/src/components/procedural/PrepareSessionModal.tsx
@@ -122,7 +122,7 @@ export function PrepareSessionModal({
value={ticketNumber}
onChange={(e) => setTicketNumber(e.target.value)}
placeholder="e.g. TKT-12345"
- className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
+ className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
/>
@@ -132,7 +132,7 @@ export function PrepareSessionModal({
value={clientName}
onChange={(e) => setClientName(e.target.value)}
placeholder="e.g. Acme Corp"
- className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
+ className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
/>
@@ -147,7 +147,7 @@ export function PrepareSessionModal({
setAssignedToId(e.target.value)}
- className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
+ className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
>
Unassigned (visible to all)
{teamMembers.map(m => (
@@ -178,7 +178,7 @@ export function PrepareSessionModal({
handleFieldChange(field.variable_name, e.target.value)}
- className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
+ className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
>
{field.placeholder || 'Select...'}
{field.options.map(opt => (
@@ -191,7 +191,7 @@ export function PrepareSessionModal({
onChange={(e) => handleFieldChange(field.variable_name, e.target.value)}
placeholder={field.placeholder}
rows={3}
- className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
+ className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
/>
) : (
handleFieldChange(field.variable_name, e.target.value)}
placeholder={field.placeholder}
- className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
+ className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
/>
)}
{field.help_text && (
diff --git a/frontend/src/components/script-builder/ScriptBuilderChat.tsx b/frontend/src/components/script-builder/ScriptBuilderChat.tsx
index de5527af..a7c263cd 100644
--- a/frontend/src/components/script-builder/ScriptBuilderChat.tsx
+++ b/frontend/src/components/script-builder/ScriptBuilderChat.tsx
@@ -56,7 +56,7 @@ export function ScriptBuilderChat({
)}
>
{msg.role === 'assistant' && (
-
+
)}
@@ -65,7 +65,7 @@ export function ScriptBuilderChat({
className={cn(
"max-w-[85%] rounded-xl px-4 py-3 text-sm",
msg.role === 'user'
- ? "bg-[rgba(249,115,22,0.08)] border border-[rgba(249,115,22,0.15)] text-foreground"
+ ? "bg-[rgba(96,165,250,0.08)] border border-[rgba(96,165,250,0.15)] text-foreground"
: "card-flat"
)}
>
@@ -98,7 +98,7 @@ export function ScriptBuilderChat({
{isLoading && (
-
+
diff --git a/frontend/src/components/script-builder/ScriptBuilderInput.tsx b/frontend/src/components/script-builder/ScriptBuilderInput.tsx
index d98973ec..47bb10ab 100644
--- a/frontend/src/components/script-builder/ScriptBuilderInput.tsx
+++ b/frontend/src/components/script-builder/ScriptBuilderInput.tsx
@@ -56,7 +56,7 @@ export function ScriptBuilderInput({
className={cn(
"flex-1 resize-none rounded-xl px-4 py-2.5 text-sm",
"bg-card border border-border text-foreground placeholder:text-muted-foreground",
- "focus:outline-none focus:border-[rgba(249,115,22,0.3)] transition-colors",
+ "focus:outline-none focus:border-[rgba(96,165,250,0.3)] transition-colors",
"disabled:opacity-50"
)}
style={{ maxHeight: 120 }}
diff --git a/frontend/src/components/script-editor/ParameterCard.tsx b/frontend/src/components/script-editor/ParameterCard.tsx
index 97ebd5f0..875d4c82 100644
--- a/frontend/src/components/script-editor/ParameterCard.tsx
+++ b/frontend/src/components/script-editor/ParameterCard.tsx
@@ -104,7 +104,7 @@ export function ParameterCard({
value={param.type}
onChange={e => update({ type: e.target.value as ScriptParameter['type'] })}
disabled={disabled}
- className="w-full rounded-lg border border-border bg-card text-foreground px-3 py-2 text-sm focus:outline-none focus:border-[rgba(249,115,22,0.3)] disabled:cursor-not-allowed disabled:opacity-50"
+ className="w-full rounded-lg border border-border bg-card text-foreground px-3 py-2 text-sm focus:outline-none focus:border-[rgba(96,165,250,0.3)] disabled:cursor-not-allowed disabled:opacity-50"
>
{PARAM_TYPES.map(t => (
{t.label}
diff --git a/frontend/src/components/script-editor/ParameterDetectorStepper.tsx b/frontend/src/components/script-editor/ParameterDetectorStepper.tsx
index 186471af..65a59c02 100644
--- a/frontend/src/components/script-editor/ParameterDetectorStepper.tsx
+++ b/frontend/src/components/script-editor/ParameterDetectorStepper.tsx
@@ -174,7 +174,7 @@ export function ParameterDetectorStepper({
setType(e.target.value as ScriptParameter['type'])}
- className="w-full rounded-lg border border-border bg-card text-foreground px-3 py-2 text-sm focus:outline-none focus:border-[rgba(249,115,22,0.3)]"
+ className="w-full rounded-lg border border-border bg-card text-foreground px-3 py-2 text-sm focus:outline-none focus:border-[rgba(96,165,250,0.3)]"
>
{PARAM_TYPES.map(t => (
{t.label}
diff --git a/frontend/src/components/script-editor/ParameterSchemaBuilder.tsx b/frontend/src/components/script-editor/ParameterSchemaBuilder.tsx
index bb2d9bd2..6466c5a7 100644
--- a/frontend/src/components/script-editor/ParameterSchemaBuilder.tsx
+++ b/frontend/src/components/script-editor/ParameterSchemaBuilder.tsx
@@ -159,7 +159,7 @@ export function ParameterSchemaBuilder({ schema, onChange, disabled }: Props) {
onChange={e => { setJsonText(e.target.value); setJsonError(null) }}
disabled={disabled}
spellCheck={false}
- className="w-full min-h-[300px] resize-y font-sans text-xs text-sm bg-card border border-border rounded-xl p-4 text-foreground focus:outline-none focus:border-[rgba(249,115,22,0.3)] disabled:cursor-not-allowed disabled:opacity-50"
+ className="w-full min-h-[300px] resize-y font-sans text-xs text-sm bg-card border border-border rounded-xl p-4 text-foreground focus:outline-none focus:border-[rgba(96,165,250,0.3)] disabled:cursor-not-allowed disabled:opacity-50"
placeholder='{ "parameters": [...] }'
/>
{jsonError && (
diff --git a/frontend/src/components/script-editor/ScriptTemplateEditor.tsx b/frontend/src/components/script-editor/ScriptTemplateEditor.tsx
index d0e3228e..01ece62a 100644
--- a/frontend/src/components/script-editor/ScriptTemplateEditor.tsx
+++ b/frontend/src/components/script-editor/ScriptTemplateEditor.tsx
@@ -360,7 +360,7 @@ export function ScriptTemplateEditor({ templateId, onBack, onSaved }: Props) {
updateField('category_id', e.target.value)}
- className="w-full rounded-lg border border-border bg-card text-foreground px-3 py-2 text-sm focus:outline-none focus:border-[rgba(249,115,22,0.3)]"
+ className="w-full rounded-lg border border-border bg-card text-foreground px-3 py-2 text-sm focus:outline-none focus:border-[rgba(96,165,250,0.3)]"
>
Select category…
{categories.map(c => (
@@ -373,7 +373,7 @@ export function ScriptTemplateEditor({ templateId, onBack, onSaved }: Props) {
updateField('complexity', e.target.value as FormState['complexity'])}
- className="w-full rounded-lg border border-border bg-card text-foreground px-3 py-2 text-sm focus:outline-none focus:border-[rgba(249,115,22,0.3)]"
+ className="w-full rounded-lg border border-border bg-card text-foreground px-3 py-2 text-sm focus:outline-none focus:border-[rgba(96,165,250,0.3)]"
>
Beginner
Intermediate
diff --git a/frontend/src/components/script-editor/ScriptTemplateListView.tsx b/frontend/src/components/script-editor/ScriptTemplateListView.tsx
index bbef2e30..ad444502 100644
--- a/frontend/src/components/script-editor/ScriptTemplateListView.tsx
+++ b/frontend/src/components/script-editor/ScriptTemplateListView.tsx
@@ -105,7 +105,7 @@ export function ScriptTemplateListView({ onEdit, onCreate }: Props) {
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder="Search templates…"
- className="w-full pl-8 pr-3 py-1.5 text-sm rounded-md border border-border bg-card text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-[rgba(249,115,22,0.3)] focus:ring-1 focus:ring-[rgba(249,115,22,0.2)]"
+ className="w-full pl-8 pr-3 py-1.5 text-sm rounded-md border border-border bg-card text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-[rgba(96,165,250,0.3)] focus:ring-1 focus:ring-[rgba(96,165,250,0.2)]"
/>
diff --git a/frontend/src/components/scripts/ScriptParameterField.tsx b/frontend/src/components/scripts/ScriptParameterField.tsx
index d80c46db..9bbd8ec4 100644
--- a/frontend/src/components/scripts/ScriptParameterField.tsx
+++ b/frontend/src/components/scripts/ScriptParameterField.tsx
@@ -93,7 +93,7 @@ export function ScriptParameterField({ param, value, error, disabled }: Props) {
value={value}
onChange={handleChange}
disabled={disabled}
- className="w-full rounded-[10px] border border-border bg-card text-foreground px-3 py-2 text-sm focus:outline-none focus:border-[rgba(249,115,22,0.3)] disabled:cursor-not-allowed disabled:opacity-50"
+ className="w-full rounded-[10px] border border-border bg-card text-foreground px-3 py-2 text-sm focus:outline-none focus:border-[rgba(96,165,250,0.3)] disabled:cursor-not-allowed disabled:opacity-50"
>
Select…
{(param.options ?? []).map(opt => (
diff --git a/frontend/src/components/session/AddSupportingDataModal.tsx b/frontend/src/components/session/AddSupportingDataModal.tsx
index 1bed41b4..c9c598f8 100644
--- a/frontend/src/components/session/AddSupportingDataModal.tsx
+++ b/frontend/src/components/session/AddSupportingDataModal.tsx
@@ -166,7 +166,7 @@ export function AddSupportingDataModal({ isOpen, onClose, sessionId, onAdded }:
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm',
'text-foreground placeholder:text-muted-foreground',
- 'focus:border-[rgba(249,115,22,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20'
+ 'focus:border-[rgba(96,165,250,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
/>
@@ -186,7 +186,7 @@ export function AddSupportingDataModal({ isOpen, onClose, sessionId, onAdded }:
className={cn(
'w-full resize-y rounded-md border border-border bg-card px-3 py-2 text-sm',
'font-mono text-foreground placeholder:text-muted-foreground',
- 'focus:border-[rgba(249,115,22,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20'
+ 'focus:border-[rgba(96,165,250,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
/>
diff --git a/frontend/src/components/session/TicketContextPanel.tsx b/frontend/src/components/session/TicketContextPanel.tsx
index e3889a1c..9f645528 100644
--- a/frontend/src/components/session/TicketContextPanel.tsx
+++ b/frontend/src/components/session/TicketContextPanel.tsx
@@ -62,7 +62,7 @@ export function TicketContextPanel({ context, loading, error, onRefresh }: Ticke
return (
{/* Header */}
-
+
Ticket Context
diff --git a/frontend/src/components/settings/BrandingSettings.tsx b/frontend/src/components/settings/BrandingSettings.tsx
index 1bcba1d6..4ccdeb40 100644
--- a/frontend/src/components/settings/BrandingSettings.tsx
+++ b/frontend/src/components/settings/BrandingSettings.tsx
@@ -154,7 +154,7 @@ export function BrandingSettings({ teamId }: BrandingSettingsProps) {
className={cn(
'mt-2 w-full max-w-md rounded-md border border-border bg-card px-3 py-2 text-sm',
'text-foreground placeholder:text-muted-foreground',
- 'focus:border-[rgba(249,115,22,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20'
+ 'focus:border-[rgba(96,165,250,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
/>
diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx
index ad6d41c1..e15ee397 100644
--- a/frontend/src/pages/AssistantChatPage.tsx
+++ b/frontend/src/pages/AssistantChatPage.tsx
@@ -664,7 +664,7 @@ export default function AssistantChatPage() {
'relative rounded-xl border transition-all',
loading ? 'border-border/50 opacity-50' :
isDragOver ? 'border-primary/50 bg-primary/5' :
- 'border-border focus-within:border-[rgba(249,115,22,0.3)] focus-within:ring-1 focus-within:ring-primary/20'
+ 'border-border focus-within:border-[rgba(96,165,250,0.3)] focus-within:ring-1 focus-within:ring-primary/20'
)} style={{ background: 'var(--color-bg-card)' }}>
{/* Drag overlay */}
{isDragOver && (
@@ -734,7 +734,7 @@ export default function AssistantChatPage() {
onChange={(e) => setLogContent(e.target.value)}
placeholder="Paste event viewer logs, error messages, PowerShell output..."
rows={3}
- className="w-full resize-none rounded-lg border border-border bg-background p-2 font-mono text-xs text-foreground placeholder:text-muted-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-none"
+ className="w-full resize-none rounded-lg border border-border bg-background p-2 font-mono text-xs text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none"
/>
)}
diff --git a/frontend/src/pages/FlowPilotSessionPage.tsx b/frontend/src/pages/FlowPilotSessionPage.tsx
index f5b67de6..457c369a 100644
--- a/frontend/src/pages/FlowPilotSessionPage.tsx
+++ b/frontend/src/pages/FlowPilotSessionPage.tsx
@@ -414,7 +414,7 @@ export default function FlowPilotSessionPage() {
value={resolutionSummary}
onChange={(e) => setResolutionSummary(e.target.value)}
placeholder="What resolved the issue?"
- className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-none resize-none"
+ className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none resize-none"
rows={4}
autoFocus
/>
diff --git a/frontend/src/pages/PublicTemplatesPage.tsx b/frontend/src/pages/PublicTemplatesPage.tsx
index e8989921..ec12e088 100644
--- a/frontend/src/pages/PublicTemplatesPage.tsx
+++ b/frontend/src/pages/PublicTemplatesPage.tsx
@@ -161,7 +161,7 @@ export default function PublicTemplatesPage() {
@@ -211,7 +211,7 @@ export default function PublicTemplatesPage() {
placeholder="Search templates..."
value={searchQuery}
onChange={(e) => handleSearchChange(e.target.value)}
- className="w-full pl-12 pr-4 py-3.5 rounded-2xl text-sm bg-card border border-border text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-[rgba(249,115,22,0.3)] transition-colors"
+ className="w-full pl-12 pr-4 py-3.5 rounded-2xl text-sm bg-card border border-border text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-[rgba(96,165,250,0.3)] transition-colors"
/>
diff --git a/frontend/src/pages/SessionHistoryPage.tsx b/frontend/src/pages/SessionHistoryPage.tsx
index ef4c1901..d814b587 100644
--- a/frontend/src/pages/SessionHistoryPage.tsx
+++ b/frontend/src/pages/SessionHistoryPage.tsx
@@ -329,7 +329,7 @@ export function SessionHistoryPage() {
value={aiSearchInput}
onChange={(e) => setAiSearchInput(e.target.value)}
placeholder="Search sessions..."
- className="w-full rounded-lg border border-border bg-card pl-8 pr-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-none"
+ className="w-full rounded-lg border border-border bg-card pl-8 pr-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none"
/>
@@ -356,7 +356,7 @@ export function SessionHistoryPage() {
value={aiFilters.problem_domain}
onChange={(e) => setAiFilters((f) => ({ ...f, problem_domain: e.target.value }))}
title="Filter by problem domain"
- className="rounded-lg border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-none [&>option]:bg-card [&>option]:text-foreground"
+ className="rounded-lg border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none [&>option]:bg-card [&>option]:text-foreground"
>
All domains
Active Directory
@@ -396,7 +396,7 @@ export function SessionHistoryPage() {
value={aiFilters.date_from}
onChange={(e) => setAiFilters((f) => ({ ...f, date_from: e.target.value }))}
title="From date"
- className="rounded-lg border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-none [color-scheme:dark]"
+ className="rounded-lg border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none [color-scheme:dark]"
/>
to
setAiFilters((f) => ({ ...f, date_to: e.target.value }))}
title="To date"
- className="rounded-lg border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-none [color-scheme:dark]"
+ className="rounded-lg border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none [color-scheme:dark]"
/>
@@ -647,7 +647,7 @@ export function SessionHistoryPage() {
value={closeOutcome}
onChange={(e) => setCloseOutcome(e.target.value as SessionOutcome)}
title="Session outcome"
- className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-none mb-3"
+ className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none mb-3"
>
Select outcome...
Resolved
@@ -664,7 +664,7 @@ export function SessionHistoryPage() {
onChange={(e) => setCloseNotes(e.target.value)}
rows={2}
placeholder="Add closure notes..."
- className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-none resize-none mb-3"
+ className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none resize-none mb-3"
/>
diff --git a/frontend/src/pages/SurveyPage.tsx b/frontend/src/pages/SurveyPage.tsx
index aea932d3..3d62ad80 100644
--- a/frontend/src/pages/SurveyPage.tsx
+++ b/frontend/src/pages/SurveyPage.tsx
@@ -279,7 +279,7 @@ export default function SurveyPage() {
-
+
Already Submitted
@@ -329,7 +329,7 @@ export default function SurveyPage() {
{/* Hero — visible only on first slide */}
{currentSlide === 0 && !isComplete && (
-
+
@@ -507,7 +507,7 @@ export default function SurveyPage() {
function ScenarioBox({ scenario }: { scenario: { title: string; symptom: string; details: string } }) {
return (
-
+
{scenario.title}
Symptom:
@@ -537,7 +537,7 @@ function QuestionCard({ question: q, answer, setAnswer }: { question: SurveyQues
onClick={() => setAnswer(q.id, opt)}
className="flex items-start gap-3 px-3.5 py-3 sm:px-4 rounded-[9px] text-left text-[13px] sm:text-sm transition-all duration-150 select-none"
style={{
- background: answer === opt ? 'rgba(249, 115, 22, 0.10)' : 'rgba(16, 17, 20, 0.6)',
+ background: answer === opt ? 'rgba(96, 165, 250, 0.10)' : 'rgba(16, 17, 20, 0.6)',
border: `1px solid ${answer === opt ? 'var(--color-primary)' : 'var(--color-border-default)'}`,
color: answer === opt ? 'var(--color-foreground)' : 'var(--color-muted-foreground)',
}}
@@ -564,7 +564,7 @@ function QuestionCard({ question: q, answer, setAnswer }: { question: SurveyQues
}}
className="flex items-start gap-3 px-3.5 py-3 sm:px-4 rounded-[9px] text-left text-[13px] sm:text-sm transition-all duration-150 select-none"
style={{
- background: selected ? 'rgba(249, 115, 22, 0.10)' : 'rgba(16, 17, 20, 0.6)',
+ background: selected ? 'rgba(96, 165, 250, 0.10)' : 'rgba(16, 17, 20, 0.6)',
border: `1px solid ${selected ? 'var(--color-primary)' : 'var(--color-border-default)'}`,
color: selected ? 'var(--color-foreground)' : 'var(--color-muted-foreground)',
}}
@@ -593,7 +593,7 @@ function QuestionCard({ question: q, answer, setAnswer }: { question: SurveyQues
background: 'rgba(16, 17, 20, 0.6)',
border: '1px solid var(--color-border-default)',
}}
- onFocus={e => { e.currentTarget.style.borderColor = 'var(--color-primary)'; e.currentTarget.style.boxShadow = '0 0 0 3px rgba(249, 115, 22, 0.10)' }}
+ onFocus={e => { e.currentTarget.style.borderColor = 'var(--color-primary)'; e.currentTarget.style.boxShadow = '0 0 0 3px rgba(96, 165, 250, 0.10)' }}
onBlur={e => { e.currentTarget.style.borderColor = 'var(--color-border-default)'; e.currentTarget.style.boxShadow = 'none' }}
/>
)}
@@ -717,7 +717,7 @@ function DragRank({ items, onChange }: { items: string[]; onChange: (items: stri
onTouchStart={() => handleTouchStart(idx)}
className="flex items-center gap-2.5 sm:gap-3 px-3 py-3 sm:px-4 sm:py-2.5 rounded-[9px] text-[13px] sm:text-sm transition-all duration-150 select-none"
style={{
- background: overIdx === idx ? 'rgba(249, 115, 22, 0.10)' : 'rgba(16, 17, 20, 0.6)',
+ background: overIdx === idx ? 'rgba(96, 165, 250, 0.10)' : 'rgba(16, 17, 20, 0.6)',
border: `1px solid ${overIdx === idx || draggingIdx === idx ? 'var(--color-primary)' : 'var(--color-border-default)'}`,
opacity: draggingIdx === idx ? 0.5 : 1,
cursor: 'grab',
diff --git a/frontend/src/pages/account/BrandingSettingsPage.tsx b/frontend/src/pages/account/BrandingSettingsPage.tsx
index 7196502c..7fb3fbf6 100644
--- a/frontend/src/pages/account/BrandingSettingsPage.tsx
+++ b/frontend/src/pages/account/BrandingSettingsPage.tsx
@@ -137,7 +137,7 @@ export function BrandingSettingsPage() {
className={cn(
'mt-1 w-full rounded-lg border border-border bg-card px-3 py-2 text-sm',
'text-foreground placeholder:text-muted-foreground',
- 'focus:border-[rgba(249,115,22,0.3)] focus:outline-none focus:ring-1 focus:ring-primary/20'
+ 'focus:border-[rgba(96,165,250,0.3)] focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
/>
@@ -163,7 +163,7 @@ export function BrandingSettingsPage() {
className={cn(
'mt-1 w-full rounded-lg border border-border bg-card px-3 py-2 text-sm',
'text-foreground placeholder:text-muted-foreground font-mono',
- 'focus:border-[rgba(249,115,22,0.3)] focus:outline-none focus:ring-1 focus:ring-primary/20'
+ 'focus:border-[rgba(96,165,250,0.3)] focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
/>
@@ -212,7 +212,7 @@ export function BrandingSettingsPage() {
className={cn(
'w-32 rounded-lg border border-border bg-card px-3 py-2 text-sm',
'text-foreground placeholder:text-muted-foreground font-mono',
- 'focus:border-[rgba(249,115,22,0.3)] focus:outline-none focus:ring-1 focus:ring-primary/20'
+ 'focus:border-[rgba(96,165,250,0.3)] focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
/>
handleMemberChange(user.user_id, e.target.value)}
className={cn(
'w-full rounded-lg border bg-card px-3 py-1.5 text-sm text-foreground',
- 'border-border focus:border-[rgba(249,115,22,0.3)] focus:outline-none',
+ 'border-border focus:border-[rgba(96,165,250,0.3)] focus:outline-none',
!currentMapping && 'text-muted-foreground'
)}
>
@@ -1084,7 +1084,7 @@ function SettingSelect({
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
- className="w-full max-w-xs rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground focus:border-[rgba(249,115,22,0.3)] focus:outline-none disabled:opacity-50"
+ className="w-full max-w-xs rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-none disabled:opacity-50"
>
{options.map((opt) => (
{opt.label}
diff --git a/frontend/src/pages/admin/GalleryManagementPage.tsx b/frontend/src/pages/admin/GalleryManagementPage.tsx
index 6771f442..6711b982 100644
--- a/frontend/src/pages/admin/GalleryManagementPage.tsx
+++ b/frontend/src/pages/admin/GalleryManagementPage.tsx
@@ -90,7 +90,7 @@ function SortOrderInput({
}}
className={cn(
'w-20 rounded-[8px] border border-border bg-card px-2 py-1 text-sm text-foreground',
- 'focus:border-[rgba(249,115,22,0.3)] focus:outline-none',
+ 'focus:border-[rgba(96,165,250,0.3)] focus:outline-none',
disabled && 'cursor-not-allowed opacity-50',
)}
/>
@@ -125,7 +125,7 @@ function FilterBar({
onChange={e => onSearchChange(e.target.value)}
className={cn(
'w-full rounded-lg border border-border bg-card pl-9 pr-3 py-2 text-sm text-foreground placeholder:text-muted-foreground',
- 'focus:border-[rgba(249,115,22,0.3)] focus:outline-none',
+ 'focus:border-[rgba(96,165,250,0.3)] focus:outline-none',
)}
/>
diff --git a/frontend/src/pages/admin/SurveyResponsesPage.tsx b/frontend/src/pages/admin/SurveyResponsesPage.tsx
index b4956a41..d2325f7d 100644
--- a/frontend/src/pages/admin/SurveyResponsesPage.tsx
+++ b/frontend/src/pages/admin/SurveyResponsesPage.tsx
@@ -91,7 +91,7 @@ function ExpandedDetail({ response }: { response: SurveyResponseDetail }) {
className="px-6 py-5"
style={{
background: 'rgba(0, 0, 0, 0.15)',
- borderTop: '1px solid rgba(249, 115, 22, 0.1)',
+ borderTop: '1px solid rgba(96, 165, 250, 0.1)',
}}
>
@@ -500,7 +500,7 @@ export default function SurveyResponsesPage() {
{selectedIds.size > 0 && (
{selectedIds.size} selected
diff --git a/frontend/src/styles/landing.css b/frontend/src/styles/landing.css
index fdd83ad5..266f4ca8 100644
--- a/frontend/src/styles/landing.css
+++ b/frontend/src/styles/landing.css
@@ -22,7 +22,7 @@
pointer-events: none;
z-index: 0;
overflow: hidden;
- background: radial-gradient(circle at 50% 0%, rgba(249, 115, 22, 0.12) 0%, transparent 60%);
+ background: radial-gradient(circle at 50% 0%, rgba(96, 165, 250, 0.12) 0%, transparent 60%);
}
.landing-ambient-glow::before,
@@ -482,7 +482,7 @@
}
.landing-preview-sidebar-item.active {
- background: rgba(249, 115, 22, 0.08);
+ background: rgba(96, 165, 250, 0.08);
color: var(--color-accent-text);
border-left: 3px solid var(--color-accent);
}
@@ -535,7 +535,7 @@
.landing-tree-connector {
width: 2px;
height: 24px;
- background: rgba(249, 115, 22, 0.2);
+ background: rgba(96, 165, 250, 0.2);
}
.landing-tree-branch {
@@ -552,7 +552,7 @@
width: calc(100% - 100px);
transform: translateX(-50%);
height: 2px;
- background: rgba(249, 115, 22, 0.15);
+ background: rgba(96, 165, 250, 0.15);
}
.landing-tree-branch-arm {
@@ -756,7 +756,7 @@
}
.landing-step-card:hover {
- border-color: rgba(249, 115, 22, 0.2);
+ border-color: rgba(96, 165, 250, 0.2);
}
.landing-step-card::before {
@@ -814,9 +814,9 @@
}
.landing-mock-node.start {
- background: rgba(249, 115, 22, 0.15);
+ background: rgba(96, 165, 250, 0.15);
color: var(--color-accent-text);
- border: 1px solid rgba(249, 115, 22, 0.2);
+ border: 1px solid rgba(96, 165, 250, 0.2);
}
.landing-mock-node.step {
@@ -920,7 +920,7 @@
}
.landing-feature-card:hover {
- border-color: rgba(249, 115, 22, 0.2);
+ border-color: rgba(96, 165, 250, 0.2);
transform: translateY(-3px);
}
@@ -928,7 +928,7 @@
width: 40px;
height: 40px;
border-radius: 10px;
- background: rgba(249, 115, 22, 0.08);
+ background: rgba(96, 165, 250, 0.08);
display: flex;
align-items: center;
justify-content: center;
@@ -951,7 +951,7 @@
}
.landing-feature-card.highlight {
- border-color: rgba(249, 115, 22, 0.15);
+ border-color: rgba(96, 165, 250, 0.15);
background: var(--color-bg-card);
grid-column: span 2;
}
@@ -1228,7 +1228,7 @@
.landing-cta-email-input:focus {
border-color: #f97316;
- box-shadow: 0 0 0 3px rgba(249, 115, 22, 0.1);
+ box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
}
.landing-cta-fine-print {
From 61c410e366f37eb56302ff84304fd81ea248368f Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 16:20:24 +0000
Subject: [PATCH 46/49] feat: replace orange-* Tailwind classes with blue-*
equivalents
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
orange-400→blue-400, orange-500→blue-500, orange-600→blue-600
across ~21 component files.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../src/components/assistant/ConcludeSessionModal.tsx | 2 +-
frontend/src/components/dashboard/PreparedSessions.tsx | 6 +++---
frontend/src/components/dashboard/WeeklyCalendar.tsx | 2 +-
.../src/components/flowpilot/FlowPilotActionBar.tsx | 2 +-
frontend/src/components/flowpilot/FlowPilotSession.tsx | 2 +-
.../src/components/flowpilot/StatusUpdateModal.tsx | 6 +++---
frontend/src/components/library/TreeGridView.tsx | 2 +-
frontend/src/components/library/TreeListView.tsx | 2 +-
frontend/src/components/library/TreeTableView.tsx | 2 +-
.../src/components/procedural/InlineVariablePrompt.tsx | 10 +++++-----
.../src/components/procedural/PrepareSessionModal.tsx | 2 +-
.../components/script-builder/ScriptBuilderChat.tsx | 6 +++---
.../src/components/script-builder/ScriptCodeBlock.tsx | 2 +-
.../components/script-builder/ScriptPreviewModal.tsx | 2 +-
.../components/scripts/ParameterizeAndSavePanel.tsx | 2 +-
.../src/components/session/TicketLinkIndicator.tsx | 2 +-
frontend/src/components/session/UpdateTicketModal.tsx | 2 +-
frontend/src/pages/AssistantChatPage.tsx | 2 +-
frontend/src/pages/FlowPilotSessionPage.tsx | 6 +++---
frontend/src/pages/ProceduralNavigationPage.tsx | 10 +++++-----
frontend/src/pages/SessionHistoryPage.tsx | 2 +-
21 files changed, 37 insertions(+), 37 deletions(-)
diff --git a/frontend/src/components/assistant/ConcludeSessionModal.tsx b/frontend/src/components/assistant/ConcludeSessionModal.tsx
index 7fa87ad3..9f995c8d 100644
--- a/frontend/src/components/assistant/ConcludeSessionModal.tsx
+++ b/frontend/src/components/assistant/ConcludeSessionModal.tsx
@@ -467,7 +467,7 @@ export function ConcludeSessionModal({
{/* Paused/Escalated: generating spinner */}
{(outcome === 'paused' || outcome === 'escalated') && generatingUpdate && (
-
+
Generating status update...
)}
diff --git a/frontend/src/components/dashboard/PreparedSessions.tsx b/frontend/src/components/dashboard/PreparedSessions.tsx
index 8a4005ca..5664644e 100644
--- a/frontend/src/components/dashboard/PreparedSessions.tsx
+++ b/frontend/src/components/dashboard/PreparedSessions.tsx
@@ -31,9 +31,9 @@ export function PreparedSessions() {
-
+
Prepared for You
-
+
{sessions.length}
@@ -52,7 +52,7 @@ export function PreparedSessions() {
onClick={() => handleStart(session)}
className={cn(
'group flex w-full items-center justify-between gap-3 rounded-lg border border-border px-4 py-3',
- 'text-left transition-all hover:border-orange-500/30 hover:bg-orange-500/5'
+ 'text-left transition-all hover:border-blue-500/30 hover:bg-blue-500/5'
)}
>
diff --git a/frontend/src/components/dashboard/WeeklyCalendar.tsx b/frontend/src/components/dashboard/WeeklyCalendar.tsx
index ba590e5a..c50bcdb1 100644
--- a/frontend/src/components/dashboard/WeeklyCalendar.tsx
+++ b/frontend/src/components/dashboard/WeeklyCalendar.tsx
@@ -56,7 +56,7 @@ export function WeeklyCalendar({ events = {} }: WeeklyCalendarProps) {
borderBottom: day.isToday ? '2px solid #ea580c' : '1px solid var(--color-border-default)',
}}
>
-
+
{day.label}
diff --git a/frontend/src/components/flowpilot/FlowPilotActionBar.tsx b/frontend/src/components/flowpilot/FlowPilotActionBar.tsx
index 30fc9994..df72f812 100644
--- a/frontend/src/components/flowpilot/FlowPilotActionBar.tsx
+++ b/frontend/src/components/flowpilot/FlowPilotActionBar.tsx
@@ -108,7 +108,7 @@ export function FlowPilotActionBar({
setShowStatusUpdate(true)}
disabled={isProcessing}
- className="flex items-center justify-center gap-1.5 rounded-lg bg-orange-500/10 border border-orange-500/20 px-2.5 sm:px-4 py-2 min-h-[40px] sm:min-h-[44px] text-xs sm:text-sm font-medium text-orange-400 hover:bg-orange-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
+ className="flex items-center justify-center gap-1.5 rounded-lg bg-blue-500/10 border border-blue-500/20 px-2.5 sm:px-4 py-2 min-h-[40px] sm:min-h-[44px] text-xs sm:text-sm font-medium text-blue-400 hover:bg-blue-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
title="Share Update"
>
diff --git a/frontend/src/components/flowpilot/FlowPilotSession.tsx b/frontend/src/components/flowpilot/FlowPilotSession.tsx
index 190fd9cf..b3693feb 100644
--- a/frontend/src/components/flowpilot/FlowPilotSession.tsx
+++ b/frontend/src/components/flowpilot/FlowPilotSession.tsx
@@ -148,7 +148,7 @@ export function FlowPilotSession({
setShowShareCommunication(true)}
- className="flex items-center gap-2 rounded-lg bg-orange-500/10 border border-orange-500/20 px-4 py-2.5 text-sm font-medium text-orange-400 hover:bg-orange-500/20 transition-colors"
+ className="flex items-center gap-2 rounded-lg bg-blue-500/10 border border-blue-500/20 px-4 py-2.5 text-sm font-medium text-blue-400 hover:bg-blue-500/20 transition-colors"
>
{shareLabel}
diff --git a/frontend/src/components/flowpilot/StatusUpdateModal.tsx b/frontend/src/components/flowpilot/StatusUpdateModal.tsx
index 67b9e10a..4279807e 100644
--- a/frontend/src/components/flowpilot/StatusUpdateModal.tsx
+++ b/frontend/src/components/flowpilot/StatusUpdateModal.tsx
@@ -168,7 +168,7 @@ export function StatusUpdateModal({ open, onClose, onGenerate, context = 'status
{/* Step 3: Generating */}
{step === 'generating' && (
-
+
Generating {audience === 'email_draft' ? 'email draft' : audience === 'client_update' ? 'client update' : 'ticket notes'}...
@@ -180,7 +180,7 @@ export function StatusUpdateModal({ open, onClose, onGenerate, context = 'status
{/* Meta badges */}
-
+
{AUDIENCES.find(a => a.value === result.audience)?.label}
@@ -209,7 +209,7 @@ export function StatusUpdateModal({ open, onClose, onGenerate, context = 'status
'flex items-center gap-2 rounded-lg px-4 py-2 min-h-[44px] text-sm font-medium transition-colors',
copied
? 'bg-emerald-500/20 border border-emerald-500/30 text-emerald-400'
- : 'bg-orange-500/10 border border-orange-500/20 text-orange-400 hover:bg-orange-500/20'
+ : 'bg-blue-500/10 border border-blue-500/20 text-blue-400 hover:bg-blue-500/20'
)}
>
{copied ? : }
diff --git a/frontend/src/components/library/TreeGridView.tsx b/frontend/src/components/library/TreeGridView.tsx
index 751305b0..13ca1293 100644
--- a/frontend/src/components/library/TreeGridView.tsx
+++ b/frontend/src/components/library/TreeGridView.tsx
@@ -145,7 +145,7 @@ export function TreeGridView({
onClick={() => onPrepareSession(tree)}
className={cn(
'rounded-md border border-border p-2 text-muted-foreground',
- 'hover:bg-orange-500/10 hover:text-orange-400 hover:border-orange-500/30'
+ 'hover:bg-blue-500/10 hover:text-blue-400 hover:border-blue-500/30'
)}
title="Prepare session for engineer"
aria-label="Prepare session"
diff --git a/frontend/src/components/library/TreeListView.tsx b/frontend/src/components/library/TreeListView.tsx
index 2a9093e3..f172038c 100644
--- a/frontend/src/components/library/TreeListView.tsx
+++ b/frontend/src/components/library/TreeListView.tsx
@@ -148,7 +148,7 @@ export function TreeListView({
onClick={() => onPrepareSession(tree)}
className={cn(
'rounded-md border border-border p-1.5 text-muted-foreground',
- 'hover:bg-orange-500/10 hover:text-orange-400 hover:border-orange-500/30'
+ 'hover:bg-blue-500/10 hover:text-blue-400 hover:border-blue-500/30'
)}
title="Prepare session for engineer"
aria-label="Prepare session"
diff --git a/frontend/src/components/library/TreeTableView.tsx b/frontend/src/components/library/TreeTableView.tsx
index 4277f079..8b3a92b7 100644
--- a/frontend/src/components/library/TreeTableView.tsx
+++ b/frontend/src/components/library/TreeTableView.tsx
@@ -252,7 +252,7 @@ export function TreeTableView({
onClick={() => onPrepareSession(tree)}
className={cn(
'rounded-md border border-border p-1.5 text-muted-foreground',
- 'hover:bg-orange-500/10 hover:text-orange-400 hover:border-orange-500/30'
+ 'hover:bg-blue-500/10 hover:text-blue-400 hover:border-blue-500/30'
)}
title="Prepare session for engineer"
aria-label="Prepare session"
diff --git a/frontend/src/components/procedural/InlineVariablePrompt.tsx b/frontend/src/components/procedural/InlineVariablePrompt.tsx
index 0a264cd9..67114e4b 100644
--- a/frontend/src/components/procedural/InlineVariablePrompt.tsx
+++ b/frontend/src/components/procedural/InlineVariablePrompt.tsx
@@ -59,10 +59,10 @@ export function InlineVariablePrompt({
disabled={disabled}
className={cn(
'inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-sm font-medium transition-all',
- 'border-orange-500/40 bg-orange-500/5 text-orange-400',
- 'hover:border-orange-400/60 hover:bg-orange-500/10 hover:shadow-[0_0_12px_rgba(96,165,250,0.15)]',
+ 'border-blue-500/40 bg-blue-500/5 text-blue-400',
+ 'hover:border-blue-400/60 hover:bg-blue-500/10 hover:shadow-[0_0_12px_rgba(96,165,250,0.15)]',
'disabled:opacity-50 disabled:cursor-not-allowed',
- isRequired && 'ring-1 ring-orange-500/20'
+ isRequired && 'ring-1 ring-blue-500/20'
)}
>
@@ -88,7 +88,7 @@ export function InlineVariablePrompt({
onBlur={() => {
if (!value) setIsEditing(false)
}}
- className="rounded-md border border-orange-500/40 bg-orange-500/5 px-2.5 py-1 text-sm text-foreground shadow-[0_0_12px_rgba(96,165,250,0.15)] focus:border-orange-400 focus:outline-hidden focus:ring-1 focus:ring-orange-400/30"
+ className="rounded-md border border-blue-500/40 bg-blue-500/5 px-2.5 py-1 text-sm text-foreground shadow-[0_0_12px_rgba(96,165,250,0.15)] focus:border-blue-400 focus:outline-hidden focus:ring-1 focus:ring-blue-400/30"
>
{placeholder}
{options.map((opt) => (
@@ -117,7 +117,7 @@ export function InlineVariablePrompt({
onBlur={handleSubmit}
onKeyDown={handleKeyDown}
placeholder={placeholder}
- className="w-48 rounded-md border border-orange-500/40 bg-orange-500/5 px-2.5 py-1 text-sm text-foreground shadow-[0_0_12px_rgba(96,165,250,0.15)] placeholder:text-muted-foreground focus:border-orange-400 focus:outline-hidden focus:ring-1 focus:ring-orange-400/30"
+ className="w-48 rounded-md border border-blue-500/40 bg-blue-500/5 px-2.5 py-1 text-sm text-foreground shadow-[0_0_12px_rgba(96,165,250,0.15)] placeholder:text-muted-foreground focus:border-blue-400 focus:outline-hidden focus:ring-1 focus:ring-blue-400/30"
/>
{helpText && (
diff --git a/frontend/src/components/procedural/PrepareSessionModal.tsx b/frontend/src/components/procedural/PrepareSessionModal.tsx
index b09bb373..010968a2 100644
--- a/frontend/src/components/procedural/PrepareSessionModal.tsx
+++ b/frontend/src/components/procedural/PrepareSessionModal.tsx
@@ -94,7 +94,7 @@ export function PrepareSessionModal({
{/* Header */}
-
+
Prepare Session
{msg.role === 'assistant' && (
-
+
)}
@@ -99,10 +99,10 @@ export function ScriptBuilderChat({
{isLoading && (
-
+
-
+
Generating script...
diff --git a/frontend/src/components/script-builder/ScriptCodeBlock.tsx b/frontend/src/components/script-builder/ScriptCodeBlock.tsx
index 662c7fcc..9b9eb487 100644
--- a/frontend/src/components/script-builder/ScriptCodeBlock.tsx
+++ b/frontend/src/components/script-builder/ScriptCodeBlock.tsx
@@ -55,7 +55,7 @@ export function ScriptCodeBlock({
{/* Header */}
-
+
{filename || 'script'}
{lineCount != null && (
diff --git a/frontend/src/components/script-builder/ScriptPreviewModal.tsx b/frontend/src/components/script-builder/ScriptPreviewModal.tsx
index 02d865a4..5230e7bd 100644
--- a/frontend/src/components/script-builder/ScriptPreviewModal.tsx
+++ b/frontend/src/components/script-builder/ScriptPreviewModal.tsx
@@ -62,7 +62,7 @@ export function ScriptPreviewModal({
{/* Header */}
-
+
{filename || 'script'}
diff --git a/frontend/src/components/scripts/ParameterizeAndSavePanel.tsx b/frontend/src/components/scripts/ParameterizeAndSavePanel.tsx
index 95be3cfb..7cc3051f 100644
--- a/frontend/src/components/scripts/ParameterizeAndSavePanel.tsx
+++ b/frontend/src/components/scripts/ParameterizeAndSavePanel.tsx
@@ -440,7 +440,7 @@ export function ParameterizeAndSavePanel({
type="checkbox"
checked={shareWithTeam}
onChange={(e) => setShareWithTeam(e.target.checked)}
- className="w-4 h-4 rounded border-border bg-card text-orange-500 focus:ring-orange-500/20"
+ className="w-4 h-4 rounded border-border bg-card text-blue-500 focus:ring-blue-500/20"
/>
Share with team
diff --git a/frontend/src/components/session/TicketLinkIndicator.tsx b/frontend/src/components/session/TicketLinkIndicator.tsx
index cefcb16c..16b2ec29 100644
--- a/frontend/src/components/session/TicketLinkIndicator.tsx
+++ b/frontend/src/components/session/TicketLinkIndicator.tsx
@@ -36,7 +36,7 @@ export function TicketLinkIndicator({ session, hasConnection, onLinkClick, onUnl
// Ticket linked
return (
-
+
CW #{session.psa_ticket_id}
diff --git a/frontend/src/components/session/UpdateTicketModal.tsx b/frontend/src/components/session/UpdateTicketModal.tsx
index e80262ed..5f297e66 100644
--- a/frontend/src/components/session/UpdateTicketModal.tsx
+++ b/frontend/src/components/session/UpdateTicketModal.tsx
@@ -259,7 +259,7 @@ export function UpdateTicketModal({ open, onClose, sessionId, onPosted }: Props)
value={opt.value}
checked={noteType === opt.value}
onChange={() => setNoteType(opt.value)}
- className="mt-0.5 accent-orange-400"
+ className="mt-0.5 accent-blue-400"
/>
{opt.label}
diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx
index e15ee397..da71faa2 100644
--- a/frontend/src/pages/AssistantChatPage.tsx
+++ b/frontend/src/pages/AssistantChatPage.tsx
@@ -755,7 +755,7 @@ export default function AssistantChatPage() {
)}
{messages.length >= 2 && (
<>
-
setShowStatusUpdate(true)} disabled={loading} className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:text-orange-400 hover:bg-orange-500/10 transition-colors disabled:opacity-40" title="Share status update">
+ setShowStatusUpdate(true)} disabled={loading} className="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs text-muted-foreground hover:text-blue-400 hover:bg-blue-500/10 transition-colors disabled:opacity-40" title="Share status update">
Update
diff --git a/frontend/src/pages/FlowPilotSessionPage.tsx b/frontend/src/pages/FlowPilotSessionPage.tsx
index 457c369a..55927daf 100644
--- a/frontend/src/pages/FlowPilotSessionPage.tsx
+++ b/frontend/src/pages/FlowPilotSessionPage.tsx
@@ -207,7 +207,7 @@ export default function FlowPilotSessionPage() {
blocker.reset()}
- className="flex-1 rounded-lg bg-gradient-to-r from-orange-500 to-orange-400 px-4 py-2.5 text-sm font-semibold text-white hover:brightness-110 active:scale-[0.98] transition-all"
+ className="flex-1 rounded-lg bg-gradient-to-r from-blue-500 to-blue-400 px-4 py-2.5 text-sm font-semibold text-white hover:brightness-110 active:scale-[0.98] transition-all"
>
Stay in Session
@@ -263,7 +263,7 @@ export default function FlowPilotSessionPage() {
setShowStatusUpdate(true)}
disabled={fp.isProcessing}
- className="flex items-center gap-1.5 rounded-lg bg-orange-500/10 border border-orange-500/20 px-3 py-1.5 text-xs font-medium text-orange-400 hover:bg-orange-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
+ className="flex items-center gap-1.5 rounded-lg bg-blue-500/10 border border-blue-500/20 px-3 py-1.5 text-xs font-medium text-blue-400 hover:bg-blue-500/20 disabled:opacity-40 disabled:pointer-events-none transition-colors"
title="Share Update"
>
@@ -333,7 +333,7 @@ export default function FlowPilotSessionPage() {
{fp.allSteps.length >= 2 && (
{ setShowOverflow(false); setShowStatusUpdate(true) }}
- className="flex w-full items-center gap-2 px-3 py-2.5 text-xs text-orange-400 hover:bg-orange-500/10 transition-colors"
+ className="flex w-full items-center gap-2 px-3 py-2.5 text-xs text-blue-400 hover:bg-blue-500/10 transition-colors"
>
Share Update
diff --git a/frontend/src/pages/ProceduralNavigationPage.tsx b/frontend/src/pages/ProceduralNavigationPage.tsx
index 469fd70d..49b3aaf8 100644
--- a/frontend/src/pages/ProceduralNavigationPage.tsx
+++ b/frontend/src/pages/ProceduralNavigationPage.tsx
@@ -872,7 +872,7 @@ export function ProceduralNavigationPage() {
key={field.variable_name}
className={cn(
'rounded-lg border px-3 py-2.5',
- isFilled ? 'border-border bg-accent' : 'border-orange-500/20 bg-orange-500/5'
+ isFilled ? 'border-border bg-accent' : 'border-blue-500/20 bg-blue-500/5'
)}
>
@@ -880,7 +880,7 @@ export function ProceduralNavigationPage() {
{isFilled ? (
) : (
-
+
)}
{field.label}
@@ -903,7 +903,7 @@ export function ProceduralNavigationPage() {
value={editingVarValue}
onChange={(e) => setEditingVarValue(e.target.value)}
autoFocus
- className="w-full rounded-md border border-orange-500/30 bg-card px-2.5 py-1.5 text-sm text-foreground focus:border-orange-400 focus:outline-hidden focus:ring-1 focus:ring-orange-400/30"
+ className="w-full rounded-md border border-blue-500/30 bg-card px-2.5 py-1.5 text-sm text-foreground focus:border-blue-400 focus:outline-hidden focus:ring-1 focus:ring-blue-400/30"
>
{field.placeholder || 'Select...'}
{field.options.map((opt) => (
@@ -917,7 +917,7 @@ export function ProceduralNavigationPage() {
autoFocus
rows={3}
placeholder={field.placeholder}
- className="w-full rounded-md border border-orange-500/30 bg-card px-2.5 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-orange-400 focus:outline-hidden focus:ring-1 focus:ring-orange-400/30"
+ className="w-full rounded-md border border-blue-500/30 bg-card px-2.5 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-blue-400 focus:outline-hidden focus:ring-1 focus:ring-blue-400/30"
/>
) : (
{ if (e.key === 'Enter') saveEditingVar() }}
autoFocus
placeholder={field.placeholder}
- className="w-full rounded-md border border-orange-500/30 bg-card px-2.5 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-orange-400 focus:outline-hidden focus:ring-1 focus:ring-orange-400/30"
+ className="w-full rounded-md border border-blue-500/30 bg-card px-2.5 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-blue-400 focus:outline-hidden focus:ring-1 focus:ring-blue-400/30"
/>
)}
{field.help_text && (
diff --git a/frontend/src/pages/SessionHistoryPage.tsx b/frontend/src/pages/SessionHistoryPage.tsx
index d814b587..1a10b230 100644
--- a/frontend/src/pages/SessionHistoryPage.tsx
+++ b/frontend/src/pages/SessionHistoryPage.tsx
@@ -564,7 +564,7 @@ export function SessionHistoryPage() {
session.outcome === 'escalated' && 'bg-blue-500/20 text-blue-300',
session.outcome === 'unresolved' && 'bg-rose-500/20 text-rose-300',
session.outcome === 'cancelled' && 'bg-zinc-500/20 text-zinc-300',
- session.outcome === 'resolved_externally' && 'bg-orange-500/20 text-orange-300',
+ session.outcome === 'resolved_externally' && 'bg-blue-500/20 text-blue-300',
!session.outcome && 'bg-accent text-muted-foreground'
)}
>
From 1152b023bf374a6a15a1953887c7bb1d62f96066 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 16:20:52 +0000
Subject: [PATCH 47/49] feat: replace hardcoded orange hex values with blue
equivalents
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
BrandLogo gradient, EmptyStateIllustrations SVGs, categoryColors,
landing page, brand SVG assets, and all remaining files.
Warning #eab308 → #fbbf24 (amber). categoryColors deduped.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/assets/brand/icon.svg | 4 +-
frontend/src/assets/brand/logo-horizontal.svg | 4 +-
.../src/assets/brand/logo-with-tagline.svg | 4 +-
.../account/NotificationSettings.tsx | 2 +-
.../analytics/FlowAnalyticsPanel.tsx | 2 +-
.../components/analytics/PsaMetricsPanel.tsx | 6 +-
frontend/src/components/common/BrandLogo.tsx | 4 +-
.../common/EmptyStateIllustrations.tsx | 110 +++++++++---------
.../src/components/dashboard/FiltersBar.tsx | 2 +-
.../dashboard/GreetingStatStrip.tsx | 2 +-
.../dashboard/KnowledgeBaseCards.tsx | 2 +-
.../components/dashboard/PerformanceCards.tsx | 4 +-
.../src/components/dashboard/QuickActions.tsx | 6 +-
.../components/dashboard/RecentActivity.tsx | 6 +-
.../dashboard/RecentFlowPilotSessions.tsx | 2 +-
.../dashboard/StartSessionInput.tsx | 2 +-
.../src/components/dashboard/TeamSummary.tsx | 4 +-
.../components/dashboard/WeeklyCalendar.tsx | 4 +-
.../src/components/layout/CommandPalette.tsx | 2 +-
frontend/src/components/layout/Sidebar.tsx | 2 +-
frontend/src/components/layout/TopBar.tsx | 2 +-
.../components/library/FolderEditModal.tsx | 4 +-
.../components/sidebar/SidebarStatsBar.tsx | 2 +-
.../components/tree-editor/FlowCanvasNode.tsx | 2 +-
.../src/components/tree-editor/GlowEdge.tsx | 6 +-
frontend/src/constants/categoryColors.ts | 4 +-
frontend/src/pages/FlowPilotAnalyticsPage.tsx | 14 +--
frontend/src/pages/LandingPage.tsx | 8 +-
frontend/src/pages/MyAnalyticsPage.tsx | 2 +-
frontend/src/pages/TeamAnalyticsPage.tsx | 2 +-
.../pages/account/BrandingSettingsPage.tsx | 6 +-
frontend/src/styles/landing.css | 10 +-
32 files changed, 118 insertions(+), 118 deletions(-)
diff --git a/frontend/src/assets/brand/icon.svg b/frontend/src/assets/brand/icon.svg
index 29db5ad9..8091157d 100644
--- a/frontend/src/assets/brand/icon.svg
+++ b/frontend/src/assets/brand/icon.svg
@@ -1,8 +1,8 @@
-
-
+
+
diff --git a/frontend/src/assets/brand/logo-horizontal.svg b/frontend/src/assets/brand/logo-horizontal.svg
index 236e4a7d..8b298f0f 100644
--- a/frontend/src/assets/brand/logo-horizontal.svg
+++ b/frontend/src/assets/brand/logo-horizontal.svg
@@ -1,8 +1,8 @@
-
-
+
+
diff --git a/frontend/src/assets/brand/logo-with-tagline.svg b/frontend/src/assets/brand/logo-with-tagline.svg
index 7b043bd6..cccbee06 100644
--- a/frontend/src/assets/brand/logo-with-tagline.svg
+++ b/frontend/src/assets/brand/logo-with-tagline.svg
@@ -1,8 +1,8 @@
-
-
+
+
diff --git a/frontend/src/components/account/NotificationSettings.tsx b/frontend/src/components/account/NotificationSettings.tsx
index 8cb492f1..6ef79682 100644
--- a/frontend/src/components/account/NotificationSettings.tsx
+++ b/frontend/src/components/account/NotificationSettings.tsx
@@ -294,7 +294,7 @@ export function NotificationSettings() {
type="checkbox"
checked={config.events_enabled[eventKey] ?? false}
onChange={() => handleToggleEvent(config, eventKey)}
- className="h-3.5 w-3.5 rounded border-border bg-card text-primary focus:ring-primary/30 focus:ring-offset-0 cursor-pointer accent-[#ea580c]"
+ className="h-3.5 w-3.5 rounded border-border bg-card text-primary focus:ring-primary/30 focus:ring-offset-0 cursor-pointer accent-[#3b82f6]"
/>
{eventLabel}
diff --git a/frontend/src/components/analytics/FlowAnalyticsPanel.tsx b/frontend/src/components/analytics/FlowAnalyticsPanel.tsx
index 0edca63f..19923775 100644
--- a/frontend/src/components/analytics/FlowAnalyticsPanel.tsx
+++ b/frontend/src/components/analytics/FlowAnalyticsPanel.tsx
@@ -16,7 +16,7 @@ import type { FlowAnalyticsResponse, AnalyticsPeriod } from '@/types'
const CHART_COLORS = {
resolved: '#34d399',
escalated: '#f87171',
- workaround: '#eab308',
+ workaround: '#fbbf24',
unresolved: '#94a3b8',
}
diff --git a/frontend/src/components/analytics/PsaMetricsPanel.tsx b/frontend/src/components/analytics/PsaMetricsPanel.tsx
index 5347d223..c2b5eb1f 100644
--- a/frontend/src/components/analytics/PsaMetricsPanel.tsx
+++ b/frontend/src/components/analytics/PsaMetricsPanel.tsx
@@ -102,14 +102,14 @@ export default function PsaMetricsPanel({ data }: PsaMetricsPanelProps) {
yAxisId="hours"
type="monotone"
dataKey="hours"
- stroke="#f97316"
+ stroke="#60a5fa"
fill="url(#psaHoursGradient)"
strokeWidth={2}
/>
-
-
+
+
diff --git a/frontend/src/components/common/BrandLogo.tsx b/frontend/src/components/common/BrandLogo.tsx
index 52ab9552..f6cadc95 100644
--- a/frontend/src/components/common/BrandLogo.tsx
+++ b/frontend/src/components/common/BrandLogo.tsx
@@ -6,7 +6,7 @@ interface BrandLogoProps {
}
/**
- * Brand logo mark: gradient orange square with rounded corners
+ * Brand logo mark: gradient blue square with rounded corners
* containing a white lightning bolt.
*/
export function BrandLogo({ size = 'sm', className }: BrandLogoProps) {
@@ -19,7 +19,7 @@ export function BrandLogo({ size = 'sm', className }: BrandLogoProps) {
width: dim,
height: dim,
borderRadius: size === 'sm' ? 8 : 14,
- background: 'linear-gradient(135deg, #ea580c, #f97316)',
+ background: 'linear-gradient(135deg, #3b82f6, #60a5fa)',
}}
>
{/* Root node */}
-
+
{/* Branches */}
-
-
+
+
{/* Left child */}
-
+
{/* Right child */}
-
+
{/* Leaf branches */}
-
-
-
-
+
+
+
+
)
}
@@ -29,12 +29,12 @@ export function AnalyticsIllustration() {
return (
{/* Bars */}
-
-
-
-
+
+
+
+
{/* Baseline */}
-
+
)
}
@@ -43,17 +43,17 @@ export function SessionIllustration() {
return (
{/* Card 1 */}
-
-
-
+
+
+
{/* Card 2 */}
-
-
-
+
+
+
{/* Card 3 */}
-
-
-
+
+
+
)
}
@@ -62,19 +62,19 @@ export function IntegrationIllustration() {
return (
{/* Left box */}
-
-
-
+
+
+
{/* Right box */}
-
-
-
+
+
+
{/* Dashed arrows */}
-
-
+
+
{/* Arrow tips */}
-
-
+
+
)
}
@@ -83,14 +83,14 @@ export function StepLibraryIllustration() {
return (
{/* List items */}
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
)
}
@@ -99,16 +99,16 @@ export function ScriptIllustration() {
return (
{/* Terminal window */}
-
+
{/* Title bar */}
-
-
-
+
+
+
{/* Code lines */}
-
-
-
-
+
+
+
+
)
}
@@ -117,14 +117,14 @@ export function ShareIllustration() {
return (
{/* Center node */}
-
+
{/* Top-right node */}
-
+
{/* Bottom-right node */}
-
+
{/* Connecting lines */}
-
-
+
+
)
}
diff --git a/frontend/src/components/dashboard/FiltersBar.tsx b/frontend/src/components/dashboard/FiltersBar.tsx
index 3b1d664e..5bde93a5 100644
--- a/frontend/src/components/dashboard/FiltersBar.tsx
+++ b/frontend/src/components/dashboard/FiltersBar.tsx
@@ -22,7 +22,7 @@ export function FiltersBar({ filters, activeFilter, onFilterChange }: FiltersBar
className={cn(
'shrink-0 rounded-lg border px-3 py-1.5 text-[0.8125rem] font-medium transition-colors',
activeFilter === f.id
- ? 'border-[#f97316]/30 bg-accent-dim text-primary'
+ ? 'border-[#60a5fa]/30 bg-accent-dim text-primary'
: 'border-border bg-card text-muted-foreground hover:border-border/80 hover:text-foreground'
)}
>
diff --git a/frontend/src/components/dashboard/GreetingStatStrip.tsx b/frontend/src/components/dashboard/GreetingStatStrip.tsx
index f3c88fe7..901c700f 100644
--- a/frontend/src/components/dashboard/GreetingStatStrip.tsx
+++ b/frontend/src/components/dashboard/GreetingStatStrip.tsx
@@ -30,7 +30,7 @@ export function GreetingStatStrip() {
const stats: StatItem[] = [
{ icon: CheckCircle, value: resolved, label: 'resolved today', color: '#34d399' },
- { icon: Zap, value: active, label: 'active now', color: '#f97316' },
+ { icon: Zap, value: active, label: 'active now', color: '#60a5fa' },
{ icon: Clock, value: avgMttr, label: 'avg MTTR', color: '#848b9b' },
]
diff --git a/frontend/src/components/dashboard/KnowledgeBaseCards.tsx b/frontend/src/components/dashboard/KnowledgeBaseCards.tsx
index 93258740..7a83286b 100644
--- a/frontend/src/components/dashboard/KnowledgeBaseCards.tsx
+++ b/frontend/src/components/dashboard/KnowledgeBaseCards.tsx
@@ -16,7 +16,7 @@ export function KnowledgeBaseCards() {
const items = [
{ label: 'Flows', value: flowCount, icon: Network, color: '#a78bfa', href: '/trees' },
{ label: 'Scripts', value: '\u2014', icon: Code2, color: '#2dd4bf', href: '/scripts' },
- { label: 'Pending Review', value: '\u2014', icon: ListChecks, color: '#eab308', href: '/review-queue' },
+ { label: 'Pending Review', value: '\u2014', icon: ListChecks, color: '#fbbf24', href: '/review-queue' },
]
return (
diff --git a/frontend/src/components/dashboard/PerformanceCards.tsx b/frontend/src/components/dashboard/PerformanceCards.tsx
index 60816ba5..6ae67aa6 100644
--- a/frontend/src/components/dashboard/PerformanceCards.tsx
+++ b/frontend/src/components/dashboard/PerformanceCards.tsx
@@ -45,7 +45,7 @@ export function PerformanceCards() {
label: 'Avg Resolution',
value: avgMttr > 0 ? `${avgMttr}m` : '\u2014',
icon: Clock,
- iconColor: '#f97316',
+ iconColor: '#60a5fa',
href: '/analytics',
},
{
@@ -59,7 +59,7 @@ export function PerformanceCards() {
label: 'Session Time',
value: totalMinutes > 0 ? `${totalMinutes}m` : '\u2014',
icon: Timer,
- iconColor: '#eab308',
+ iconColor: '#fbbf24',
href: '/analytics',
},
]
diff --git a/frontend/src/components/dashboard/QuickActions.tsx b/frontend/src/components/dashboard/QuickActions.tsx
index a9e96588..1dfd30f8 100644
--- a/frontend/src/components/dashboard/QuickActions.tsx
+++ b/frontend/src/components/dashboard/QuickActions.tsx
@@ -2,10 +2,10 @@ import { useNavigate } from 'react-router-dom'
import { Plus, Play, BookOpen, UserPlus } from 'lucide-react'
const ACTIONS = [
- { icon: Plus, label: 'New Flow', description: 'Create a new flow', href: '/trees/new', color: '#ea580c' },
+ { icon: Plus, label: 'New Flow', description: 'Create a new flow', href: '/trees/new', color: '#3b82f6' },
{ icon: Play, label: 'Resume Session', description: 'Continue where you left off', href: '/sessions', color: '#34d399' },
- { icon: BookOpen, label: 'Browse Solutions', description: 'Explore solutions library', href: '/step-library', color: '#eab308' },
- { icon: UserPlus, label: 'Invite Team', description: 'Add team members', href: '/account', color: '#ea580c' },
+ { icon: BookOpen, label: 'Browse Solutions', description: 'Explore solutions library', href: '/step-library', color: '#fbbf24' },
+ { icon: UserPlus, label: 'Invite Team', description: 'Add team members', href: '/account', color: '#3b82f6' },
] as const
export function QuickActions() {
diff --git a/frontend/src/components/dashboard/RecentActivity.tsx b/frontend/src/components/dashboard/RecentActivity.tsx
index 023d3b73..4a6f235c 100644
--- a/frontend/src/components/dashboard/RecentActivity.tsx
+++ b/frontend/src/components/dashboard/RecentActivity.tsx
@@ -16,9 +16,9 @@ interface RecentActivityProps {
const DEFAULT_ACTIVITIES: ActivityItem[] = [
{ id: '1', icon: Play, iconColor: '#34d399', iconBg: 'rgba(52, 211, 153, 0.1)', description: 'Started VPN Connectivity Triage session', timestamp: '2 min ago' },
- { id: '2', icon: CheckCircle, iconColor: '#ea580c', iconBg: 'rgba(96, 165, 250, 0.1)', description: 'Completed M365 License Provisioning', timestamp: '15 min ago' },
- { id: '3', icon: Edit, iconColor: '#eab308', iconBg: 'rgba(234, 179, 8, 0.1)', description: 'Updated Printer Troubleshooting flow', timestamp: '1 hr ago' },
- { id: '4', icon: GitBranch, iconColor: '#ea580c', iconBg: 'rgba(96, 165, 250, 0.1)', description: 'Created new DNS Resolution flow', timestamp: '3 hr ago' },
+ { id: '2', icon: CheckCircle, iconColor: '#3b82f6', iconBg: 'rgba(96, 165, 250, 0.1)', description: 'Completed M365 License Provisioning', timestamp: '15 min ago' },
+ { id: '3', icon: Edit, iconColor: '#fbbf24', iconBg: 'rgba(234, 179, 8, 0.1)', description: 'Updated Printer Troubleshooting flow', timestamp: '1 hr ago' },
+ { id: '4', icon: GitBranch, iconColor: '#3b82f6', iconBg: 'rgba(96, 165, 250, 0.1)', description: 'Created new DNS Resolution flow', timestamp: '3 hr ago' },
{ id: '5', icon: FileText, iconColor: '#8891a0', iconBg: 'rgba(136, 145, 160, 0.1)', description: 'Exported session report #TK-4821', timestamp: 'Yesterday' },
]
diff --git a/frontend/src/components/dashboard/RecentFlowPilotSessions.tsx b/frontend/src/components/dashboard/RecentFlowPilotSessions.tsx
index 26b21a8c..080353a7 100644
--- a/frontend/src/components/dashboard/RecentFlowPilotSessions.tsx
+++ b/frontend/src/components/dashboard/RecentFlowPilotSessions.tsx
@@ -18,7 +18,7 @@ function timeAgo(dateStr: string): string {
const STATUS_CONFIG: Record = {
resolved: { icon: CheckCircle, color: '#34d399' },
- escalated: { icon: AlertTriangle, color: '#eab308' },
+ escalated: { icon: AlertTriangle, color: '#fbbf24' },
abandoned: { icon: XCircle, color: '#8891a0' },
}
diff --git a/frontend/src/components/dashboard/StartSessionInput.tsx b/frontend/src/components/dashboard/StartSessionInput.tsx
index e710cfa7..4b32c2ca 100644
--- a/frontend/src/components/dashboard/StartSessionInput.tsx
+++ b/frontend/src/components/dashboard/StartSessionInput.tsx
@@ -346,7 +346,7 @@ export function StartSessionInput() {
onClick={() => handleSuggestionClick(label)}
className="group flex items-center gap-1.5 rounded-md border border-border bg-card px-3 py-1.5 text-xs text-muted-foreground transition-all hover:border-[var(--color-border-hover)] hover:bg-[var(--color-bg-elevated)] hover:text-foreground active:scale-[0.97]"
>
-
+
{label}
))}
diff --git a/frontend/src/components/dashboard/TeamSummary.tsx b/frontend/src/components/dashboard/TeamSummary.tsx
index bbfa7ccf..a739aaa3 100644
--- a/frontend/src/components/dashboard/TeamSummary.tsx
+++ b/frontend/src/components/dashboard/TeamSummary.tsx
@@ -19,8 +19,8 @@ export function TeamSummary() {
if (!isAccountOwner) return null
const items = [
- { label: 'Escalations', value: escalationCount, icon: AlertTriangle, color: '#eab308', href: '/escalations' },
- { label: 'Team Activity', value: '\u2014', icon: Activity, color: '#f97316', href: '/analytics' },
+ { label: 'Escalations', value: escalationCount, icon: AlertTriangle, color: '#fbbf24', href: '/escalations' },
+ { label: 'Team Activity', value: '\u2014', icon: Activity, color: '#60a5fa', href: '/analytics' },
{ label: 'Members', value: '\u2014', icon: Users, color: '#a78bfa', href: '/account' },
]
diff --git a/frontend/src/components/dashboard/WeeklyCalendar.tsx b/frontend/src/components/dashboard/WeeklyCalendar.tsx
index c50bcdb1..96f6caeb 100644
--- a/frontend/src/components/dashboard/WeeklyCalendar.tsx
+++ b/frontend/src/components/dashboard/WeeklyCalendar.tsx
@@ -53,7 +53,7 @@ export function WeeklyCalendar({ events = {} }: WeeklyCalendarProps) {
@@ -72,7 +72,7 @@ export function WeeklyCalendar({ events = {} }: WeeklyCalendarProps) {
key={event.id}
className="rounded-md px-2 py-1.5 text-[0.6875rem] cursor-pointer hover:bg-accent/30 transition-colors"
style={{
- borderLeft: `3px solid ${event.type === 'maintenance' ? '#eab308' : '#ea580c'}`,
+ borderLeft: `3px solid ${event.type === 'maintenance' ? '#fbbf24' : '#3b82f6'}`,
background: 'rgba(255, 255, 255, 0.02)',
}}
>
diff --git a/frontend/src/components/layout/CommandPalette.tsx b/frontend/src/components/layout/CommandPalette.tsx
index 03fe9f25..a83f8e28 100644
--- a/frontend/src/components/layout/CommandPalette.tsx
+++ b/frontend/src/components/layout/CommandPalette.tsx
@@ -355,7 +355,7 @@ export function CommandPalette({ open, onClose }: CommandPaletteProps) {
{/* Section label */}
-
+
{group.label}
diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx
index 1a1a7e7a..c1199dff 100644
--- a/frontend/src/components/layout/Sidebar.tsx
+++ b/frontend/src/components/layout/Sidebar.tsx
@@ -450,7 +450,7 @@ export function Sidebar() {
>
{/* Drawer header */}
-
+
{activeFlyoutGroup.label}
diff --git a/frontend/src/components/layout/TopBar.tsx b/frontend/src/components/layout/TopBar.tsx
index 49c477bf..3555b7db 100644
--- a/frontend/src/components/layout/TopBar.tsx
+++ b/frontend/src/components/layout/TopBar.tsx
@@ -126,7 +126,7 @@ export function TopBar() {
setUserMenuOpen(!userMenuOpen)}
className="flex h-8 w-8 items-center justify-center rounded-[10px] text-xs font-heading font-bold text-white hover:opacity-90 transition-opacity"
- style={{ background: 'linear-gradient(135deg, #ea580c, #f97316)' }}
+ style={{ background: 'linear-gradient(135deg, #3b82f6, #60a5fa)' }}
title={user?.name || user?.email || 'User'}
>
{initials}
diff --git a/frontend/src/components/library/FolderEditModal.tsx b/frontend/src/components/library/FolderEditModal.tsx
index 03a87b83..83ed455e 100644
--- a/frontend/src/components/library/FolderEditModal.tsx
+++ b/frontend/src/components/library/FolderEditModal.tsx
@@ -12,8 +12,8 @@ const FOLDER_COLORS = [
'#8b5cf6', // Violet
'#ec4899', // Pink
'#ef4444', // Red
- '#f97316', // Orange
- '#eab308', // Yellow
+ '#60a5fa', // Orange
+ '#fbbf24', // Yellow
'#22c55e', // Green
'#14b8a6', // Teal
'#3b82f6', // Blue
diff --git a/frontend/src/components/sidebar/SidebarStatsBar.tsx b/frontend/src/components/sidebar/SidebarStatsBar.tsx
index 3ac32a9a..85139a22 100644
--- a/frontend/src/components/sidebar/SidebarStatsBar.tsx
+++ b/frontend/src/components/sidebar/SidebarStatsBar.tsx
@@ -69,7 +69,7 @@ export function SidebarStatsBar({ resolved, active, completedMinutes, activeSess
{active}
diff --git a/frontend/src/components/tree-editor/FlowCanvasNode.tsx b/frontend/src/components/tree-editor/FlowCanvasNode.tsx
index 63969bc4..009ac2f1 100644
--- a/frontend/src/components/tree-editor/FlowCanvasNode.tsx
+++ b/frontend/src/components/tree-editor/FlowCanvasNode.tsx
@@ -23,7 +23,7 @@ const NODE_TYPE_CONFIG: Record
, {
label: 'Action',
borderClass: 'border-l-4 border-l-yellow-500',
badgeClass: 'bg-yellow-500/20 text-yellow-400',
- minimapColor: '#eab308',
+ minimapColor: '#fbbf24',
},
solution: {
icon: CheckCircle,
diff --git a/frontend/src/components/tree-editor/GlowEdge.tsx b/frontend/src/components/tree-editor/GlowEdge.tsx
index 154c10ad..5e0841c9 100644
--- a/frontend/src/components/tree-editor/GlowEdge.tsx
+++ b/frontend/src/components/tree-editor/GlowEdge.tsx
@@ -54,14 +54,14 @@ export function GlowEdgeDefs() {
{/* Downstream: accent brand */}
-
-
+
+
{/* Upstream: amber */}
-
+
{/* Glow filters */}
diff --git a/frontend/src/constants/categoryColors.ts b/frontend/src/constants/categoryColors.ts
index db181e09..e0e01d0d 100644
--- a/frontend/src/constants/categoryColors.ts
+++ b/frontend/src/constants/categoryColors.ts
@@ -4,9 +4,9 @@ export const CATEGORY_COLORS = [
'#f59e0b', // amber
'#ef4444', // red
'#8b5cf6', // violet
- '#ea580c', // deep orange
+ '#0891b2', // cyan
'#ec4899', // pink
- '#f97316', // orange
+ '#60a5fa', // sky blue
'#14b8a6', // teal
'#6366f1', // indigo
] as const
diff --git a/frontend/src/pages/FlowPilotAnalyticsPage.tsx b/frontend/src/pages/FlowPilotAnalyticsPage.tsx
index a1e4f134..6219876b 100644
--- a/frontend/src/pages/FlowPilotAnalyticsPage.tsx
+++ b/frontend/src/pages/FlowPilotAnalyticsPage.tsx
@@ -217,7 +217,7 @@ export default function FlowPilotAnalyticsPage() {
icon={BarChart3}
label="Total Sessions"
value={dashboard.total_sessions}
- iconColor="#f97316"
+ iconColor="#60a5fa"
/>
-
-
+
+
@@ -313,7 +313,7 @@ export default function FlowPilotAnalyticsPage() {
labelStyle={{ color: '#f0f2f5' }}
/>
-
+
) : (
@@ -343,7 +343,7 @@ export default function FlowPilotAnalyticsPage() {
label="Exploring"
count={conf.exploring_sessions}
rate={conf.exploring_resolution_rate}
- color="#eab308"
+ color="#fbbf24"
total={conf.guided_sessions + conf.exploring_sessions + conf.discovery_sessions}
/>
@@ -221,15 +221,15 @@ export default function LandingPage() {
User can't access shared drive after password reset
- FlowPilot:
+ FlowPilot:
This is likely a cached credential issue. Let's check a few things:
- FlowPilot:
+ FlowPilot:
1. Run klist purge to clear Kerberos tickets
- FlowPilot:
+ FlowPilot:
2. Open Credential Manager → remove saved entries for the share
diff --git a/frontend/src/pages/MyAnalyticsPage.tsx b/frontend/src/pages/MyAnalyticsPage.tsx
index e760e278..90eb81c9 100644
--- a/frontend/src/pages/MyAnalyticsPage.tsx
+++ b/frontend/src/pages/MyAnalyticsPage.tsx
@@ -20,7 +20,7 @@ import type { PersonalAnalyticsResponse, AnalyticsPeriod } from '@/types'
const OUTCOME_COLORS: Record
= {
resolved: '#34d399',
escalated: '#f87171',
- workaround: '#eab308',
+ workaround: '#fbbf24',
unresolved: '#94a3b8',
}
diff --git a/frontend/src/pages/TeamAnalyticsPage.tsx b/frontend/src/pages/TeamAnalyticsPage.tsx
index 421a4f97..d0cd1393 100644
--- a/frontend/src/pages/TeamAnalyticsPage.tsx
+++ b/frontend/src/pages/TeamAnalyticsPage.tsx
@@ -21,7 +21,7 @@ import type { TeamAnalyticsResponse, AnalyticsPeriod } from '@/types'
const CHART_COLORS = {
resolved: '#34d399',
escalated: '#f87171',
- workaround: '#eab308',
+ workaround: '#fbbf24',
unresolved: '#94a3b8',
}
diff --git a/frontend/src/pages/account/BrandingSettingsPage.tsx b/frontend/src/pages/account/BrandingSettingsPage.tsx
index 7fb3fbf6..459afe2d 100644
--- a/frontend/src/pages/account/BrandingSettingsPage.tsx
+++ b/frontend/src/pages/account/BrandingSettingsPage.tsx
@@ -11,7 +11,7 @@ interface AccountBranding {
company_name: string | null
}
-const DEFAULT_COLOR = '#ea580c'
+const DEFAULT_COLOR = '#3b82f6'
export function BrandingSettingsPage() {
const [branding, setBranding] = useState(null)
@@ -207,7 +207,7 @@ export function BrandingSettingsPage() {
const val = e.target.value
setPrimaryColor(val)
}}
- placeholder="#ea580c"
+ placeholder="#3b82f6"
maxLength={7}
className={cn(
'w-32 rounded-lg border border-border bg-card px-3 py-2 text-sm',
@@ -224,7 +224,7 @@ export function BrandingSettingsPage() {
- Hex color code for the primary accent color (e.g. #ea580c).
+ Hex color code for the primary accent color (e.g. #3b82f6).
diff --git a/frontend/src/styles/landing.css b/frontend/src/styles/landing.css
index 266f4ca8..c2bdc99d 100644
--- a/frontend/src/styles/landing.css
+++ b/frontend/src/styles/landing.css
@@ -978,7 +978,7 @@
}
.landing-pricing-card.featured {
- border-color: #f97316;
+ border-color: #60a5fa;
background: var(--color-bg-card);
position: relative;
}
@@ -1062,7 +1062,7 @@
.landing-pricing-features li::before {
content: '\2713';
- color: #f97316;
+ color: #60a5fa;
font-weight: 700;
font-size: 0.8rem;
margin-top: 2px;
@@ -1142,7 +1142,7 @@
.landing-testimonial-quote::before {
content: '\201C';
font-size: 4rem;
- color: #f97316;
+ color: #60a5fa;
opacity: 0.3;
vertical-align: super;
margin-right: 1rem;
@@ -1154,7 +1154,7 @@
.landing-testimonial-quote::after {
content: '\201D';
font-size: 4rem;
- color: #f97316;
+ color: #60a5fa;
opacity: 0.3;
vertical-align: sub;
margin-left: 1rem;
@@ -1227,7 +1227,7 @@
}
.landing-cta-email-input:focus {
- border-color: #f97316;
+ border-color: #60a5fa;
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
}
From 99a1af3585009c8481d3c408f1a28595915c4470 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Sun, 29 Mar 2026 16:21:27 +0000
Subject: [PATCH 48/49] fix: update meta theme-color to new sidebar color
(#0e1016)
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/index.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/index.html b/frontend/index.html
index 35e65ff1..6e98ac5c 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -14,7 +14,7 @@
-
+