@burdenoff/microfe-vibecontrols 2026.528.8 → 2026.528.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/agent-manager/AgentManagerInputArea.js +70 -64
- package/dist/components/agent-manager/AgentManagerInputArea.js.map +1 -1
- package/dist/generated/wspace-operations.js +1533 -1512
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.js +15 -13
- package/dist/generated/wspace-types.js.map +1 -1
- package/dist/index.js +10 -10
- package/dist/pages/SessionDetailsPage.js +460 -404
- package/dist/pages/SessionDetailsPage.js.map +1 -1
- package/dist/services/schedulerApi.js +37 -15
- package/dist/services/schedulerApi.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionDetailsPage.js","names":[],"sources":["../../src/pages/SessionDetailsPage.tsx"],"sourcesContent":["import { useState, useCallback, useEffect, useRef } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { VIBE_PERMISSIONS } from '../constants/permissions';\nimport { EntityTagPicker } from '@/components/shared/EntityTagPicker';\nimport { PageSkeleton } from '@/components/shared/PageSkeleton';\nimport { useParams } from 'react-router-dom';\nimport { useVibeControlsStore } from '@/store/vibeControlsStore';\nimport {\n ArrowLeft,\n Terminal,\n Play,\n Square,\n RotateCcw,\n ExternalLink,\n Server,\n Zap,\n Clock,\n Folder,\n Code,\n Variable,\n Hash,\n RefreshCw,\n Loader2,\n AlertCircle,\n Maximize2,\n Minimize2,\n ChevronDown,\n ChevronUp,\n Pencil,\n Check,\n X,\n Tag,\n} from 'lucide-react';\nimport {\n PageHeader,\n Card,\n StatusBadge,\n EmptyState,\n ErrorState,\n ConfirmDialog,\n} from '@/components/shared';\nimport { MotionPage, MotionList, MotionItem } from '@burdenoff/fe-libs/shared/motion';\nimport { useVibeControls, useVibeNavigate } from '@/providers/VibeControlsProvider';\nimport {\n useGetSessionQuery,\n useStartSessionMutation,\n useStopSessionMutation,\n useRestartSessionMutation,\n useStartSessionTerminalMutation,\n useRestartSessionTerminalMutation,\n useUpdateSessionMutation,\n} from '@/generated/wspace-operations';\nimport {\n getQuotaDialogContent,\n markQuotaHandledLocally,\n isQuotaExhaustedError,\n} from '@/utils/quotaUtils';\n\n/**\n * Resolves the terminal URL for a session.\n *\n * The agent's auth middleware accepts `?apiKey=` on `/terminal/*` (browser\n * embeds can't set custom headers on an iframe), so we append it from the\n * session's source agent. Without this, ttyd returns 401 inside the iframe.\n *\n * KNOWN SECURITY FINDING (BOFF-2636 follow-up): embedding the long-lived\n * `agentApiKey` in the URL leaks it to browser history, the Referer header,\n * and proxy logs. The svc already exposes a `terminalAccessUrl` field that\n * mints a short-lived HMAC `?vt=<token>`, but ttyd's WebSocket upgrade in\n * a cross-site iframe doesn't currently propagate that auth (Chrome CHIPS\n * / third-party cookie partitioning). Pinned to the apiKey path until the\n * WS layer is fixed — see Linear ticket for the design.\n */\nfunction getTerminalUrl(session: {\n ttydUrl?: string | null;\n agent?: { agentApiKey?: string | null } | null;\n}): string | null {\n const url = session.ttydUrl;\n if (!url) return null;\n const apiKey = session.agent?.agentApiKey;\n if (!apiKey) return url;\n try {\n const u = new URL(url);\n if (!u.searchParams.has('apiKey')) {\n u.searchParams.set('apiKey', apiKey);\n }\n return u.toString();\n } catch {\n return url;\n }\n}\n\n// Escape regex meta-chars for use inside `new RegExp(...)`. Pure,\n// no closure state, so hoisted to module scope to avoid re-creating\n// the function on every `tr()` invocation.\nconst escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n/**\n * Session details page - shows comprehensive information about a single session\n * with an integrated terminal panel.\n */\nexport function SessionDetailsPage() {\n const { t } = useI18n();\n // tr(key, fallback, params?) — forwards params to useI18n().t so\n // `{{name}}` placeholders are interpolated. Also interpolates into\n // the fallback when the i18n key isn't in the catalog yet, so a\n // missing key doesn't render literal `{{name}}` text to users.\n const tr = useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string => {\n const value = t(key, params);\n if (value !== key) return value;\n if (!params) return fallback;\n // Mirror the i18n layer's `{{key}}` placeholder syntax. Replacer\n // function (not a string) so values containing `$$`/`$&` aren't\n // interpreted as `String.prototype.replace` specials.\n return Object.entries(params).reduce(\n (acc, [k, v]) =>\n acc.replace(new RegExp(`\\\\{\\\\{${escapeRe(k)}\\\\}\\\\}`, 'g'), () => String(v)),\n fallback\n );\n },\n [t]\n );\n const { id } = useParams<{ id: string }>();\n const { basePath: basePathProp } = useVibeControls();\n const basePath = basePathProp || '';\n const navigate = useVibeNavigate();\n const { hasPermission, isLoading: permissionsLoading, permissions } = usePermissions();\n\n const [startSession] = useStartSessionMutation({ refetchQueries: ['GetSession'] });\n const [stopSession] = useStopSessionMutation({ refetchQueries: ['GetSession'] });\n const [restartSession] = useRestartSessionMutation({ refetchQueries: ['GetSession'] });\n const [startSessionTerminal] = useStartSessionTerminalMutation({\n refetchQueries: ['GetSession'],\n });\n const [restartSessionTerminal] = useRestartSessionTerminalMutation({\n refetchQueries: ['GetSession'],\n });\n const [updateSession] = useUpdateSessionMutation({ refetchQueries: ['GetSession'] });\n\n // Command/working-directory inline edit state\n const [editingCommand, setEditingCommand] = useState(false);\n const [editCommand, setEditCommand] = useState('');\n const [editWorkingDirectory, setEditWorkingDirectory] = useState('');\n const [editSaving, setEditSaving] = useState(false);\n\n const handleEditCommandOpen = () => {\n setEditCommand(session?.command || '');\n setEditWorkingDirectory(session?.workingDirectory || '');\n setEditingCommand(true);\n };\n\n const handleEditCommandSave = async () => {\n if (!session?.id) return;\n setEditSaving(true);\n try {\n await updateSession({\n variables: {\n id: session.id,\n input: {\n command: editCommand || null,\n workingDirectory: editWorkingDirectory || null,\n },\n },\n });\n setEditingCommand(false);\n } finally {\n setEditSaving(false);\n }\n };\n\n // Fetch session by ID from GraphQL API\n const { data, loading, error, refetch } = useGetSessionQuery({\n variables: { id: id || '' },\n skip: !id,\n fetchPolicy: 'cache-and-network',\n });\n\n // Get session from response\n const session = data?.session;\n\n // Get related agent and vibe from the session response\n const agent = session?.agent || null;\n const vibe = session?.vibe || null;\n\n // ── Sync active context ────────────────────────────────────────────────\n useEffect(() => {\n if (session) {\n useVibeControlsStore.setState({\n activeAgentId: (agent?.id as string) ?? null,\n selectedAgentId: (agent?.id as string) ?? null,\n activeVibeId: (vibe?.id as string) ?? null,\n selectedVibeId: (vibe?.id as string) ?? null,\n activeSessionId: session.id,\n selectedSessionId: session.id,\n });\n }\n }, [session?.id, agent?.id, vibe?.id]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Terminal state\n const [terminalOpen, setTerminalOpen] = useState(true);\n const [terminalMaximized, setTerminalMaximized] = useState(false);\n const [terminalLoading, setTerminalLoading] = useState(false);\n const [terminalError, setTerminalError] = useState<string | null>(null);\n const [showQuotaExhausted, setShowQuotaExhausted] = useState(false);\n const [showRestartTerminalConfirm, setShowRestartTerminalConfirm] = useState(false);\n const [exitedNotice, setExitedNotice] = useState<{\n exitCode: number | null;\n } | null>(null);\n const prevStatusRef = useRef<string | null>(null);\n\n // Ref target for scrollIntoView when the terminal opens.\n const terminalPanelRef = useRef<HTMLDivElement | null>(null);\n\n // Track first-render + the previous open state so the toggle-driven\n // scroll only fires on a deliberate closed→open transition, not on\n // initial mount (terminalOpen defaults to true).\n const didInitialMountRef = useRef(false);\n const prevTerminalOpenRef = useRef(false);\n\n const terminalUrl = session ? getTerminalUrl(session) : null;\n\n const handleBack = () => {\n navigate('/sessions');\n };\n\n const handleStartSession = async () => {\n if (!session?.id) return;\n try {\n await startSession({ variables: { id: session.id } });\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n return;\n }\n\n console.error('Failed to start session:', err);\n }\n };\n\n const handleStopSession = async () => {\n if (!session?.id) return;\n try {\n await stopSession({ variables: { id: session.id } });\n } catch (err) {\n console.error('Failed to stop session:', err);\n }\n };\n\n const handleRestartSession = async () => {\n if (!session?.id) return;\n try {\n await restartSession({ variables: { id: session.id } });\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n return;\n }\n\n console.error('Failed to restart session:', err);\n }\n };\n\n // Scroll the terminal panel into view as a fire-and-forget op,\n // gated on the panel ref being mounted. Owned by the handler that\n // triggered the action, not by an effect — so a successful\n // start/restart always scrolls exactly once, even when the URL\n // didn't change (no useEffect re-run otherwise).\n const scrollTerminalIntoView = useCallback(() => {\n requestAnimationFrame(() => {\n terminalPanelRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' });\n });\n }, []);\n\n // Shared bookkeeping for start/restart-terminal: identical try/catch\n // /finally shape, identical quota-exhausted + generic-error handling;\n // only the mutation and the fallback error string differ. Takes the\n // session id as an explicit arg so callers don't need a non-null\n // assertion against `session` — the helper owns the null guard.\n const withTerminalAction = useCallback(\n async <T,>(\n action: (id: string) => Promise<{\n error?: { message: string } | null | undefined;\n data?: T | null | undefined;\n }>,\n dataKey: keyof T,\n fallbackErrorMsg: string\n ) => {\n const id = session?.id;\n if (!id) return;\n setTerminalLoading(true);\n setTerminalError(null);\n let succeeded = false;\n try {\n const result = await action(id);\n if (result.error && isQuotaExhaustedError(result.error)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n } else if (result.error) {\n setTerminalError(result.error.message);\n } else if (!result.data || !result.data[dataKey]) {\n setTerminalError(fallbackErrorMsg);\n } else {\n setTerminalOpen(true);\n succeeded = true;\n }\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n return;\n }\n const message = err instanceof Error ? err.message : fallbackErrorMsg;\n setTerminalError(message);\n } finally {\n setTerminalLoading(false);\n if (succeeded) scrollTerminalIntoView();\n }\n },\n [session?.id, scrollTerminalIntoView]\n );\n\n const handleStartTerminal = useCallback(\n () =>\n withTerminalAction(\n (id) => startSessionTerminal({ variables: { id } }),\n 'startSessionTerminal',\n 'Failed to start terminal'\n ),\n [startSessionTerminal, withTerminalAction]\n );\n\n const handleRestartTerminal = useCallback(\n () =>\n withTerminalAction(\n (id) => restartSessionTerminal({ variables: { id } }),\n 'restartSessionTerminal',\n 'Failed to restart terminal'\n ),\n [restartSessionTerminal, withTerminalAction]\n );\n\n // Scroll on the toggle button — closed→open. User-initiated start/\n // restart paths scroll inline from their handlers (see\n // `scrollTerminalIntoView`), which avoids the stale-flag problem\n // where a no-op success leaves a ref stuck true and a later\n // background refetch fires an unexpected scroll.\n useEffect(() => {\n if (!didInitialMountRef.current) {\n didInitialMountRef.current = true;\n prevTerminalOpenRef.current = terminalOpen;\n return;\n }\n const justOpened = terminalOpen && !prevTerminalOpenRef.current;\n prevTerminalOpenRef.current = terminalOpen;\n if (justOpened) scrollTerminalIntoView();\n }, [terminalOpen, scrollTerminalIntoView]);\n\n // Track the last session whose status we processed, so a route\n // change to a sibling /sessions/:id reseeds `prevStatusRef` with\n // the new session's *current* status — not `null`. Reseeding to\n // null was the bug: if the next status update happened to equal\n // the seed, the transition guard (prev === 'RUNNING'/'STARTING')\n // never matched and the command-exited notice never fired.\n const sessionId = session?.id ?? null;\n const lastProcessedSessionRef = useRef<string | null>(null);\n\n // Surface \"command exited\" inline when the session's status flips\n // to STOPPED while the terminal panel is still open — otherwise\n // the empty iframe looks like a crash.\n useEffect(() => {\n const status = session?.status ?? null;\n const exitCode = session?.exitCode ?? null;\n\n // Session changed: reseed prev to the new session's current\n // status and clear any stale notice. Don't fire a transition on\n // this first observation — we have no \"before\" state yet.\n if (sessionId !== lastProcessedSessionRef.current) {\n lastProcessedSessionRef.current = sessionId;\n prevStatusRef.current = status;\n setExitedNotice(null);\n return;\n }\n\n if (status === 'RUNNING' || status === 'STARTING') {\n // Clear stale notice once the session is running again, then\n // record the status. Mutually exclusive with the STOPPED paths\n // below, so handle it with an early return.\n setExitedNotice(null);\n prevStatusRef.current = status;\n return;\n }\n\n const prev = prevStatusRef.current;\n if (status === 'STOPPED' && (prev === 'RUNNING' || prev === 'STARTING') && terminalOpen) {\n setExitedNotice({ exitCode });\n } else if (status === 'STOPPED' && terminalOpen && exitCode !== null) {\n // exitCode can land in a later GraphQL response than the\n // status transition (poll N returns STOPPED + null code, poll\n // N+1 returns the real code). Patch the existing notice so\n // the banner shows the real code rather than \"(exit null)\".\n setExitedNotice((cur) => (cur && cur.exitCode === null ? { exitCode } : cur));\n }\n prevStatusRef.current = status;\n }, [sessionId, session?.status, session?.exitCode, terminalOpen]);\n\n const handleOpenInNewTab = useCallback(() => {\n if (terminalUrl) {\n window.open(terminalUrl, '_blank');\n }\n }, [terminalUrl]);\n\n if (permissionsLoading)\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <p className=\"text-text-secondary\">\n {tr('sessionDetails.loadingPermissions', 'Loading permissions...')}\n </p>\n </div>\n );\n if (\n permissions &&\n (permissions.permissionStrings?.length ?? 0) > 0 &&\n !hasPermission(VIBE_PERMISSIONS.SESSION_READ)\n )\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <h2 className=\"text-lg font-semibold text-foreground\">\n {tr('sessionDetails.accessDenied', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'sessionDetails.accessDeniedDesc',\n \"You don't have permission to access this page.\"\n )}\n </p>\n </div>\n </div>\n );\n\n const handleAgentClick = () => {\n if (!agent) return;\n navigate(`/agents/${agent.id}`);\n };\n\n const handleVibeClick = () => {\n if (!vibe) return;\n navigate(`/vibes/${vibe.id}`);\n };\n\n const formatTimestamp = (timestamp: string | null | undefined) => {\n if (!timestamp) return 'Never';\n const diff = Date.now() - new Date(timestamp).getTime();\n if (diff < 60000) return 'Just now';\n if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;\n if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;\n return new Date(timestamp).toLocaleDateString();\n };\n\n // Guard states wrapped in MotionPage to prevent unmount/remount animation replay\n if (loading && !session) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('sessionDetails.backToSessions', 'Back to Sessions')}\n </button>\n <PageSkeleton variant=\"detail\" />\n </MotionPage>\n );\n }\n\n if (error && !session) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n Back to Sessions\n </button>\n <ErrorState\n title={tr('sessionDetails.failedLoad', 'Failed to load session')}\n message={error.message}\n onRetry={() => refetch()}\n />\n </MotionPage>\n );\n }\n\n if (!session) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n Back to Sessions\n </button>\n <EmptyState\n icon={<Terminal className=\"size-8 text-text-secondary\" />}\n title={tr('sessionDetails.notFound', 'Session not found')}\n description={`${tr('sessionDetails.notFoundDescPrefix', 'No session with ID')} \"${id}\" ${tr('sessionDetails.notFoundDescSuffix', 'exists')}`}\n action={\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors\"\n >\n {tr('sessionDetails.backToSessions', 'Back to Sessions')}\n </button>\n }\n />\n </MotionPage>\n );\n }\n\n const isRunning = session.status === 'RUNNING';\n const terminalHeight = terminalMaximized ? 'h-[calc(100vh-200px)]' : 'h-[280px] sm:h-[420px]';\n\n return (\n <MotionPage className=\"flex flex-col bg-bg-surface min-h-screen\">\n {/* Main content - scrollable */}\n <div\n className={`flex-1 overflow-y-auto space-y-6 p-4 sm:p-6 ${terminalOpen && terminalUrl ? 'pb-2' : ''}`}\n >\n {/* Back Button */}\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n Back to Sessions\n </button>\n\n {/* Header */}\n <PageHeader\n title={session.name}\n description={session.command || 'No command specified'}\n breadcrumb={[\n { label: 'Sessions', path: basePath ? `${basePath}/sessions` : '/sessions' },\n { label: session.name },\n ]}\n actions={\n <div className=\"flex items-center gap-2 flex-wrap\">\n <button\n type=\"button\"\n onClick={() => refetch()}\n disabled={loading}\n className=\"flex items-center gap-2 px-3 py-2 bg-bg-sunken text-text-primary rounded-md hover:bg-bg-sunken/80 transition-colors disabled:opacity-50\"\n title=\"Refresh\"\n >\n <RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />\n </button>\n {isRunning ? (\n <>\n <button\n type=\"button\"\n onClick={handleStopSession}\n className=\"flex items-center gap-2 px-3 py-2 bg-bg-sunken text-text-primary rounded-md hover:bg-bg-sunken/80 transition-colors\"\n >\n <Square className=\"size-4\" />\n {tr('sessionDetails.stopCommand', 'Stop Command')}\n </button>\n <button\n type=\"button\"\n onClick={handleRestartSession}\n className=\"flex items-center gap-2 px-3 py-2 bg-bg-sunken text-text-primary rounded-md hover:bg-bg-sunken/80 transition-colors\"\n >\n <RotateCcw className=\"size-4\" />\n {tr('sessionDetails.restartCommand', 'Restart Command')}\n </button>\n </>\n ) : (\n <button\n type=\"button\"\n onClick={handleStartSession}\n className=\"flex items-center gap-2 px-3 py-2 bg-status-success-bg text-status-success-text rounded-md hover:opacity-80 transition-colors\"\n >\n <Play className=\"size-4\" />\n {tr('sessionDetails.runCommand', 'Run Command')}\n </button>\n )}\n {/* Terminal toggle + open in new tab */}\n {terminalUrl ? (\n <>\n <button\n type=\"button\"\n onClick={() => setTerminalOpen((prev) => !prev)}\n className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors ${\n terminalOpen\n ? 'bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover'\n : 'bg-bg-sunken text-text-primary hover:bg-bg-sunken/80'\n }`}\n title={\n terminalOpen\n ? tr('sessionDetails.hideTerminal', 'Hide terminal')\n : tr('sessionDetails.showTerminal', 'Show terminal')\n }\n >\n <Terminal className=\"size-4\" />\n {tr('sessionDetails.terminal', 'Terminal')}\n </button>\n <button\n type=\"button\"\n onClick={() => setShowRestartTerminalConfirm(true)}\n disabled={terminalLoading}\n className=\"flex items-center gap-1.5 p-2 bg-bg-sunken text-text-secondary rounded-md hover:text-text-primary hover:bg-bg-sunken/80 transition-colors disabled:opacity-50\"\n title={tr(\n 'sessionDetails.restartTerminalTitle',\n 'Restart terminal — kills the tmux session and re-runs the command'\n )}\n >\n {terminalLoading ? (\n <Loader2 className=\"size-3.5 animate-spin\" />\n ) : (\n <RotateCcw className=\"size-3.5\" />\n )}\n <span className=\"text-xs\">\n {tr('sessionDetails.restartTerminalShort', 'Restart')}\n </span>\n </button>\n <button\n type=\"button\"\n onClick={handleOpenInNewTab}\n className=\"flex items-center p-2 bg-bg-sunken text-text-secondary rounded-md hover:text-text-primary hover:bg-bg-sunken/80 transition-colors\"\n title={tr('sessionDetails.openInNewTab', 'Open terminal in new tab')}\n >\n <ExternalLink className=\"size-4\" />\n </button>\n </>\n ) : (\n <button\n type=\"button\"\n onClick={handleStartTerminal}\n disabled={terminalLoading || !agent}\n className=\"flex items-center gap-2 px-3 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n title={\n !agent\n ? tr('sessionDetails.noAgentAssigned', 'No agent assigned')\n : !agent.tunnelUrl\n ? tr(\n 'sessionDetails.agentNoTunnelUrl',\n 'Agent has no tunnel URL configured'\n )\n : tr('sessionDetails.openTerminal', 'Open terminal')\n }\n >\n {terminalLoading ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : (\n <Terminal className=\"size-4\" />\n )}\n {tr('sessionDetails.openTerminal', 'Open Terminal')}\n </button>\n )}\n </div>\n }\n />\n\n {/* Status Cards */}\n <MotionList stagger={0.04} className=\"grid grid-cols-2 sm:grid-cols-4 gap-4\">\n <MotionItem>\n <Card className=\"p-4\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Terminal className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('sessionDetails.statusLabel', 'Status')}\n </p>\n <StatusBadge status={session.status} size=\"md\" />\n </div>\n </div>\n </Card>\n </MotionItem>\n <MotionItem>\n <Card className=\"p-4\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Code className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('sessionDetails.typeLabel', 'Type')}\n </p>\n <p className=\"font-semibold text-text-primary\">{session.type}</p>\n </div>\n </div>\n </Card>\n </MotionItem>\n <MotionItem>\n <Card className=\"p-4\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Hash className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('sessionDetails.pidLabel', 'PID')}\n </p>\n <p className=\"font-semibold text-text-primary\">{session.pid || 'N/A'}</p>\n </div>\n </div>\n </Card>\n </MotionItem>\n <MotionItem>\n <Card className=\"p-4\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Server className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('sessionDetails.agentLabel', 'Agent')}\n </p>\n <p className=\"font-semibold text-text-primary truncate\">\n {agent?.name || 'Unknown'}\n </p>\n </div>\n </div>\n </Card>\n </MotionItem>\n </MotionList>\n\n {/* Command */}\n <Card>\n <div className=\"p-4 border-b border-border-default flex items-center justify-between\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Code className=\"size-5\" />\n {tr('sessionDetails.commandTitle', 'Command')}\n </h3>\n {!editingCommand && (\n <button\n type=\"button\"\n onClick={handleEditCommandOpen}\n className=\"p-1.5 rounded text-text-secondary hover:text-text-primary hover:bg-bg-sunken transition-colors\"\n title={tr('sessionDetails.editCommandTitle', 'Edit command and working directory')}\n >\n <Pencil className=\"size-4\" />\n </button>\n )}\n </div>\n <div className=\"p-4\">\n {editingCommand ? (\n <div className=\"space-y-3\">\n <div>\n <label className=\"block text-xs font-medium text-text-secondary mb-1\">\n {tr('sessionDetails.commandLabel', 'Command')}\n </label>\n <input\n aria-label=\"control\"\n type=\"text\"\n value={editCommand}\n onChange={(e) => setEditCommand(e.target.value)}\n placeholder={tr('sessionDetails.commandPlaceholder', 'bun run dev')}\n className=\"w-full px-3 py-2 text-sm bg-bg-sunken border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg font-mono\"\n />\n </div>\n <div>\n <label className=\"block text-xs font-medium text-text-secondary mb-1\">\n {tr('sessionDetails.workingDirLabel', 'Working Directory')}\n </label>\n <input\n aria-label=\"control\"\n type=\"text\"\n value={editWorkingDirectory}\n onChange={(e) => setEditWorkingDirectory(e.target.value)}\n placeholder={tr('sessionDetails.workingDirPlaceholder', '/home/user/myproject')}\n className=\"w-full px-3 py-2 text-sm bg-bg-sunken border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg font-mono\"\n />\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={handleEditCommandSave}\n disabled={editSaving}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n >\n {editSaving ? (\n <Loader2 className=\"size-3.5 animate-spin\" />\n ) : (\n <Check className=\"size-3.5\" />\n )}\n {tr('sessionDetails.save', 'Save')}\n </button>\n <button\n type=\"button\"\n onClick={() => setEditingCommand(false)}\n disabled={editSaving}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-sm bg-bg-sunken text-text-primary rounded-md hover:bg-bg-elevated transition-colors disabled:opacity-50\"\n >\n <X className=\"size-3.5\" />\n {tr('sessionDetails.cancel', 'Cancel')}\n </button>\n </div>\n </div>\n ) : (\n <>\n <div className=\"bg-bg-sunken rounded-md p-3 font-mono text-sm text-text-primary overflow-x-auto\">\n ${' '}\n {session.command ||\n tr('sessionDetails.noCommandSpecified', 'No command specified')}\n </div>\n <div className=\"mt-3 flex items-center gap-2 text-sm text-text-secondary\">\n <Folder className=\"size-4\" />\n {tr('sessionDetails.workingDirectory', 'Working Directory')}:{' '}\n <span className=\"text-text-primary font-mono\">\n {session.workingDirectory || (\n <em className=\"not-italic text-text-placeholder\">\n {tr('sessionDetails.workingDirDefault', 'not set — defaults to OS home')}\n </em>\n )}\n </span>\n </div>\n </>\n )}\n </div>\n </Card>\n\n {/* Environment Variables */}\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Variable className=\"size-5\" />\n {tr('sessionDetails.envVars', 'Environment Variables')}\n </h3>\n </div>\n <div className=\"p-4\">\n {session.environmentVariables &&\n Object.keys(session.environmentVariables).length > 0 ? (\n <div className=\"flex flex-wrap gap-2\">\n {Object.entries(session.environmentVariables).map(([key, value]) => (\n <span\n key={key}\n className=\"px-3 py-1 bg-bg-sunken text-text-primary text-sm rounded-full font-mono\"\n >\n {key}={String(value)}\n </span>\n ))}\n </div>\n ) : (\n <p className=\"text-text-secondary\">\n {tr('sessionDetails.noEnvVars', 'No environment variables configured')}\n </p>\n )}\n </div>\n </Card>\n\n {/* Last Output */}\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Terminal className=\"size-5\" />\n {tr('sessionDetails.lastOutput', 'Last Output')}\n </h3>\n </div>\n <div className=\"p-4\">\n {session.lastOutput ? (\n <div className=\"bg-bg-sunken rounded-md p-3 font-mono text-sm text-text-primary overflow-x-auto whitespace-pre-wrap\">\n {session.lastOutput}\n </div>\n ) : (\n <p className=\"text-text-secondary\">\n {tr('sessionDetails.noOutput', 'No output captured')}\n </p>\n )}\n </div>\n </Card>\n\n {/* Related Vibe */}\n {vibe && (\n <Card onClick={handleVibeClick} hoverable>\n <div className=\"p-4 flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Zap className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('sessionDetails.relatedVibe', 'Related Vibe')}\n </p>\n <p className=\"font-medium text-text-primary\">{vibe.name}</p>\n {vibe.path && (\n <p className=\"text-sm text-text-secondary truncate max-w-md\">{vibe.path}</p>\n )}\n </div>\n </div>\n <StatusBadge status={vibe.status} />\n </div>\n </Card>\n )}\n\n {/* Related Agent */}\n {agent && (\n <Card onClick={handleAgentClick} hoverable>\n <div className=\"p-4 flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Server className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('sessionDetails.runningOnAgent', 'Running on Agent')}\n </p>\n <p className=\"font-medium text-text-primary\">{agent.name}</p>\n <p className=\"text-sm text-text-secondary\">{agent.hostname}</p>\n </div>\n </div>\n <StatusBadge status={agent.isActive ? 'ACTIVE' : 'OFFLINE'} />\n </div>\n </Card>\n )}\n\n {/* Metadata */}\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Clock className=\"size-5\" />\n {tr('sessionDetails.timeline', 'Timeline')}\n </h3>\n </div>\n <div className=\"p-4\">\n <div className=\"grid grid-cols-1 sm:grid-cols-3 gap-4 text-sm\">\n <div>\n <p className=\"text-text-secondary\">\n {tr('sessionDetails.autoStart', 'Auto Start')}\n </p>\n <p className=\"font-medium text-text-primary\">\n {session.autoStart\n ? tr('sessionDetails.yes', 'Yes')\n : tr('sessionDetails.no', 'No')}\n </p>\n </div>\n <div>\n <p className=\"text-text-secondary\">{tr('sessionDetails.created', 'Created')}</p>\n <p className=\"font-medium text-text-primary\">\n {formatTimestamp(session.createdAt)}\n </p>\n </div>\n <div>\n <p className=\"text-text-secondary\">{tr('sessionDetails.updated', 'Updated')}</p>\n <p className=\"font-medium text-text-primary\">\n {formatTimestamp(session.updatedAt)}\n </p>\n </div>\n </div>\n {session.exitCode !== null && session.exitCode !== undefined && (\n <div className=\"mt-4 pt-4 border-t border-border-default\">\n <p className=\"text-sm text-text-secondary\">\n {tr('sessionDetails.exitCode', 'Exit Code')}\n </p>\n <p\n className={`font-medium ${session.exitCode === 0 ? 'text-status-success-text' : 'text-status-error-text'}`}\n >\n {session.exitCode}\n </p>\n </div>\n )}\n </div>\n </Card>\n\n {/* Tags */}\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Tag className=\"size-5\" />\n {tr('sessionDetails.tags', 'Tags')}\n </h3>\n </div>\n <div className=\"p-4\">\n <EntityTagPicker entityType=\"VIBECONTROLS_SESSION\" entityId={session.id} />\n </div>\n </Card>\n </div>\n\n {/* Integrated Terminal Panel - docked at bottom */}\n {terminalOpen && (\n <div\n ref={terminalPanelRef}\n className=\"border-t border-border-default bg-bg-surface shrink-0\"\n >\n {/* Terminal header bar */}\n <div className=\"flex items-center bg-bg-sunken border-b border-border-default\">\n <button\n type=\"button\"\n onClick={() => setTerminalOpen(false)}\n className=\"flex items-center gap-1.5 px-3 py-2 text-text-secondary hover:text-text-primary transition-colors text-xs font-medium uppercase tracking-wide\"\n title={tr('sessionDetails.collapseTerminal', 'Collapse terminal')}\n >\n <Terminal className=\"size-3.5\" />\n {tr('sessionDetails.terminal', 'Terminal')}\n <ChevronDown className=\"size-3\" />\n </button>\n <div className=\"flex items-center px-3 py-1.5 text-xs bg-bg-surface text-text-primary border-r border-border-subtle border-b-2 border-b-action-primary-bg\">\n <Terminal className=\"size-3 mr-1.5\" />\n {session.name}\n </div>\n <div className=\"flex-1\" />\n <div className=\"flex items-center gap-1 px-2\">\n {terminalUrl && (\n <button\n type=\"button\"\n onClick={handleOpenInNewTab}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors\"\n title={tr('sessionDetails.openInNewTabShort', 'Open in new tab')}\n >\n <ExternalLink className=\"size-3.5\" />\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => setTerminalMaximized((prev) => !prev)}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors\"\n title={\n terminalMaximized\n ? tr('sessionDetails.restoreSize', 'Restore size')\n : tr('sessionDetails.maximize', 'Maximize')\n }\n >\n {terminalMaximized ? (\n <Minimize2 className=\"size-3.5\" />\n ) : (\n <Maximize2 className=\"size-3.5\" />\n )}\n </button>\n </div>\n </div>\n\n {exitedNotice && (\n <div\n role=\"status\"\n className=\"flex items-start justify-between gap-3 px-3 py-2 bg-status-warning-bg/30 text-status-warning-text border-b border-status-warning-bg/50 text-xs\"\n >\n <div className=\"flex items-center gap-2 flex-1\">\n <AlertCircle className=\"size-3.5 shrink-0\" />\n <span>\n {tr(\n 'sessionDetails.commandExited',\n 'Command exited{{code}} — click Run Command to run it again.',\n {\n code:\n exitedNotice.exitCode !== null ? ` (exit ${exitedNotice.exitCode})` : '',\n }\n )}\n </span>\n </div>\n <button\n type=\"button\"\n onClick={() => setExitedNotice(null)}\n className=\"text-text-secondary hover:text-text-primary\"\n title={tr('common.dismiss', 'Dismiss')}\n >\n <X className=\"size-3.5\" />\n </button>\n </div>\n )}\n\n {/* Terminal content */}\n <div className={`${terminalHeight} bg-bg-inverse text-text-inverse`}>\n {terminalLoading ? (\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"text-center\">\n <Loader2 className=\"size-8 text-text-secondary animate-spin mx-auto mb-3\" />\n <p className=\"text-text-secondary text-sm\">\n {tr('sessionDetails.startingTerminal', 'Starting terminal for')} {session.name}\n ...\n </p>\n <p className=\"text-text-tertiary text-xs mt-1\">\n {tr(\n 'sessionDetails.settingUpTunnel',\n 'Setting up tmux + ttyd + cloudflared tunnel'\n )}\n </p>\n </div>\n </div>\n ) : terminalError ? (\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"text-center max-w-md\">\n <AlertCircle className=\"size-8 text-status-error-text mx-auto mb-3\" />\n <p className=\"text-status-error-text text-sm font-medium\">\n {tr('sessionDetails.failedStartTerminal', 'Failed to start terminal')}\n </p>\n <p className=\"text-text-secondary text-xs mt-2\">{terminalError}</p>\n <button\n type=\"button\"\n onClick={handleStartTerminal}\n className=\"mt-4 px-4 py-2 bg-bg-muted text-text-primary text-sm rounded-md hover:bg-bg-sunken transition-colors\"\n >\n {tr('sessionDetails.retry', 'Retry')}\n </button>\n </div>\n </div>\n ) : terminalUrl ? (\n <iframe\n src={terminalUrl}\n title={`Terminal - ${session.name}`}\n className=\"w-full h-full border-0\"\n allow=\"clipboard-read; clipboard-write\"\n />\n ) : (\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"text-center\">\n <Terminal className=\"size-8 text-text-tertiary mx-auto mb-3\" />\n <p className=\"text-text-secondary text-sm\">\n {tr('sessionDetails.noTerminalRunning', 'No terminal running')}\n </p>\n <p className=\"text-text-tertiary text-xs mt-1\">\n {agent\n ? tr(\n 'sessionDetails.clickOpenTerminalHint',\n 'Click \"Open Terminal\" to create a tmux session with a browser terminal.'\n )\n : tr(\n 'sessionDetails.noAgentForSession',\n 'No agent is assigned to this session.'\n )}\n </p>\n {agent && (\n <button\n type=\"button\"\n onClick={handleStartTerminal}\n disabled={terminalLoading}\n className=\"mt-4 px-4 py-2 bg-bg-muted text-text-primary text-sm rounded-md hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n {terminalLoading\n ? tr('sessionDetails.opening', 'Opening...')\n : tr('sessionDetails.openTerminal', 'Open Terminal')}\n </button>\n )}\n </div>\n </div>\n )}\n </div>\n </div>\n )}\n\n {/* Terminal collapsed indicator - show a clickable bar */}\n {!terminalOpen && (terminalUrl || terminalError) && (\n <div className=\"border-t border-border-default bg-bg-sunken shrink-0\">\n <button\n type=\"button\"\n onClick={() => setTerminalOpen(true)}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-text-secondary hover:text-text-primary transition-colors text-xs font-medium uppercase tracking-wide w-full\"\n title={tr('sessionDetails.expandTerminal', 'Expand terminal')}\n >\n <Terminal className=\"size-3.5\" />\n {tr('sessionDetails.terminal', 'Terminal')} - {session.name}\n <ChevronUp className=\"size-3 ml-1\" />\n </button>\n </div>\n )}\n\n <ConfirmDialog\n isOpen={showQuotaExhausted}\n onClose={() => setShowQuotaExhausted(false)}\n onConfirm={() => {\n setShowQuotaExhausted(false);\n navigate('/billing');\n }}\n title={getQuotaDialogContent('session').title}\n message={getQuotaDialogContent('session').message}\n confirmLabel=\"View Billing\"\n cancelLabel=\"Dismiss\"\n variant=\"warning\"\n />\n\n {/* Restart-terminal kills tmux and re-runs the command, so\n confirm before firing. */}\n <ConfirmDialog\n isOpen={showRestartTerminalConfirm}\n onClose={() => setShowRestartTerminalConfirm(false)}\n onConfirm={() => {\n setShowRestartTerminalConfirm(false);\n void handleRestartTerminal();\n }}\n title={tr('sessionDetails.restartTerminalConfirmTitle', 'Restart terminal?')}\n message={tr(\n 'sessionDetails.restartTerminalConfirmBody',\n 'This kills the current tmux session and re-runs the command from scratch. Any in-progress output will be lost.'\n )}\n confirmLabel={tr('sessionDetails.restartTerminalConfirmLabel', 'Restart terminal')}\n cancelLabel={tr('common.cancel', 'Cancel')}\n variant=\"warning\"\n />\n </MotionPage>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SessionDetailsPage.js","names":[],"sources":["../../src/pages/SessionDetailsPage.tsx"],"sourcesContent":["import { useState, useCallback, useEffect, useRef } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { VIBE_PERMISSIONS } from '../constants/permissions';\nimport { EntityTagPicker } from '@/components/shared/EntityTagPicker';\nimport { PageSkeleton } from '@/components/shared/PageSkeleton';\nimport { useParams } from 'react-router-dom';\nimport { useVibeControlsStore } from '@/store/vibeControlsStore';\nimport {\n ArrowLeft,\n Terminal,\n Play,\n Square,\n RotateCcw,\n ExternalLink,\n Server,\n Zap,\n Clock,\n Folder,\n Code,\n Variable,\n Hash,\n RefreshCw,\n Loader2,\n AlertCircle,\n Maximize2,\n Minimize2,\n ChevronDown,\n ChevronUp,\n Pencil,\n Check,\n X,\n Tag,\n} from 'lucide-react';\nimport {\n PageHeader,\n Card,\n StatusBadge,\n EmptyState,\n ErrorState,\n ConfirmDialog,\n} from '@/components/shared';\nimport { MotionPage, MotionList, MotionItem } from '@burdenoff/fe-libs/shared/motion';\nimport { useVibeControls, useVibeNavigate } from '@/providers/VibeControlsProvider';\nimport {\n useGetSessionQuery,\n useStartSessionMutation,\n useStopSessionMutation,\n useRestartSessionMutation,\n useStartSessionTerminalMutation,\n useRestartSessionTerminalMutation,\n useUpdateSessionMutation,\n useMintAgentCapabilityMutation,\n} from '@/generated/wspace-operations';\nimport { AgentCapabilityScope } from '@/generated/wspace-types';\nimport {\n getQuotaDialogContent,\n markQuotaHandledLocally,\n isQuotaExhaustedError,\n} from '@/utils/quotaUtils';\n\n/**\n * Returns the bare ttyd URL for a session — no embedded credentials.\n *\n * F3 cutover: the long-lived `agentApiKey` is NO LONGER appended to the URL.\n * The browser instead calls `mintAgentCapability(TERMINAL_SESSION)` to mint a\n * short-lived HMAC token, then loads the iframe with `#vt=<token>` as a URL\n * fragment. The agent's `/terminal/<sid>` bootstrap exchanges that fragment\n * token for a session-scoped HttpOnly cookie before proxying to ttyd.\n *\n * Returns null when the session has no ttydUrl yet (terminal not started).\n */\nfunction getTerminalBaseUrl(session: { ttydUrl?: string | null }): string | null {\n const url = session.ttydUrl;\n if (!url) return null;\n // Defensively strip any legacy `?apiKey=` that a stale server build may\n // have persisted to ttydUrl. The agent now rejects `?apiKey` on /terminal\n // paths so leaving it would just look messy; remove it to keep history /\n // Referer clean.\n try {\n const u = new URL(url);\n u.searchParams.delete('apiKey');\n return u.toString();\n } catch {\n return url;\n }\n}\n\n// Escape regex meta-chars for use inside `new RegExp(...)`. Pure,\n// no closure state, so hoisted to module scope to avoid re-creating\n// the function on every `tr()` invocation.\nconst escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n/**\n * Session details page - shows comprehensive information about a single session\n * with an integrated terminal panel.\n */\nexport function SessionDetailsPage() {\n const { t } = useI18n();\n // tr(key, fallback, params?) — forwards params to useI18n().t so\n // `{{name}}` placeholders are interpolated. Also interpolates into\n // the fallback when the i18n key isn't in the catalog yet, so a\n // missing key doesn't render literal `{{name}}` text to users.\n const tr = useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string => {\n const value = t(key, params);\n if (value !== key) return value;\n if (!params) return fallback;\n // Mirror the i18n layer's `{{key}}` placeholder syntax. Replacer\n // function (not a string) so values containing `$$`/`$&` aren't\n // interpreted as `String.prototype.replace` specials.\n return Object.entries(params).reduce(\n (acc, [k, v]) =>\n acc.replace(new RegExp(`\\\\{\\\\{${escapeRe(k)}\\\\}\\\\}`, 'g'), () => String(v)),\n fallback\n );\n },\n [t]\n );\n const { id } = useParams<{ id: string }>();\n const { basePath: basePathProp } = useVibeControls();\n const basePath = basePathProp || '';\n const navigate = useVibeNavigate();\n const { hasPermission, isLoading: permissionsLoading, permissions } = usePermissions();\n\n const [startSession] = useStartSessionMutation({ refetchQueries: ['GetSession'] });\n const [stopSession] = useStopSessionMutation({ refetchQueries: ['GetSession'] });\n const [restartSession] = useRestartSessionMutation({ refetchQueries: ['GetSession'] });\n const [startSessionTerminal] = useStartSessionTerminalMutation({\n refetchQueries: ['GetSession'],\n });\n const [restartSessionTerminal] = useRestartSessionTerminalMutation({\n refetchQueries: ['GetSession'],\n });\n const [updateSession] = useUpdateSessionMutation({ refetchQueries: ['GetSession'] });\n\n // Command/working-directory inline edit state\n const [editingCommand, setEditingCommand] = useState(false);\n const [editCommand, setEditCommand] = useState('');\n const [editWorkingDirectory, setEditWorkingDirectory] = useState('');\n const [editSaving, setEditSaving] = useState(false);\n\n const handleEditCommandOpen = () => {\n setEditCommand(session?.command || '');\n setEditWorkingDirectory(session?.workingDirectory || '');\n setEditingCommand(true);\n };\n\n const handleEditCommandSave = async () => {\n if (!session?.id) return;\n setEditSaving(true);\n try {\n await updateSession({\n variables: {\n id: session.id,\n input: {\n command: editCommand || null,\n workingDirectory: editWorkingDirectory || null,\n },\n },\n });\n setEditingCommand(false);\n } finally {\n setEditSaving(false);\n }\n };\n\n // Fetch session by ID from GraphQL API\n const { data, loading, error, refetch } = useGetSessionQuery({\n variables: { id: id || '' },\n skip: !id,\n fetchPolicy: 'cache-and-network',\n });\n\n // Get session from response\n const session = data?.session;\n\n // Get related agent and vibe from the session response\n const agent = session?.agent || null;\n const vibe = session?.vibe || null;\n\n // ── Sync active context ────────────────────────────────────────────────\n useEffect(() => {\n if (session) {\n useVibeControlsStore.setState({\n activeAgentId: (agent?.id as string) ?? null,\n selectedAgentId: (agent?.id as string) ?? null,\n activeVibeId: (vibe?.id as string) ?? null,\n selectedVibeId: (vibe?.id as string) ?? null,\n activeSessionId: session.id,\n selectedSessionId: session.id,\n });\n }\n }, [session?.id, agent?.id, vibe?.id]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Terminal state\n const [terminalOpen, setTerminalOpen] = useState(true);\n const [terminalMaximized, setTerminalMaximized] = useState(false);\n const [terminalLoading, setTerminalLoading] = useState(false);\n const [terminalError, setTerminalError] = useState<string | null>(null);\n const [showQuotaExhausted, setShowQuotaExhausted] = useState(false);\n const [showRestartTerminalConfirm, setShowRestartTerminalConfirm] = useState(false);\n const [exitedNotice, setExitedNotice] = useState<{\n exitCode: number | null;\n } | null>(null);\n const prevStatusRef = useRef<string | null>(null);\n\n // Ref target for scrollIntoView when the terminal opens.\n const terminalPanelRef = useRef<HTMLDivElement | null>(null);\n\n // Track first-render + the previous open state so the toggle-driven\n // scroll only fires on a deliberate closed→open transition, not on\n // initial mount (terminalOpen defaults to true).\n const didInitialMountRef = useRef(false);\n const prevTerminalOpenRef = useRef(false);\n\n // Bare ttyd URL — does NOT include the capability token. The fragment\n // (`#vt=`) is appended below once `mintAgentCapability` resolves.\n const terminalBaseUrl = session ? getTerminalBaseUrl(session) : null;\n\n // F3 — short-lived capability mint. The token is appended to the iframe\n // src as a URL fragment (`#vt=<token>`). Fragments stay client-side, so\n // the token doesn't appear in proxy logs, Referer, or browser history.\n const [mintAgentCapability] = useMintAgentCapabilityMutation();\n const [capabilityToken, setCapabilityToken] = useState<string | null>(null);\n const [capabilityError, setCapabilityError] = useState<string | null>(null);\n const [capabilityLoading, setCapabilityLoading] = useState(false);\n // Re-mint counter — bump to force a new mint (used on iframe error / on\n // session/agent change).\n const [mintNonce, setMintNonce] = useState(0);\n const requestRemint = useCallback(() => setMintNonce((n) => n + 1), []);\n\n const agentIdForMint = agent?.id ?? null;\n const sessionIdForMint = session?.id ?? null;\n\n useEffect(() => {\n let cancelled = false;\n if (!terminalBaseUrl || !agentIdForMint || !sessionIdForMint) {\n setCapabilityToken(null);\n setCapabilityError(null);\n return undefined;\n }\n setCapabilityLoading(true);\n setCapabilityError(null);\n mintAgentCapability({\n variables: {\n agentId: agentIdForMint,\n // Multi-profile is a follow-up — see useActiveAgentProfile. The agent\n // mounts /api/profiles/default/* by default, which is what the F3\n // exchange endpoint resolves to as well.\n profile: 'default',\n scope: AgentCapabilityScope.TerminalSession,\n resourceId: sessionIdForMint,\n ttlSeconds: 60,\n },\n })\n .then((res) => {\n if (cancelled) return;\n const token = res.data?.mintAgentCapability?.token ?? null;\n if (!token) {\n setCapabilityError('Failed to mint terminal capability');\n setCapabilityToken(null);\n } else {\n setCapabilityToken(token);\n }\n })\n .catch((err) => {\n if (cancelled) return;\n setCapabilityError(\n err instanceof Error ? err.message : 'Failed to mint terminal capability'\n );\n setCapabilityToken(null);\n })\n .finally(() => {\n if (!cancelled) setCapabilityLoading(false);\n });\n return () => {\n cancelled = true;\n };\n }, [terminalBaseUrl, agentIdForMint, sessionIdForMint, mintNonce, mintAgentCapability]);\n\n const terminalUrl =\n terminalBaseUrl && capabilityToken\n ? `${terminalBaseUrl}#vt=${encodeURIComponent(capabilityToken)}`\n : null;\n\n const handleBack = () => {\n navigate('/sessions');\n };\n\n const handleStartSession = async () => {\n if (!session?.id) return;\n try {\n await startSession({ variables: { id: session.id } });\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n return;\n }\n\n console.error('Failed to start session:', err);\n }\n };\n\n const handleStopSession = async () => {\n if (!session?.id) return;\n try {\n await stopSession({ variables: { id: session.id } });\n } catch (err) {\n console.error('Failed to stop session:', err);\n }\n };\n\n const handleRestartSession = async () => {\n if (!session?.id) return;\n try {\n await restartSession({ variables: { id: session.id } });\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n return;\n }\n\n console.error('Failed to restart session:', err);\n }\n };\n\n // Scroll the terminal panel into view as a fire-and-forget op,\n // gated on the panel ref being mounted. Owned by the handler that\n // triggered the action, not by an effect — so a successful\n // start/restart always scrolls exactly once, even when the URL\n // didn't change (no useEffect re-run otherwise).\n const scrollTerminalIntoView = useCallback(() => {\n requestAnimationFrame(() => {\n terminalPanelRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' });\n });\n }, []);\n\n // Shared bookkeeping for start/restart-terminal: identical try/catch\n // /finally shape, identical quota-exhausted + generic-error handling;\n // only the mutation and the fallback error string differ. Takes the\n // session id as an explicit arg so callers don't need a non-null\n // assertion against `session` — the helper owns the null guard.\n const withTerminalAction = useCallback(\n async <T,>(\n action: (id: string) => Promise<{\n error?: { message: string } | null | undefined;\n data?: T | null | undefined;\n }>,\n dataKey: keyof T,\n fallbackErrorMsg: string\n ) => {\n const id = session?.id;\n if (!id) return;\n setTerminalLoading(true);\n setTerminalError(null);\n let succeeded = false;\n try {\n const result = await action(id);\n if (result.error && isQuotaExhaustedError(result.error)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n } else if (result.error) {\n setTerminalError(result.error.message);\n } else if (!result.data || !result.data[dataKey]) {\n setTerminalError(fallbackErrorMsg);\n } else {\n setTerminalOpen(true);\n succeeded = true;\n }\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n return;\n }\n const message = err instanceof Error ? err.message : fallbackErrorMsg;\n setTerminalError(message);\n } finally {\n setTerminalLoading(false);\n if (succeeded) scrollTerminalIntoView();\n }\n },\n [session?.id, scrollTerminalIntoView]\n );\n\n const handleStartTerminal = useCallback(\n () =>\n withTerminalAction(\n (id) => startSessionTerminal({ variables: { id } }),\n 'startSessionTerminal',\n 'Failed to start terminal'\n ),\n [startSessionTerminal, withTerminalAction]\n );\n\n const handleRestartTerminal = useCallback(\n () =>\n withTerminalAction(\n (id) => restartSessionTerminal({ variables: { id } }),\n 'restartSessionTerminal',\n 'Failed to restart terminal'\n ),\n [restartSessionTerminal, withTerminalAction]\n );\n\n // Scroll on the toggle button — closed→open. User-initiated start/\n // restart paths scroll inline from their handlers (see\n // `scrollTerminalIntoView`), which avoids the stale-flag problem\n // where a no-op success leaves a ref stuck true and a later\n // background refetch fires an unexpected scroll.\n useEffect(() => {\n if (!didInitialMountRef.current) {\n didInitialMountRef.current = true;\n prevTerminalOpenRef.current = terminalOpen;\n return;\n }\n const justOpened = terminalOpen && !prevTerminalOpenRef.current;\n prevTerminalOpenRef.current = terminalOpen;\n if (justOpened) scrollTerminalIntoView();\n }, [terminalOpen, scrollTerminalIntoView]);\n\n // Track the last session whose status we processed, so a route\n // change to a sibling /sessions/:id reseeds `prevStatusRef` with\n // the new session's *current* status — not `null`. Reseeding to\n // null was the bug: if the next status update happened to equal\n // the seed, the transition guard (prev === 'RUNNING'/'STARTING')\n // never matched and the command-exited notice never fired.\n const sessionId = session?.id ?? null;\n const lastProcessedSessionRef = useRef<string | null>(null);\n\n // Surface \"command exited\" inline when the session's status flips\n // to STOPPED while the terminal panel is still open — otherwise\n // the empty iframe looks like a crash.\n useEffect(() => {\n const status = session?.status ?? null;\n const exitCode = session?.exitCode ?? null;\n\n // Session changed: reseed prev to the new session's current\n // status and clear any stale notice. Don't fire a transition on\n // this first observation — we have no \"before\" state yet.\n if (sessionId !== lastProcessedSessionRef.current) {\n lastProcessedSessionRef.current = sessionId;\n prevStatusRef.current = status;\n setExitedNotice(null);\n return;\n }\n\n if (status === 'RUNNING' || status === 'STARTING') {\n // Clear stale notice once the session is running again, then\n // record the status. Mutually exclusive with the STOPPED paths\n // below, so handle it with an early return.\n setExitedNotice(null);\n prevStatusRef.current = status;\n return;\n }\n\n const prev = prevStatusRef.current;\n if (status === 'STOPPED' && (prev === 'RUNNING' || prev === 'STARTING') && terminalOpen) {\n setExitedNotice({ exitCode });\n } else if (status === 'STOPPED' && terminalOpen && exitCode !== null) {\n // exitCode can land in a later GraphQL response than the\n // status transition (poll N returns STOPPED + null code, poll\n // N+1 returns the real code). Patch the existing notice so\n // the banner shows the real code rather than \"(exit null)\".\n setExitedNotice((cur) => (cur && cur.exitCode === null ? { exitCode } : cur));\n }\n prevStatusRef.current = status;\n }, [sessionId, session?.status, session?.exitCode, terminalOpen]);\n\n const handleOpenInNewTab = useCallback(() => {\n if (terminalUrl) {\n window.open(terminalUrl, '_blank');\n }\n }, [terminalUrl]);\n\n if (permissionsLoading)\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <p className=\"text-text-secondary\">\n {tr('sessionDetails.loadingPermissions', 'Loading permissions...')}\n </p>\n </div>\n );\n if (\n permissions &&\n (permissions.permissionStrings?.length ?? 0) > 0 &&\n !hasPermission(VIBE_PERMISSIONS.SESSION_READ)\n )\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <h2 className=\"text-lg font-semibold text-foreground\">\n {tr('sessionDetails.accessDenied', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'sessionDetails.accessDeniedDesc',\n \"You don't have permission to access this page.\"\n )}\n </p>\n </div>\n </div>\n );\n\n const handleAgentClick = () => {\n if (!agent) return;\n navigate(`/agents/${agent.id}`);\n };\n\n const handleVibeClick = () => {\n if (!vibe) return;\n navigate(`/vibes/${vibe.id}`);\n };\n\n const formatTimestamp = (timestamp: string | null | undefined) => {\n if (!timestamp) return 'Never';\n const diff = Date.now() - new Date(timestamp).getTime();\n if (diff < 60000) return 'Just now';\n if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;\n if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;\n return new Date(timestamp).toLocaleDateString();\n };\n\n // Guard states wrapped in MotionPage to prevent unmount/remount animation replay\n if (loading && !session) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('sessionDetails.backToSessions', 'Back to Sessions')}\n </button>\n <PageSkeleton variant=\"detail\" />\n </MotionPage>\n );\n }\n\n if (error && !session) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n Back to Sessions\n </button>\n <ErrorState\n title={tr('sessionDetails.failedLoad', 'Failed to load session')}\n message={error.message}\n onRetry={() => refetch()}\n />\n </MotionPage>\n );\n }\n\n if (!session) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n Back to Sessions\n </button>\n <EmptyState\n icon={<Terminal className=\"size-8 text-text-secondary\" />}\n title={tr('sessionDetails.notFound', 'Session not found')}\n description={`${tr('sessionDetails.notFoundDescPrefix', 'No session with ID')} \"${id}\" ${tr('sessionDetails.notFoundDescSuffix', 'exists')}`}\n action={\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors\"\n >\n {tr('sessionDetails.backToSessions', 'Back to Sessions')}\n </button>\n }\n />\n </MotionPage>\n );\n }\n\n const isRunning = session.status === 'RUNNING';\n const terminalHeight = terminalMaximized ? 'h-[calc(100vh-200px)]' : 'h-[280px] sm:h-[420px]';\n\n return (\n <MotionPage className=\"flex flex-col bg-bg-surface min-h-screen\">\n {/* Main content - scrollable */}\n <div\n className={`flex-1 overflow-y-auto space-y-6 p-4 sm:p-6 ${terminalOpen && terminalUrl ? 'pb-2' : ''}`}\n >\n {/* Back Button */}\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n Back to Sessions\n </button>\n\n {/* Header */}\n <PageHeader\n title={session.name}\n description={session.command || 'No command specified'}\n breadcrumb={[\n { label: 'Sessions', path: basePath ? `${basePath}/sessions` : '/sessions' },\n { label: session.name },\n ]}\n actions={\n <div className=\"flex items-center gap-2 flex-wrap\">\n <button\n type=\"button\"\n onClick={() => refetch()}\n disabled={loading}\n className=\"flex items-center gap-2 px-3 py-2 bg-bg-sunken text-text-primary rounded-md hover:bg-bg-sunken/80 transition-colors disabled:opacity-50\"\n title=\"Refresh\"\n >\n <RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />\n </button>\n {isRunning ? (\n <>\n <button\n type=\"button\"\n onClick={handleStopSession}\n className=\"flex items-center gap-2 px-3 py-2 bg-bg-sunken text-text-primary rounded-md hover:bg-bg-sunken/80 transition-colors\"\n >\n <Square className=\"size-4\" />\n {tr('sessionDetails.stopCommand', 'Stop Command')}\n </button>\n <button\n type=\"button\"\n onClick={handleRestartSession}\n className=\"flex items-center gap-2 px-3 py-2 bg-bg-sunken text-text-primary rounded-md hover:bg-bg-sunken/80 transition-colors\"\n >\n <RotateCcw className=\"size-4\" />\n {tr('sessionDetails.restartCommand', 'Restart Command')}\n </button>\n </>\n ) : (\n <button\n type=\"button\"\n onClick={handleStartSession}\n className=\"flex items-center gap-2 px-3 py-2 bg-status-success-bg text-status-success-text rounded-md hover:opacity-80 transition-colors\"\n >\n <Play className=\"size-4\" />\n {tr('sessionDetails.runCommand', 'Run Command')}\n </button>\n )}\n {/* Terminal toggle + open in new tab */}\n {terminalUrl ? (\n <>\n <button\n type=\"button\"\n onClick={() => setTerminalOpen((prev) => !prev)}\n className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors ${\n terminalOpen\n ? 'bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover'\n : 'bg-bg-sunken text-text-primary hover:bg-bg-sunken/80'\n }`}\n title={\n terminalOpen\n ? tr('sessionDetails.hideTerminal', 'Hide terminal')\n : tr('sessionDetails.showTerminal', 'Show terminal')\n }\n >\n <Terminal className=\"size-4\" />\n {tr('sessionDetails.terminal', 'Terminal')}\n </button>\n <button\n type=\"button\"\n onClick={() => setShowRestartTerminalConfirm(true)}\n disabled={terminalLoading}\n className=\"flex items-center gap-1.5 p-2 bg-bg-sunken text-text-secondary rounded-md hover:text-text-primary hover:bg-bg-sunken/80 transition-colors disabled:opacity-50\"\n title={tr(\n 'sessionDetails.restartTerminalTitle',\n 'Restart terminal — kills the tmux session and re-runs the command'\n )}\n >\n {terminalLoading ? (\n <Loader2 className=\"size-3.5 animate-spin\" />\n ) : (\n <RotateCcw className=\"size-3.5\" />\n )}\n <span className=\"text-xs\">\n {tr('sessionDetails.restartTerminalShort', 'Restart')}\n </span>\n </button>\n <button\n type=\"button\"\n onClick={handleOpenInNewTab}\n className=\"flex items-center p-2 bg-bg-sunken text-text-secondary rounded-md hover:text-text-primary hover:bg-bg-sunken/80 transition-colors\"\n title={tr('sessionDetails.openInNewTab', 'Open terminal in new tab')}\n >\n <ExternalLink className=\"size-4\" />\n </button>\n </>\n ) : (\n <button\n type=\"button\"\n onClick={handleStartTerminal}\n disabled={terminalLoading || !agent}\n className=\"flex items-center gap-2 px-3 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n title={\n !agent\n ? tr('sessionDetails.noAgentAssigned', 'No agent assigned')\n : !agent.tunnelUrl\n ? tr(\n 'sessionDetails.agentNoTunnelUrl',\n 'Agent has no tunnel URL configured'\n )\n : tr('sessionDetails.openTerminal', 'Open terminal')\n }\n >\n {terminalLoading ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : (\n <Terminal className=\"size-4\" />\n )}\n {tr('sessionDetails.openTerminal', 'Open Terminal')}\n </button>\n )}\n </div>\n }\n />\n\n {/* Status Cards */}\n <MotionList stagger={0.04} className=\"grid grid-cols-2 sm:grid-cols-4 gap-4\">\n <MotionItem>\n <Card className=\"p-4\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Terminal className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('sessionDetails.statusLabel', 'Status')}\n </p>\n <StatusBadge status={session.status} size=\"md\" />\n </div>\n </div>\n </Card>\n </MotionItem>\n <MotionItem>\n <Card className=\"p-4\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Code className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('sessionDetails.typeLabel', 'Type')}\n </p>\n <p className=\"font-semibold text-text-primary\">{session.type}</p>\n </div>\n </div>\n </Card>\n </MotionItem>\n <MotionItem>\n <Card className=\"p-4\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Hash className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('sessionDetails.pidLabel', 'PID')}\n </p>\n <p className=\"font-semibold text-text-primary\">{session.pid || 'N/A'}</p>\n </div>\n </div>\n </Card>\n </MotionItem>\n <MotionItem>\n <Card className=\"p-4\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Server className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('sessionDetails.agentLabel', 'Agent')}\n </p>\n <p className=\"font-semibold text-text-primary truncate\">\n {agent?.name || 'Unknown'}\n </p>\n </div>\n </div>\n </Card>\n </MotionItem>\n </MotionList>\n\n {/* Command */}\n <Card>\n <div className=\"p-4 border-b border-border-default flex items-center justify-between\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Code className=\"size-5\" />\n {tr('sessionDetails.commandTitle', 'Command')}\n </h3>\n {!editingCommand && (\n <button\n type=\"button\"\n onClick={handleEditCommandOpen}\n className=\"p-1.5 rounded text-text-secondary hover:text-text-primary hover:bg-bg-sunken transition-colors\"\n title={tr('sessionDetails.editCommandTitle', 'Edit command and working directory')}\n >\n <Pencil className=\"size-4\" />\n </button>\n )}\n </div>\n <div className=\"p-4\">\n {editingCommand ? (\n <div className=\"space-y-3\">\n <div>\n <label className=\"block text-xs font-medium text-text-secondary mb-1\">\n {tr('sessionDetails.commandLabel', 'Command')}\n </label>\n <input\n aria-label=\"control\"\n type=\"text\"\n value={editCommand}\n onChange={(e) => setEditCommand(e.target.value)}\n placeholder={tr('sessionDetails.commandPlaceholder', 'bun run dev')}\n className=\"w-full px-3 py-2 text-sm bg-bg-sunken border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg font-mono\"\n />\n </div>\n <div>\n <label className=\"block text-xs font-medium text-text-secondary mb-1\">\n {tr('sessionDetails.workingDirLabel', 'Working Directory')}\n </label>\n <input\n aria-label=\"control\"\n type=\"text\"\n value={editWorkingDirectory}\n onChange={(e) => setEditWorkingDirectory(e.target.value)}\n placeholder={tr('sessionDetails.workingDirPlaceholder', '/home/user/myproject')}\n className=\"w-full px-3 py-2 text-sm bg-bg-sunken border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg font-mono\"\n />\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={handleEditCommandSave}\n disabled={editSaving}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n >\n {editSaving ? (\n <Loader2 className=\"size-3.5 animate-spin\" />\n ) : (\n <Check className=\"size-3.5\" />\n )}\n {tr('sessionDetails.save', 'Save')}\n </button>\n <button\n type=\"button\"\n onClick={() => setEditingCommand(false)}\n disabled={editSaving}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-sm bg-bg-sunken text-text-primary rounded-md hover:bg-bg-elevated transition-colors disabled:opacity-50\"\n >\n <X className=\"size-3.5\" />\n {tr('sessionDetails.cancel', 'Cancel')}\n </button>\n </div>\n </div>\n ) : (\n <>\n <div className=\"bg-bg-sunken rounded-md p-3 font-mono text-sm text-text-primary overflow-x-auto\">\n ${' '}\n {session.command ||\n tr('sessionDetails.noCommandSpecified', 'No command specified')}\n </div>\n <div className=\"mt-3 flex items-center gap-2 text-sm text-text-secondary\">\n <Folder className=\"size-4\" />\n {tr('sessionDetails.workingDirectory', 'Working Directory')}:{' '}\n <span className=\"text-text-primary font-mono\">\n {session.workingDirectory || (\n <em className=\"not-italic text-text-placeholder\">\n {tr('sessionDetails.workingDirDefault', 'not set — defaults to OS home')}\n </em>\n )}\n </span>\n </div>\n </>\n )}\n </div>\n </Card>\n\n {/* Environment Variables */}\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Variable className=\"size-5\" />\n {tr('sessionDetails.envVars', 'Environment Variables')}\n </h3>\n </div>\n <div className=\"p-4\">\n {session.environmentVariables &&\n Object.keys(session.environmentVariables).length > 0 ? (\n <div className=\"flex flex-wrap gap-2\">\n {Object.entries(session.environmentVariables).map(([key, value]) => (\n <span\n key={key}\n className=\"px-3 py-1 bg-bg-sunken text-text-primary text-sm rounded-full font-mono\"\n >\n {key}={String(value)}\n </span>\n ))}\n </div>\n ) : (\n <p className=\"text-text-secondary\">\n {tr('sessionDetails.noEnvVars', 'No environment variables configured')}\n </p>\n )}\n </div>\n </Card>\n\n {/* Last Output */}\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Terminal className=\"size-5\" />\n {tr('sessionDetails.lastOutput', 'Last Output')}\n </h3>\n </div>\n <div className=\"p-4\">\n {session.lastOutput ? (\n <div className=\"bg-bg-sunken rounded-md p-3 font-mono text-sm text-text-primary overflow-x-auto whitespace-pre-wrap\">\n {session.lastOutput}\n </div>\n ) : (\n <p className=\"text-text-secondary\">\n {tr('sessionDetails.noOutput', 'No output captured')}\n </p>\n )}\n </div>\n </Card>\n\n {/* Related Vibe */}\n {vibe && (\n <Card onClick={handleVibeClick} hoverable>\n <div className=\"p-4 flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Zap className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('sessionDetails.relatedVibe', 'Related Vibe')}\n </p>\n <p className=\"font-medium text-text-primary\">{vibe.name}</p>\n {vibe.path && (\n <p className=\"text-sm text-text-secondary truncate max-w-md\">{vibe.path}</p>\n )}\n </div>\n </div>\n <StatusBadge status={vibe.status} />\n </div>\n </Card>\n )}\n\n {/* Related Agent */}\n {agent && (\n <Card onClick={handleAgentClick} hoverable>\n <div className=\"p-4 flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Server className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('sessionDetails.runningOnAgent', 'Running on Agent')}\n </p>\n <p className=\"font-medium text-text-primary\">{agent.name}</p>\n <p className=\"text-sm text-text-secondary\">{agent.hostname}</p>\n </div>\n </div>\n <StatusBadge status={agent.isActive ? 'ACTIVE' : 'OFFLINE'} />\n </div>\n </Card>\n )}\n\n {/* Metadata */}\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Clock className=\"size-5\" />\n {tr('sessionDetails.timeline', 'Timeline')}\n </h3>\n </div>\n <div className=\"p-4\">\n <div className=\"grid grid-cols-1 sm:grid-cols-3 gap-4 text-sm\">\n <div>\n <p className=\"text-text-secondary\">\n {tr('sessionDetails.autoStart', 'Auto Start')}\n </p>\n <p className=\"font-medium text-text-primary\">\n {session.autoStart\n ? tr('sessionDetails.yes', 'Yes')\n : tr('sessionDetails.no', 'No')}\n </p>\n </div>\n <div>\n <p className=\"text-text-secondary\">{tr('sessionDetails.created', 'Created')}</p>\n <p className=\"font-medium text-text-primary\">\n {formatTimestamp(session.createdAt)}\n </p>\n </div>\n <div>\n <p className=\"text-text-secondary\">{tr('sessionDetails.updated', 'Updated')}</p>\n <p className=\"font-medium text-text-primary\">\n {formatTimestamp(session.updatedAt)}\n </p>\n </div>\n </div>\n {session.exitCode !== null && session.exitCode !== undefined && (\n <div className=\"mt-4 pt-4 border-t border-border-default\">\n <p className=\"text-sm text-text-secondary\">\n {tr('sessionDetails.exitCode', 'Exit Code')}\n </p>\n <p\n className={`font-medium ${session.exitCode === 0 ? 'text-status-success-text' : 'text-status-error-text'}`}\n >\n {session.exitCode}\n </p>\n </div>\n )}\n </div>\n </Card>\n\n {/* Tags */}\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Tag className=\"size-5\" />\n {tr('sessionDetails.tags', 'Tags')}\n </h3>\n </div>\n <div className=\"p-4\">\n <EntityTagPicker entityType=\"VIBECONTROLS_SESSION\" entityId={session.id} />\n </div>\n </Card>\n </div>\n\n {/* Integrated Terminal Panel - docked at bottom */}\n {terminalOpen && (\n <div\n ref={terminalPanelRef}\n className=\"border-t border-border-default bg-bg-surface shrink-0\"\n >\n {/* Terminal header bar */}\n <div className=\"flex items-center bg-bg-sunken border-b border-border-default\">\n <button\n type=\"button\"\n onClick={() => setTerminalOpen(false)}\n className=\"flex items-center gap-1.5 px-3 py-2 text-text-secondary hover:text-text-primary transition-colors text-xs font-medium uppercase tracking-wide\"\n title={tr('sessionDetails.collapseTerminal', 'Collapse terminal')}\n >\n <Terminal className=\"size-3.5\" />\n {tr('sessionDetails.terminal', 'Terminal')}\n <ChevronDown className=\"size-3\" />\n </button>\n <div className=\"flex items-center px-3 py-1.5 text-xs bg-bg-surface text-text-primary border-r border-border-subtle border-b-2 border-b-action-primary-bg\">\n <Terminal className=\"size-3 mr-1.5\" />\n {session.name}\n </div>\n <div className=\"flex-1\" />\n <div className=\"flex items-center gap-1 px-2\">\n {terminalUrl && (\n <button\n type=\"button\"\n onClick={handleOpenInNewTab}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors\"\n title={tr('sessionDetails.openInNewTabShort', 'Open in new tab')}\n >\n <ExternalLink className=\"size-3.5\" />\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => setTerminalMaximized((prev) => !prev)}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors\"\n title={\n terminalMaximized\n ? tr('sessionDetails.restoreSize', 'Restore size')\n : tr('sessionDetails.maximize', 'Maximize')\n }\n >\n {terminalMaximized ? (\n <Minimize2 className=\"size-3.5\" />\n ) : (\n <Maximize2 className=\"size-3.5\" />\n )}\n </button>\n </div>\n </div>\n\n {exitedNotice && (\n <div\n role=\"status\"\n className=\"flex items-start justify-between gap-3 px-3 py-2 bg-status-warning-bg/30 text-status-warning-text border-b border-status-warning-bg/50 text-xs\"\n >\n <div className=\"flex items-center gap-2 flex-1\">\n <AlertCircle className=\"size-3.5 shrink-0\" />\n <span>\n {tr(\n 'sessionDetails.commandExited',\n 'Command exited{{code}} — click Run Command to run it again.',\n {\n code:\n exitedNotice.exitCode !== null ? ` (exit ${exitedNotice.exitCode})` : '',\n }\n )}\n </span>\n </div>\n <button\n type=\"button\"\n onClick={() => setExitedNotice(null)}\n className=\"text-text-secondary hover:text-text-primary\"\n title={tr('common.dismiss', 'Dismiss')}\n >\n <X className=\"size-3.5\" />\n </button>\n </div>\n )}\n\n {/* Terminal content */}\n <div className={`${terminalHeight} bg-bg-inverse text-text-inverse`}>\n {terminalLoading ? (\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"text-center\">\n <Loader2 className=\"size-8 text-text-secondary animate-spin mx-auto mb-3\" />\n <p className=\"text-text-secondary text-sm\">\n {tr('sessionDetails.startingTerminal', 'Starting terminal for')} {session.name}\n ...\n </p>\n <p className=\"text-text-tertiary text-xs mt-1\">\n {tr(\n 'sessionDetails.settingUpTunnel',\n 'Setting up tmux + ttyd + cloudflared tunnel'\n )}\n </p>\n </div>\n </div>\n ) : terminalError ? (\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"text-center max-w-md\">\n <AlertCircle className=\"size-8 text-status-error-text mx-auto mb-3\" />\n <p className=\"text-status-error-text text-sm font-medium\">\n {tr('sessionDetails.failedStartTerminal', 'Failed to start terminal')}\n </p>\n <p className=\"text-text-secondary text-xs mt-2\">{terminalError}</p>\n <button\n type=\"button\"\n onClick={handleStartTerminal}\n className=\"mt-4 px-4 py-2 bg-bg-muted text-text-primary text-sm rounded-md hover:bg-bg-sunken transition-colors\"\n >\n {tr('sessionDetails.retry', 'Retry')}\n </button>\n </div>\n </div>\n ) : terminalUrl ? (\n <iframe\n src={terminalUrl}\n title={`Terminal - ${session.name}`}\n className=\"w-full h-full border-0\"\n allow=\"clipboard-read; clipboard-write\"\n // F3 — the cookie-shim cookie has a 10-minute lifetime. If the\n // iframe errors after that (e.g. user leaves the page idle), the\n // bootstrap exchange will 401 and we need a fresh capability.\n onError={requestRemint}\n />\n ) : terminalBaseUrl && (capabilityLoading || capabilityError) ? (\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"text-center\">\n {capabilityError ? (\n <>\n <AlertCircle className=\"size-8 text-status-error mx-auto mb-3\" />\n <p className=\"text-text-secondary text-sm\">\n {tr(\n 'sessionDetails.terminalCapabilityFailed',\n 'Could not prepare terminal access'\n )}\n </p>\n <p className=\"text-text-tertiary text-xs mt-1\">{capabilityError}</p>\n <button\n type=\"button\"\n onClick={requestRemint}\n className=\"mt-4 px-4 py-2 bg-bg-muted text-text-primary text-sm rounded-md hover:bg-bg-sunken transition-colors\"\n >\n {tr('sessionDetails.retry', 'Retry')}\n </button>\n </>\n ) : (\n <>\n <Loader2 className=\"size-6 text-text-tertiary mx-auto mb-3 animate-spin\" />\n <p className=\"text-text-secondary text-sm\">\n {tr('sessionDetails.preparingTerminal', 'Preparing terminal…')}\n </p>\n </>\n )}\n </div>\n </div>\n ) : (\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"text-center\">\n <Terminal className=\"size-8 text-text-tertiary mx-auto mb-3\" />\n <p className=\"text-text-secondary text-sm\">\n {tr('sessionDetails.noTerminalRunning', 'No terminal running')}\n </p>\n <p className=\"text-text-tertiary text-xs mt-1\">\n {agent\n ? tr(\n 'sessionDetails.clickOpenTerminalHint',\n 'Click \"Open Terminal\" to create a tmux session with a browser terminal.'\n )\n : tr(\n 'sessionDetails.noAgentForSession',\n 'No agent is assigned to this session.'\n )}\n </p>\n {agent && (\n <button\n type=\"button\"\n onClick={handleStartTerminal}\n disabled={terminalLoading}\n className=\"mt-4 px-4 py-2 bg-bg-muted text-text-primary text-sm rounded-md hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n {terminalLoading\n ? tr('sessionDetails.opening', 'Opening...')\n : tr('sessionDetails.openTerminal', 'Open Terminal')}\n </button>\n )}\n </div>\n </div>\n )}\n </div>\n </div>\n )}\n\n {/* Terminal collapsed indicator - show a clickable bar */}\n {!terminalOpen && (terminalUrl || terminalError) && (\n <div className=\"border-t border-border-default bg-bg-sunken shrink-0\">\n <button\n type=\"button\"\n onClick={() => setTerminalOpen(true)}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-text-secondary hover:text-text-primary transition-colors text-xs font-medium uppercase tracking-wide w-full\"\n title={tr('sessionDetails.expandTerminal', 'Expand terminal')}\n >\n <Terminal className=\"size-3.5\" />\n {tr('sessionDetails.terminal', 'Terminal')} - {session.name}\n <ChevronUp className=\"size-3 ml-1\" />\n </button>\n </div>\n )}\n\n <ConfirmDialog\n isOpen={showQuotaExhausted}\n onClose={() => setShowQuotaExhausted(false)}\n onConfirm={() => {\n setShowQuotaExhausted(false);\n navigate('/billing');\n }}\n title={getQuotaDialogContent('session').title}\n message={getQuotaDialogContent('session').message}\n confirmLabel=\"View Billing\"\n cancelLabel=\"Dismiss\"\n variant=\"warning\"\n />\n\n {/* Restart-terminal kills tmux and re-runs the command, so\n confirm before firing. */}\n <ConfirmDialog\n isOpen={showRestartTerminalConfirm}\n onClose={() => setShowRestartTerminalConfirm(false)}\n onConfirm={() => {\n setShowRestartTerminalConfirm(false);\n void handleRestartTerminal();\n }}\n title={tr('sessionDetails.restartTerminalConfirmTitle', 'Restart terminal?')}\n message={tr(\n 'sessionDetails.restartTerminalConfirmBody',\n 'This kills the current tmux session and re-runs the command from scratch. Any in-progress output will be lost.'\n )}\n confirmLabel={tr('sessionDetails.restartTerminalConfirmLabel', 'Restart terminal')}\n cancelLabel={tr('common.cancel', 'Cancel')}\n variant=\"warning\"\n />\n </MotionPage>\n );\n}\n"],"mappings":"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"}
|
|
@@ -4,9 +4,40 @@ import { graphqlFetch as t } from "@burdenoff/fe-libs/shared/graphql/fetch";
|
|
|
4
4
|
function n() {
|
|
5
5
|
return e() ?? "";
|
|
6
6
|
}
|
|
7
|
-
|
|
7
|
+
function r() {
|
|
8
|
+
try {
|
|
9
|
+
return !1;
|
|
10
|
+
} catch {
|
|
11
|
+
return !1;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function i(e) {
|
|
8
15
|
e.workspaceId || n();
|
|
9
|
-
let
|
|
16
|
+
let i = r() && e.agentRef ? {
|
|
17
|
+
executorType: "GRAPHQL",
|
|
18
|
+
executorConfig: {
|
|
19
|
+
operation: "executeScheduledPrompt",
|
|
20
|
+
variables: {
|
|
21
|
+
agentId: e.agentRef.agentId,
|
|
22
|
+
profile: e.agentRef.profile,
|
|
23
|
+
sessionId: e.sessionId,
|
|
24
|
+
prompt: e.prompt
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} : {
|
|
28
|
+
executorType: "WEBHOOK",
|
|
29
|
+
executorConfig: {
|
|
30
|
+
url: `${e.agentTunnelUrl}/api/ai/sessions/${e.sessionId}/send`,
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: {
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
...e.agentApiKey ? { "X-API-Key": e.agentApiKey } : {}
|
|
35
|
+
},
|
|
36
|
+
body: JSON.stringify({ prompt: e.prompt })
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
if (i.executorType === "WEBHOOK" && !e.agentApiKey) throw Error("schedulePrompt: legacy WEBHOOK executor requires agentApiKey. Set VITE_SCHEDULER_AGENT_REF_ENABLED=true and pass agentRef to use the secure path.");
|
|
40
|
+
let a = await t({
|
|
10
41
|
gateway: "workspace",
|
|
11
42
|
query: "mutation CreateScheduledJob($input: CreateScheduledJobInput!) {\n createScheduledJob(input: $input) { id status }\n }",
|
|
12
43
|
variables: { input: {
|
|
@@ -15,26 +46,17 @@ async function r(e) {
|
|
|
15
46
|
scheduleType: "ONE_TIME",
|
|
16
47
|
startDate: e.scheduledAt,
|
|
17
48
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
18
|
-
|
|
19
|
-
executorConfig: {
|
|
20
|
-
url: `${e.agentTunnelUrl}/api/ai/sessions/${e.sessionId}/send`,
|
|
21
|
-
method: "POST",
|
|
22
|
-
headers: {
|
|
23
|
-
"Content-Type": "application/json",
|
|
24
|
-
"X-API-Key": e.agentApiKey
|
|
25
|
-
},
|
|
26
|
-
body: JSON.stringify({ prompt: e.prompt })
|
|
27
|
-
},
|
|
49
|
+
...i,
|
|
28
50
|
isActive: !0
|
|
29
51
|
} }
|
|
30
52
|
});
|
|
31
|
-
if (
|
|
53
|
+
if (a.errors?.length) throw Error(a.errors[0].message);
|
|
32
54
|
return {
|
|
33
|
-
jobId:
|
|
55
|
+
jobId: a.data?.createScheduledJob?.id || "",
|
|
34
56
|
scheduledAt: e.scheduledAt
|
|
35
57
|
};
|
|
36
58
|
}
|
|
37
59
|
//#endregion
|
|
38
|
-
export {
|
|
60
|
+
export { i as schedulePrompt };
|
|
39
61
|
|
|
40
62
|
//# sourceMappingURL=schedulerApi.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schedulerApi.js","names":[],"sources":["../../src/services/schedulerApi.ts"],"sourcesContent":["/**\n * Scheduler API service for scheduling AI prompts.\n *\n * Creates one-time scheduled jobs via the workspace scheduler\n * that fire webhook calls to the AI session send endpoint.\n */\n\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql/fetch';\nimport { getStoredWorkspaceId } from '@burdenoff/fe-libs/shared/graphql';\n\n// Reads from ActiveContextStorage with URL precedence so the workspace id\n// is found even when the calling MFE route doesn't carry `?workspace=`.\nfunction getWorkspaceId(): string {\n return getStoredWorkspaceId() ?? '';\n}\n\nexport async function schedulePrompt(params: {\n sessionId: string;\n agentTunnelUrl: string;\n
|
|
1
|
+
{"version":3,"file":"schedulerApi.js","names":[],"sources":["../../src/services/schedulerApi.ts"],"sourcesContent":["/**\n * Scheduler API service for scheduling AI prompts.\n *\n * Creates one-time scheduled jobs via the workspace scheduler\n * that fire webhook calls to the AI session send endpoint.\n *\n * SECURITY-F12 (2026-05-28): The legacy WEBHOOK path persists the agent's\n * long-lived `agentApiKey` in `executorConfig.headers['X-API-Key']` inside\n * the scheduler DB. New code paths should instead send `agentRef:{agentId,\n * profile}` so the scheduler invokes a backend mutation that mints a\n * short-lived (≤60s) ticket at execution time. See\n * `~/products/dev/platform/context/scheduler/SECURITY_F12.md` for the full\n * migration plan and why phases 2-4 are deferred.\n *\n * This file ships the microfe half (phase 1) behind a feature flag. With\n * the flag OFF (default) the legacy webhook payload is preserved so\n * running scheduled jobs continue to fire. With the flag ON AND `agentRef`\n * supplied, the secret-free GRAPHQL executor payload is emitted instead.\n */\n\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql/fetch';\nimport { getStoredWorkspaceId } from '@burdenoff/fe-libs/shared/graphql';\n\n// Reads from ActiveContextStorage with URL precedence so the workspace id\n// is found even when the calling MFE route doesn't carry `?workspace=`.\nfunction getWorkspaceId(): string {\n return getStoredWorkspaceId() ?? '';\n}\n\nfunction isAgentRefEnabled(): boolean {\n // Vite-injected env var. Default off — the backend (phase 2 + 3) isn't\n // shipped yet, flipping this without the backend would break new\n // scheduled jobs the moment they fire.\n try {\n return import.meta.env?.VITE_SCHEDULER_AGENT_REF_ENABLED === 'true';\n } catch {\n return false;\n }\n}\n\nexport interface AgentRef {\n agentId: string;\n profile: string;\n}\n\nexport async function schedulePrompt(params: {\n sessionId: string;\n agentTunnelUrl: string;\n /**\n * Legacy long-lived agent API key. SECURITY-F12: do not persist in new\n * executor configs once the `agentRef` path is enabled. Still accepted\n * here so existing call sites and feature-flag-off deployments work.\n */\n agentApiKey?: string;\n /**\n * Preferred (phase 1+): reference the agent without embedding its key.\n * Backend mints a short-lived ticket at execution time.\n */\n agentRef?: AgentRef;\n prompt: string;\n scheduledAt: string; // ISO datetime\n workspaceId?: string;\n}): Promise<{ jobId: string; scheduledAt: string }> {\n const wsId = params.workspaceId || getWorkspaceId();\n\n const useAgentRef = isAgentRefEnabled() && !!params.agentRef;\n\n // SECURITY-F12 phase 1: when the flag is on AND we have an agentRef,\n // emit the secret-free GRAPHQL executor payload. The scheduler will\n // dispatch this through wspace-int-gateway to the (not-yet-shipped)\n // `executeScheduledPrompt` mutation on wspace-vibecontrols-svc, which\n // mints a short-lived ticket at execution time.\n const executor = useAgentRef\n ? {\n executorType: 'GRAPHQL' as const,\n executorConfig: {\n operation: 'executeScheduledPrompt',\n variables: {\n agentId: params.agentRef!.agentId,\n profile: params.agentRef!.profile,\n sessionId: params.sessionId,\n prompt: params.prompt,\n },\n },\n }\n : {\n // Legacy path: WEBHOOK with the raw long-lived `agentApiKey`\n // embedded in headers. Kept for backwards compatibility until\n // SECURITY-F12 phase 2 + 3 ship and this branch is removed in\n // phase 4. Refuses to schedule without a key (the legacy\n // executor cannot authenticate against the agent without one).\n executorType: 'WEBHOOK' as const,\n executorConfig: {\n url: `${params.agentTunnelUrl}/api/ai/sessions/${params.sessionId}/send`,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(params.agentApiKey ? { 'X-API-Key': params.agentApiKey } : {}),\n },\n body: JSON.stringify({ prompt: params.prompt }),\n },\n };\n\n if (executor.executorType === 'WEBHOOK' && !params.agentApiKey) {\n throw new Error(\n 'schedulePrompt: legacy WEBHOOK executor requires agentApiKey. Set VITE_SCHEDULER_AGENT_REF_ENABLED=true and pass agentRef to use the secure path.'\n );\n }\n\n const result = await graphqlFetch<{\n createScheduledJob?: { id?: string; status?: string };\n }>({\n gateway: 'workspace',\n query: `mutation CreateScheduledJob($input: CreateScheduledJobInput!) {\n createScheduledJob(input: $input) { id status }\n }`,\n variables: {\n input: {\n name: `AI Prompt - ${params.sessionId.slice(0, 8)}`,\n description: `Scheduled prompt for AI session`,\n scheduleType: 'ONE_TIME',\n startDate: params.scheduledAt,\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n ...executor,\n isActive: true,\n },\n },\n });\n\n if (result.errors?.length) throw new Error(result.errors[0].message);\n const job = result.data?.createScheduledJob;\n return { jobId: job?.id || '', scheduledAt: params.scheduledAt };\n}\n"],"mappings":";;;AAyBA,SAAS,IAAyB;AAChC,QAAO,GAAsB,IAAI;;AAGnC,SAAS,IAA6B;AAIpC,KAAI;AACF,SAAO;SACD;AACN,SAAO;;;AASX,eAAsB,EAAe,GAiBe;AACrC,GAAO,eAAe,GAAgB;CASnD,IAAM,IAPc,GAAmB,IAAM,EAAO,WAQhD;EACE,cAAc;EACd,gBAAgB;GACd,WAAW;GACX,WAAW;IACT,SAAS,EAAO,SAAU;IAC1B,SAAS,EAAO,SAAU;IAC1B,WAAW,EAAO;IAClB,QAAQ,EAAO;IAChB;GACF;EACF,GACD;EAME,cAAc;EACd,gBAAgB;GACd,KAAK,GAAG,EAAO,eAAe,mBAAmB,EAAO,UAAU;GAClE,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,GAAI,EAAO,cAAc,EAAE,aAAa,EAAO,aAAa,GAAG,EAAE;IAClE;GACD,MAAM,KAAK,UAAU,EAAE,QAAQ,EAAO,QAAQ,CAAC;GAChD;EACF;AAEL,KAAI,EAAS,iBAAiB,aAAa,CAAC,EAAO,YACjD,OAAU,MACR,oJACD;CAGH,IAAM,IAAS,MAAM,EAElB;EACD,SAAS;EACT,OAAO;EAGP,WAAW,EACT,OAAO;GACL,MAAM,eAAe,EAAO,UAAU,MAAM,GAAG,EAAE;GACjD,aAAa;GACb,cAAc;GACd,WAAW,EAAO;GAClB,UAAU,KAAK,gBAAgB,CAAC,iBAAiB,CAAC;GAClD,GAAG;GACH,UAAU;GACX,EACF;EACF,CAAC;AAEF,KAAI,EAAO,QAAQ,OAAQ,OAAU,MAAM,EAAO,OAAO,GAAG,QAAQ;AAEpE,QAAO;EAAE,OADG,EAAO,MAAM,oBACJ,MAAM;EAAI,aAAa,EAAO;EAAa"}
|