generated from nhcarrigan/template
feat: add guided project workflow panel (Discuss → Plan → Execute → Verify)
This commit is contained in:
@@ -0,0 +1,833 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { get } from "svelte/store";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
workflowStore,
|
||||
buildDiscussPrompt,
|
||||
buildVerifyPrompt,
|
||||
canAdvancePhase,
|
||||
canGoBack,
|
||||
getPhaseLabel,
|
||||
type WorkflowPhase,
|
||||
type CriterionStatus,
|
||||
} from "$lib/stores/workflow";
|
||||
import { prdStore } from "$lib/stores/prd";
|
||||
import { taskLoopStore, countByStatus } from "$lib/stores/taskLoop";
|
||||
import { characterState } from "$lib/stores/character";
|
||||
import { claudeStore } from "$lib/stores/claude";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onOpenPrdPanel: () => void;
|
||||
onOpenTaskLoop: () => void;
|
||||
workingDirectory: string;
|
||||
}
|
||||
|
||||
const { onClose, onOpenPrdPanel, onOpenTaskLoop, workingDirectory }: Props = $props();
|
||||
|
||||
const workflowState = $derived(workflowStore.state);
|
||||
const prdTasks = $derived(prdStore.tasks);
|
||||
const prdIsLoaded = $derived(prdStore.isLoaded);
|
||||
const loopTasks = $derived(taskLoopStore.tasks);
|
||||
const loopStatus = $derived(taskLoopStore.loopStatus);
|
||||
|
||||
let previousCharacterState = $state<string>("idle");
|
||||
let isWaitingForDiscuss = $state(false);
|
||||
let isWaitingForVerify = $state(false);
|
||||
let contextContent = $state<string | null>(null);
|
||||
let verifyContent = $state<string | null>(null);
|
||||
let newCriterionText = $state("");
|
||||
let isLoadingContext = $state(false);
|
||||
let isLoadingVerify = $state(false);
|
||||
|
||||
const PHASES: WorkflowPhase[] = [1, 2, 3, 4];
|
||||
|
||||
onMount(async () => {
|
||||
await workflowStore.loadState(workingDirectory);
|
||||
await prdStore.loadFromFile(workingDirectory);
|
||||
await tryLoadContextFile();
|
||||
await tryLoadVerifyFile();
|
||||
});
|
||||
|
||||
// Watch characterState to detect when Claude finishes working
|
||||
$effect(() => {
|
||||
const currentState = $characterState;
|
||||
if (isWaitingForDiscuss && previousCharacterState !== "idle" && currentState === "idle") {
|
||||
isWaitingForDiscuss = false;
|
||||
void tryLoadContextFile().then(() => {
|
||||
if (contextContent !== null) {
|
||||
workflowStore.markContextCaptured();
|
||||
void workflowStore.saveState(workingDirectory);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (isWaitingForVerify && previousCharacterState !== "idle" && currentState === "idle") {
|
||||
isWaitingForVerify = false;
|
||||
void tryLoadVerifyFile().then(() => {
|
||||
if (verifyContent !== null) {
|
||||
workflowStore.completeVerification(verifyContent);
|
||||
void workflowStore.saveState(workingDirectory);
|
||||
}
|
||||
});
|
||||
}
|
||||
previousCharacterState = currentState;
|
||||
});
|
||||
|
||||
async function tryLoadContextFile(): Promise<void> {
|
||||
isLoadingContext = true;
|
||||
try {
|
||||
const content = await invoke<string>("read_file_content", {
|
||||
path: `${workingDirectory}/CONTEXT.md`,
|
||||
});
|
||||
contextContent = content;
|
||||
} catch {
|
||||
contextContent = null;
|
||||
} finally {
|
||||
isLoadingContext = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function tryLoadVerifyFile(): Promise<void> {
|
||||
isLoadingVerify = true;
|
||||
try {
|
||||
const content = await invoke<string>("read_file_content", {
|
||||
path: `${workingDirectory}/VERIFY.md`,
|
||||
});
|
||||
verifyContent = content;
|
||||
} catch {
|
||||
verifyContent = null;
|
||||
} finally {
|
||||
isLoadingVerify = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStartDiscussion(): Promise<void> {
|
||||
const description = $workflowState.discuss.description.trim();
|
||||
if (!description) return;
|
||||
const conversationId = get(claudeStore.activeConversationId);
|
||||
if (!conversationId) return;
|
||||
const prompt = buildDiscussPrompt(description);
|
||||
isWaitingForDiscuss = true;
|
||||
await invoke("send_prompt", { conversationId, message: prompt });
|
||||
}
|
||||
|
||||
async function handleQuickCaptureContext(): Promise<void> {
|
||||
const description = $workflowState.discuss.description.trim();
|
||||
if (!description) return;
|
||||
const content = `# CONTEXT\n\n## Goal\n\n${description}\n`;
|
||||
await invoke("write_file_content", { path: `${workingDirectory}/CONTEXT.md`, content });
|
||||
contextContent = content;
|
||||
workflowStore.markContextCaptured();
|
||||
await workflowStore.saveState(workingDirectory);
|
||||
}
|
||||
|
||||
async function handleApprovePlan(): Promise<void> {
|
||||
workflowStore.approvePlan();
|
||||
await workflowStore.saveState(workingDirectory);
|
||||
}
|
||||
|
||||
async function handleCompleteExecution(): Promise<void> {
|
||||
workflowStore.completeExecution();
|
||||
await workflowStore.saveState(workingDirectory);
|
||||
}
|
||||
|
||||
async function handleExtractCriteria(): Promise<void> {
|
||||
if (!contextContent) return;
|
||||
const lines = contextContent.split("\n");
|
||||
let inCriteria = false;
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (/^##\s*acceptance criteria/i.test(trimmed)) {
|
||||
inCriteria = true;
|
||||
continue;
|
||||
}
|
||||
if (inCriteria && /^##/.test(trimmed)) {
|
||||
inCriteria = false;
|
||||
continue;
|
||||
}
|
||||
if (inCriteria && /^\d+\./.test(trimmed)) {
|
||||
const text = trimmed.replace(/^\d+\.\s*/, "").trim();
|
||||
if (text) workflowStore.addCriterion(text);
|
||||
}
|
||||
}
|
||||
await workflowStore.saveState(workingDirectory);
|
||||
}
|
||||
|
||||
async function handleRunVerification(): Promise<void> {
|
||||
const criteria = $workflowState.verify.criteria;
|
||||
const conversationId = get(claudeStore.activeConversationId);
|
||||
if (!conversationId) return;
|
||||
const prompt = buildVerifyPrompt(criteria);
|
||||
isWaitingForVerify = true;
|
||||
await invoke("send_prompt", { conversationId, message: prompt });
|
||||
}
|
||||
|
||||
async function handleAddCriterion(): Promise<void> {
|
||||
const text = newCriterionText.trim();
|
||||
if (!text) return;
|
||||
workflowStore.addCriterion(text);
|
||||
newCriterionText = "";
|
||||
await workflowStore.saveState(workingDirectory);
|
||||
}
|
||||
|
||||
async function handleRemoveCriterion(id: string): Promise<void> {
|
||||
workflowStore.removeCriterion(id);
|
||||
await workflowStore.saveState(workingDirectory);
|
||||
}
|
||||
|
||||
async function handleSetCriterionStatus(id: string, status: CriterionStatus): Promise<void> {
|
||||
workflowStore.updateCriterionStatus(id, status);
|
||||
await workflowStore.saveState(workingDirectory);
|
||||
}
|
||||
|
||||
async function handleAdvance(): Promise<void> {
|
||||
const current = $workflowState.currentPhase;
|
||||
if (current < 4) {
|
||||
workflowStore.setPhase((current + 1) as WorkflowPhase);
|
||||
await workflowStore.saveState(workingDirectory);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBack(): Promise<void> {
|
||||
const current = $workflowState.currentPhase;
|
||||
if (canGoBack(current)) {
|
||||
workflowStore.setPhase((current - 1) as WorkflowPhase);
|
||||
await workflowStore.saveState(workingDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReset(): Promise<void> {
|
||||
workflowStore.reset();
|
||||
contextContent = null;
|
||||
verifyContent = null;
|
||||
await workflowStore.saveState(workingDirectory);
|
||||
}
|
||||
|
||||
function completedTaskCount(): number {
|
||||
return countByStatus($loopTasks, "completed");
|
||||
}
|
||||
|
||||
function failedTaskCount(): number {
|
||||
return countByStatus($loopTasks, "failed");
|
||||
}
|
||||
|
||||
function allTasksDone(): boolean {
|
||||
const tasks = $loopTasks;
|
||||
return (
|
||||
tasks.length > 0 && tasks.every((t) => t.status === "completed" || t.status === "failed")
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||
onclick={onClose}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={(e) => e.key === "Escape" && onClose()}
|
||||
>
|
||||
<div
|
||||
class="bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] flex flex-col"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-labelledby="workflow-panel-title"
|
||||
tabindex="-1"
|
||||
>
|
||||
<!-- Header: title + phase stepper + quick mode -->
|
||||
<div class="flex flex-col gap-3 p-6 pb-4 border-b border-[var(--border-color)]">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 id="workflow-panel-title" class="text-xl font-semibold text-[var(--text-primary)]">
|
||||
Guided Workflow
|
||||
</h2>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<span class="text-xs text-[var(--text-secondary)]">Quick Mode</span>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={$workflowState.quickMode}
|
||||
onclick={() => {
|
||||
workflowStore.setQuickMode(!$workflowState.quickMode);
|
||||
void workflowStore.saveState(workingDirectory);
|
||||
}}
|
||||
class="w-9 h-5 rounded-full transition-colors {$workflowState.quickMode
|
||||
? 'bg-[var(--accent-primary)]'
|
||||
: 'bg-[var(--bg-tertiary)]'} relative"
|
||||
>
|
||||
<span
|
||||
class="absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform {$workflowState.quickMode
|
||||
? 'translate-x-4'
|
||||
: 'translate-x-0'}"
|
||||
></span>
|
||||
</button>
|
||||
</label>
|
||||
<button
|
||||
onclick={onClose}
|
||||
class="p-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
|
||||
aria-label="Close"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phase stepper -->
|
||||
<div class="flex items-center gap-1">
|
||||
{#each PHASES as phase (phase)}
|
||||
{@const isActive = $workflowState.currentPhase === phase}
|
||||
{@const isDone = $workflowState.currentPhase > phase}
|
||||
<button
|
||||
onclick={() => {
|
||||
workflowStore.setPhase(phase);
|
||||
void workflowStore.saveState(workingDirectory);
|
||||
}}
|
||||
aria-label="Go to phase {phase}: {getPhaseLabel(phase)}"
|
||||
class="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs transition-colors {isActive
|
||||
? 'bg-[var(--accent-primary)]/20 text-[var(--accent-primary)] font-semibold'
|
||||
: isDone
|
||||
? 'text-green-400'
|
||||
: 'text-[var(--text-tertiary)]'}"
|
||||
>
|
||||
<span
|
||||
class="w-5 h-5 rounded-full flex items-center justify-center text-[10px] font-bold {isActive
|
||||
? 'bg-[var(--accent-primary)] text-white'
|
||||
: isDone
|
||||
? 'bg-green-500/20 text-green-400 border border-green-500/40'
|
||||
: 'bg-[var(--bg-secondary)] border border-[var(--border-color)]'}"
|
||||
>
|
||||
{#if isDone}✓{:else}{phase}{/if}
|
||||
</span>
|
||||
<span class="hidden sm:inline">{getPhaseLabel(phase)}</span>
|
||||
</button>
|
||||
{#if phase < 4}
|
||||
<svg
|
||||
class="w-3 h-3 text-[var(--text-tertiary)] shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="flex-1 overflow-y-auto p-6 min-h-0">
|
||||
{#if $workflowState.currentPhase === 1}
|
||||
<!-- ── Phase 1: Discuss ── -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label
|
||||
for="workflow-description"
|
||||
class="block text-sm font-medium text-[var(--text-secondary)] mb-2"
|
||||
>
|
||||
Describe what you want to build
|
||||
</label>
|
||||
<textarea
|
||||
id="workflow-description"
|
||||
value={$workflowState.discuss.description}
|
||||
oninput={(e) => {
|
||||
workflowStore.setDiscussDescription((e.target as HTMLTextAreaElement).value);
|
||||
}}
|
||||
rows={5}
|
||||
class="w-full bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-4 text-sm text-[var(--text-primary)] resize-none focus:outline-none focus:border-[var(--accent-primary)] leading-relaxed"
|
||||
placeholder="Describe the feature or project you want to build..."
|
||||
spellcheck="false"
|
||||
disabled={isWaitingForDiscuss}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
{#if isWaitingForDiscuss}
|
||||
<div
|
||||
class="flex items-center gap-3 p-4 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-color)]"
|
||||
>
|
||||
<div class="text-xl animate-spin">⚙️</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-[var(--text-primary)]">Claude is working...</p>
|
||||
<p class="text-xs text-[var(--text-secondary)]">
|
||||
Writing CONTEXT.md — will auto-detect when complete.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if contextContent !== null}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-green-400">✓ CONTEXT.md captured</span>
|
||||
<button
|
||||
onclick={tryLoadContextFile}
|
||||
class="text-xs text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] transition-colors"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
value={contextContent}
|
||||
readonly
|
||||
rows={8}
|
||||
class="w-full bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-3 font-mono text-xs text-[var(--text-secondary)] resize-y focus:outline-none leading-relaxed"
|
||||
></textarea>
|
||||
</div>
|
||||
{:else if isLoadingContext}
|
||||
<p class="text-xs text-[var(--text-tertiary)]">Checking for CONTEXT.md...</p>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-xs text-[var(--text-tertiary)]">
|
||||
{#if $workflowState.quickMode}
|
||||
Quick mode: your description will be saved directly to CONTEXT.md without
|
||||
discussion.
|
||||
{:else}
|
||||
Claude will analyse your description and write a structured CONTEXT.md with
|
||||
acceptance criteria.
|
||||
{/if}
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
{#if !$workflowState.quickMode}
|
||||
<button
|
||||
onclick={handleStartDiscussion}
|
||||
disabled={!$workflowState.discuss.description.trim()}
|
||||
class="px-4 py-1.5 text-sm btn-trans-gradient rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Start Discussion
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
onclick={handleQuickCaptureContext}
|
||||
disabled={!$workflowState.discuss.description.trim()}
|
||||
class="px-4 py-1.5 text-sm btn-trans-gradient rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Save to CONTEXT.md
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
onclick={tryLoadContextFile}
|
||||
class="px-3 py-1.5 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg transition-colors"
|
||||
>
|
||||
Check for CONTEXT.md
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if $workflowState.currentPhase === 2}
|
||||
<!-- ── Phase 2: Plan ── -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-sm text-[var(--text-secondary)]">
|
||||
Use the PRD Creator to generate your task breakdown, then approve it here to advance.
|
||||
</p>
|
||||
|
||||
<!-- PRD status card -->
|
||||
<div class="p-4 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-color)]">
|
||||
{#if $prdIsLoaded && $prdTasks.length > 0}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-[var(--text-primary)]">
|
||||
{$prdTasks.length} task{$prdTasks.length === 1 ? "" : "s"} ready
|
||||
</p>
|
||||
<p class="text-xs text-[var(--text-tertiary)] mt-0.5">hikari-tasks.json loaded</p>
|
||||
</div>
|
||||
<span
|
||||
class="text-xs px-2 py-0.5 rounded-full bg-green-500/20 text-green-400 border border-green-500/30"
|
||||
>
|
||||
Ready
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-[var(--text-tertiary)]">No tasks generated yet</p>
|
||||
<p class="text-xs text-[var(--text-tertiary)] mt-0.5">
|
||||
Open PRD Creator to generate a task breakdown
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
class="text-xs px-2 py-0.5 rounded-full bg-[var(--bg-tertiary)] text-[var(--text-tertiary)] border border-[var(--border-color)]"
|
||||
>
|
||||
Pending
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
onclick={onOpenPrdPanel}
|
||||
class="px-4 py-1.5 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg transition-colors"
|
||||
>
|
||||
Open PRD Creator →
|
||||
</button>
|
||||
<button
|
||||
onclick={async () => {
|
||||
await prdStore.loadFromFile(workingDirectory);
|
||||
}}
|
||||
class="px-3 py-1.5 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg transition-colors"
|
||||
>
|
||||
Reload Tasks
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if $workflowState.plan.tasksApproved}
|
||||
<div
|
||||
class="flex items-center gap-2 p-3 bg-green-500/10 rounded-lg border border-green-500/20"
|
||||
>
|
||||
<span class="text-green-400 text-sm"
|
||||
>✓ Plan approved — ready to advance to Execute</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if $workflowState.currentPhase === 3}
|
||||
<!-- ── Phase 3: Execute ── -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-sm text-[var(--text-secondary)]">
|
||||
Run your tasks in the Task Loop panel, then mark execution complete here.
|
||||
</p>
|
||||
|
||||
<!-- Task Loop progress -->
|
||||
<div class="p-4 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-color)]">
|
||||
{#if $loopTasks.length > 0}
|
||||
{@const done = completedTaskCount()}
|
||||
{@const failed = failedTaskCount()}
|
||||
{@const total = $loopTasks.length}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm font-medium text-[var(--text-primary)]">
|
||||
{done} / {total} tasks completed
|
||||
</p>
|
||||
{#if $loopStatus !== "idle"}
|
||||
<span
|
||||
class="text-xs px-2 py-0.5 rounded-full bg-blue-500/20 text-blue-400 border border-blue-500/30 capitalize"
|
||||
>
|
||||
{$loopStatus}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Progress bar -->
|
||||
<div class="w-full bg-[var(--bg-primary)] rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
class="h-full bg-[var(--accent-primary)] transition-all duration-300"
|
||||
style="width: {total > 0 ? (done / total) * 100 : 0}%"
|
||||
></div>
|
||||
</div>
|
||||
{#if failed > 0}
|
||||
<p class="text-xs text-red-400">
|
||||
{failed} task{failed === 1 ? "" : "s"} failed
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-[var(--text-tertiary)]">
|
||||
No tasks loaded — open Task Loop to load and run tasks
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
onclick={onOpenTaskLoop}
|
||||
class="px-4 py-1.5 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg transition-colors"
|
||||
>
|
||||
Open Task Loop →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if $workflowState.execute.completed}
|
||||
<div
|
||||
class="flex items-center gap-2 p-3 bg-green-500/10 rounded-lg border border-green-500/20"
|
||||
>
|
||||
<span class="text-green-400 text-sm"
|
||||
>✓ Execution complete — ready to advance to Verify</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if $workflowState.currentPhase === 4}
|
||||
<!-- ── Phase 4: Verify ── -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-sm text-[var(--text-secondary)]">
|
||||
Verify the implementation against your acceptance criteria.
|
||||
</p>
|
||||
|
||||
<!-- Criteria list -->
|
||||
{#if $workflowState.verify.criteria.length > 0}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-xs font-medium text-[var(--text-secondary)]">Acceptance Criteria</p>
|
||||
{#if contextContent !== null}
|
||||
<button
|
||||
onclick={handleExtractCriteria}
|
||||
class="text-xs text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] transition-colors"
|
||||
>
|
||||
Re-extract from CONTEXT.md
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#each $workflowState.verify.criteria as criterion (criterion.id)}
|
||||
<div
|
||||
class="flex items-center gap-2 p-3 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-color)]"
|
||||
>
|
||||
<p class="flex-1 text-sm text-[var(--text-primary)]">{criterion.text}</p>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<button
|
||||
onclick={() => handleSetCriterionStatus(criterion.id, "pass")}
|
||||
class="px-2 py-0.5 text-xs rounded transition-colors {criterion.status ===
|
||||
'pass'
|
||||
? 'bg-green-500/20 text-green-400 border border-green-500/30'
|
||||
: 'text-[var(--text-tertiary)] hover:text-green-400'}"
|
||||
>
|
||||
Pass
|
||||
</button>
|
||||
<button
|
||||
onclick={() => handleSetCriterionStatus(criterion.id, "partial")}
|
||||
class="px-2 py-0.5 text-xs rounded transition-colors {criterion.status ===
|
||||
'partial'
|
||||
? 'bg-amber-500/20 text-amber-400 border border-amber-500/30'
|
||||
: 'text-[var(--text-tertiary)] hover:text-amber-400'}"
|
||||
>
|
||||
Partial
|
||||
</button>
|
||||
<button
|
||||
onclick={() => handleSetCriterionStatus(criterion.id, "fail")}
|
||||
class="px-2 py-0.5 text-xs rounded transition-colors {criterion.status ===
|
||||
'fail'
|
||||
? 'bg-red-500/20 text-red-400 border border-red-500/30'
|
||||
: 'text-[var(--text-tertiary)] hover:text-red-400'}"
|
||||
>
|
||||
Fail
|
||||
</button>
|
||||
<button
|
||||
onclick={() => handleRemoveCriterion(criterion.id)}
|
||||
class="p-0.5 text-[var(--text-tertiary)] hover:text-red-400 transition-colors ml-1"
|
||||
aria-label="Remove criterion"
|
||||
>
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-col gap-2 p-4 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-color)]"
|
||||
>
|
||||
<p class="text-sm text-[var(--text-tertiary)]">No criteria yet.</p>
|
||||
{#if contextContent !== null}
|
||||
<button
|
||||
onclick={handleExtractCriteria}
|
||||
class="self-start text-xs px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] bg-[var(--bg-primary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg transition-colors"
|
||||
>
|
||||
Extract from CONTEXT.md
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Add criterion -->
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newCriterionText}
|
||||
onkeydown={(e) => e.key === "Enter" && handleAddCriterion()}
|
||||
class="flex-1 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg px-3 py-1.5 text-sm text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent-primary)]"
|
||||
placeholder="Add criterion..."
|
||||
/>
|
||||
<button
|
||||
onclick={handleAddCriterion}
|
||||
disabled={!newCriterionText.trim()}
|
||||
class="px-3 py-1.5 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Verification actions -->
|
||||
{#if isWaitingForVerify}
|
||||
<div
|
||||
class="flex items-center gap-3 p-4 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-color)]"
|
||||
>
|
||||
<div class="text-xl animate-spin">⚙️</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-[var(--text-primary)]">Claude is verifying...</p>
|
||||
<p class="text-xs text-[var(--text-secondary)]">
|
||||
Writing VERIFY.md — will auto-detect when complete.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if verifyContent !== null}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-green-400">✓ VERIFY.md generated</span>
|
||||
<button
|
||||
onclick={tryLoadVerifyFile}
|
||||
class="text-xs text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] transition-colors"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
value={verifyContent}
|
||||
readonly
|
||||
rows={8}
|
||||
class="w-full bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-3 font-mono text-xs text-[var(--text-secondary)] resize-y focus:outline-none leading-relaxed"
|
||||
></textarea>
|
||||
</div>
|
||||
{:else if isLoadingVerify}
|
||||
<p class="text-xs text-[var(--text-tertiary)]">Checking for VERIFY.md...</p>
|
||||
{:else}
|
||||
<div class="flex gap-2">
|
||||
{#if !$workflowState.quickMode}
|
||||
<button
|
||||
onclick={handleRunVerification}
|
||||
class="px-4 py-1.5 text-sm btn-trans-gradient rounded-lg transition-colors"
|
||||
>
|
||||
Run Verification
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
onclick={tryLoadVerifyFile}
|
||||
class="px-3 py-1.5 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg transition-colors"
|
||||
>
|
||||
Check for VERIFY.md
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $workflowState.verify.verificationComplete}
|
||||
<div
|
||||
class="flex items-center gap-2 p-3 bg-green-500/10 rounded-lg border border-green-500/20"
|
||||
>
|
||||
<span class="text-green-400 text-sm">✓ Verification complete</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Footer navigation -->
|
||||
<div
|
||||
class="flex items-center justify-between p-4 pt-2 border-t border-[var(--border-color)] gap-3"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onclick={handleBack}
|
||||
disabled={!canGoBack($workflowState.currentPhase)}
|
||||
class="px-3 py-1.5 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
<button
|
||||
onclick={handleReset}
|
||||
class="px-3 py-1.5 text-sm text-[var(--text-secondary)] hover:text-red-400 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg transition-colors"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Phase-specific primary action -->
|
||||
{#if $workflowState.currentPhase === 2 && !$workflowState.plan.tasksApproved}
|
||||
<button
|
||||
onclick={handleApprovePlan}
|
||||
disabled={!$workflowState.quickMode && (!$prdIsLoaded || $prdTasks.length === 0)}
|
||||
class="px-4 py-1.5 text-sm btn-trans-gradient rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Approve Plan
|
||||
</button>
|
||||
{:else if $workflowState.currentPhase === 3 && !$workflowState.execute.completed}
|
||||
<button
|
||||
onclick={handleCompleteExecution}
|
||||
class="px-4 py-1.5 text-sm btn-trans-gradient rounded-lg transition-colors"
|
||||
>
|
||||
{allTasksDone() ? "Mark Complete ✓" : "Mark Complete (Override)"}
|
||||
</button>
|
||||
{:else if $workflowState.currentPhase === 4 && !$workflowState.verify.verificationComplete && $workflowState.quickMode}
|
||||
<button
|
||||
onclick={() => {
|
||||
workflowStore.completeVerification("Manual verification complete.");
|
||||
void workflowStore.saveState(workingDirectory);
|
||||
}}
|
||||
class="px-4 py-1.5 text-sm btn-trans-gradient rounded-lg transition-colors"
|
||||
>
|
||||
Complete Workflow
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Advance / Close -->
|
||||
<button
|
||||
onclick={handleAdvance}
|
||||
disabled={!canAdvancePhase($workflowState)}
|
||||
class="px-4 py-1.5 text-sm btn-trans-gradient rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{$workflowState.currentPhase === 4 ? "Close" : "Next →"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
[role="dialog"] {
|
||||
animation: slideIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border-color) transparent;
|
||||
}
|
||||
|
||||
textarea::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
textarea::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
textarea::-webkit-scrollbar-thumb {
|
||||
background-color: var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
textarea::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--accent-primary);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user