generated from nhcarrigan/template
834 lines
32 KiB
Svelte
834 lines
32 KiB
Svelte
<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>
|