generated from nhcarrigan/template
198 lines
5.8 KiB
TypeScript
198 lines
5.8 KiB
TypeScript
import { writable, get } from "svelte/store";
|
||
import { invoke } from "@tauri-apps/api/core";
|
||
|
||
export interface PrdTask {
|
||
id: string;
|
||
title: string;
|
||
prompt: string;
|
||
priority: "high" | "medium" | "low";
|
||
dependsOn?: string[];
|
||
}
|
||
|
||
export interface PrdFile {
|
||
version: 1;
|
||
goal: string;
|
||
tasks: PrdTask[];
|
||
}
|
||
|
||
export const PRD_FILENAME = "hikari-tasks.json";
|
||
|
||
function buildPrdPrompt(userGoal: string, workingDirectory: string): string {
|
||
return `Please create a PRD task breakdown for the following goal and write it as \`hikari-tasks.json\` in the working directory.
|
||
|
||
Goal: ${userGoal}
|
||
|
||
Write the file to \`${workingDirectory}/hikari-tasks.json\` containing valid JSON in this exact format:
|
||
\`\`\`json
|
||
{
|
||
"version": 1,
|
||
"goal": "<the goal>",
|
||
"tasks": [
|
||
{
|
||
"id": "task-1",
|
||
"title": "<short descriptive title>",
|
||
"prompt": "<detailed prompt that Claude Code can execute to complete this task>",
|
||
"priority": "<high|medium|low>",
|
||
"dependsOn": []
|
||
}
|
||
]
|
||
}
|
||
\`\`\`
|
||
|
||
Guidelines:
|
||
- Break the goal into 3–10 concrete, actionable tasks
|
||
- Each task should be completable in a single Claude Code session
|
||
- Prompts should be specific and actionable, not vague
|
||
- Order tasks logically (dependencies first)
|
||
- Assign priority: high for critical path, medium for features, low for polish/cleanup
|
||
- Fill in \`dependsOn\` with IDs of tasks that must complete before this one (use \`[]\` if none)
|
||
- Write only the JSON file — no explanations needed`;
|
||
}
|
||
|
||
function createPrdStore() {
|
||
const goal = writable<string>("");
|
||
const tasks = writable<PrdTask[]>([]);
|
||
const isGenerating = writable<boolean>(false);
|
||
const isLoaded = writable<boolean>(false);
|
||
const isLoading = writable<boolean>(false);
|
||
const isSaving = writable<boolean>(false);
|
||
let idCounter = 0;
|
||
|
||
async function loadFromFile(workingDirectory: string): Promise<void> {
|
||
isLoading.set(true);
|
||
try {
|
||
const path = `${workingDirectory}/${PRD_FILENAME}`;
|
||
const content = await invoke<string>("read_file_content", { path });
|
||
const data = JSON.parse(content) as PrdFile;
|
||
goal.set(data.goal);
|
||
tasks.set(data.tasks);
|
||
isLoaded.set(true);
|
||
} catch (error) {
|
||
console.error("Failed to load PRD file:", error);
|
||
isLoaded.set(false);
|
||
} finally {
|
||
isLoading.set(false);
|
||
}
|
||
}
|
||
|
||
async function saveToFile(workingDirectory: string): Promise<boolean> {
|
||
isSaving.set(true);
|
||
try {
|
||
const path = `${workingDirectory}/${PRD_FILENAME}`;
|
||
const data: PrdFile = {
|
||
version: 1,
|
||
goal: get(goal),
|
||
tasks: get(tasks),
|
||
};
|
||
await invoke("write_file_content", { path, content: JSON.stringify(data, null, 2) });
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Failed to save PRD file:", error);
|
||
return false;
|
||
} finally {
|
||
isSaving.set(false);
|
||
}
|
||
}
|
||
|
||
async function generatePrd(
|
||
userGoal: string,
|
||
workingDirectory: string,
|
||
conversationId: string
|
||
): Promise<void> {
|
||
isGenerating.set(true);
|
||
goal.set(userGoal);
|
||
try {
|
||
const prompt = buildPrdPrompt(userGoal, workingDirectory);
|
||
await invoke("send_prompt", { conversationId, message: prompt });
|
||
} catch (error) {
|
||
console.error("Failed to generate PRD:", error);
|
||
isGenerating.set(false);
|
||
}
|
||
}
|
||
|
||
function finishGenerating(): void {
|
||
isGenerating.set(false);
|
||
}
|
||
|
||
async function executePrd(workingDirectory: string, conversationId: string): Promise<void> {
|
||
await saveToFile(workingDirectory);
|
||
const currentTasks = get(tasks);
|
||
const currentGoal = get(goal);
|
||
const taskList = currentTasks
|
||
.map((t, i) => `${i + 1}. [${t.priority}] **${t.title}**\n ${t.prompt}`)
|
||
.join("\n\n");
|
||
const prompt = `Please execute the following task list for the goal: "${currentGoal}"
|
||
|
||
Work through each task in order, completing it fully before moving to the next:
|
||
|
||
${taskList}
|
||
|
||
The task list is also saved in \`${workingDirectory}/hikari-tasks.json\` for reference.`;
|
||
await invoke("send_prompt", { conversationId, message: prompt });
|
||
}
|
||
|
||
function addTask(task: Omit<PrdTask, "id">): void {
|
||
idCounter += 1;
|
||
const id = `task-${idCounter}`;
|
||
tasks.update((current) => [...current, { ...task, id }]);
|
||
}
|
||
|
||
function updateTask(id: string, changes: Partial<Omit<PrdTask, "id">>): void {
|
||
tasks.update((current) => current.map((t) => (t.id === id ? { ...t, ...changes } : t)));
|
||
}
|
||
|
||
function removeTask(id: string): void {
|
||
tasks.update((current) => current.filter((t) => t.id !== id));
|
||
}
|
||
|
||
function moveTaskUp(id: string): void {
|
||
tasks.update((current) => {
|
||
const index = current.findIndex((t) => t.id === id);
|
||
if (index <= 0) return current;
|
||
const result = [...current];
|
||
[result[index - 1], result[index]] = [result[index], result[index - 1]];
|
||
return result;
|
||
});
|
||
}
|
||
|
||
function moveTaskDown(id: string): void {
|
||
tasks.update((current) => {
|
||
const index = current.findIndex((t) => t.id === id);
|
||
if (index < 0 || index >= current.length - 1) return current;
|
||
const result = [...current];
|
||
[result[index], result[index + 1]] = [result[index + 1], result[index]];
|
||
return result;
|
||
});
|
||
}
|
||
|
||
function reset(): void {
|
||
goal.set("");
|
||
tasks.set([]);
|
||
isLoaded.set(false);
|
||
isGenerating.set(false);
|
||
idCounter = 0;
|
||
}
|
||
|
||
return {
|
||
goal: { subscribe: goal.subscribe },
|
||
tasks: { subscribe: tasks.subscribe },
|
||
isGenerating: { subscribe: isGenerating.subscribe },
|
||
isLoaded: { subscribe: isLoaded.subscribe },
|
||
isLoading: { subscribe: isLoading.subscribe },
|
||
isSaving: { subscribe: isSaving.subscribe },
|
||
loadFromFile,
|
||
saveToFile,
|
||
generatePrd,
|
||
finishGenerating,
|
||
executePrd,
|
||
addTask,
|
||
updateTask,
|
||
removeTask,
|
||
moveTaskUp,
|
||
moveTaskDown,
|
||
reset,
|
||
};
|
||
}
|
||
|
||
export const prdStore = createPrdStore();
|