Files
hikari-desktop/src/lib/stores/prd.ts
T

198 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 310 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();