generated from nhcarrigan/template
feat: prd creator panel with hikari-tasks.json format and distinct icon
Adds a PRD Creator panel accessible from the status bar that lets Naomi describe a goal and have Claude break it down into actionable tasks written to hikari-tasks.json. Tasks can be reviewed, edited, reordered, and executed directly from the panel. Uses the Lucide ScrollText icon to distinguish it visually from the Todo List clipboard icon. Also adds CODEBASE.md to the repo and gitignores hikari-tasks.json as a user-generated data file.
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
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";
|
||||
}
|
||||
|
||||
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>"
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
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
|
||||
- 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();
|
||||
Reference in New Issue
Block a user