@agimon-ai/doompi-workflow 0.0.1-alpha.113 → 0.0.1-alpha.115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1136,9 +1136,15 @@ var apiRoutes_default = defineApiRoutes({
1136
1136
  path: "/launch",
1137
1137
  response: apiResponse()
1138
1138
  },
1139
+ openRunDirectory: {
1140
+ method: "POST",
1141
+ path: `${RUN}/open-directory`,
1142
+ response: apiResponse()
1143
+ },
1139
1144
  artifacts: {
1140
1145
  method: "GET",
1141
1146
  path: `${RUN}/artifacts`,
1147
+ query: ["directory"],
1142
1148
  response: apiResponse()
1143
1149
  },
1144
1150
  /** One artifact: its metadata and text, or its raw bytes under `raw=1`. */
@@ -1331,8 +1337,17 @@ async function steerStep(workspace, runKey, message, sessionId, step) {
1331
1337
  if (result.data.delivered === true) return { result: { delivered: true } };
1332
1338
  return { error: "The workflow hub returned an invalid steer response." };
1333
1339
  }
1334
- async function fetchArtifacts(workspace, runKey, sessionId) {
1335
- const result = await scoped(sessionId).artifacts({ params: runParams(workspace, runKey) });
1340
+ /** Opens the current registry folder, never a browser-supplied path. */
1341
+ async function openRunDirectory(workspace, runKey, sessionId) {
1342
+ const result = await scoped(sessionId).openRunDirectory({ params: runParams(workspace, runKey) });
1343
+ if (!result.ok) return { error: messageOf(result, "The run folder could not be opened in Finder.") };
1344
+ return result.data.opened === true ? {} : { error: "The workflow hub returned an invalid folder response." };
1345
+ }
1346
+ async function fetchArtifacts(workspace, runKey, sessionId, directory) {
1347
+ const result = await scoped(sessionId).artifacts({
1348
+ params: runParams(workspace, runKey),
1349
+ query: { directory }
1350
+ });
1336
1351
  if (!result.ok) return { error: messageOf(result, "This run has no directory to read.") };
1337
1352
  return { artifacts: result.data };
1338
1353
  }
@@ -1700,13 +1715,7 @@ function ArtifactViewerPanel({ workspace, runKey, path, sessionId }) {
1700
1715
  ]
1701
1716
  });
1702
1717
  }
