From e33f86cc292f87bac7c54caff92989d7ec46d3ad Mon Sep 17 00:00:00 2001 From: Jon Fox Date: Sat, 9 May 2026 20:53:24 -0700 Subject: [PATCH 1/2] Add artifact folders and web sorting --- README.md | 45 +++++ artifact/add.go | 13 ++ artifact/filter.go | 12 +- artifact/manifest.go | 44 +++++ artifact/manifest_test.go | 105 +++++++++++ cmd/artifact_add.go | 5 +- cmd/artifact_list.go | 58 ++++++- cmd/artifact_test.go | 117 +++++++++++++ docs/artifacts.md | 104 +++++++++++ web/app/src/lib/Terminal.svelte | 2 +- web/app/src/lib/artifacts/ArtifactPane.svelte | 89 +++++++++- web/artifacts_api.go | 6 +- web/artifacts_api_test.go | 25 +++ web/dist/assets/index-C-eGZceh.js | 163 ------------------ web/dist/assets/index-Cb9dmfi2.css | 1 + web/dist/assets/index-CkFD9JOf.js | 163 ++++++++++++++++++ web/dist/assets/index-D3TGCEBb.css | 1 - web/dist/index.html | 4 +- 18 files changed, 779 insertions(+), 178 deletions(-) create mode 100644 docs/artifacts.md delete mode 100644 web/dist/assets/index-C-eGZceh.js create mode 100644 web/dist/assets/index-Cb9dmfi2.css create mode 100644 web/dist/assets/index-CkFD9JOf.js delete mode 100644 web/dist/assets/index-D3TGCEBb.css diff --git a/README.md b/README.md index b326169..3c51554 100644 --- a/README.md +++ b/README.md @@ -711,6 +711,7 @@ devx includes an optional web interface for managing sessions from any browser - **Session list** — view all sessions grouped by project, with attention flag indicators (◆) - **In-browser terminal** — full tmux access via ttyd, with tmux window tabs in the header - **Mobile-friendly** — responsive layout, soft key toolbar (Tab, Ctrl, arrows) for touchscreens +- **Artifacts** — attach plans, reports, screenshots, logs, recordings, and docs to a session; view them next to the terminal and organize them into folders - **Image upload** — paste, drag-and-drop, or use the `[img]` button to inject an image path into the terminal - **Create & delete sessions** — from the browser, same as the CLI - **Service links** — tap "svc" on any session to open its Caddy routes in the browser @@ -745,6 +746,50 @@ devx web stop # stop daemon > **Auto-start:** Set `web_autostart: true` to have the web daemon start automatically when you open the TUI. +### Session artifacts + +Artifacts are files stored under a session worktree's `.artifacts/` directory and indexed by `.artifacts/manifest.json`. They are useful for agent-generated plans, reviews, screenshots, QA reports, logs, diffs, and proof-of-work reports. + +```bash +# Add a normal flat artifact +devx artifact add ./report.md \ + --title "Completion report" \ + --type report \ + --agent pi + +# Group related workflow outputs under a safe relative folder path +devx artifact add ./10-plan.md \ + --title "Implementation plan" \ + --type plan \ + --folder workflow/run-123 \ + --file 10-plan.md + +# List all artifacts, filter one folder, or print a grouped tree +devx artifact list +devx artifact list --folder workflow/run-123 +devx artifact list --tree + +# Print web, local, or same-folder/embed URLs +devx artifact url +devx artifact url --local +devx artifact url --embed +``` + +When `--folder` is supplied, DevX writes the file under `.artifacts//` and records both the full manifest `file` path and the artifact `folder`. Folder paths must be safe relative paths: no absolute paths, `..`, or empty segments such as `workflow//run`. + +Example workflow layout: + +```text +.artifacts/ + workflow/run-123/00-office-hours.md + workflow/run-123/10-plan.md + workflow/run-123/20-review.md + workflow/run-123/30-qa/results.log + workflow/run-123/40-proof-of-work.html +``` + +DevX Web shows artifact folders as groups and keeps artifacts without a folder in **Unfiled**. See [docs/artifacts.md](docs/artifacts.md) for more examples. + ### External domain routing (optional) To access devx services from your phone via a custom domain: diff --git a/artifact/add.go b/artifact/add.go index 189cb0b..cdb761b 100644 --- a/artifact/add.go +++ b/artifact/add.go @@ -15,6 +15,7 @@ type AddOptions struct { Source string Reader io.Reader Destination string + Folder string ID string Type string Title string @@ -72,6 +73,14 @@ func addLocked(sess *session.Session, opts AddOptions) (Artifact, error) { if strings.TrimSpace(id) == "" || strings.ContainsAny(id, `/\`) { return Artifact{}, fmt.Errorf("invalid artifact id %q", id) } + folder := "" + if strings.TrimSpace(opts.Folder) != "" { + var err error + folder, err = NormalizeFolderPath(opts.Folder) + if err != nil { + return Artifact{}, fmt.Errorf("invalid folder: %w", err) + } + } destRel := opts.Destination if destRel == "" { destRel = DefaultDestination(artifactType, opts.Source) @@ -79,6 +88,9 @@ func addLocked(sess *session.Session, opts AddOptions) (Artifact, error) { if err := ValidateRelativePath(destRel); err != nil { return Artifact{}, fmt.Errorf("invalid destination: %w", err) } + if folder != "" { + destRel = filepath.Join(folder, destRel) + } if isReservedArtifactPath(destRel) { return Artifact{}, fmt.Errorf("destination %q is reserved", destRel) } @@ -120,6 +132,7 @@ func addLocked(sess *session.Session, opts AddOptions) (Artifact, error) { Type: artifactType, Title: opts.Title, File: filepath.ToSlash(finalRel), + Folder: folder, Created: now.UTC(), Agent: agent, Retention: retention, diff --git a/artifact/filter.go b/artifact/filter.go index 0cb4417..5bc5cae 100644 --- a/artifact/filter.go +++ b/artifact/filter.go @@ -7,10 +7,17 @@ type FilterOptions struct { Tag string Agent string Search string + Folder string } func Filter(artifacts []Artifact, opts FilterOptions) []Artifact { search := strings.ToLower(strings.TrimSpace(opts.Search)) + folder := strings.TrimSpace(opts.Folder) + if folder != "" { + if normalized, err := NormalizeFolderPath(folder); err == nil { + folder = normalized + } + } out := make([]Artifact, 0, len(artifacts)) for _, a := range artifacts { if opts.Type != "" && a.Type != opts.Type { @@ -19,6 +26,9 @@ func Filter(artifacts []Artifact, opts FilterOptions) []Artifact { if opts.Agent != "" && a.Agent != opts.Agent { continue } + if folder != "" && a.Folder != folder { + continue + } if opts.Tag != "" && !hasTag(a, opts.Tag) { continue } @@ -41,7 +51,7 @@ func hasTag(a Artifact, tag string) bool { } func matchesSearch(a Artifact, search string) bool { - fields := []string{a.ID, a.Type, a.Title, a.File, a.Agent, a.Retention} + fields := []string{a.ID, a.Type, a.Title, a.File, a.Folder, a.Agent, a.Retention} if a.Summary != nil { fields = append(fields, *a.Summary) } diff --git a/artifact/manifest.go b/artifact/manifest.go index 544c280..f5ddb88 100644 --- a/artifact/manifest.go +++ b/artifact/manifest.go @@ -8,6 +8,7 @@ import ( "sort" "strings" "time" + "unicode" "github.com/jfox85/devx/session" ) @@ -50,6 +51,7 @@ type Artifact struct { Type string `json:"type"` Title string `json:"title"` File string `json:"file"` + Folder string `json:"folder,omitempty"` Created time.Time `json:"created"` Agent string `json:"agent,omitempty"` Retention string `json:"retention,omitempty"` @@ -179,6 +181,16 @@ func ValidateArtifact(a Artifact) error { if err := ValidateRelativePath(a.File); err != nil { return fmt.Errorf("invalid artifact file %q: %w", a.File, err) } + if strings.TrimSpace(a.Folder) != "" { + folder, err := NormalizeFolderPath(a.Folder) + if err != nil { + return fmt.Errorf("invalid artifact folder %q: %w", a.Folder, err) + } + file := filepath.ToSlash(filepath.Clean(a.File)) + if file != folder && !strings.HasPrefix(file, folder+"/") { + return fmt.Errorf("artifact file %q is not under folder %q", a.File, folder) + } + } if a.Created.IsZero() { return fmt.Errorf("artifact created time is required") } @@ -225,6 +237,38 @@ func ValidateRelativePath(p string) error { return nil } +func NormalizeFolderPath(folder string) (string, error) { + folder = strings.TrimSpace(folder) + if folder == "" { + return "", fmt.Errorf("folder is empty") + } + if filepath.IsAbs(folder) || hasWindowsVolumeName(folder) { + return "", fmt.Errorf("absolute paths are not allowed") + } + slash := strings.ReplaceAll(folder, "\\", "/") + if strings.HasPrefix(slash, "/") { + return "", fmt.Errorf("absolute paths are not allowed") + } + parts := strings.Split(slash, "/") + for _, part := range parts { + if part == "" { + return "", fmt.Errorf("empty path segments are not allowed") + } + if part == "." || part == ".." { + return "", fmt.Errorf("path traversal is not allowed") + } + } + clean := filepath.ToSlash(filepath.Clean(slash)) + if clean == "." || clean == ".." || strings.HasPrefix(clean, "../") { + return "", fmt.Errorf("path traversal is not allowed") + } + return clean, nil +} + +func hasWindowsVolumeName(p string) bool { + return len(p) >= 2 && p[1] == ':' && unicode.IsLetter(rune(p[0])) +} + func SafeJoin(base, rel string) (string, error) { if err := ValidateRelativePath(rel); err != nil { return "", err diff --git a/artifact/manifest_test.go b/artifact/manifest_test.go index e462a8c..ae45a9a 100644 --- a/artifact/manifest_test.go +++ b/artifact/manifest_test.go @@ -71,6 +71,52 @@ func TestValidateRelativePathRejectsTraversal(t *testing.T) { } } +func TestValidateFolderPathRejectsUnsafeFolders(t *testing.T) { + bad := []string{"", "../secret", "a/../secret", "/tmp/file", "..", "workflow//run", "workflow/", "./workflow", "workflow/./run", `C:\tmp\file`} + for _, p := range bad { + if _, err := NormalizeFolderPath(p); err == nil { + t.Fatalf("expected %q to be rejected", p) + } + } + got, err := NormalizeFolderPath(`workflow\run-1\qa`) + if err != nil { + t.Fatalf("expected safe folder: %v", err) + } + if got != "workflow/run-1/qa" { + t.Fatalf("normalized folder = %q", got) + } +} + +func TestLoadManifestMissingFolderIsBackwardCompatible(t *testing.T) { + sess := testSession(t) + if err := os.MkdirAll(DirForSession(sess), 0o755); err != nil { + t.Fatal(err) + } + data := []byte(`{ + "version": 1, + "session": "feature/test", + "artifacts": [ + { + "id": "doc-old", + "type": "document", + "title": "Old Doc", + "file": "old.md", + "created": "2026-04-25T10:30:00Z" + } + ] +}`) + if err := os.WriteFile(ManifestPath(sess), data, 0o644); err != nil { + t.Fatal(err) + } + m, err := LoadManifest(sess) + if err != nil { + t.Fatalf("LoadManifest: %v", err) + } + if len(m.Artifacts) != 1 || m.Artifacts[0].Folder != "" || m.Artifacts[0].File != "old.md" { + t.Fatalf("unexpected manifest: %#v", m.Artifacts) + } +} + func TestSafeJoinStaysInsideBase(t *testing.T) { base := t.TempDir() joined, err := SafeJoin(base, "logs/test.log") @@ -136,6 +182,50 @@ func TestAddCreatesManifestFileAndTheme(t *testing.T) { } } +func TestAddCreatesArtifactInNestedFolder(t *testing.T) { + sess := testSession(t) + source := filepath.Join(t.TempDir(), "plan.md") + if err := os.WriteFile(source, []byte("# Plan"), 0o644); err != nil { + t.Fatal(err) + } + a, err := Add(sess, AddOptions{Source: source, Type: "document", Title: "Plan", Folder: "workflow/run-123", Destination: "10-plan.md"}) + if err != nil { + t.Fatalf("Add: %v", err) + } + if a.Folder != "workflow/run-123" || a.File != "workflow/run-123/10-plan.md" { + t.Fatalf("unexpected artifact paths: %#v", a) + } + if _, err := os.Stat(filepath.Join(DirForSession(sess), "workflow", "run-123", "10-plan.md")); err != nil { + t.Fatalf("nested artifact file missing: %v", err) + } + m, err := LoadManifest(sess) + if err != nil { + t.Fatal(err) + } + if len(m.Artifacts) != 1 || m.Artifacts[0].Folder != "workflow/run-123" || m.Artifacts[0].File != "workflow/run-123/10-plan.md" { + t.Fatalf("manifest did not persist folder: %#v", m.Artifacts) + } +} + +func TestAddNestedFolderAvoidsCollisions(t *testing.T) { + sess := testSession(t) + source := filepath.Join(t.TempDir(), "plan.md") + if err := os.WriteFile(source, []byte("# Plan"), 0o644); err != nil { + t.Fatal(err) + } + first, err := Add(sess, AddOptions{Source: source, Type: "document", Title: "Plan A", Folder: "workflow/run", Destination: "plan.md"}) + if err != nil { + t.Fatal(err) + } + second, err := Add(sess, AddOptions{Source: source, Type: "document", Title: "Plan B", Folder: "workflow/run", Destination: "plan.md"}) + if err != nil { + t.Fatal(err) + } + if first.File != "workflow/run/plan.md" || second.File != "workflow/run/plan-2.md" { + t.Fatalf("unexpected collision paths: %q %q", first.File, second.File) + } +} + func TestSecureExistingPathRejectsSymlink(t *testing.T) { base := t.TempDir() outside := filepath.Join(t.TempDir(), "secret.txt") @@ -220,6 +310,21 @@ func TestAddRejectsUnsafeDestination(t *testing.T) { } } +func TestAddRejectsUnsafeFolder(t *testing.T) { + sess := testSession(t) + source := filepath.Join(t.TempDir(), "plan.html") + if err := os.WriteFile(source, []byte("x"), 0o644); err != nil { + t.Fatal(err) + } + _, err := Add(sess, AddOptions{Source: source, Type: "plan", Title: "Plan", Folder: "workflow/../secret", Destination: "plan.html"}) + if err == nil { + t.Fatal("expected unsafe folder error") + } + if _, err := os.Lstat(filepath.Join(DirForSession(sess), "secret", "plan.html")); !os.IsNotExist(err) { + t.Fatalf("unsafe folder wrote a file or stat failed unexpectedly: %v", err) + } +} + func TestRemovePreservesAssetThatIsStandaloneArtifact(t *testing.T) { sess := testSession(t) assetSource := filepath.Join(t.TempDir(), "image.png") diff --git a/cmd/artifact_add.go b/cmd/artifact_add.go index 7615d4d..60d139d 100644 --- a/cmd/artifact_add.go +++ b/cmd/artifact_add.go @@ -23,6 +23,7 @@ var artifactAddFlags struct { focus bool id string file string + folder string } var artifactAddCmd = &cobra.Command{ @@ -43,7 +44,8 @@ func init() { artifactAddCmd.Flags().StringVar(&artifactAddFlags.tags, "tags", "", "Comma-separated tags") artifactAddCmd.Flags().BoolVar(&artifactAddFlags.focus, "focus", false, "Flag the session for attention and auto-open this artifact") artifactAddCmd.Flags().StringVar(&artifactAddFlags.id, "id", "", "Custom artifact ID") - artifactAddCmd.Flags().StringVar(&artifactAddFlags.file, "file", "", "Destination path under .artifacts/ (required when reading from stdin)") + artifactAddCmd.Flags().StringVar(&artifactAddFlags.file, "file", "", "Destination path under .artifacts/ (required when reading from stdin; relative to --folder when supplied)") + artifactAddCmd.Flags().StringVar(&artifactAddFlags.folder, "folder", "", "Optional artifact folder/group path under .artifacts/") } func runArtifactAdd(cmd *cobra.Command, args []string) error { @@ -66,6 +68,7 @@ func runArtifactAdd(cmd *cobra.Command, args []string) error { opts := artifactpkg.AddOptions{ Source: source, Destination: artifactAddFlags.file, + Folder: artifactAddFlags.folder, ID: artifactAddFlags.id, Type: artifactAddFlags.artifactType, Title: artifactAddFlags.title, diff --git a/cmd/artifact_list.go b/cmd/artifact_list.go index 0ed7aab..2d4a3fd 100644 --- a/cmd/artifact_list.go +++ b/cmd/artifact_list.go @@ -3,6 +3,8 @@ package cmd import ( "encoding/json" "fmt" + "io" + "sort" "text/tabwriter" artifactpkg "github.com/jfox85/devx/artifact" @@ -14,7 +16,9 @@ var artifactListFlags struct { tag string agent string search string + folder string json bool + tree bool } var artifactListCmd = &cobra.Command{ @@ -31,7 +35,9 @@ func init() { artifactListCmd.Flags().StringVar(&artifactListFlags.tag, "tag", "", "Filter by tag") artifactListCmd.Flags().StringVar(&artifactListFlags.agent, "agent", "", "Filter by agent") artifactListCmd.Flags().StringVar(&artifactListFlags.search, "search", "", "Search title, file, summary, tags, and ID") + artifactListCmd.Flags().StringVar(&artifactListFlags.folder, "folder", "", "Filter by artifact folder/group path") artifactListCmd.Flags().BoolVar(&artifactListFlags.json, "json", false, "Output artifacts as JSON") + artifactListCmd.Flags().BoolVar(&artifactListFlags.tree, "tree", false, "Print artifacts grouped by folder") } func runArtifactList(cmd *cobra.Command, args []string) error { @@ -43,13 +49,24 @@ func runArtifactList(cmd *cobra.Command, args []string) error { if err != nil { return err } - items := artifactpkg.Filter(manifest.Artifacts, artifactpkg.FilterOptions{Type: artifactListFlags.artifactType, Tag: artifactListFlags.tag, Agent: artifactListFlags.agent, Search: artifactListFlags.search}) + folder := artifactListFlags.folder + if folder != "" { + var err error + folder, err = artifactpkg.NormalizeFolderPath(folder) + if err != nil { + return fmt.Errorf("invalid --folder: %w", err) + } + } + items := artifactpkg.Filter(manifest.Artifacts, artifactpkg.FilterOptions{Type: artifactListFlags.artifactType, Tag: artifactListFlags.tag, Agent: artifactListFlags.agent, Search: artifactListFlags.search, Folder: folder}) computed := artifactpkg.WithComputedFields(sess.Name, items) if artifactListFlags.json { enc := json.NewEncoder(cmd.OutOrStdout()) enc.SetIndent("", " ") return enc.Encode(computed) } + if artifactListFlags.tree { + return printArtifactTree(cmd.OutOrStdout(), computed) + } w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0) fmt.Fprintln(w, "ID\tTYPE\tTITLE\tCREATED\tRETENTION") for _, item := range computed { @@ -65,3 +82,42 @@ func runArtifactList(cmd *cobra.Command, args []string) error { } return w.Flush() } + +func printArtifactTree(w io.Writer, items []artifactpkg.ListItem) error { + groups := map[string][]artifactpkg.ListItem{} + for _, item := range items { + folder := item.Folder + if folder == "" { + folder = "Unfiled" + } + groups[folder] = append(groups[folder], item) + } + folders := make([]string, 0, len(groups)) + for folder := range groups { + folders = append(folders, folder) + } + sort.Slice(folders, func(i, j int) bool { + if folders[i] == "Unfiled" { + return true + } + if folders[j] == "Unfiled" { + return false + } + return folders[i] < folders[j] + }) + for _, folder := range folders { + if _, err := fmt.Fprintf(w, "%s/\n", folder); err != nil { + return err + } + for _, item := range groups[folder] { + retention := item.Retention + if retention == "" { + retention = artifactpkg.DefaultRetention + } + if _, err := fmt.Fprintf(w, " - %s [%s] %s (%s, %s)\n", item.ID, item.Type, item.Title, item.File, retention); err != nil { + return err + } + } + } + return nil +} diff --git a/cmd/artifact_test.go b/cmd/artifact_test.go index 6c060ce..43d7cc9 100644 --- a/cmd/artifact_test.go +++ b/cmd/artifact_test.go @@ -44,13 +44,16 @@ func resetArtifactGlobals() { focus bool id string file string + folder string }{} artifactListFlags = struct { artifactType string tag string agent string search string + folder string json bool + tree bool }{} artifactURLFlags = struct { absolute bool @@ -136,6 +139,120 @@ func TestArtifactAddFromStdinRequiresFile(t *testing.T) { } } +func TestArtifactAddListFolderAndTree(t *testing.T) { + defer resetArtifactGlobals() + sess, _ := setupArtifactCommandTest(t) + sourceDir := t.TempDir() + office := filepath.Join(sourceDir, "office.md") + plan := filepath.Join(sourceDir, "plan.md") + proof := filepath.Join(sourceDir, "proof.html") + for path, content := range map[string]string{office: "office", plan: "plan", proof: "proof"} { + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + } + + artifactSessionFlag = sess.Name + artifactAddFlags.title = "Office Hours" + artifactAddFlags.artifactType = "document" + artifactAddFlags.folder = "workflow/run-1" + artifactAddFlags.file = "00-office-hours.md" + cmd := &cobra.Command{} + cmd.SetOut(&bytes.Buffer{}) + if err := runArtifactAdd(cmd, []string{office}); err != nil { + t.Fatalf("add office: %v", err) + } + + artifactAddFlags.title = "Plan" + artifactAddFlags.file = "10-plan.md" + if err := runArtifactAdd(cmd, []string{plan}); err != nil { + t.Fatalf("add plan: %v", err) + } + + artifactAddFlags.title = "Proof" + artifactAddFlags.artifactType = "report" + artifactAddFlags.folder = "" + artifactAddFlags.file = "proof.html" + if err := runArtifactAdd(cmd, []string{proof}); err != nil { + t.Fatalf("add proof: %v", err) + } + + manifest, err := artifactpkg.LoadManifest(sess) + if err != nil { + t.Fatal(err) + } + if len(manifest.Artifacts) != 3 { + t.Fatalf("expected three artifacts, got %#v", manifest.Artifacts) + } + if _, err := os.Stat(filepath.Join(sess.Path, ".artifacts", "workflow", "run-1", "10-plan.md")); err != nil { + t.Fatalf("nested artifact missing: %v", err) + } + + artifactListFlags = struct { + artifactType string + tag string + agent string + search string + folder string + json bool + tree bool + }{folder: "workflow/run-1", json: true} + var listOut bytes.Buffer + listCmd := &cobra.Command{} + listCmd.SetOut(&listOut) + if err := runArtifactList(listCmd, nil); err != nil { + t.Fatalf("runArtifactList: %v", err) + } + var listed []artifactpkg.ListItem + if err := json.Unmarshal(listOut.Bytes(), &listed); err != nil { + t.Fatalf("list JSON invalid: %v\n%s", err, listOut.String()) + } + if len(listed) != 2 { + t.Fatalf("expected folder filter to return two artifacts, got %#v", listed) + } + var nestedPlanID string + for _, item := range listed { + if item.Folder != "workflow/run-1" || !strings.HasPrefix(item.File, "workflow/run-1/") { + t.Fatalf("unexpected filtered item: %#v", item) + } + if item.File == "workflow/run-1/10-plan.md" { + nestedPlanID = item.ID + } + } + if nestedPlanID == "" { + t.Fatalf("nested plan artifact not found in %#v", listed) + } + var urlOut bytes.Buffer + urlCmd := &cobra.Command{} + urlCmd.SetOut(&urlOut) + if err := runArtifactURL(urlCmd, []string{nestedPlanID}); err != nil { + t.Fatalf("runArtifactURL: %v", err) + } + if got := strings.TrimSpace(urlOut.String()); got != "/sessions/feature-artifacts/artifacts/workflow/run-1/10-plan.md" { + t.Fatalf("unexpected nested URL: %q", got) + } + + artifactListFlags = struct { + artifactType string + tag string + agent string + search string + folder string + json bool + tree bool + }{tree: true} + var treeOut bytes.Buffer + treeCmd := &cobra.Command{} + treeCmd.SetOut(&treeOut) + if err := runArtifactList(treeCmd, nil); err != nil { + t.Fatalf("runArtifactList tree: %v", err) + } + tree := treeOut.String() + if !strings.Contains(tree, "workflow/run-1/") || !strings.Contains(tree, "Unfiled/") || !strings.Contains(tree, "10-plan.md") || !strings.Contains(tree, "proof.html") { + t.Fatalf("unexpected tree output:\n%s", tree) + } +} + func TestArtifactAddFocusSetsAttentionFlag(t *testing.T) { defer resetArtifactGlobals() sess, _ := setupArtifactCommandTest(t) diff --git a/docs/artifacts.md b/docs/artifacts.md new file mode 100644 index 0000000..6adb479 --- /dev/null +++ b/docs/artifacts.md @@ -0,0 +1,104 @@ +# DevX Artifacts + +DevX artifacts are session-scoped files stored under a worktree's `.artifacts/` directory and indexed in `.artifacts/manifest.json`. Use them for plans, reports, screenshots, logs, diffs, recordings, QA notes, and proof-of-work outputs that should be visible in DevX Web. + +## Add artifacts + +```bash +# Flat artifact: .artifacts/report.md +devx artifact add ./report.md \ + --title "Completion report" \ + --type report \ + --agent pi + +# Artifact from stdin: .artifacts/notes.md +cat notes.md | devx artifact add - \ + --file notes.md \ + --title "Notes" \ + --type document +``` + +## Foldered artifacts + +Use `--folder` to organize related outputs into a collection. The folder is a safe relative path under `.artifacts/`. + +```bash +RUN_ID="2026-05-09T120000Z" + +devx artifact add ./00-office-hours.md \ + --title "Office hours notes" \ + --type document \ + --folder "workflow/$RUN_ID" \ + --file 00-office-hours.md + +devx artifact add ./10-plan.md \ + --title "Implementation plan" \ + --type plan \ + --folder "workflow/$RUN_ID" \ + --file 10-plan.md + +devx artifact add ./40-proof-of-work.html \ + --title "Proof of work" \ + --type report \ + --folder "workflow/$RUN_ID" \ + --file 40-proof-of-work.html \ + --focus +``` + +This creates paths such as: + +```text +.artifacts/workflow//00-office-hours.md +.artifacts/workflow//10-plan.md +.artifacts/workflow//40-proof-of-work.html +``` + +Manifest entries include both: + +- `file`: the full path relative to `.artifacts/`, for example `workflow//10-plan.md` +- `folder`: the grouping path, for example `workflow/` + +Older flat artifacts do not need a `folder` field and continue to load normally. + +## Folder safety rules + +Artifact folders must be safe relative paths: + +- no absolute paths (`/tmp/report`, `C:\\tmp\\report`) +- no `..` or `.` segments +- no empty segments (`workflow//run`, `workflow/`) + +Invalid folders are rejected before files are written. + +## Listing and URLs + +```bash +# All artifacts +devx artifact list + +# Only artifacts in one folder +devx artifact list --folder workflow/ + +# Grouped text output +devx artifact list --tree + +# Machine-readable output still includes folder, file, path, and url +devx artifact list --json + +# URLs and local references work with nested paths +devx artifact url +devx artifact url --local +devx artifact url --embed +``` + +Example tree output: + +```text +Unfiled/ + - report-completion [...] Completion report (report.md, session) +workflow// + - plan-implementation-plan [...] Implementation plan (workflow//10-plan.md, session) + - report-proof-of-work [...] Proof of work (workflow//40-proof-of-work.html, archive) +``` + +DevX Web groups artifacts by `folder`; artifacts with no folder appear under **Unfiled**. Focused artifact behavior is unchanged: `--focus` still opens the selected artifact in the web artifact pane. diff --git a/web/app/src/lib/Terminal.svelte b/web/app/src/lib/Terminal.svelte index 79d63a9..8d79ebe 100644 --- a/web/app/src/lib/Terminal.svelte +++ b/web/app/src/lib/Terminal.svelte @@ -45,7 +45,7 @@ $: filteredArtifactSearchItems = artifactSearchItems.filter(a => { const q = artifactQuery.trim().toLowerCase() if (!q) return true - return [a.title, a.file, a.type, ...(a.tags || [])].join(' ').toLowerCase().includes(q) + return [a.title, a.file, a.folder, a.type, ...(a.tags || [])].join(' ').toLowerCase().includes(q) }) // Drag-and-drop state diff --git a/web/app/src/lib/artifacts/ArtifactPane.svelte b/web/app/src/lib/artifacts/ArtifactPane.svelte index c34954b..bdd73fa 100644 --- a/web/app/src/lib/artifacts/ArtifactPane.svelte +++ b/web/app/src/lib/artifacts/ArtifactPane.svelte @@ -23,6 +23,8 @@ let listCollapsed = false let listHeight = 220 let resizingList = false + let artifactSort = 'newest' + let relativeNow = Date.now() let lastAppliedSelectedArtifactID = null let wasFullScreen = false @@ -43,6 +45,8 @@ $: selectedURL = selected?.url || '' $: selectedExt = selected?.file?.split('.').pop()?.toLowerCase() || '' + $: sortedArtifacts = sortArtifacts(artifacts, artifactSort) + $: artifactGroups = groupArtifactsByFolder(sortedArtifacts, artifactSort) $: if (fullScreen && !wasFullScreen) { listCollapsed = true wasFullScreen = true @@ -78,6 +82,61 @@ } } + function artifactTime(a) { + const t = Date.parse(a?.created || '') + return Number.isFinite(t) ? t : 0 + } + + function sortArtifacts(items, mode) { + return [...(items || [])].sort((a, b) => { + if (mode === 'oldest') return artifactTime(a) - artifactTime(b) || (a.title || '').localeCompare(b.title || '') + if (mode === 'title') return (a.title || '').localeCompare(b.title || '') || artifactTime(b) - artifactTime(a) + return artifactTime(b) - artifactTime(a) || (a.title || '').localeCompare(b.title || '') + }) + } + + function groupArtifactsByFolder(items, mode) { + const groups = new Map() + for (const a of items || []) { + const name = a.folder || 'Unfiled' + if (!groups.has(name)) groups.set(name, []) + groups.get(name).push(a) + } + return Array.from(groups.entries()) + .sort(([aName, aItems], [bName, bItems]) => { + if (mode === 'newest') return Math.max(...bItems.map(artifactTime)) - Math.max(...aItems.map(artifactTime)) || aName.localeCompare(bName) + if (mode === 'oldest') return Math.min(...aItems.map(artifactTime)) - Math.min(...bItems.map(artifactTime)) || aName.localeCompare(bName) + if (aName === 'Unfiled') return -1 + if (bName === 'Unfiled') return 1 + return aName.localeCompare(bName) + }) + .map(([name, items]) => ({ name, items })) + } + + function relativeTime(created) { + const t = Date.parse(created || '') + if (!Number.isFinite(t)) return '' + const seconds = Math.max(0, Math.floor((relativeNow - t) / 1000)) + if (seconds < 45) return 'just now' + const minutes = Math.floor(seconds / 60) + if (minutes < 60) return `${minutes}m ago` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours}h ago` + const days = Math.floor(hours / 24) + if (days < 14) return `${days}d ago` + const weeks = Math.floor(days / 7) + if (weeks < 8) return `${weeks}w ago` + const months = Math.floor(days / 30) + if (months < 12) return `${months}mo ago` + return `${Math.floor(days / 365)}y ago` + } + + function absoluteTime(created) { + const t = Date.parse(created || '') + if (!Number.isFinite(t)) return '' + return new Date(t).toLocaleString() + } + function renderKind(a = selected) { if (!a) return 'other' const ext = a.file?.split('.').pop()?.toLowerCase() || '' @@ -311,12 +370,24 @@ window.addEventListener('mouseup', up) } - onMount(load) + onMount(() => { + load() + const timer = window.setInterval(() => { relativeNow = Date.now() }, 60_000) + return () => window.clearInterval(timer) + })
{ if (Array.from(e.dataTransfer?.items || []).some(i => i.kind === 'file')) dragOver = true }} on:dragover={(e) => e.preventDefault()} on:dragleave={() => { dragOver = false }} on:drop={handleDrop}>
artifacts {artifacts.length ? `(${artifacts.length})` : ''}
+ @@ -344,11 +415,17 @@ {:else if artifacts.length === 0}
No artifacts — drop files, paste text, or click [upload]/[new].
{:else} - {#each artifacts as a} - + {#each artifactGroups as group} +
{group.name} ({group.items.length})
+ {#each group.items as a} + + {/each} {/each} {/if}
diff --git a/web/artifacts_api.go b/web/artifacts_api.go index 0b60dbf..932163f 100644 --- a/web/artifacts_api.go +++ b/web/artifacts_api.go @@ -62,6 +62,7 @@ func handleListArtifacts(w http.ResponseWriter, r *http.Request) { Tag: r.URL.Query().Get("tag"), Agent: r.URL.Query().Get("agent"), Search: r.URL.Query().Get("search"), + Folder: r.URL.Query().Get("folder"), }) writeJSON(w, http.StatusOK, map[string]any{"session": sess.Name, "artifacts": artifactpkg.WithComputedFields(sess.Name, items)}) } @@ -227,6 +228,7 @@ func handleUploadArtifact(w http.ResponseWriter, r *http.Request) { retention := r.FormValue("retention") tags := artifactpkg.ParseTags(r.FormValue("tags")) summary := r.FormValue("summary") + folder := r.FormValue("folder") var added []artifactpkg.ListItem var addedIDs []string @@ -253,7 +255,7 @@ func handleUploadArtifact(w http.ResponseWriter, r *http.Request) { format = "md" } dest := artifactpkg.Slugify(title) + "." + format - a, err := artifactpkg.Add(sess, artifactpkg.AddOptions{Source: "-", Reader: strings.NewReader(text), Destination: dest, Type: artifactType, Title: title, Summary: summary, Agent: "human", Retention: retention, Tags: tags}) + a, err := artifactpkg.Add(sess, artifactpkg.AddOptions{Source: "-", Reader: strings.NewReader(text), Destination: dest, Folder: folder, Type: artifactType, Title: title, Summary: summary, Agent: "human", Retention: retention, Tags: tags}) if err != nil { log.Printf("failed to create text artifact for session %q: %v", sess.Name, err) writeJSON(w, http.StatusBadRequest, map[string]string{"error": "failed to create artifact"}) @@ -287,7 +289,7 @@ func handleUploadArtifact(w http.ResponseWriter, r *http.Request) { itemType = artifactpkg.DetectType(header.Filename) } dest := artifactpkg.DefaultDestination(itemType, header.Filename) - a, addErr := artifactpkg.Add(sess, artifactpkg.AddOptions{Source: header.Filename, Reader: file, Destination: dest, Type: itemType, Title: itemTitle, Summary: summary, Agent: "human", Retention: retention, Tags: tags}) + a, addErr := artifactpkg.Add(sess, artifactpkg.AddOptions{Source: header.Filename, Reader: file, Destination: dest, Folder: folder, Type: itemType, Title: itemTitle, Summary: summary, Agent: "human", Retention: retention, Tags: tags}) _ = file.Close() if addErr != nil { if rollbackErr := rollbackAdded(); rollbackErr != nil { diff --git a/web/artifacts_api_test.go b/web/artifacts_api_test.go index 3028bd3..b0f0166 100644 --- a/web/artifacts_api_test.go +++ b/web/artifacts_api_test.go @@ -131,6 +131,31 @@ func TestServeArtifactAllowsReferencedAsset(t *testing.T) { } } +func TestServeNestedArtifactURL(t *testing.T) { + sess := setupArtifactAPITest(t) + source := filepath.Join(t.TempDir(), "proof.html") + if err := os.WriteFile(source, []byte("

Proof

"), 0o644); err != nil { + t.Fatal(err) + } + a, err := artifactpkg.Add(sess, artifactpkg.AddOptions{Source: source, Type: "report", Title: "Proof", Folder: "workflow/run-1/40-proof", Destination: "proof.html"}) + if err != nil { + t.Fatal(err) + } + item := artifactpkg.WithComputedFields(sess.Name, []artifactpkg.Artifact{a})[0] + if item.URL != "/sessions/feature%2Fweb-artifacts/artifacts/workflow/run-1/40-proof/proof.html" || item.Path != ".artifacts/workflow/run-1/40-proof/proof.html" { + t.Fatalf("unexpected computed paths: %#v", item) + } + req := httptest.NewRequest("GET", item.URL, nil) + w := httptest.NewRecorder() + artifactMux().ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("expected nested artifact 200, got %d: %s", w.Code, w.Body.String()) + } + if got := w.Body.String(); got != "

Proof

" { + t.Fatalf("nested artifact body = %q", got) + } +} + func TestServeArtifactRequiresManifestEntry(t *testing.T) { sess := setupArtifactAPITest(t) if err := os.MkdirAll(artifactpkg.DirForSession(sess), 0o755); err != nil { diff --git a/web/dist/assets/index-C-eGZceh.js b/web/dist/assets/index-C-eGZceh.js deleted file mode 100644 index fd97489..0000000 --- a/web/dist/assets/index-C-eGZceh.js +++ /dev/null @@ -1,163 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))a(o);new MutationObserver(o=>{for(const i of o)if(i.type==="childList")for(const l of i.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&a(l)}).observe(document,{childList:!0,subtree:!0});function r(o){const i={};return o.integrity&&(i.integrity=o.integrity),o.referrerPolicy&&(i.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?i.credentials="include":o.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function a(o){if(o.ep)return;o.ep=!0;const i=r(o);fetch(o.href,i)}})();const Br=!1;var vr=Array.isArray,No=Array.prototype.indexOf,Hn=Array.prototype.includes,Pr=Array.from,Fo=Object.defineProperty,jn=Object.getOwnPropertyDescriptor,Pa=Object.getOwnPropertyDescriptors,Uo=Object.prototype,zo=Array.prototype,ta=Object.getPrototypeOf,va=Object.isExtensible;const Ko=()=>{};function Vo(e){return e()}function Hr(e){for(var t=0;t{e=a,t=o});return{promise:r,resolve:e,reject:t}}function pa(e,t){if(Array.isArray(e))return e;if(!(Symbol.iterator in e))return Array.from(e);const r=[];for(const a of e)if(r.push(a),r.length===t)break;return r}const Be=2,pr=4,hr=8,Na=1<<24,cn=16,Mt=32,zn=64,Wr=128,bt=512,Pe=1024,He=2048,yt=4096,ct=8192,on=16384,Mn=32768,Wn=65536,ha=1<<17,Fa=1<<18,Jn=1<<19,Ua=1<<20,an=1<<25,Nn=65536,Zr=1<<21,na=1<<22,gn=1<<23,sn=Symbol("$state"),qo=Symbol("legacy props"),Bo=Symbol(""),In=new class extends Error{name="StaleReactionError";message="The reaction that called `getAbortSignal()` was re-run or destroyed"},Ho=!!globalThis.document?.contentType&&globalThis.document.contentType.includes("xml");function ra(e){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function Wo(){throw new Error("https://svelte.dev/e/async_derived_orphan")}function Zo(e,t,r){throw new Error("https://svelte.dev/e/each_key_duplicate")}function Yo(e){throw new Error("https://svelte.dev/e/effect_in_teardown")}function $o(){throw new Error("https://svelte.dev/e/effect_in_unowned_derived")}function Xo(e){throw new Error("https://svelte.dev/e/effect_orphan")}function Jo(){throw new Error("https://svelte.dev/e/effect_update_depth_exceeded")}function Go(e){throw new Error("https://svelte.dev/e/props_invalid_value")}function Qo(){throw new Error("https://svelte.dev/e/state_descriptors_fixed")}function ei(){throw new Error("https://svelte.dev/e/state_prototype_fixed")}function ti(){throw new Error("https://svelte.dev/e/state_unsafe_mutation")}function ni(){throw new Error("https://svelte.dev/e/svelte_boundary_reset_onerror")}const ri=1,ai=2,za=4,oi=8,ii=16,si=1,li=2,ci=4,fi=8,ui=16,di=1,vi=2,qe=Symbol(),Ka="http://www.w3.org/1999/xhtml";function pi(){console.warn("https://svelte.dev/e/select_multiple_invalid_value")}function hi(){console.warn("https://svelte.dev/e/svelte_boundary_reset_noop")}function Va(e){return e===this.v}function mi(e,t){return e!=e?t==t:e!==t||e!==null&&typeof e=="object"||typeof e=="function"}function qa(e){return!mi(e,this.v)}let Gn=!1,_i=!1;function bi(){Gn=!0}let be=null;function Zn(e){be=e}function ft(e,t=!1,r){be={p:be,i:!1,c:null,e:null,s:e,x:null,l:Gn&&!t?{s:null,u:null,$:[]}:null}}function ut(e){var t=be,r=t.e;if(r!==null){t.e=null;for(var a of r)lo(a)}return e!==void 0&&(t.x=e),t.i=!0,be=t.p,e??{}}function Qn(){return!Gn||be!==null&&be.l===null}let Ln=[];function Ba(){var e=Ln;Ln=[],Hr(e)}function Kt(e){if(Ln.length===0&&!lr){var t=Ln;queueMicrotask(()=>{t===Ln&&Ba()})}Ln.push(e)}function xi(){for(;Ln.length>0;)Ba()}function Ha(e){var t=ie;if(t===null)return ae.f|=gn,e;if((t.f&Mn)===0&&(t.f&pr)===0)throw e;bn(e,t)}function bn(e,t){for(;t!==null;){if((t.f&Wr)!==0){if((t.f&Mn)===0)throw e;try{t.b.error(e);return}catch(r){e=r}}t=t.parent}throw e}const gi=-7169;function De(e,t){e.f=e.f&gi|t}function aa(e){(e.f&bt)!==0||e.deps===null?De(e,Pe):De(e,yt)}function Wa(e){if(e!==null)for(const t of e)(t.f&Be)===0||(t.f&Nn)===0||(t.f^=Nn,Wa(t.deps))}function Za(e,t,r){(e.f&He)!==0?t.add(e):(e.f&yt)!==0&&r.add(e),Wa(e.deps),De(e,Pe)}const kr=new Set;let oe=null,Dr=null,Lt=null,rt=[],Mr=null,Yr=!1,lr=!1;class yn{current=new Map;previous=new Map;#t=new Set;#s=new Set;#e=0;#i=0;#r=null;#o=new Set;#n=new Set;#a=new Map;is_fork=!1;#l=!1;#f(){return this.is_fork||this.#i>0}skip_effect(t){this.#a.has(t)||this.#a.set(t,{d:[],m:[]})}unskip_effect(t){var r=this.#a.get(t);if(r){this.#a.delete(t);for(var a of r.d)De(a,He),Rt(a);for(a of r.m)De(a,yt),Rt(a)}}process(t){rt=[],this.apply();var r=[],a=[];for(const o of t)this.#c(o,r,a);if(this.#f()){this.#u(a),this.#u(r);for(const[o,i]of this.#a)Ja(o,i)}else{for(const o of this.#t)o();this.#t.clear(),this.#e===0&&this.#v(),Dr=this,oe=null,ma(a),ma(r),this.#o.clear(),this.#n.clear(),Dr=null,this.#r?.resolve()}Lt=null}#c(t,r,a){t.f^=Pe;for(var o=t.first;o!==null;){var i=o.f,l=(i&(Mt|zn))!==0,u=l&&(i&Pe)!==0,c=u||(i&ct)!==0||this.#a.has(o);if(!c&&o.fn!==null){l?o.f^=Pe:(i&pr)!==0?r.push(o):tr(o)&&((i&cn)!==0&&this.#n.add(o),Un(o));var f=o.first;if(f!==null){o=f;continue}}for(;o!==null;){var b=o.next;if(b!==null){o=b;break}o=o.parent}}}#u(t){for(var r=0;r0){if(Ya(),oe!==null&&oe!==this)return}else this.#e===0&&this.process([]);this.deactivate()}discard(){for(const t of this.#s)t(this);this.#s.clear()}#v(){if(kr.size>1){this.previous.clear();var t=Lt,r=!0;for(const o of kr){if(o===this){r=!1;continue}const i=[];for(const[u,c]of this.current){if(o.current.has(u))if(r&&c!==o.current.get(u))o.current.set(u,c);else continue;i.push(u)}if(i.length===0)continue;const l=[...o.current.keys()].filter(u=>!this.current.has(u));if(l.length>0){var a=rt;rt=[];const u=new Set,c=new Map;for(const f of i)$a(f,l,u,c);if(rt.length>0){oe=o,o.apply();for(const f of rt)o.#c(f,[],[]);o.deactivate()}rt=a}}oe=null,Lt=t}this.#a.clear(),kr.delete(this)}increment(t){this.#e+=1,t&&(this.#i+=1)}decrement(t){this.#e-=1,t&&(this.#i-=1),!this.#l&&(this.#l=!0,Kt(()=>{this.#l=!1,this.#f()?rt.length>0&&this.flush():this.revive()}))}revive(){for(const t of this.#o)this.#n.delete(t),De(t,He),Rt(t);for(const t of this.#n)De(t,yt),Rt(t);this.flush()}oncommit(t){this.#t.add(t)}ondiscard(t){this.#s.add(t)}settled(){return(this.#r??=Ma()).promise}static ensure(){if(oe===null){const t=oe=new yn;kr.add(oe),lr||Kt(()=>{oe===t&&t.flush()})}return oe}apply(){}}function yi(e){var t=lr;lr=!0;try{for(var r;;){if(xi(),rt.length===0&&(oe?.flush(),rt.length===0))return Mr=null,r;Ya()}}finally{lr=t}}function Ya(){Yr=!0;var e=null;try{for(var t=0;rt.length>0;){var r=yn.ensure();if(t++>1e3){var a,o;wi()}r.process(rt),wn.clear()}}finally{rt=[],Yr=!1,Mr=null}}function wi(){try{Jo()}catch(e){bn(e,Mr)}}let nn=null;function ma(e){var t=e.length;if(t!==0){for(var r=0;r0)){wn.clear();for(const o of nn){if((o.f&(on|ct))!==0)continue;const i=[o];let l=o.parent;for(;l!==null;)nn.has(l)&&(nn.delete(l),i.push(l)),l=l.parent;for(let u=i.length-1;u>=0;u--){const c=i[u];(c.f&(on|ct))===0&&Un(c)}}nn.clear()}}nn=null}}function $a(e,t,r,a){if(!r.has(e)&&(r.add(e),e.reactions!==null))for(const o of e.reactions){const i=o.f;(i&Be)!==0?$a(o,t,r,a):(i&(na|cn))!==0&&(i&He)===0&&Xa(o,t,a)&&(De(o,He),Rt(o))}}function Xa(e,t,r){const a=r.get(e);if(a!==void 0)return a;if(e.deps!==null)for(const o of e.deps){if(Hn.call(t,o))return!0;if((o.f&Be)!==0&&Xa(o,t,r))return r.set(o,!0),!0}return r.set(e,!1),!1}function Rt(e){var t=Mr=e,r=t.b;if(r?.is_pending&&(e.f&(pr|hr|Na))!==0&&(e.f&Mn)===0){r.defer_effect(e);return}for(;t.parent!==null;){t=t.parent;var a=t.f;if(Yr&&t===ie&&(a&cn)!==0&&(a&Fa)===0&&(a&Mn)!==0)return;if((a&(zn|Mt))!==0){if((a&Pe)===0)return;t.f^=Pe}}rt.push(t)}function Ja(e,t){if(!((e.f&Mt)!==0&&(e.f&Pe)!==0)){(e.f&He)!==0?t.d.push(e):(e.f&yt)!==0&&t.m.push(e),De(e,Pe);for(var r=e.first;r!==null;)Ja(r,t),r=r.next}}function ki(e){let t=0,r=Fn(0),a;return()=>{ia()&&(n(r),br(()=>(t===0&&(a=I(()=>e(()=>cr(r)))),t+=1,()=>{Kt(()=>{t-=1,t===0&&(a?.(),a=void 0,cr(r))})})))}}var Ei=Wn|Jn;function Si(e,t,r,a){new Ti(e,t,r,a)}class Ti{parent;is_pending=!1;transform_error;#t;#s=null;#e;#i;#r;#o=null;#n=null;#a=null;#l=null;#f=0;#c=0;#u=!1;#v=new Set;#p=new Set;#d=null;#x=ki(()=>(this.#d=Fn(this.#f),()=>{this.#d=null}));constructor(t,r,a,o){this.#t=t,this.#e=r,this.#i=i=>{var l=ie;l.b=this,l.f|=Wr,a(i)},this.parent=ie.b,this.transform_error=o??this.parent?.transform_error??(i=>i),this.#r=Ur(()=>{this.#_()},Ei)}#g(){try{this.#o=mt(()=>this.#i(this.#t))}catch(t){this.error(t)}}#y(t){const r=this.#e.failed;r&&(this.#a=mt(()=>{r(this.#t,()=>t,()=>()=>{})}))}#w(){const t=this.#e.pending;t&&(this.is_pending=!0,this.#n=mt(()=>t(this.#t)),Kt(()=>{var r=this.#l=document.createDocumentFragment(),a=ln();r.append(a),this.#o=this.#m(()=>(yn.ensure(),mt(()=>this.#i(a)))),this.#c===0&&(this.#t.before(r),this.#l=null,On(this.#n,()=>{this.#n=null}),this.#h())}))}#_(){try{if(this.is_pending=this.has_pending_snippet(),this.#c=0,this.#f=0,this.#o=mt(()=>{this.#i(this.#t)}),this.#c>0){var t=this.#l=document.createDocumentFragment();ho(this.#o,t);const r=this.#e.pending;this.#n=mt(()=>r(this.#t))}else this.#h()}catch(r){this.error(r)}}#h(){this.is_pending=!1;for(const t of this.#v)De(t,He),Rt(t);for(const t of this.#p)De(t,yt),Rt(t);this.#v.clear(),this.#p.clear()}defer_effect(t){Za(t,this.#v,this.#p)}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!this.#e.pending}#m(t){var r=ie,a=ae,o=be;qt(this.#r),wt(this.#r),Zn(this.#r.ctx);try{return t()}catch(i){return Ha(i),null}finally{qt(r),wt(a),Zn(o)}}#b(t){if(!this.has_pending_snippet()){this.parent&&this.parent.#b(t);return}this.#c+=t,this.#c===0&&(this.#h(),this.#n&&On(this.#n,()=>{this.#n=null}),this.#l&&(this.#t.before(this.#l),this.#l=null))}update_pending_count(t){this.#b(t),this.#f+=t,!(!this.#d||this.#u)&&(this.#u=!0,Kt(()=>{this.#u=!1,this.#d&&Yn(this.#d,this.#f)}))}get_effect_pending(){return this.#x(),n(this.#d)}error(t){var r=this.#e.onerror;let a=this.#e.failed;if(!r&&!a)throw t;this.#o&&(ot(this.#o),this.#o=null),this.#n&&(ot(this.#n),this.#n=null),this.#a&&(ot(this.#a),this.#a=null);var o=!1,i=!1;const l=()=>{if(o){hi();return}o=!0,i&&ni(),this.#a!==null&&On(this.#a,()=>{this.#a=null}),this.#m(()=>{yn.ensure(),this.#_()})},u=c=>{try{i=!0,r?.(c,l),i=!1}catch(f){bn(f,this.#r&&this.#r.parent)}a&&(this.#a=this.#m(()=>{yn.ensure();try{return mt(()=>{var f=ie;f.b=this,f.f|=Wr,a(this.#t,()=>c,()=>l)})}catch(f){return bn(f,this.#r.parent),null}}))};Kt(()=>{var c;try{c=this.transform_error(t)}catch(f){bn(f,this.#r&&this.#r.parent);return}c!==null&&typeof c=="object"&&typeof c.then=="function"?c.then(u,f=>bn(f,this.#r&&this.#r.parent)):u(c)})}}function Ai(e,t,r,a){const o=Qn()?mr:ht;var i=e.filter(_=>!_.settled);if(r.length===0&&i.length===0){a(t.map(o));return}var l=ie,u=Ci(),c=i.length===1?i[0].promise:i.length>1?Promise.all(i.map(_=>_.promise)):null;function f(_){u();try{a(_)}catch(d){(l.f&on)===0&&bn(d,l)}$r()}if(r.length===0){c.then(()=>f(t.map(o)));return}function b(){u(),Promise.all(r.map(_=>Ii(_))).then(_=>f([...t.map(o),..._])).catch(_=>bn(_,l))}c?c.then(b):b()}function Ci(){var e=ie,t=ae,r=be,a=oe;return function(i=!0){qt(e),wt(t),Zn(r),i&&a?.activate()}}function $r(e=!0){qt(null),wt(null),Zn(null),e&&oe?.deactivate()}function Di(){var e=ie.b,t=oe,r=e.is_rendered();return e.update_pending_count(1),t.increment(r),()=>{e.update_pending_count(-1),t.decrement(r)}}function mr(e){var t=Be|He,r=ae!==null&&(ae.f&Be)!==0?ae:null;return ie!==null&&(ie.f|=Jn),{ctx:be,deps:null,effects:null,equals:Va,f:t,fn:e,reactions:null,rv:0,v:qe,wv:0,parent:r??ie,ac:null}}function Ii(e,t,r){ie===null&&Wo();var o=void 0,i=Fn(qe),l=!ae,u=new Map;return qi(()=>{var c=Ma();o=c.promise;try{Promise.resolve(e()).then(c.resolve,c.reject).finally($r)}catch(d){c.reject(d),$r()}var f=oe;if(l){var b=Di();u.get(f)?.reject(In),u.delete(f),u.set(f,c)}const _=(d,S=void 0)=>{if(f.activate(),S)S!==In&&(i.f|=gn,Yn(i,S));else{(i.f&gn)!==0&&(i.f^=gn),Yn(i,d);for(const[w,C]of u){if(u.delete(w),w===f)break;C.reject(In)}}b&&b()};c.promise.then(_,d=>_(null,d||"unknown"))}),Fr(()=>{for(const c of u.values())c.reject(In)}),new Promise(c=>{function f(b){function _(){b===o?c(i):f(o)}b.then(_,_)}f(o)})}function rn(e){const t=mr(e);return mo(t),t}function ht(e){const t=mr(e);return t.equals=qa,t}function Li(e){var t=e.effects;if(t!==null){e.effects=null;for(var r=0;r0&&!eo&&Oi()}return t}function Oi(){eo=!1;for(const e of Xr)(e.f&Pe)!==0&&De(e,yt),tr(e)&&Un(e);Xr.clear()}function Ar(e,t=1){var r=n(e),a=t===1?r++:r--;return s(e,r),a}function cr(e){s(e,e.v+1)}function to(e,t){var r=e.reactions;if(r!==null)for(var a=Qn(),o=r.length,i=0;i{if(Pn===i)return u();var c=ae,f=Pn;wt(null),ga(i);var b=u();return wt(c),ga(f),b};return a&&r.set("length",mn(e.length)),new Proxy(e,{defineProperty(u,c,f){(!("value"in f)||f.configurable===!1||f.enumerable===!1||f.writable===!1)&&Qo();var b=r.get(c);return b===void 0?l(()=>{var _=mn(f.value);return r.set(c,_),_}):s(b,f.value,!0),!0},deleteProperty(u,c){var f=r.get(c);if(f===void 0){if(c in u){const b=l(()=>mn(qe));r.set(c,b),cr(o)}}else s(f,qe),cr(o);return!0},get(u,c,f){if(c===sn)return e;var b=r.get(c),_=c in u;if(b===void 0&&(!_||jn(u,c)?.writable)&&(b=l(()=>{var S=Bn(_?u[c]:qe),w=mn(S);return w}),r.set(c,b)),b!==void 0){var d=n(b);return d===qe?void 0:d}return Reflect.get(u,c,f)},getOwnPropertyDescriptor(u,c){var f=Reflect.getOwnPropertyDescriptor(u,c);if(f&&"value"in f){var b=r.get(c);b&&(f.value=n(b))}else if(f===void 0){var _=r.get(c),d=_?.v;if(_!==void 0&&d!==qe)return{enumerable:!0,configurable:!0,value:d,writable:!0}}return f},has(u,c){if(c===sn)return!0;var f=r.get(c),b=f!==void 0&&f.v!==qe||Reflect.has(u,c);if(f!==void 0||ie!==null&&(!b||jn(u,c)?.writable)){f===void 0&&(f=l(()=>{var d=b?Bn(u[c]):qe,S=mn(d);return S}),r.set(c,f));var _=n(f);if(_===qe)return!1}return b},set(u,c,f,b){var _=r.get(c),d=c in u;if(a&&c==="length")for(var S=f;S<_.v;S+=1){var w=r.get(S+"");w!==void 0?s(w,qe):S in u&&(w=l(()=>mn(qe)),r.set(S+"",w))}if(_===void 0)(!d||jn(u,c)?.writable)&&(_=l(()=>mn(void 0)),s(_,Bn(f)),r.set(c,_));else{d=_.v!==qe;var C=l(()=>Bn(f));s(_,C)}var m=Reflect.getOwnPropertyDescriptor(u,c);if(m?.set&&m.set.call(b,f),!d){if(a&&typeof c=="string"){var E=r.get("length"),F=Number(c);Number.isInteger(F)&&F>=E.v&&s(E,F+1)}cr(o)}return!0},ownKeys(u){n(o);var c=Reflect.ownKeys(u).filter(_=>{var d=r.get(_);return d===void 0||d.v!==qe});for(var[f,b]of r)b.v!==qe&&!(f in u)&&c.push(f);return c},setPrototypeOf(){ei()}})}function _a(e){try{if(e!==null&&typeof e=="object"&&sn in e)return e[sn]}catch{}return e}function Pi(e,t){return Object.is(_a(e),_a(t))}var Ir,no,ro,ao;function Mi(){if(Ir===void 0){Ir=window,no=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,r=Text.prototype;ro=jn(t,"firstChild").get,ao=jn(t,"nextSibling").get,va(e)&&(e.__click=void 0,e.__className=void 0,e.__attributes=null,e.__style=void 0,e.__e=void 0),va(r)&&(r.__t=void 0)}}function ln(e=""){return document.createTextNode(e)}function Lr(e){return ro.call(e)}function _r(e){return ao.call(e)}function h(e,t){return Lr(e)}function jt(e,t=!1){{var r=Lr(e);return r instanceof Comment&&r.data===""?_r(r):r}}function p(e,t=1,r=!1){let a=e;for(;t--;)a=_r(a);return a}function Ni(e){e.textContent=""}function oo(){return!1}function Fi(e,t,r){return document.createElementNS(Ka,e,void 0)}function ur(e,t){{const r=document.body;e.autofocus=!0,Kt(()=>{document.activeElement===r&&e.focus()})}}let ba=!1;function Ui(){ba||(ba=!0,document.addEventListener("reset",e=>{Promise.resolve().then(()=>{if(!e.defaultPrevented)for(const t of e.target.elements)t.__on_r?.()})},{capture:!0}))}function Nr(e){var t=ae,r=ie;wt(null),qt(null);try{return e()}finally{wt(t),qt(r)}}function io(e,t,r,a=r){e.addEventListener(t,()=>Nr(r));const o=e.__on_r;o?e.__on_r=()=>{o(),a(!0)}:e.__on_r=()=>a(!0),Ui()}function so(e){ie===null&&(ae===null&&Xo(),$o()),kn&&Yo()}function zi(e,t){var r=t.last;r===null?t.last=t.first=e:(r.next=e,e.prev=r,t.last=e)}function Bt(e,t,r){var a=ie;a!==null&&(a.f&ct)!==0&&(e|=ct);var o={ctx:be,deps:null,nodes:null,f:e|He|bt,first:null,fn:t,last:null,next:null,parent:a,b:a&&a.b,prev:null,teardown:null,wv:0,ac:null};if(r)try{Un(o)}catch(u){throw ot(o),u}else t!==null&&Rt(o);var i=o;if(r&&i.deps===null&&i.teardown===null&&i.nodes===null&&i.first===i.last&&(i.f&Jn)===0&&(i=i.first,(e&cn)!==0&&(e&Wn)!==0&&i!==null&&(i.f|=Wn)),i!==null&&(i.parent=a,a!==null&&zi(i,a),ae!==null&&(ae.f&Be)!==0&&(e&zn)===0)){var l=ae;(l.effects??=[]).push(i)}return o}function ia(){return ae!==null&&!Ot}function Fr(e){const t=Bt(hr,null,!1);return De(t,Pe),t.teardown=e,t}function Jr(e){so();var t=ie.f,r=!ae&&(t&Mt)!==0&&(t&Mn)===0;if(r){var a=be;(a.e??=[]).push(e)}else return lo(e)}function lo(e){return Bt(pr|Ua,e,!1)}function Ki(e){return so(),Bt(hr|Ua,e,!0)}function Vi(e){yn.ensure();const t=Bt(zn|Jn,e,!0);return(r={})=>new Promise(a=>{r.outro?On(t,()=>{ot(t),a(void 0)}):(ot(t),a(void 0))})}function co(e){return Bt(pr,e,!1)}function ve(e,t){var r=be,a={effect:null,ran:!1,deps:e};r.l.$.push(a),a.effect=br(()=>{e(),!a.ran&&(a.ran=!0,I(t))})}function er(){var e=be;br(()=>{for(var t of e.l.$){t.deps();var r=t.effect;(r.f&Pe)!==0&&r.deps!==null&&De(r,yt),tr(r)&&Un(r),t.ran=!1}})}function qi(e){return Bt(na|Jn,e,!0)}function br(e,t=0){return Bt(hr|t,e,!0)}function V(e,t=[],r=[],a=[]){Ai(a,t,r,o=>{Bt(hr,()=>e(...o.map(n)),!0)})}function Ur(e,t=0){var r=Bt(cn|t,e,!0);return r}function mt(e){return Bt(Mt|Jn,e,!0)}function fo(e){var t=e.teardown;if(t!==null){const r=kn,a=ae;xa(!0),wt(null);try{t.call(null)}finally{xa(r),wt(a)}}}function sa(e,t=!1){var r=e.first;for(e.first=e.last=null;r!==null;){const o=r.ac;o!==null&&Nr(()=>{o.abort(In)});var a=r.next;(r.f&zn)!==0?r.parent=null:ot(r,t),r=a}}function Bi(e){for(var t=e.first;t!==null;){var r=t.next;(t.f&Mt)===0&&ot(t),t=r}}function ot(e,t=!0){var r=!1;(t||(e.f&Fa)!==0)&&e.nodes!==null&&e.nodes.end!==null&&(Hi(e.nodes.start,e.nodes.end),r=!0),sa(e,t&&!r),dr(e,0),De(e,on);var a=e.nodes&&e.nodes.t;if(a!==null)for(const i of a)i.stop();fo(e);var o=e.parent;o!==null&&o.first!==null&&uo(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes=e.ac=null}function Hi(e,t){for(;e!==null;){var r=e===t?null:_r(e);e.remove(),e=r}}function uo(e){var t=e.parent,r=e.prev,a=e.next;r!==null&&(r.next=a),a!==null&&(a.prev=r),t!==null&&(t.first===e&&(t.first=a),t.last===e&&(t.last=r))}function On(e,t,r=!0){var a=[];vo(e,a,!0);var o=()=>{r&&ot(e),t&&t()},i=a.length;if(i>0){var l=()=>--i||o();for(var u of a)u.out(l)}else o()}function vo(e,t,r){if((e.f&ct)===0){e.f^=ct;var a=e.nodes&&e.nodes.t;if(a!==null)for(const u of a)(u.is_global||r)&&t.push(u);for(var o=e.first;o!==null;){var i=o.next,l=(o.f&Wn)!==0||(o.f&Mt)!==0&&(e.f&cn)!==0;vo(o,t,l?r:!1),o=i}}}function la(e){po(e,!0)}function po(e,t){if((e.f&ct)!==0){e.f^=ct,(e.f&Pe)===0&&(De(e,He),Rt(e));for(var r=e.first;r!==null;){var a=r.next,o=(r.f&Wn)!==0||(r.f&Mt)!==0;po(r,o?t:!1),r=a}var i=e.nodes&&e.nodes.t;if(i!==null)for(const l of i)(l.is_global||t)&&l.in()}}function ho(e,t){if(e.nodes)for(var r=e.nodes.start,a=e.nodes.end;r!==null;){var o=r===a?null:_r(r);t.append(r),r=o}}let Cr=!1,kn=!1;function xa(e){kn=e}let ae=null,Ot=!1;function wt(e){ae=e}let ie=null;function qt(e){ie=e}let xt=null;function mo(e){ae!==null&&(xt===null?xt=[e]:xt.push(e))}let at=null,lt=0,pt=null;function Wi(e){pt=e}let _o=1,Rn=0,Pn=Rn;function ga(e){Pn=e}function bo(){return++_o}function tr(e){var t=e.f;if((t&He)!==0)return!0;if(t&Be&&(e.f&=~Nn),(t&yt)!==0){for(var r=e.deps,a=r.length,o=0;oe.wv)return!0}(t&bt)!==0&&Lt===null&&De(e,Pe)}return!1}function xo(e,t,r=!0){var a=e.reactions;if(a!==null&&!(xt!==null&&Hn.call(xt,e)))for(var o=0;o{e.ac.abort(In)}),e.ac=null);try{e.f|=Zr;var b=e.fn,_=b();e.f|=Mn;var d=e.deps,S=oe?.is_fork;if(at!==null){var w;if(S||dr(e,lt),d!==null&<>0)for(d.length=lt+at.length,w=0;wr?.call(this,i))}return e.startsWith("pointer")||e.startsWith("touch")||e==="wheel"?Kt(()=>{t.addEventListener(e,o,a)}):t.addEventListener(e,o,a),o}function T(e,t,r,a,o){var i={capture:a,passive:o},l=Ji(e,t,r,i);(t===document.body||t===window||t===document||t instanceof HTMLMediaElement)&&Fr(()=>{t.removeEventListener(e,l,i)})}let wa=null;function Qr(e){var t=this,r=t.ownerDocument,a=e.type,o=e.composedPath?.()||[],i=o[0]||e.target;wa=e;var l=0,u=wa===e&&e[Er];if(u){var c=o.indexOf(u);if(c!==-1&&(t===document||t===window)){e[Er]=t;return}var f=o.indexOf(t);if(f===-1)return;c<=f&&(l=c)}if(i=o[l]||e.target,i!==t){Fo(e,"currentTarget",{configurable:!0,get(){return i||r}});var b=ae,_=ie;wt(null),qt(null);try{for(var d,S=[];i!==null;){var w=i.assignedSlot||i.parentNode||i.host||null;try{var C=i[Er]?.[a];C!=null&&(!i.disabled||e.target===i)&&C.call(i,e)}catch(m){d?S.push(m):d=m}if(e.cancelBubble||w===t||w===null)break;i=w}if(d){for(let m of S)queueMicrotask(()=>{throw m});throw d}}finally{e[Er]=t,delete e.currentTarget,wt(b),qt(_)}}}const Gi=globalThis?.window?.trustedTypes&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:e=>e});function Qi(e){return Gi?.createHTML(e)??e}function es(e){var t=Fi("template");return t.innerHTML=Qi(e.replaceAll("","")),t.content}function Rr(e,t){var r=ie;r.nodes===null&&(r.nodes={start:e,end:t,a:null,t:null})}function D(e,t){var r=(t&di)!==0,a=(t&vi)!==0,o,i=!e.startsWith("");return()=>{o===void 0&&(o=es(i?e:""+e),r||(o=Lr(o)));var l=a||no?document.importNode(o,!0):o.cloneNode(!0);if(r){var u=Lr(l),c=l.lastChild;Rr(u,c)}else Rr(l,l);return l}}function ts(e=""){{var t=ln(e+"");return Rr(t,t),t}}function $n(){var e=document.createDocumentFragment(),t=document.createComment(""),r=ln();return e.append(t,r),Rr(t,r),e}function A(e,t){e!==null&&e.before(t)}function Z(e,t){var r=t==null?"":typeof t=="object"?`${t}`:t;r!==(e.__t??=e.nodeValue)&&(e.__t=r,e.nodeValue=`${r}`)}function ns(e,t){return rs(e,t)}const Sr=new Map;function rs(e,{target:t,anchor:r,props:a={},events:o,context:i,intro:l=!0,transformError:u}){Mi();var c=void 0,f=Vi(()=>{var b=r??t.appendChild(ln());Si(b,{pending:()=>{}},S=>{ft({});var w=be;i&&(w.c=i),o&&(a.$$events=o),c=e(S,a)||{},ut()},u);var _=new Set,d=S=>{for(var w=0;w{for(var S of _)for(const m of[t,document]){var w=Sr.get(m),C=w.get(S);--C==0?(m.removeEventListener(S,Qr),w.delete(S),w.size===0&&Sr.delete(m)):w.set(S,C)}ya.delete(d),b!==r&&b.parentNode?.removeChild(b)}});return as.set(c,f),c}let as=new WeakMap;class Eo{anchor;#t=new Map;#s=new Map;#e=new Map;#i=new Set;#r=!0;constructor(t,r=!0){this.anchor=t,this.#r=r}#o=()=>{var t=oe;if(this.#t.has(t)){var r=this.#t.get(t),a=this.#s.get(r);if(a)la(a),this.#i.delete(r);else{var o=this.#e.get(r);o&&(this.#s.set(r,o.effect),this.#e.delete(r),o.fragment.lastChild.remove(),this.anchor.before(o.fragment),a=o.effect)}for(const[i,l]of this.#t){if(this.#t.delete(i),i===t)break;const u=this.#e.get(l);u&&(ot(u.effect),this.#e.delete(l))}for(const[i,l]of this.#s){if(i===r||this.#i.has(i))continue;const u=()=>{if(Array.from(this.#t.values()).includes(i)){var f=document.createDocumentFragment();ho(l,f),f.append(ln()),this.#e.set(i,{effect:l,fragment:f})}else ot(l);this.#i.delete(i),this.#s.delete(i)};this.#r||!a?(this.#i.add(i),On(l,u,!1)):u()}}};#n=t=>{this.#t.delete(t);const r=Array.from(this.#t.values());for(const[a,o]of this.#e)r.includes(a)||(ot(o.effect),this.#e.delete(a))};ensure(t,r){var a=oe,o=oo();if(r&&!this.#s.has(t)&&!this.#e.has(t))if(o){var i=document.createDocumentFragment(),l=ln();i.append(l),this.#e.set(t,{effect:mt(()=>r(l)),fragment:i})}else this.#s.set(t,mt(()=>r(this.anchor)));if(this.#t.set(a,t),o){for(const[u,c]of this.#s)u===t?a.unskip_effect(c):a.skip_effect(c);for(const[u,c]of this.#e)u===t?a.unskip_effect(c.effect):a.skip_effect(c.effect);a.oncommit(this.#o),a.ondiscard(this.#n)}else this.#o()}}function q(e,t,r=!1){var a=new Eo(e),o=r?Wn:0;function i(l,u){a.ensure(l,u)}Ur(()=>{var l=!1;t((u,c=0)=>{l=!0,i(c,u)}),l||i(!1,null)},o)}const os=Symbol("NaN");function Kr(e,t,r){var a=new Eo(e),o=!Qn();Ur(()=>{var i=t();i!==i&&(i=os),o&&i!==null&&typeof i=="object"&&(i={}),a.ensure(i,r)})}function Pt(e,t){return t}function is(e,t,r){for(var a=[],o=t.length,i,l=t.length,u=0;u{if(i){if(i.pending.delete(_),i.done.add(_),i.pending.size===0){var d=e.outrogroups;ea(Pr(i.done)),d.delete(i),d.size===0&&(e.outrogroups=null)}}else l-=1},!1)}if(l===0){var c=a.length===0&&r!==null;if(c){var f=r,b=f.parentNode;Ni(b),b.append(f),e.items.clear()}ea(t,!c)}else i={pending:new Set(t),done:new Set},(e.outrogroups??=new Set).add(i)}function ea(e,t=!0){for(var r=0;r{var E=r();return vr(E)?E:E==null?[]:Pr(E)}),d,S=!0;function w(){m.fallback=b,ss(m,d,l,t,a),b!==null&&(d.length===0?(b.f&an)===0?la(b):(b.f^=an,ir(b,null,l)):On(b,()=>{b=null}))}var C=Ur(()=>{d=n(_);for(var E=d.length,F=new Set,P=oe,R=oo(),K=0;Ki(l)):(b=mt(()=>i(ka??=ln())),b.f|=an)),E>F.size&&Zo(),!S)if(R){for(const[H,M]of u)F.has(H)||P.skip_effect(M.e);P.oncommit(w),P.ondiscard(()=>{})}else w();n(_)}),m={effect:C,items:u,outrogroups:null,fallback:b};S=!1}function or(e){for(;e!==null&&(e.f&Mt)===0;)e=e.next;return e}function ss(e,t,r,a,o){var i=(a&oi)!==0,l=t.length,u=e.items,c=or(e.effect.first),f,b=null,_,d=[],S=[],w,C,m,E;if(i)for(E=0;E0){var H=(a&za)!==0&&l===0?r:null;if(i){for(E=0;E{if(_!==void 0)for(m of _)m.nodes?.a?.apply()})}function ls(e,t,r,a,o,i,l,u){var c=(l&ri)!==0?(l&ii)===0?y(r,!1,!1):Fn(r):null,f=(l&ai)!==0?Fn(o):null;return{v:c,i:f,e:mt(()=>(i(t,c??r,f??o,u),()=>{e.delete(a)}))}}function ir(e,t,r){if(e.nodes)for(var a=e.nodes.start,o=e.nodes.end,i=t&&(t.f&an)===0?t.nodes.start:r;a!==null;){var l=_r(a);if(i.before(a),a===o)return;a=l}}function _n(e,t,r){t===null?e.effect.first=r:t.next=r,r===null?e.effect.last=t:r.prev=t}const Ea=[...` -\r\f \v\uFEFF`];function cs(e,t,r){var a=e==null?"":""+e;if(t&&(a=a?a+" "+t:t),r){for(var o of Object.keys(r))if(r[o])a=a?a+" "+o:o;else if(a.length)for(var i=o.length,l=0;(l=a.indexOf(o,l))>=0;){var u=l+i;(l===0||Ea.includes(a[l-1]))&&(u===a.length||Ea.includes(a[u]))?a=(l===0?"":a.substring(0,l))+a.substring(u+1):l=u}}return a===""?null:a}function fs(e,t){return e==null?null:String(e)}function Vt(e,t,r,a,o,i){var l=e.__className;if(l!==r||l===void 0){var u=cs(r,a,i);u==null?e.removeAttribute("class"):e.className=u,e.__className=r}else if(i&&o!==i)for(var c in i){var f=!!i[c];(o==null||f!==!!o[c])&&e.classList.toggle(c,f)}return i}function jr(e,t,r,a){var o=e.__style;if(o!==t){var i=fs(t);i==null?e.removeAttribute("style"):e.style.cssText=i,e.__style=t}return a}function So(e,t,r=!1){if(e.multiple){if(t==null)return;if(!vr(t))return pi();for(var a of e.options)a.selected=t.includes(fr(a));return}for(a of e.options){var o=fr(a);if(Pi(o,t)){a.selected=!0;return}}(!r||t!==void 0)&&(e.selectedIndex=-1)}function us(e){var t=new MutationObserver(()=>{So(e,e.__value)});t.observe(e,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["value"]}),Fr(()=>{t.disconnect()})}function xn(e,t,r=t){var a=new WeakSet,o=!0;io(e,"change",i=>{var l=i?"[selected]":":checked",u;if(e.multiple)u=[].map.call(e.querySelectorAll(l),fr);else{var c=e.querySelector(l)??e.querySelector("option:not([disabled])");u=c&&fr(c)}r(u),oe!==null&&a.add(oe)}),co(()=>{var i=t();if(e===document.activeElement){var l=Dr??oe;if(a.has(l))return}if(So(e,i,o),o&&i===void 0){var u=e.querySelector(":checked");u!==null&&(i=fr(u),r(i))}e.__value=i,o=!1}),us(e)}function fr(e){return"__value"in e?e.__value:e.value}const ds=Symbol("is custom element"),vs=Symbol("is html"),ps=Ho?"progress":"PROGRESS";function hs(e,t){var r=To(e);r.value===(r.value=t??void 0)||e.value===t&&(t!==0||e.nodeName!==ps)||(e.value=t??"")}function _e(e,t,r,a){var o=To(e);o[t]!==(o[t]=r)&&(t==="loading"&&(e[Bo]=r),r==null?e.removeAttribute(t):typeof r!="string"&&ms(e).includes(t)?e[t]=r:e.setAttribute(t,r))}function To(e){return e.__attributes??={[ds]:e.nodeName.includes("-"),[vs]:e.namespaceURI===Ka}}var Sa=new Map;function ms(e){var t=e.getAttribute("is")||e.nodeName,r=Sa.get(t);if(r)return r;Sa.set(t,r=[]);for(var a,o=e,i=Element.prototype;i!==o;){a=Pa(o);for(var l in a)a[l].set&&r.push(l);o=ta(o)}return r}function _t(e,t,r=t){var a=new WeakSet;io(e,"input",async o=>{var i=o?e.defaultValue:e.value;if(i=Vr(e)?qr(i):i,r(i),oe!==null&&a.add(oe),await yo(),i!==(i=t())){var l=e.selectionStart,u=e.selectionEnd,c=e.value.length;if(e.value=i??"",u!==null){var f=e.value.length;l===u&&u===c&&f>c?(e.selectionStart=f,e.selectionEnd=f):(e.selectionStart=l,e.selectionEnd=Math.min(u,f))}}}),I(t)==null&&e.value&&(r(Vr(e)?qr(e.value):e.value),oe!==null&&a.add(oe)),br(()=>{var o=t();if(e===document.activeElement){var i=Dr??oe;if(a.has(i))return}Vr(e)&&o===qr(e.value)||e.type==="date"&&!o&&!e.value||o!==e.value&&(e.value=o??"")})}function Vr(e){var t=e.type;return t==="number"||t==="range"}function qr(e){return e===""?null:+e}function _s(e,t,r){var a=jn(e,t);a&&a.set&&(e[t]=r,Fr(()=>{e[t]=null}))}function Ta(e,t){return e===t||e?.[sn]===t}function Xn(e={},t,r,a){return co(()=>{var o,i;return br(()=>{o=i,i=[],I(()=>{e!==r(...i)&&(t(e,...i),o&&Ta(r(...o),e)&&t(null,...o))})}),()=>{Kt(()=>{i&&Ta(r(...i),e)&&t(null,...i)})}}),e}function bs(e){return function(...t){var r=t[0];r.target===this&&e?.apply(this,t)}}function sr(e){return function(...t){var r=t[0];return r.stopPropagation(),e?.apply(this,t)}}function Or(e){return function(...t){var r=t[0];return r.preventDefault(),e?.apply(this,t)}}function kt(e=!1){const t=be,r=t.l.u;if(!r)return;let a=()=>re(t.s);if(e){let o=0,i={};const l=mr(()=>{let u=!1;const c=t.s;for(const f in c)c[f]!==i[f]&&(i[f]=c[f],u=!0);return u&&o++,o});a=()=>n(l)}r.b.length&&Ki(()=>{Aa(t,a),Hr(r.b)}),Jr(()=>{const o=I(()=>r.m.map(Vo));return()=>{for(const i of o)typeof i=="function"&&i()}}),r.a.length&&Jr(()=>{Aa(t,a),Hr(r.a)})}function Aa(e,t){if(e.l.s)for(const r of e.l.s)n(r);t()}function Ao(e,t){var r=e.$$events?.[t.type],a=vr(r)?r.slice():r==null?[]:[r];for(var o of a)o.call(this,t)}let Tr=!1;function xs(e){var t=Tr;try{return Tr=!1,[e(),Tr]}finally{Tr=t}}function B(e,t,r,a){var o=!Gn||(r&li)!==0,i=(r&fi)!==0,l=(r&ui)!==0,u=a,c=!0,f=()=>(c&&(c=!1,u=l?I(a):a),u),b;if(i){var _=sn in e||qo in e;b=jn(e,t)?.set??(_&&t in e?P=>e[t]=P:void 0)}var d,S=!1;i?[d,S]=xs(()=>e[t]):d=e[t],d===void 0&&a!==void 0&&(d=f(),b&&(o&&Go(),b(d)));var w;if(o?w=()=>{var P=e[t];return P===void 0?f():(c=!0,P)}:w=()=>{var P=e[t];return P!==void 0&&(u=void 0),P===void 0?u:P},o&&(r&ci)===0)return w;if(b){var C=e.$$legacy;return(function(P,R){return arguments.length>0?((!o||!R||C||S)&&b(R?w():P),P):w()})}var m=!1,E=((r&si)!==0?mr:ht)(()=>(m=!1,w()));i&&n(E);var F=ie;return(function(P,R){if(arguments.length>0){const K=R?n(E):o&&i?Bn(P):P;return s(E,K),m=!0,u!==void 0&&(u=K),P}return kn&&m||(F.f&on)!==0?E.v:n(E)})}function En(e){be===null&&ra(),Gn&&be.l!==null?ws(be).m.push(e):Jr(()=>{const t=I(e);if(typeof t=="function")return t})}function xr(e){be===null&&ra(),En(()=>()=>I(e))}function gs(e,t,{bubbles:r=!1,cancelable:a=!1}={}){return new CustomEvent(e,{detail:t,bubbles:r,cancelable:a})}function ys(){const e=be;return e===null&&ra(),(t,r,a)=>{const o=e.s.$$events?.[t];if(o){const i=vr(o)?o.slice():[o],l=gs(t,r,a);for(const u of i)u.call(e.x,l);return!l.defaultPrevented}return!0}}function ws(e){var t=e.l;return t.u??={a:[],b:[],m:[]}}const ks="5";typeof window<"u"&&((window.__svelte??={}).v??=new Set).add(ks);bi();const gr="/api";function zr(){localStorage.removeItem("devx_authed"),window.location.reload()}async function je(e,t={}){const r=await fetch(gr+e,{...t,credentials:"same-origin",headers:{"Content-Type":"application/json",...t.headers}});if(r.status===401)throw zr(),new Error("Unauthorized");return r}async function Co(){return(await(await je("/sessions")).json()).sessions||[]}async function Es(e,t){const r=await je("/sessions",{method:"POST",body:JSON.stringify({name:e,project:t})});if(!r.ok){const a=await r.json();throw new Error(a.error||"Failed to create session")}return r.json()}async function Ss(e){const t=await je("/sessions?name="+encodeURIComponent(e),{method:"DELETE"});if(!t.ok)throw new Error(`Failed to delete session: ${t.status}`)}async function Ca(e){const t=await je("/sessions/flag?name="+encodeURIComponent(e),{method:"DELETE"});if(!t.ok)throw new Error(`Failed to unflag session: ${t.status}`)}async function Da(e,t){const r=new URLSearchParams({name:e});t!=null&&r.set("display_name",t);const a=await je("/sessions/rename?"+r.toString(),{method:"POST"});if(!a.ok){const o=await a.json().catch(()=>({}));throw new Error(o.error||"Rename failed")}}async function Ts(e,t){const r=await je("/sessions/color?name="+encodeURIComponent(e)+"&color="+encodeURIComponent(t),{method:"POST"});if(!r.ok){const a=await r.json().catch(()=>({}));throw new Error(a.error||"Color change failed")}}async function As(e){if(!(await fetch(gr+"/login",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:e})})).ok)throw new Error("Invalid token");localStorage.setItem("devx_authed","1")}async function Ia(e){return(await(await je("/windows?name="+encodeURIComponent(e))).json()).windows||[]}async function Cs(e){return(await(await je("/active-pane?name="+encodeURIComponent(e))).json()).pane}async function La(e,t){await je("/switch-window?name="+encodeURIComponent(e)+"&window="+t,{method:"POST"})}async function Ds(){return(await(await je("/projects")).json()).projects||[]}async function Is(){const e=await je("/settings");return e.ok?e.json():{artifact_trigger_key:"Ctrl+Space"}}async function Ra(e){await je("/refresh?name="+encodeURIComponent(e),{method:"POST"})}async function Do(e,t){if(e.ok)return;const r=await e.json().catch(()=>({}));throw new Error(r.error||t||`Request failed: ${e.status}`)}async function Ls(e,t){const r=await je("/send-keys?name="+encodeURIComponent(e)+"&keys="+encodeURIComponent(t),{method:"POST"});await Do(r,"Failed to send keys")}async function ja(e,t){const r=await je("/send-keys?mode=literal&name="+encodeURIComponent(e)+"&keys="+encodeURIComponent(t),{method:"POST"});await Do(r,"Failed to send text")}async function Rs(e){const t=new FormData;t.append("image",e);const r=await fetch(gr+"/upload-image",{method:"POST",credentials:"same-origin",body:t});if(r.status===401)throw zr(),new Error("Unauthorized");if(!r.ok){const a=await r.json().catch(()=>({}));throw new Error(a.error||"Upload failed")}return r.json()}async function Dn(e,t={}){const r=new URLSearchParams({session:e});for(const[i,l]of Object.entries(t))l&&r.set(i,l);const a=await je("/artifacts?"+r.toString());if(!a.ok)throw new Error(`Failed to list artifacts: ${a.status}`);return(await a.json()).artifacts||[]}async function js(e,t,{title:r="",tags:a="",retention:o="session",type:i="",summary:l=""}={}){const u=new FormData;for(const b of t||[])u.append("file",b);r&&u.append("title",r),a&&u.append("tags",a),o&&u.append("retention",o),i&&u.append("type",i),l&&u.append("summary",l);const c=await fetch(gr+"/artifacts/upload?session="+encodeURIComponent(e),{method:"POST",credentials:"same-origin",body:u});if(c.status===401)throw zr(),new Error("Unauthorized");if(!c.ok){const b=await c.json().catch(()=>({}));throw new Error(b.error||"Artifact upload failed")}return(await c.json()).artifacts||[]}async function Os(e,{title:t,text:r,format:a="md",tags:o="",retention:i="session",type:l="document"}){const u=new FormData;u.append("title",t),u.append("text",r),u.append("format",a),u.append("type",l),o&&u.append("tags",o),i&&u.append("retention",i);const c=await fetch(gr+"/artifacts/upload?session="+encodeURIComponent(e),{method:"POST",credentials:"same-origin",body:u});if(c.status===401)throw zr(),new Error("Unauthorized");if(!c.ok){const b=await c.json().catch(()=>({}));throw new Error(b.error||"Artifact creation failed")}return(await c.json()).artifacts||[]}async function Ps(e,t){const r=await je("/artifacts/archive?session="+encodeURIComponent(e)+"&id="+encodeURIComponent(t),{method:"POST"});if(!r.ok){const a=await r.json().catch(()=>({}));throw new Error(a.error||"Artifact archive failed")}return r.json()}async function Ms(e,t){const r=await je("/artifacts/item?session="+encodeURIComponent(e)+"&id="+encodeURIComponent(t),{method:"DELETE"});if(!r.ok){const a=await r.json().catch(()=>({}));throw new Error(a.error||"Artifact remove failed")}}async function Ns(e){const t=await je("/artifacts/focus?session="+encodeURIComponent(e),{method:"DELETE"});if(!t.ok)throw new Error(`Failed to clear artifact focus: ${t.status}`)}async function Fs(e,t,r){const a=await je("/artifacts/rename?session="+encodeURIComponent(e)+"&id="+encodeURIComponent(t),{method:"POST",body:JSON.stringify(r)});if(!a.ok){const o=await a.json().catch(()=>({}));throw new Error(o.error||"Artifact rename failed")}return a.json()}async function Us(e){const t=await je("/share-intents/"+encodeURIComponent(e));if(!t.ok){const r=await t.json().catch(()=>({}));throw new Error(r.error||"Shared content not found")}return t.json()}async function zs(e,t){const r=await je("/share-intents/"+encodeURIComponent(e),{method:"POST",body:JSON.stringify(t)});if(!r.ok){const o=await r.json().catch(()=>({}));throw new Error(o.error||"Failed to create artifact from shared content")}return(await r.json()).artifacts||[]}function Ks(){return!!localStorage.getItem("devx_authed")}function Vs(e){const t=new EventSource("/api/events",{withCredentials:!0});for(const[r,a]of Object.entries(e))t.addEventListener(r,o=>{try{a(JSON.parse(o.data))}catch{}});return()=>t.close()}function qs(){typeof Notification>"u"||Notification.permission==="default"&&Notification.requestPermission().catch(()=>{})}function Bs(e,{onNavigate:t,showFallback:r}){const{session:a,reason:o}=e;if(!document.hasFocus()&&typeof Notification<"u"&&Notification.permission==="granted"){const l=new Notification("devx ◆",{body:`${a} — ${o}`,tag:`devx-flag-${a}`});l.onclick=()=>{window.focus(),t(a),l.close()}}else r(e)}var Hs=D('

'),Ws=D(`
devx
enter access token
`);function Zs(e,t){ft(t,!1);let r=y(""),a=y(""),o=y(!1);async function i(){s(o,!0),s(a,"");try{await As(n(r)),window.location.reload()}catch(C){s(a,C.message)}finally{s(o,!1)}}kt();var l=Ws(),u=h(l),c=h(u),f=p(h(c),4),b=h(f);ur(b);var _=p(b,2);{var d=C=>{var m=Hs(),E=h(m);V(()=>Z(E,n(a))),A(C,m)};q(_,C=>{n(a)&&C(d)})}var S=p(_,2),w=h(S);V(()=>{S.disabled=n(o),Z(w,n(o)?"authenticating...":"[ sign in ]")}),_t(b,()=>n(r),C=>s(r,C)),T("submit",f,Or(i)),A(e,l),ut()}var Ys=D(""),$s=D(`
`),Xs=D('

'),Js=D(``);function Gs(e,t){ft(t,!1);const r=ys(),a="devx_last_project";let o=y(""),i=y(localStorage.getItem(a)||""),l=y([]),u=y(""),c=y(!1),f=y();En(async()=>{n(f)?.focus();try{s(l,await Ds()),n(i)&&!n(l).includes(n(i))&&s(i,""),!n(i)&&n(l).length===1&&s(i,n(l)[0])}catch{}});async function b(){if(!n(o).trim()){s(u,"session name is required");return}s(c,!0),s(u,"");try{await Es(n(o).trim(),n(i)||void 0),n(i)&&localStorage.setItem(a,n(i)),r("created"),r("close")}catch(M){s(u,M.message)}finally{s(c,!1)}}kt();var _=Js(),d=h(_),S=h(d),w=p(h(S),2),C=p(S,2),m=h(C),E=p(h(m),2);Xn(E,M=>s(f,M),()=>n(f));var F=p(m,2);{var P=M=>{var xe=$s(),ee=p(h(xe),2),U=h(ee),ce=h(U);ce.value=ce.__value="";var ye=p(ce);gt(ye,1,()=>n(l),Pt,(te,we)=>{var Se=Ys(),ke=h(Se),pe={};V(()=>{Z(ke,n(we)),pe!==(pe=n(we))&&(Se.value=(Se.__value=n(we))??"")}),A(te,Se)}),xn(U,()=>n(i),te=>s(i,te)),T("keydown",U,te=>{te.key==="Enter"&&(te.preventDefault(),b())}),A(M,xe)};q(F,M=>{n(l).length>0&&M(P)})}var R=p(F,2);{var K=M=>{var xe=Xs(),ee=h(xe);V(()=>Z(ee,n(u))),A(M,xe)};q(R,M=>{n(u)&&M(K)})}var Q=p(R,2),j=h(Q),k=p(j,2),H=h(k);V(()=>{k.disabled=n(c),Z(H,n(c)?"creating...":"[ create ]")}),T("click",w,()=>r("close")),_t(E,()=>n(o),M=>s(o,M)),T("click",j,()=>r("close")),T("submit",C,Or(b)),T("click",_,bs(()=>r("close"))),T("keydown",_,M=>M.key==="Escape"&&r("close")),A(e,_),ut()}var Qs=D(''),el=D('
'),tl=D('
loading...
'),nl=D('

could not load sessions

'),rl=D('

no active sessions

',1),al=D('
'),ol=D('
no matches
'),il=D('
'),sl=D(''),ll=D(' '),cl=D(' '),fl=D(' '),ul=D(''),dl=D(``),vl=D(' '),pl=D('
'),hl=D(`
`,1),ml=D('
'),_l=D('
devx
/
',1);function bl(e,t){ft(t,!1);const r=y(),a=y(),o=y(),i=y();let l=B(t,"onOpenTerminal",8),u=B(t,"activeSessionName",8,null),c=B(t,"onDeleteSession",8,null),f=B(t,"refreshTrigger",8,0),b=B(t,"flashSession",8,null),_=y([]),d=y(!0),S=y(!1),w=y(""),C=y(""),m=y(0),E=y(!1),F=y(),P=y(null),R=y(null),K=y(null),Q=y("");const j={red:"#ef4444",blue:"#3b82f6",green:"#22c55e",yellow:"#eab308",purple:"#a855f7",orange:"#f97316",pink:"#ec4899",cyan:"#06b6d4"},k=["red","blue","green","yellow","purple","orange","pink","cyan"];function H(g){s(K,g.name),s(Q,g.display_name||g.name)}async function M(g){const O=n(Q).trim();try{O===""||O===g.name?await Da(g.name,null):await Da(g.name,O),await U({background:!0})}catch(Y){s(w,Y.message)}s(K,null)}function xe(){s(K,null)}async function ee(g){const O=k.indexOf(g.color||"blue"),Y=k[(O+1)%k.length];try{await Ts(g.name,Y),g.color=Y,s(_,[...n(_)])}catch(se){s(w,se.message)}}async function U({background:g=!1}={}){g||s(d,!0),s(w,"");try{s(_,await Co())}catch(O){s(w,O.message)}finally{g||s(d,!1)}}function ce(){s(S,!0)}const ye=5e3;En(()=>{U();let g=null;function O(){g=setInterval(()=>{document.hidden||U({background:!0})},ye)}function Y(){document.hidden||U({background:!0})}return O(),document.addEventListener("visibilitychange",Y),window.addEventListener("devx:focusSessionList",te),window.addEventListener("devx:newSession",ce),()=>{clearInterval(g),document.removeEventListener("visibilitychange",Y),window.removeEventListener("devx:focusSessionList",te),window.removeEventListener("devx:newSession",ce)}});function te(){const g=n(o).findIndex(O=>O.name===u());g>=0&&s(m,g),n(F)?.focus(),n(F)?.select()}let we=y(f());function Se(g){s(C,""),s(m,0),l()(g)}function ke(g){const O=(document.activeElement?.tagName==="INPUT"||document.activeElement?.tagName==="TEXTAREA")&&document.activeElement!==n(F);g.key==="ArrowDown"&&!O?(g.preventDefault(),s(m,Math.min(n(m)+1,n(o).length-1))):g.key==="ArrowUp"&&!O?(g.preventDefault(),s(m,Math.max(n(m)-1,0))):g.key==="Enter"&&!O?n(o)[n(m)]&&Se(n(o)[n(m)]):g.key==="Escape"?(s(C,""),n(F)?.blur()):g.key==="/"&&!O&&document.activeElement!==n(F)||g.ctrlKey&&g.shiftKey&&(g.key==="s"||g.key==="S")?(g.preventDefault(),te()):g.ctrlKey&&g.shiftKey&&(g.key==="c"||g.key==="C")&&(g.preventDefault(),n(S)||s(S,!0))}function pe(g){const O={};for(const[Y,se]of Object.entries(g.routes||{}))O[Y]=se.startsWith("http")?se:"https://"+se;for(const[Y,se]of Object.entries(g.external_routes||{}))O[Y]="https://"+se;return O}async function X(g){if(n(R)!==g.name){s(R,g.name),setTimeout(()=>{n(R)===g.name&&s(R,null)},3e3);return}s(R,null),s(w,"");try{await Ss(g.name),g.name===u()&&c()?.(),await U()}catch(O){s(w,O.message||"Delete failed")}}ve(()=>(n(_),n(C)),()=>{s(r,n(_).filter(g=>!n(C)||g.name.toLowerCase().includes(n(C).toLowerCase())||g.display_name&&g.display_name.toLowerCase().includes(n(C).toLowerCase())))}),ve(()=>n(r),()=>{s(a,(()=>{const g={};for(const O of n(r)){const Y=O.project_alias||"";g[Y]||(g[Y]=[]),g[Y].push(O)}return Object.entries(g).sort(([O],[Y])=>O===""?1:Y===""?-1:O.localeCompare(Y))})())}),ve(()=>n(a),()=>{s(o,n(a).flatMap(([,g])=>g))}),ve(()=>(re(f()),n(we)),()=>{f()!==n(we)&&(s(we,f()),U({background:!0}))}),ve(()=>n(C),()=>{n(C),s(m,0)}),ve(()=>(n(E),n(C)),()=>{s(i,n(E)||n(C).length>0)}),er(),kt();var Ie=_l();T("keydown",Ir,ke);var Ne=jt(Ie),it=h(Ne),fn=p(h(it),2),Et=p(it,2),$e=p(h(Et),2);Xn($e,g=>s(F,g),()=>n(F));var St=p($e,2);{var Tt=g=>{var O=Qs();T("click",O,()=>{s(C,""),n(F)?.focus()}),A(g,O)};q(St,g=>{n(C)&&g(Tt)})}var dt=p(Et,2);{var vt=g=>{var O=el(),Y=h(O),se=h(Y),Le=p(Y,2);V(()=>Z(se,n(w))),T("click",Le,()=>s(w,"")),A(g,O)};q(dt,g=>{n(w)&&g(vt)})}var Nt=p(dt,2),At=h(Nt);{var Sn=g=>{var O=tl();A(g,O)},Ht=g=>{var O=al(),Y=h(O);{var se=Te=>{var Xe=nl();A(Te,Xe)},Le=Te=>{var Xe=rl(),We=p(jt(Xe),2);T("click",We,()=>s(S,!0)),A(Te,Xe)};q(Y,Te=>{n(w)?Te(se):Te(Le,!1)})}A(g,O)},Wt=g=>{var O=ol();A(g,O)},Tn=g=>{var O=$n(),Y=jt(O);gt(Y,1,()=>n(a),Pt,(se,Le)=>{var Te=rn(()=>pa(n(Le),2));let Xe=()=>n(Te)[0],We=()=>n(Te)[1];var Zt=ml(),Ct=h(Zt);{var un=Qe=>{var N=il(),Dt=h(N);V(()=>Z(Dt,Xe())),A(Qe,N)};q(Ct,Qe=>{Xe()&&Qe(un)})}var Yt=p(Ct,2);gt(Yt,1,We,Qe=>Qe.name,(Qe,N)=>{const Dt=ht(()=>(n(N),re(u()),I(()=>n(N).name===u()))),Kn=ht(()=>(n(o),n(N),I(()=>n(o).indexOf(n(N))))),Vn=ht(()=>n(Kn)===n(m)),$t=ht(()=>(n(N),I(()=>pe(n(N))))),dn=ht(()=>(re(n($t)),I(()=>Object.keys(n($t)).length>0))),Xt=ht(()=>n(i)&&n(Vn)),Ft=ht(()=>(n(N),re(b()),I(()=>n(N).name===b())));var Jt=hl(),Gt=jt(Jt);let qn;var Qt=h(Gt),vn=h(Qt),x=p(vn,2);{var z=J=>{var G=sl();ur(G),_t(G,()=>n(Q),he=>s(Q,he)),T("click",G,sr(function(he){Ao.call(this,t,he)})),T("keydown",G,sr(he=>{he.key==="Enter"?(he.target.blur(),M(n(N))):he.key==="Escape"&&xe()})),T("blur",G,()=>{setTimeout(()=>{n(K)===n(N).name&&xe()},0)}),A(J,G)},de=J=>{var G=cl(),he=h(G),Oe=p(he);{var ze=Ke=>{var Ve=ll(),tt=h(Ve);V(()=>Z(tt,`(${n(N),I(()=>n(N).name)??""})`)),A(Ke,Ve)};q(Oe,Ke=>{n(N),I(()=>n(N).display_name&&n(N).display_name!==n(N).name)&&Ke(ze)})}V(()=>Z(he,`${n(N),I(()=>n(N).display_name||n(N).name)??""} `)),T("click",G,()=>l()(n(N))),T("dblclick",G,sr(()=>H(n(N)))),A(J,G)};q(x,J=>{n(K),n(N),I(()=>n(K)===n(N).name)?J(z):J(de,!1)})}var Ee=p(x,2);{var Ae=J=>{var G=fl(),he=h(G);V(()=>{_e(G,"title",(n(N),I(()=>`${n(N).artifact_count} artifact${n(N).artifact_count===1?"":"s"}`))),Z(he,`◆ ${n(N),I(()=>n(N).artifact_count)??""}`)}),A(J,G)};q(Ee,J=>{n(N),I(()=>n(N).artifact_count>0)&&J(Ae)})}var Ze=p(Ee,2);{var Je=J=>{var G=ul();A(J,G)};q(Ze,J=>{n(N),I(()=>n(N).attention_flag)&&J(Je)})}var Ye=p(Qt,2),Fe=h(Ye);{var ge=J=>{var G=dl();T("click",G,()=>s(P,n(P)===n(N).name?null:n(N).name)),A(J,G)};q(Fe,J=>{n(dn)&&J(ge)})}var me=p(Fe,2),Ue=h(me),et=p(Gt,2);{var Ce=J=>{var G=pl(),he=h(G);gt(he,1,()=>(re(n($t)),I(()=>Object.entries(n($t)))),Pt,(ze,Ke)=>{var Ve=rn(()=>pa(n(Ke),2));let tt=()=>n(Ve)[0],st=()=>n(Ve)[1];var Me=vl(),en=p(h(Me),2),Ut=h(en),tn=p(en,2),pn=h(tn);V(hn=>{_e(Me,"href",st()),Z(Ut,tt()),Z(pn,hn)},[()=>(st(),I(()=>st().replace("https://","")))]),A(ze,Me)});var Oe=p(he,2);T("click",Oe,()=>s(P,null)),A(J,G)};q(et,J=>{n(P),n(N),I(()=>n(P)===n(N).name)&&J(Ce)})}V(()=>{qn=Vt(Gt,1,` - group flex items-stretch border-l-2 transition-colors - ${n(Dt)?"bg-cyan-950/30 border-cyan-500":n(Xt)?"bg-gray-800/30 border-gray-600":"hover:bg-[#0d1117] border-transparent"} - `,"svelte-1idoovf",qn,{"flag-flash":n(Ft)}),Vt(Qt,1,` - flex-1 flex items-center gap-2 - pl-4 pr-2 py-3 lg:py-2 - font-mono text-sm lg:text-xs - min-w-0 - ${n(Dt)?"text-cyan-300":n(Xt)?"text-gray-200":"text-gray-500 hover:text-gray-200"} - `,"svelte-1idoovf"),jr(vn,`color: ${n(N),I(()=>j[n(N).color]||j.blue)??""}`),Vt(me,1,` - font-mono - ${n(R),n(N),I(()=>n(R)===n(N).name?"text-red-400":"text-red-700 hover:text-red-400 active:text-red-300")??""} - text-lg lg:text-[10px] - px-3 lg:px-1.5 py-4 lg:py-1.5 - transition-colors - `,"svelte-1idoovf"),_e(me,"title",(n(R),n(N),I(()=>n(R)===n(N).name?"click again to confirm":"delete"))),_e(me,"aria-label",(n(R),n(N),I(()=>n(R)===n(N).name?`confirm delete ${n(N).name}`:`delete ${n(N).name}`))),Z(Ue,(n(R),n(N),I(()=>n(R)===n(N).name?"!×":"×")))}),T("click",vn,sr(()=>ee(n(N)))),T("click",me,()=>X(n(N))),A(Qe,Jt)}),A(se,Zt)}),A(g,O)};q(At,g=>{n(d)?g(Sn):(n(_),I(()=>n(_).length===0)?g(Ht,1):(n(r),I(()=>n(r).length===0)?g(Wt,2):g(Tn,!1)))})}var ne=p(Ne,2);{var ue=g=>{Gs(g,{$$events:{close:()=>s(S,!1),created:U}})};q(ne,g=>{n(S)&&g(ue)})}T("click",fn,()=>s(S,!0)),_t($e,()=>n(C),g=>s(C,g)),T("focus",$e,()=>s(E,!0)),T("blur",$e,()=>s(E,!1)),A(e,Ie),ut()}var xl=D(``),gl=D('
'),yl=D('
');function wl(e,t){ft(t,!1);let r=B(t,"onKey",8);const a=[{label:"C-b",key:"C-b"},{label:"C-c",key:"C-c"},{label:"Esc",key:"Escape"},{label:"Tab",key:"Tab"},{label:"C-z",key:"C-z"}],o=[{label:"↑",key:"Up"},{label:"↓",key:"Down"},{label:"←",key:"Left"},{label:"→",key:"Right"}],i=[{label:"S-Tab",key:"BTab"},{label:"C-o",key:"C-o"},{label:"C-b C-b",key:"C-b C-b"}];kt();var l=yl();gt(l,5,()=>[a,o,i],Pt,(u,c)=>{var f=gl();gt(f,5,()=>n(c),Pt,(b,_)=>{var d=xl(),S=h(d);V(()=>Z(S,(n(_),I(()=>n(_).label)))),T("click",d,()=>r()(n(_).key)),A(b,d)}),A(u,f)}),A(e,l),ut()}var kl=D('Open ↗'),El=D('
'),Sl=D('
'),Tl=D('
');function Io(e,t){ft(t,!1);const r=y();let a=B(t,"upload",8,null),o=B(t,"error",8,null),i=B(t,"onDismiss",8),l=B(t,"sticky",8,!1),u=y();xr(()=>{clearTimeout(n(u))}),ve(()=>re(a()),()=>{s(r,a()?.path?a().path.split("/").slice(-2).join("/"):"")}),ve(()=>(n(u),re(l()),re(a()),re(o()),re(i())),()=>{clearTimeout(n(u)),!l()&&(a()||o())&&s(u,setTimeout(i(),3e3))}),er(),kt();var c=Tl(),f=h(c);{var b=d=>{var S=El(),w=h(S),C=p(w,2),m=h(C),E=h(m),F=p(m,2),P=h(F);{var R=j=>{var k=kl();V(()=>_e(k,"href",(re(a()),I(()=>a().url)))),T("click",k,function(...H){i()?.apply(this,H)}),A(j,k)},K=j=>{var k=ts("inserted");A(j,k)};q(P,j=>{re(a()),I(()=>a()?.url)?j(R):j(K,!1)})}var Q=p(C,2);V(()=>{_e(w,"src",(re(a()),I(()=>a().objectURL))),Z(E,n(r))}),T("click",Q,function(...j){i()?.apply(this,j)}),A(d,S)},_=d=>{var S=Sl(),w=h(S),C=h(w),m=h(C),E=p(w,2);V(()=>Z(m,o())),T("click",E,function(...F){i()?.apply(this,F)}),A(d,S)};q(f,d=>{a()?d(b):o()&&d(_,1)})}A(e,c),ut()}var Al=D('
drop files to create artifacts
html · md · jsx · images · video · logs · pdf
'),Cl=D('
'),Dl=D('
loading…
'),Il=D('
No artifacts — drop files, paste text, or click [upload]/[new].
'),Ll=D(''),Rl=D(''),jl=D('
',1),Ol=D(''),Pl=D('
loading preview…
'),Ml=D('
'),Nl=D('
',2),Fl=D('
 
'),Ul=D('
 
'),zl=D(''),Kl=D('
loading JSX preview…
'),Vl=D(''),ql=D(''),Bl=D('
No inline preview. Open artifact
'),Hl=D('
',1),Wl=D('
New text artifact
Esc cancels · ⌘/Ctrl+Enter creates · Shift+Enter inserts newline
'),Zl=D('
Edit artifact
'),Yl=D(''),$l=D('
');function Oa(e,t){ft(t,!1);const r=y(),a=y();let o=B(t,"session",8),i=B(t,"selectedArtifactID",8,null),l=B(t,"pasteArtifactNonce",8,0),u=B(t,"fullScreen",8,!1),c=B(t,"onToggleFullScreen",8,()=>{}),f=B(t,"onInsert",8,()=>{}),b=B(t,"onClose",8,()=>{}),_=y([]),d=y(null),S=y(),w=y(!1),C=y(!1),m=y(""),E=y(""),F=y("preview"),P=y(!1),R=y(!1),K=y(220),Q=y(!1),j=y(null),k=y(!1),H=y(!1),M=y(""),xe=y(""),ee=y("md"),U=y(""),ce=y("session"),ye=y(!1),te=y(""),we=y(""),Se=y(""),ke=y("session"),pe=y(l());async function X(){if(o()?.name){s(w,!0);try{s(_,await Dn(o().name)),i()&&s(d,n(_).find(x=>x.id===i())||n(d)),!n(d)&&n(_).length&&s(d,n(_).find(x=>x.focus)||n(_)[0]),n(d)&&s(d,n(_).find(x=>x.id===n(d).id)||n(_)[0]||null),s(m,""),await it()}catch(x){s(m,x.message||"Failed to load artifacts")}finally{s(w,!1)}}}function Ie(x=n(d)){if(!x)return"other";const z=x.file?.split(".").pop()?.toLowerCase()||"";return x.type==="screenshot"||["png","jpg","jpeg","gif","webp"].includes(z)?"image":x.type==="recording"||["webm","mp4","mov"].includes(z)?"video":["jsx","tsx"].includes(z)?"jsx":["log","diff"].includes(x.type)||["md","txt","log","diff","patch","js","ts","css","json"].includes(z)?"text":["html","htm"].includes(z)||["plan","report"].includes(x.type)?"html":["pdf"].includes(z)||["document"].includes(x.type)?"iframe":"other"}function Ne(x=n(d)){const z=Ie(x);return(z==="text"||z==="iframe")&&Tt(n(E))?"jsx":z}async function it(){if(s(E,""),!n(d))return;s(C,!0);const x=Ie(n(d));try{if(x==="text"||x==="jsx"){const z=await fetch(n(d).url,{credentials:"same-origin"});s(E,await z.text())}}catch{x==="text"&&s(E,"Failed to load preview.")}finally{s(C,!1)}}async function fn(x){s(d,x),s(m,""),s(j,i()),await it()}async function Et(x){if(x?.length)try{const z=await js(o().name,Array.from(x));s(_,await Dn(o().name)),s(d,n(_).find(de=>de.id===z[0]?.id)||n(_)[0]||null),s(R,!1),await it(),s(m,"")}catch(z){s(m,z.message||"Upload failed")}}function $e(x=""){s(M,""),s(xe,x),s(ee,Tt(x)?"jsx":St(x)?"html":"md"),s(U,""),s(ce,"session"),s(H,!0)}function St(x){return/<\s*(html|body|div|p|h1|h2|section|article|span|table|ul|ol|pre|code)[\s>]/i.test(x||"")}function Tt(x){return/export\s+default|function\s+\w+\s*\([^)]*\)\s*{[\s\S]*return\s*\(|const\s+[A-Z][A-Za-z0-9_$]*\s*=\s*(\([^)]*\)|[^=]+)\s*=>|className=|<>|<\/[A-Z][A-Za-z0-9]*>|<[A-Z][A-Za-z0-9]*[\s/>]/.test(x||"")}function dt(){const x=new Date,z=de=>String(de).padStart(2,"0");return`Pasted text ${x.getFullYear()}-${z(x.getMonth()+1)}-${z(x.getDate())} ${z(x.getHours())}:${z(x.getMinutes())}`}async function vt(){const x=n(xe).trim();if(!x){s(m,"Text artifact content is required");return}try{const z=n(M).trim()||dt(),de=["html","md","jsx","tsx"].includes(n(ee))?"document":"log",Ee=await Os(o().name,{title:z,text:x,format:n(ee),tags:n(U),retention:n(ce),type:de});s(_,await Dn(o().name)),s(d,n(_).find(Ae=>Ae.id===Ee[0]?.id)||n(_)[0]||null),s(H,!1),s(R,!1),await it(),s(m,"")}catch(z){s(m,z.message||"Create failed")}}function Nt(){s(H,!1)}function At(x){x.key==="Escape"?(x.preventDefault(),Nt(),s(ye,!1)):(x.metaKey||x.ctrlKey)&&x.key==="Enter"&&(x.preventDefault(),n(H)?vt():Ht())}function Sn(){n(d)&&(s(te,n(d).title||""),s(we,n(d).summary||""),s(Se,(n(d).tags||[]).join(", ")),s(ke,n(d).retention||"session"),s(ye,!0))}async function Ht(){if(n(d))try{s(d,await Fs(o().name,n(d).id,{title:n(te).trim()||n(d).title,summary:n(we),tags:n(Se).split(",").map(x=>x.trim()).filter(Boolean),retention:n(ke)})),s(_,await Dn(o().name)),s(d,n(_).find(x=>x.id===n(d).id)||n(d)),s(ye,!1),s(m,"")}catch(x){s(m,x.message||"Rename failed")}}async function Wt(){if(n(d))try{s(d,await Ps(o().name,n(d).id)),s(_,await Dn(o().name)),s(m,"")}catch(x){s(m,x.message||"Archive failed")}}async function Tn(){if(!(!n(d)||!confirm(`Remove artifact "${n(d).title}"?`)))try{await Ms(o().name,n(d).id),s(_,await Dn(o().name)),s(d,n(_)[0]||null),await it(),s(m,"")}catch(x){s(m,x.message||"Remove failed")}}function ne(x){return` - - - - - - - -
- - + +
From 9203d479a030c31df8152aa06286b584581f49ff Mon Sep 17 00:00:00 2001 From: Jon Fox Date: Sun, 10 May 2026 06:51:48 -0700 Subject: [PATCH 2/2] Normalize artifact folders in manifests --- artifact/manifest.go | 18 +++++++++++++++++- artifact/manifest_test.go | 34 +++++++++++++++++++++++++++++++++- cmd/artifact_add.go | 6 ++++-- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/artifact/manifest.go b/artifact/manifest.go index f5ddb88..90335c6 100644 --- a/artifact/manifest.go +++ b/artifact/manifest.go @@ -156,7 +156,11 @@ func SaveManifest(sess *session.Session, m *Manifest) error { func ValidateManifest(m *Manifest) error { seen := map[string]bool{} - for _, a := range m.Artifacts { + for i := range m.Artifacts { + if err := normalizeArtifact(&m.Artifacts[i]); err != nil { + return err + } + a := m.Artifacts[i] if err := ValidateArtifact(a); err != nil { return err } @@ -168,6 +172,18 @@ func ValidateManifest(m *Manifest) error { return nil } +func normalizeArtifact(a *Artifact) error { + if strings.TrimSpace(a.Folder) == "" { + return nil + } + folder, err := NormalizeFolderPath(a.Folder) + if err != nil { + return fmt.Errorf("invalid artifact folder %q: %w", a.Folder, err) + } + a.Folder = folder + return nil +} + func ValidateArtifact(a Artifact) error { if strings.TrimSpace(a.ID) == "" { return fmt.Errorf("artifact id is required") diff --git a/artifact/manifest_test.go b/artifact/manifest_test.go index ae45a9a..e41822b 100644 --- a/artifact/manifest_test.go +++ b/artifact/manifest_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "reflect" + "runtime" "sync" "testing" "time" @@ -60,7 +61,11 @@ func TestLoadManifestRejectsMalformedJSON(t *testing.T) { } func TestValidateRelativePathRejectsTraversal(t *testing.T) { - bad := []string{"", "../secret", "a/../../secret", "/tmp/file", ".."} + absPath := "/tmp/file" + if runtime.GOOS == "windows" { + absPath = `C:\tmp\file` + } + bad := []string{"", "../secret", "a/../../secret", absPath, ".."} for _, p := range bad { if err := ValidateRelativePath(p); err == nil { t.Fatalf("expected %q to be rejected", p) @@ -117,6 +122,33 @@ func TestLoadManifestMissingFolderIsBackwardCompatible(t *testing.T) { } } +func TestManifestCanonicalizesFolderOnLoadAndSave(t *testing.T) { + sess := testSession(t) + m := NewManifest(sess.Name) + m.Artifacts = append(m.Artifacts, Artifact{ + ID: "doc-plan", + Type: "document", + Title: "Plan", + File: "workflow/run-1/plan.md", + Folder: `workflow\run-1`, + Created: time.Date(2026, 4, 25, 10, 30, 0, 0, time.UTC), + Retention: DefaultRetention, + }) + if err := SaveManifest(sess, m); err != nil { + t.Fatalf("SaveManifest: %v", err) + } + if got := m.Artifacts[0].Folder; got != "workflow/run-1" { + t.Fatalf("SaveManifest should normalize folder, got %q", got) + } + loaded, err := LoadManifest(sess) + if err != nil { + t.Fatalf("LoadManifest: %v", err) + } + if len(loaded.Artifacts) != 1 || loaded.Artifacts[0].Folder != "workflow/run-1" { + t.Fatalf("LoadManifest should preserve normalized folder: %#v", loaded.Artifacts) + } +} + func TestSafeJoinStaysInsideBase(t *testing.T) { base := t.TempDir() joined, err := SafeJoin(base, "logs/test.log") diff --git a/cmd/artifact_add.go b/cmd/artifact_add.go index 60d139d..eecf33a 100644 --- a/cmd/artifact_add.go +++ b/cmd/artifact_add.go @@ -29,8 +29,10 @@ var artifactAddFlags struct { var artifactAddCmd = &cobra.Command{ Use: "add [flags] ", Short: "Add a file to the current session's artifacts", - Args: cobra.ExactArgs(1), - RunE: runArtifactAdd, + Example: ` devx artifact add --title "QA notes" docs/qa-notes.md + printf '# Plan\n' | devx artifact add --folder workflow/run-42 --file 10-plan.md --title "Plan" -`, + Args: cobra.ExactArgs(1), + RunE: runArtifactAdd, } func init() {