3e237e177a
* feat: add code-walkthrough backend (MCP tool, service, persistence, WS events) * feat: render agent code walkthroughs as line-anchored cards + editor window * test: add code-walkthrough re-emit mock-agent scenario * fix: rework code-walkthrough UX (floating card, line reveal, minimize, clickable file) * fix: improve code walkthrough editor UX * fix: backfill walkthrough launcher after websocket connect * fix: improve walkthrough requests and editor anchors * fix: clear walkthrough highlights on close * feat: add walkthrough discard and prompt customization * fix: send walkthrough requests through saved prompt * fix: support postgres built-in prompt seeding * fix: address walkthrough review feedback * fix: address walkthrough review follow-ups * fix: harden walkthrough lifecycle races * fix: reuse walkthrough notification cleanup * fix: guard stale walkthrough file opens * fix: abort stale file opens before paint * fix: stabilize github settings e2e readiness * fix: use stable github settings e2e selector
121 lines
3.4 KiB
Go
121 lines
3.4 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
)
|
|
|
|
// walkthroughAuthorAgent matches the task_walkthroughs.created_by column
|
|
// DEFAULT — walkthroughs are always agent-authored.
|
|
const walkthroughAuthorAgent = "agent"
|
|
|
|
// CreateTaskWalkthrough upserts a walkthrough for a task (one row per task,
|
|
// keyed by task_id). Steps are serialized to a JSON column.
|
|
func (r *Repository) CreateTaskWalkthrough(ctx context.Context, wt *models.TaskWalkthrough) error {
|
|
if wt.ID == "" {
|
|
wt.ID = uuid.New().String()
|
|
}
|
|
now := time.Now().UTC()
|
|
if wt.CreatedAt.IsZero() {
|
|
wt.CreatedAt = now
|
|
}
|
|
wt.UpdatedAt = now
|
|
if wt.Title == "" {
|
|
wt.Title = "Walkthrough"
|
|
}
|
|
if wt.CreatedBy == "" {
|
|
wt.CreatedBy = walkthroughAuthorAgent
|
|
}
|
|
|
|
stepsJSON, err := marshalSteps(wt.Steps)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = r.db.QueryRowContext(ctx, r.db.Rebind(`
|
|
INSERT INTO task_walkthroughs (id, task_id, title, steps, created_by, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(task_id) DO UPDATE SET
|
|
title = excluded.title,
|
|
steps = excluded.steps,
|
|
created_by = excluded.created_by,
|
|
updated_at = excluded.updated_at
|
|
RETURNING id, created_at, updated_at
|
|
`), wt.ID, wt.TaskID, wt.Title, stepsJSON, wt.CreatedBy, wt.CreatedAt, wt.UpdatedAt).Scan(
|
|
&wt.ID,
|
|
&wt.CreatedAt,
|
|
&wt.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to upsert task walkthrough: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetTaskWalkthrough retrieves a walkthrough by task ID. Returns nil, nil when
|
|
// none exists.
|
|
func (r *Repository) GetTaskWalkthrough(ctx context.Context, taskID string) (*models.TaskWalkthrough, error) {
|
|
wt := &models.TaskWalkthrough{}
|
|
var stepsJSON string
|
|
err := r.ro.QueryRowContext(ctx, r.ro.Rebind(`
|
|
SELECT id, task_id, title, steps, created_by, created_at, updated_at
|
|
FROM task_walkthroughs WHERE task_id = ?
|
|
`), taskID).Scan(&wt.ID, &wt.TaskID, &wt.Title, &stepsJSON, &wt.CreatedBy, &wt.CreatedAt, &wt.UpdatedAt)
|
|
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get task walkthrough: %w", err)
|
|
}
|
|
steps, err := unmarshalSteps(stepsJSON)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wt.Steps = steps
|
|
return wt, nil
|
|
}
|
|
|
|
// DeleteTaskWalkthrough deletes a walkthrough by task ID.
|
|
func (r *Repository) DeleteTaskWalkthrough(ctx context.Context, taskID string) error {
|
|
result, err := r.db.ExecContext(ctx, r.db.Rebind(`DELETE FROM task_walkthroughs WHERE task_id = ?`), taskID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete task walkthrough: %w", err)
|
|
}
|
|
rows, _ := result.RowsAffected()
|
|
if rows == 0 {
|
|
return fmt.Errorf("%w for task: %s", models.ErrTaskWalkthroughNotFound, taskID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// marshalSteps serializes steps to a JSON array, never returning the "null"
|
|
// literal for an empty/nil slice (the column DEFAULT is '[]').
|
|
func marshalSteps(steps []models.WalkthroughStep) (string, error) {
|
|
if len(steps) == 0 {
|
|
return "[]", nil
|
|
}
|
|
b, err := json.Marshal(steps)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to marshal walkthrough steps: %w", err)
|
|
}
|
|
return string(b), nil
|
|
}
|
|
|
|
func unmarshalSteps(raw string) ([]models.WalkthroughStep, error) {
|
|
if raw == "" {
|
|
return nil, nil
|
|
}
|
|
var steps []models.WalkthroughStep
|
|
if err := json.Unmarshal([]byte(raw), &steps); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal walkthrough steps: %w", err)
|
|
}
|
|
return steps, nil
|
|
}
|