1703
- /**
1704
- * The run directory, as the run pane's second tab.
1705
- *
1706
- * The workflow's own declaration leads, in the order it was written and
1707
- * including entries no job has produced yet, because the declaration is what
1708
- * the run is for. What the folder holds besides that follows.
1709
- */
1718
+ /** The run's file tree, including declared outputs that have not been written yet. */
1710
1719
  function ArtifactsPane({ run, sessionId, onOpen }) {
1711
1720
  const [listing, setListing] = useState();
1712
1721
  const [error, setError] = useState();
@@ -1743,56 +1752,163 @@ function ArtifactsPane({ run, sessionId, onOpen }) {
1743
1752
  return /* @__PURE__ */ jsx(ArtifactList, {
1744
1753
  listing,
1745
1754
  error,
1746
- onOpen
1755
+ onLoadDirectory: (directory) => fetchArtifacts(run.workspace, run.runKey, sessionId, directory),
1756
+ onOpen,
1757
+ onOpenDirectory: () => openRunDirectory(run.workspace, run.runKey, sessionId)
1747
1758
  });
1748
1759
  }
1749
- /**
1750
- * The run directory's listing, as read: declared entries first, in the order the
1751
- * workflow wrote them, then whatever else the folder holds.
1752
- */
1753
- function ArtifactList({ listing, error, onOpen }) {
1754
- const declared = listing?.artifacts.filter((entry) => entry.declared) ?? [];
1755
- const found = listing?.artifacts.filter((entry) => !entry.declared) ?? [];
1756
- const Row = ({ entry }) => {
1760
+ /** Directory-first file tree with declared output metadata and lazy folder reads. */
1761
+ function ArtifactList({ listing, error, onOpen, onOpenDirectory, onLoadDirectory }) {
1762
+ const [opening, setOpening] = useState(false);
1763
+ const [openError, setOpenError] = useState();
1764
+ const openDirectory = async () => {
1765
+ if (onOpenDirectory === void 0 || opening) return;
1766
+ setOpening(true);
1767
+ setOpenError(void 0);
1768
+ try {
1769
+ const result = await onOpenDirectory();
1770
+ setOpenError(result.error);
1771
+ } catch {
1772
+ setOpenError("The run folder could not be opened in Finder.");
1773
+ } finally {
1774
+ setOpening(false);
1775
+ }
1776
+ };
1777
+ const [expanded, setExpanded] = useState(/* @__PURE__ */ new Set());
1778
+ const [loaded, setLoaded] = useState({});
1779
+ const [folderErrors, setFolderErrors] = useState({});
1780
+ const [loading, setLoading] = useState(/* @__PURE__ */ new Set());
1781
+ const entries = /* @__PURE__ */ new Map();
1782
+ for (const entry of [...Object.values(loaded).flat(), ...listing?.artifacts ?? []]) entries.set(entry.path, entry);
1783
+ for (const entry of entries.values()) {
1784
+ const parts = entry.path.split("/");
1785
+ parts.pop();
1786
+ while (parts.length > 0) {
1787
+ const parent = parts.join("/");
1788
+ if (!entries.has(parent)) entries.set(parent, {
1789
+ path: parent,
1790
+ kind: "directory",
1791
+ description: "",
1792
+ producedBy: [],
1793
+ declared: false,
1794
+ state: entry.state === "pending" ? "pending" : "written"
1795
+ });
1796
+ parts.pop();
1797
+ }
1798
+ }
1799
+ const toggle = async (entry) => {
1800
+ const closing = expanded.has(entry.path);
1801
+ setExpanded((current) => {
1802
+ const next = new Set(current);
1803
+ if (closing) next.delete(entry.path);
1804
+ else next.add(entry.path);
1805
+ return next;
1806
+ });
1807
+ if (closing || loading.has(entry.path) || onLoadDirectory === void 0 || entry.state === "pending") return;
1808
+ setLoading((current) => new Set(current).add(entry.path));
1809
+ try {
1810
+ const result = await onLoadDirectory(entry.path);
1811
+ if ("error" in result) setFolderErrors((current) => ({
1812
+ ...current,
1813
+ [entry.path]: result.error
1814
+ }));
1815
+ else {
1816
+ setLoaded((current) => ({
1817
+ ...current,
1818
+ [entry.path]: result.artifacts.artifacts
1819
+ }));
1820
+ setFolderErrors((current) => ({
1821
+ ...current,
1822
+ [entry.path]: ""
1823
+ }));
1824
+ }
1825
+ } catch {
1826
+ setFolderErrors((current) => ({
1827
+ ...current,
1828
+ [entry.path]: "The folder cannot be read."
1829
+ }));
1830
+ } finally {
1831
+ setLoading((current) => {
1832
+ const next = new Set(current);
1833
+ next.delete(entry.path);
1834
+ return next;
1835
+ });
1836
+ }
1837
+ };
1838
+ const renderRow = (entry) => {
1757
1839
  const glyph = STATE_GLYPH[entry.state];
1758
- const openable = entry.kind === "file" && entry.state !== "pending";
1840
+ const openable = entry.kind === "directory" || entry.state !== "pending";
1759
1841
  return /* @__PURE__ */ jsxs("button", {
1760
1842
  type: "button",
1761
1843
  "data-testid": `artifact-row-${entry.path}`,
1762
1844
  "data-artifact-state": entry.state,
1763
1845
  disabled: !openable,
1764
- onClick: () => onOpen(entry.path),
1846
+ "aria-expanded": entry.kind === "directory" ? expanded.has(entry.path) : void 0,
1847
+ onClick: () => entry.kind === "directory" ? void toggle(entry) : onOpen(entry.path),
1765
1848
  className: `flex flex-col gap-0.5 px-3 py-1.5 text-left ${openable ? "cursor-pointer hover:bg-doom-panel" : "cursor-default"}`,
1766
1849
  children: [/* @__PURE__ */ jsxs("span", {
1767
1850
  className: "flex min-w-0 items-center gap-2",
1768
1851
  children: [
1769
1852
  /* @__PURE__ */ jsx("span", {
1770
1853
  className: `w-3 shrink-0 text-xs ${glyph.className}`,
1771
- children: glyph.glyph
1854
+ children: entry.kind === "directory" ? expanded.has(entry.path) ? "▾" : "▸" : glyph.glyph
1772
1855
  }),
1773
- /* @__PURE__ */ jsx("span", {
1856
+ /* @__PURE__ */ jsxs("span", {
1774
1857
  title: entry.path,
1775
- className: `min-w-0 truncate text-xs ${entry.state === "pending" ? "text-doom-dim" : "text-doom-hi"}`,
1776
- children: entry.path
1858
+ className: `min-w-0 flex-1 truncate text-xs ${entry.state === "pending" ? "text-doom-dim" : "text-doom-hi"}`,
1859
+ children: [entry.path.split("/").pop(), entry.kind === "directory" ? "/" : ""]
1777
1860
  }),
1778
- entry.producedBy.length === 0 ? null : /* @__PURE__ */ jsx(Badge, {
1861
+ /* @__PURE__ */ jsx("span", {
1862
+ className: "shrink-0 text-2xs text-doom-faint",
1863
+ children: entry.state === "pending" ? "pending" : entry.kind === "directory" ? "" : [formatSize(entry.size), formatWhen(entry.modifiedAt)].filter(Boolean).join(" · ")
1864
+ })
1865
+ ]
1866
+ }), entry.declared || entry.producedBy.length > 0 || entry.description !== "" ? /* @__PURE__ */ jsxs("span", {
1867
+ className: "flex min-w-0 items-center gap-1 pl-5 text-2xs text-doom-faint",
1868
+ children: [
1869
+ entry.declared ? /* @__PURE__ */ jsx(Badge, {
1779
1870
  size: "xs",
1780
1871
  tone: "violet",
1781
1872
  className: "shrink-0",
1873
+ children: "declared"
1874
+ }) : null,
1875
+ entry.producedBy.length === 0 ? null : /* @__PURE__ */ jsx("span", {
1876
+ title: entry.producedBy.join(", "),
1877
+ className: "truncate",
1782
1878
  children: entry.producedBy.join(", ")
1783
1879
  }),
1784
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
1785
- /* @__PURE__ */ jsx("span", {
1786
- className: "shrink-0 text-2xs text-doom-faint",
1787
- children: entry.state === "pending" ? "not written yet" : [formatSize(entry.size), formatWhen(entry.modifiedAt)].filter(Boolean).join(" · ")
1880
+ entry.description === "" ? null : /* @__PURE__ */ jsx("span", {
1881
+ title: entry.description,
1882
+ className: "truncate",
1883
+ children: entry.description
1788
1884
  })
1789
1885
  ]
1790
- }), entry.description === "" ? null : /* @__PURE__ */ jsx("span", {
1791
- className: "truncate pl-5 text-2xs text-doom-faint",
1792
- children: entry.description
1793
- })]
1886
+ }) : null]
1794
1887
  });
1795
1888
  };
1889
+ const renderChildren = (parent) => {
1890
+ return [...entries.values()].filter((entry) => {
1891
+ const cut = entry.path.lastIndexOf("/");
1892
+ return (cut === -1 ? "" : entry.path.slice(0, cut)) === parent;
1893
+ }).sort((left, right) => Number(right.kind === "directory") - Number(left.kind === "directory") || left.path.localeCompare(right.path)).map((entry) => /* @__PURE__ */ jsxs("div", {
1894
+ className: "flex flex-col",
1895
+ children: [renderRow(entry), entry.kind === "directory" && expanded.has(entry.path) ? /* @__PURE__ */ jsxs("div", {
1896
+ className: "ml-3 border-l border-doom-border pl-1",
1897
+ children: [
1898
+ loading.has(entry.path) ? /* @__PURE__ */ jsx("span", {
1899
+ className: "px-3 text-xs text-doom-faint",
1900
+ children: "loading…"
1901
+ }) : null,
1902
+ folderErrors[entry.path] ? /* @__PURE__ */ jsx("span", {
1903
+ role: "alert",
1904
+ className: "px-3 text-xs text-doom-yellow",
1905
+ children: folderErrors[entry.path]
1906
+ }) : null,
1907
+ renderChildren(entry.path)
1908
+ ]
1909
+ }) : null]
1910
+ }, entry.path));
1911
+ };
1796
1912
  return /* @__PURE__ */ jsxs("div", {
1797
1913
  "data-testid": "artifacts-pane",
1798
1914
  className: "flex min-h-0 flex-1 flex-col",
@@ -1802,20 +1918,28 @@ function ArtifactList({ listing, error, onOpen }) {
1802
1918
  title: "the run directory cannot be read",
1803
1919
  description: error
1804
1920
  }) : null,
1805
- declared.length === 0 && found.length === 0 && error === void 0 ? /* @__PURE__ */ jsx(EmptyState, {
1921
+ entries.size === 0 && error === void 0 ? /* @__PURE__ */ jsx(EmptyState, {
1806
1922
  className: "py-4",
1807
1923
  title: "nothing in the run directory yet"
1808
1924
  }) : null,
1809
- declared.length === 0 ? null : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
1810
- className: "px-3 pb-1 pt-2 text-2xs font-bold tracking-wider text-doom-faint",
1811
- children: ["DECLARED · ", listing?.description || "run-directory"]
1812
- }), declared.map((entry) => /* @__PURE__ */ jsx(Row, { entry }, entry.path))] }),
1813
- found.length === 0 ? null : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1814
- className: "px-3 pb-1 pt-3 text-2xs font-bold tracking-wider text-doom-faint",
1815
- children: "ALSO IN THE FOLDER"
1816
- }), found.map((entry) => /* @__PURE__ */ jsx(Row, { entry }, entry.path))] }),
1817
- listing === void 0 ? null : /* @__PURE__ */ jsx("span", {
1818
- className: "truncate px-3 py-2 text-2xs text-doom-faint",
1925
+ listing?.description ? /* @__PURE__ */ jsx("span", {
1926
+ className: "px-3 py-2 text-2xs text-doom-faint",
1927
+ children: listing.description
1928
+ }) : null,
1929
+ renderChildren(""),
1930
+ openError === void 0 ? null : /* @__PURE__ */ jsx("span", {
1931
+ role: "alert",
1932
+ className: "px-3 text-xs text-doom-yellow",
1933
+ children: openError
1934
+ }),
1935
+ listing === void 0 ? null : /* @__PURE__ */ jsx("button", {
1936
+ type: "button",
1937
+ "data-testid": "workflow-open-directory",
1938
+ "aria-label": "Open workflow folder in Finder",
1939
+ title: `${listing.runDir} (open in Finder)`,
1940
+ disabled: opening || onOpenDirectory === void 0,
1941
+ onClick: () => void openDirectory(),
1942
+ className: "truncate px-3 py-2 text-left text-2xs text-doom-faint hover:text-doom-hi disabled:cursor-default",
1819
1943
  children: listing.runDir
1820
1944
  })
1821
1945
  ]
@@ -2250,6 +2374,10 @@ function StepTerminal({ sessionId, target, run }) {
2250
2374
  //#endregion
2251
2375
  //#region src/extensions/workspaces/sessions/(frontend)/_components/WorkflowRunParts.tsx
2252
2376
  const STATE_ICON = {
2377
+ pending: {
2378
+ glyph: "○",
2379
+ className: "text-doom-faint"
2380
+ },
2253
2381
  running: {
2254
2382
  glyph: "●",
2255
2383
  className: "text-doom-blue"
@@ -2478,11 +2606,13 @@ function StepRow({ step, now, selected, onSelect }) {
2478
2606
  const icon = STATE_ICON[step.status];
2479
2607
  return /* @__PURE__ */ jsxs("button", {
2480
2608
  type: "button",
2609
+ disabled: step.status === "pending",
2610
+ title: `${step.name}: ${step.status}`,
2481
2611
  "data-testid": `step-row-${step.name}`,
2482
2612
  "data-step-status": step.status,
2483
2613
  "data-active": selected,
2484
2614
  onClick: onSelect,
2485
- className: cn("flex w-full cursor-pointer flex-col gap-0.5 rounded px-2 py-1.5 text-left hover:bg-doom-deep", selected && "bg-doom-tint-blue ring-1 ring-inset ring-doom-blue/40"),
2615
+ className: cn("flex w-full cursor-pointer flex-col gap-0.5 rounded px-2 py-1.5 text-left enabled:hover:bg-doom-deep disabled:cursor-default", selected && "bg-doom-tint-blue ring-1 ring-inset ring-doom-blue/40"),
2486
2616
  children: [/* @__PURE__ */ jsxs("span", {
2487
2617
  className: "flex items-center gap-2",
2488
2618
  children: [
@@ -2501,7 +2631,7 @@ function StepRow({ step, now, selected, onSelect }) {
2501
2631
  }),
2502
2632
  /* @__PURE__ */ jsx("span", {
2503
2633
  className: "shrink-0 text-2xs text-doom-faint",
2504
- children: spanDuration(step.startedAt, step.endedAt, now) ?? ""
2634
+ children: step.status === "pending" ? "pending" : spanDuration(step.startedAt, step.endedAt, now) ?? ""
2505
2635
  })
2506
2636
  ]
2507
2637
  }), step.reason === void 0 ? null : /* @__PURE__ */ jsx("span", {
@@ -2624,12 +2754,6 @@ function activeSteps(run) {
2624
2754
  step
2625
2755
  })));
2626
2756
  }
2627
- /** A job opens by default while it is moving or failed, so the step that needs a look is already showing. */
2628
- function openByDefault(run, job) {
2629
- if (run.position?.job === job.name) return true;
2630
- if (ACTIVE_STATES.has(job.status) || job.status === "failed") return true;
2631
- return job.steps.some((step) => ACTIVE_STATES.has(step.status) || step.status === "failed");
2632
- }
2633
2757
  function SectionHeading({ label, meta }) {
2634
2758
  return /* @__PURE__ */ jsxs("div", {
2635
2759
  className: "flex items-center px-4 pb-1 pt-3",
@@ -2785,7 +2909,7 @@ function JobTree({ run, now, onOpenStep }) {
2785
2909
  }) : /* @__PURE__ */ jsx("div", {
2786
2910
  className: "flex flex-col gap-0.5 px-2",
2787
2911
  children: run.jobs.map((job) => {
2788
- const open = toggled[job.name] ?? openByDefault(run, job);
2912
+ const open = toggled[job.name] ?? true;
2789
2913
  const icon = STATE_ICON[job.status];
2790
2914
  const finished = job.steps.filter((step) => step.status === "completed" || step.status === "skipped").length;
2791
2915
  return /* @__PURE__ */ jsxs("div", {
@@ -2795,6 +2919,7 @@ function JobTree({ run, now, onOpenStep }) {
2795
2919
  "data-testid": `job-row-${job.name}`,
2796
2920
  "data-job-status": job.status,
2797
2921
  "aria-expanded": open,
2922
+ title: `${job.name}: ${job.status}`,
2798
2923
  onClick: () => setToggled((current) => ({
2799
2924
  ...current,
2800
2925
  [job.name]: !open
@@ -2812,7 +2937,7 @@ function JobTree({ run, now, onOpenStep }) {
2812
2937
  }),
2813
2938
  /* @__PURE__ */ jsx("span", {
2814
2939
  className: "shrink-0 text-2xs text-doom-faint",
2815
- children: open || job.steps.length === 0 ? spanDuration(job.startedAt, job.endedAt, now) ?? "" : `${finished}/${job.steps.length} steps`
2940
+ children: job.status === "pending" ? "pending" : open || job.steps.length === 0 ? spanDuration(job.startedAt, job.endedAt, now) ?? "" : `${finished}/${job.steps.length} steps`
2816
2941
  })
2817
2942
  ]
2818
2943
  }), open && job.steps.length > 0 ? /* @__PURE__ */ jsx("div", {
@@ -2942,7 +3067,7 @@ function WorkflowDockPanel({ sessionId, openTransientTab }) {
2942
3067
  run,
2943
3068
  sessionId,
2944
3069
  onOpen: (path) => openTransientTab(artifactTab(run, path))
2945
- })
3070
+ }, `${workflowRunIdentity(run)}/${sessionId}`)
2946
3071
  });
2947
3072
  }
2948
3073
  //#endregion