diff --git a/frontend/app/globals.css b/frontend/app/globals.css index e1336bb..002d675 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -503,10 +503,192 @@ button { min-height: 0; } +.tutorial-brief { + margin: 58px 18px 0; + padding: 18px; + border-radius: 22px; + border: 1px solid rgba(148, 172, 208, 0.14); + background: + radial-gradient(circle at top right, rgba(122, 162, 255, 0.12), transparent 28%), + linear-gradient(180deg, rgba(18, 27, 40, 0.98), rgba(12, 18, 28, 0.96)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.04), + 0 24px 42px rgba(0, 0, 0, 0.22); + display: flex; + flex-direction: column; + gap: 14px; + flex-shrink: 0; +} + +.tutorial-brief-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.tutorial-copy { + min-width: 0; +} + +.tutorial-kicker { + font-family: var(--font-mono); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.16em; + color: rgba(165, 177, 197, 0.78); + margin-bottom: 8px; +} + +.tutorial-title { + margin: 0; + font-size: 28px; + line-height: 1.05; + letter-spacing: -0.035em; + max-width: 16ch; +} + +.tutorial-mode-pill { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 12px; + border-radius: 999px; + font-family: var(--font-mono); + font-size: 10.5px; + text-transform: uppercase; + letter-spacing: 0.12em; + border: 1px solid rgba(148, 172, 208, 0.14); + background: rgba(11, 17, 25, 0.76); + color: var(--text); + white-space: nowrap; +} + +.tutorial-mode-pill.execute { + border-color: rgba(61, 220, 151, 0.34); + background: rgba(61, 220, 151, 0.12); + color: #98efbe; +} + +.tutorial-mode-pill.plan { + border-color: rgba(200, 130, 255, 0.34); + background: rgba(200, 130, 255, 0.12); + color: #e1b4ff; +} + +.tutorial-mode-pill.merge { + border-color: rgba(245, 181, 68, 0.36); + background: rgba(245, 181, 68, 0.12); + color: #ffd991; +} + +.tutorial-mode-pill.installation { + border-color: rgba(122, 162, 255, 0.34); + background: rgba(122, 162, 255, 0.12); + color: #c6d9ff; +} + +.tutorial-summary { + margin: 0; + color: var(--text-dim); + font-size: 12.25px; + line-height: 1.65; + max-width: 68ch; +} + +.tutorial-progress { + display: flex; + flex-direction: column; + gap: 8px; +} + +.tutorial-progress-copy { + display: flex; + justify-content: space-between; + gap: 12px; + font-family: var(--font-mono); + font-size: 10.5px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); +} + +.tutorial-progress-track, +.ctrl-progress-track { + position: relative; + overflow: hidden; + border-radius: 999px; + background: rgba(255, 255, 255, 0.07); +} + +.tutorial-progress-track { + height: 7px; +} + +.tutorial-progress-track span, +.ctrl-progress-track span { + display: block; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, #3DDC97 0%, #83F0BD 55%, #7AA2FF 100%); +} + +.tutorial-meta-grid, +.tutorial-highlights, +.ctrl-guide-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 10px; +} + +.tutorial-stat, +.tutorial-highlight, +.guide-card { + border-radius: 16px; + border: 1px solid rgba(148, 172, 208, 0.12); + background: rgba(11, 17, 25, 0.56); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); +} + +.tutorial-stat { + padding: 12px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.tutorial-stat-label, +.guide-eyebrow { + font-family: var(--font-mono); + font-size: 9.75px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text-muted); +} + +.tutorial-stat-value { + font-size: 18px; + line-height: 1; + letter-spacing: -0.03em; +} + +.tutorial-stat-meta { + color: var(--text-dim); + font-size: 11px; + line-height: 1.45; +} + +.tutorial-highlight { + padding: 12px; + color: var(--text); + font-size: 11.5px; + line-height: 1.55; +} + .chat-scroll { flex: 1; overflow-y: auto; - padding: 58px 28px 22px; + padding: 18px 28px 22px; display: flex; flex-direction: column; gap: 16px; @@ -759,7 +941,7 @@ button { linear-gradient(180deg, rgba(13, 18, 27, 0.96), rgba(9, 14, 21, 0.98)); display: flex; flex-direction: column; - gap: 12px; + gap: 14px; flex-shrink: 0; backdrop-filter: blur(16px); } @@ -767,10 +949,19 @@ button { .ctrl-top { display: flex; justify-content: space-between; - align-items: center; + align-items: flex-start; + gap: 14px; font-size: 12px; } +.ctrl-stage { + min-width: 0; + flex: 1; + display: flex; + flex-direction: column; + gap: 10px; +} + .ctrl-step-label { font-weight: 600; color: var(--text); @@ -782,9 +973,31 @@ button { margin-right: 8px; } +.ctrl-progress { + display: flex; + align-items: center; + gap: 10px; +} + +.ctrl-progress-copy { + font-family: var(--font-mono); + font-size: 9.75px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text-muted); + white-space: nowrap; +} + +.ctrl-progress-track { + flex: 1; + min-width: 120px; + height: 6px; +} + .dots { display: flex; gap: 5px; + padding-top: 4px; } .dots .d { @@ -811,10 +1024,48 @@ button { font-size: 12px; color: var(--text-dim); line-height: 1.5; - min-height: 36px; + min-height: 0; max-width: 68ch; } +.guide-card { + padding: 12px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.guide-card-watch { + background: + linear-gradient(180deg, rgba(18, 27, 40, 0.92), rgba(11, 17, 25, 0.84)); +} + +.guide-card p { + margin: 0; + color: var(--text-dim); + font-size: 11.5px; + line-height: 1.55; +} + +.guide-token-grid { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.guide-token { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: 0 10px; + border-radius: 999px; + border: 1px solid rgba(148, 172, 208, 0.14); + background: rgba(255, 255, 255, 0.04); + color: var(--text); + font-size: 11px; + line-height: 1.45; +} + .ctrl-btns { display: flex; gap: 8px; @@ -1600,6 +1851,10 @@ button { .pane.right { border-right: 1px solid var(--border); } + + .tutorial-title { + max-width: none; + } } @media (max-width: 760px) { @@ -1638,14 +1893,38 @@ button { grid-template-columns: 40px 220px 1fr; } + .tutorial-brief { + margin: 52px 12px 0; + padding: 14px; + } + + .tutorial-brief-head { + flex-direction: column; + align-items: flex-start; + } + + .tutorial-title { + font-size: 22px; + } + .chat-scroll { - padding: 48px 14px 14px; + padding: 14px; } .controls { padding: 14px; } + .ctrl-top, + .ctrl-progress { + flex-direction: column; + align-items: stretch; + } + + .ctrl-progress-copy { + white-space: normal; + } + .ctrl-btns { flex-direction: column; } diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index a51c966..cb23fe3 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -118,6 +118,13 @@ interface ModeConfig { steps: TutorialStep[] } +interface ModeGuide { + eyebrow: string + title: string + summary: string + highlights: string[] +} + const MODE_ORDER: ModeKey[] = ['execute', 'plan', 'merge', 'installation'] const INSTALL_COMMAND = 'npm i -g @imdeadpool/guardex' const PRODUCT_LABEL = 'Recodee' @@ -1994,6 +2001,175 @@ const TUTORIAL: Record = { }, } +const MODE_GUIDES: Record = { + execute: { + eyebrow: 'Live sandbox flow', + title: 'Watch one prompt turn into an isolated PR', + summary: + 'Execute mode is the end-to-end story: inspect first, branch into a sandbox, stream visible diffs, ask for approval, then merge and prune the worktree.', + highlights: [ + 'Prompt, inspect, sandbox, diff, PR', + 'Parallel Codex and Claude lanes stay separated', + 'Approval and cleanup stay visible end to end', + ], + }, + plan: { + eyebrow: 'Read-only planning loop', + title: 'Teach the plan before the code exists', + summary: + 'Plan mode slows the workflow down on purpose. The agent can only read, map risks, and draft a persistent markdown plan until the human approves execution.', + highlights: [ + 'Shift+Tab locks the agent into read-only', + 'Plans persist as markdown, not hidden context', + 'Review and revise before any writes start', + ], + }, + merge: { + eyebrow: 'Conflict recovery lane', + title: 'Show how GuardeX merges without trashing either branch', + summary: + 'Merge mode visualizes the recovery path when two PRs collide. A dedicated merge lane appears, reads both intents, writes a semantic resolution, and proves it with tests.', + highlights: [ + 'Conflicts are isolated before main changes', + 'A merge agent owns the repair branch', + 'Tests run before the final review and merge', + ], + }, + installation: { + eyebrow: 'Repo onboarding', + title: 'Install, audit, wire, then start safely', + summary: + 'Installation mode explains the shortest safe path into the workflow: install the CLI, audit the repo, wire the guardrails, then use the start and finish commands instead of ad hoc git moves.', + highlights: [ + 'Doctor audits drift before setup touches files', + 'Setup wires hooks, scripts, and agent defaults', + 'Start and finish keep the branch lifecycle predictable', + ], + }, +} + +const countChangedFiles = (step: TutorialStep) => + step.worktrees.reduce((total, worktree) => total + (worktree.files?.length ?? 0), 0) + +const DEFAULT_WATCH_COPY: Record = { + execute: 'The walkthrough keeps every write inside a disposable agent lane.', + plan: 'Plan mode keeps the workflow explainable before execution begins.', + merge: 'Conflict recovery happens in a dedicated merge lane, not on the base branch.', + installation: 'The install path teaches the command order that keeps teams out of trouble.', +} + +const getStepWatchItems = (mode: ModeKey, step: TutorialStep): string[] => { + const changedFiles = countChangedFiles(step) + const items: string[] = [] + const hasReadonlyWorktree = step.worktrees.some((worktree) => worktree.kind === 'readonly') + const hasParallelWorktrees = step.worktrees.length > 1 + const hasCommitGate = step.worktrees.some((worktree) => worktree.commitReady) + const openTab = step.tabs.find((tab) => tab.active) ?? step.tabs[0] + + if (hasReadonlyWorktree) { + items.push('This lane is read-only. Write attempts stay blocked until the plan is approved.') + } + + if (hasParallelWorktrees) { + items.push('Multiple sandboxes can move at once without clobbering each other.') + } else if (step.worktrees.length === 1) { + items.push('A single disposable sandbox owns the live change you are watching.') + } + + if (changedFiles > 0) { + items.push(`${changedFiles} tracked file${changedFiles === 1 ? '' : 's'} pulse in Source Control as the step advances.`) + } + + if (openTab) { + items.push(`The editor stays anchored on ${openTab.label} so the step remains easy to follow.`) + } + + if (step.messages.some((message) => message.kind === 'tool')) { + items.push('Tool blocks narrate each transition instead of hiding the mechanics.') + } + + if (step.messages.some((message) => message.kind === 'thinking')) { + items.push('Thinking bubbles expose intent before side effects land.') + } + + if (hasCommitGate) { + items.push('The commit waits at the human approval gate even though the code is ready.') + } + + if (step.showPullAnimation) { + items.push('The final pull-back shows the sandbox disappearing after the merge lands.') + } + + if (items.length === 0) { + items.push(DEFAULT_WATCH_COPY[mode]) + } + + return items.slice(0, 3) +} + +const getGuardrailCopy = (mode: ModeKey, step: TutorialStep): string => { + if (step.worktrees.some((worktree) => worktree.kind === 'readonly')) { + return 'Read-only mode is enforced at the tool layer, so the plan stays trustworthy until a human flips the workflow back to execution.' + } + + if (step.worktrees.some((worktree) => worktree.commitReady)) { + return 'On-request access still pauses before commit, which keeps the operator in control of the irreversible step.' + } + + if (step.showPullAnimation) { + return 'Cleanup is part of the product logic. A finished lane merges, syncs back to the base branch, and prunes the sandbox instead of leaving git debt behind.' + } + + switch (mode) { + case 'execute': + return 'Execute mode protects the base branch by forcing writes into disposable worktrees first, so review and rollback stay cheap.' + case 'plan': + return 'Plan mode converts hidden agent intent into a concrete markdown artifact before any code or shell side effects can happen.' + case 'merge': + return 'Merge mode resolves conflicts in isolation, preserving both PR intents before the final branch ever gets touched.' + case 'installation': + return 'Installation mode teaches a fixed command sequence so teams do not half-install the guardrails and drift into unsafe manual workflows.' + } +} + +const getTakeawayCopy = (mode: ModeKey, step: TutorialStep): string => { + if (mode === 'execute') { + if (step.showPullAnimation) { + return 'The happy-path promise is not just “the agent can code.” It is “the repo is clean again when the change is done.”' + } + if (step.worktrees.length > 1) { + return 'Parallel agents only stay safe because each lane owns a separate branch and worktree.' + } + if (step.worktrees.some((worktree) => worktree.commitReady)) { + return 'The operator is still the final authority at commit time, even when the agent did the implementation.' + } + if (step.worktrees.length === 1) { + return 'Once the sandbox exists, every visible file mutation becomes reviewable and reversible.' + } + return 'The execute demo starts by proving that GuardeX does not skip straight to writing code.' + } + + if (mode === 'plan') { + if (step.worktrees.some((worktree) => worktree.kind === 'readonly')) { + return 'A plan is more believable when the interface proves the agent physically cannot “just start coding anyway.”' + } + return 'The planning loop keeps the future implementation aligned with an artifact the team can review, edit, and revisit later.' + } + + if (mode === 'merge') { + if (step.showPullAnimation) { + return 'Conflict resolution is only complete after the repaired branch merges cleanly and the temporary lane disappears.' + } + return 'Good merge UX is about preserving intent, not just deleting conflict markers until git stops complaining.' + } + + if (step.showPullAnimation) { + return 'The onboarding path ends where real work begins: on a clean base branch with the workflow already wired.' + } + + return 'The install sequence matters because teams follow habits. The tutorial teaches the safe habit, not just the commands.' +} + /* ======================= ICONS ======================= */ const strokeProps = { @@ -2413,6 +2589,7 @@ export default function Home() { }, []) const modeData = TUTORIAL[mode] + const modeGuide = MODE_GUIDES[mode] const steps = modeData.steps const activeStep = steps[stepIndex] @@ -2499,6 +2676,42 @@ export default function Home() { const statusSync = activeStep.statusSync ?? '↓ 0 ↑ 0' const statusErrors = activeStep.statusErrors ?? 0 const showPull = !!activeStep.showPullAnimation + const progressPercent = Math.round(((stepIndex + 1) / steps.length) * 100) + const watchItems = getStepWatchItems(mode, activeStep) + const guardrailCopy = getGuardrailCopy(mode, activeStep) + const takeawayCopy = getTakeawayCopy(mode, activeStep) + + const summaryCards = [ + { + label: 'Progress', + value: `${stepIndex + 1}/${steps.length}`, + meta: `${progressPercent}% through ${modeData.label.toLowerCase()}`, + }, + { + label: 'Lane', + value: activeStep.worktrees.some((worktree) => worktree.kind === 'readonly') + ? 'read-only' + : mode === 'merge' + ? 'merge' + : activeStep.worktrees.length > 0 + ? 'live' + : 'standby', + meta: + activeStep.worktrees.length > 0 + ? `${activeStep.worktrees.length} sandbox${activeStep.worktrees.length === 1 ? '' : 'es'} visible` + : 'base branch only', + }, + { + label: 'Tracked files', + value: `${activityChangeCount}`, + meta: activityChangeCount > 0 ? 'surfacing in Source Control' : 'no writes yet', + }, + { + label: 'Branch state', + value: statusBranch === 'dev' ? 'dev' : 'agent', + meta: statusBranch, + }, + ] return (
@@ -2615,6 +2828,48 @@ export default function Home() {
chat · {PRODUCT_LABEL.toLowerCase()}
+
+
+
+
{modeGuide.eyebrow}
+

{modeGuide.title}

+
+ + {modeData.label} + +
+ +

{modeGuide.summary}

+ +
+
+ {activeStep.stepLabel} + {progressPercent}% complete +
+
+ +
+
+ +
+ {summaryCards.map((card) => ( +
+ {card.label} + {card.value} + {card.meta} +
+ ))} +
+ +
+ {modeGuide.highlights.map((highlight) => ( +
+ {highlight} +
+ ))} +
+
+
-
- {activeStep.stepLabel} - {activeStep.label} +
+
+ {activeStep.stepLabel} + {activeStep.label} +
+
+ lesson progress +
+ +
+
{steps.map((step, i) => ( @@ -2649,6 +2912,28 @@ export default function Home() {
{activeStep.description}
+
+
+ Watch for +
+ {watchItems.map((item) => ( + + {item} + + ))} +
+
+ +
+ Guardrail +

{guardrailCopy}

+
+ +
+ Takeaway +

{takeawayCopy}

+
+