@agent-native/dispatch 0.3.0 → 0.5.0

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.
Files changed (48) hide show
  1. package/dist/actions/archive-workspace-app.d.ts +3 -0
  2. package/dist/actions/archive-workspace-app.d.ts.map +1 -0
  3. package/dist/actions/archive-workspace-app.js +15 -0
  4. package/dist/actions/archive-workspace-app.js.map +1 -0
  5. package/dist/actions/index.d.ts.map +1 -1
  6. package/dist/actions/index.js +10 -0
  7. package/dist/actions/index.js.map +1 -1
  8. package/dist/actions/list-available-workspace-templates.d.ts +3 -0
  9. package/dist/actions/list-available-workspace-templates.d.ts.map +1 -0
  10. package/dist/actions/list-available-workspace-templates.js +10 -0
  11. package/dist/actions/list-available-workspace-templates.js.map +1 -0
  12. package/dist/actions/remove-pending-workspace-app.d.ts +3 -0
  13. package/dist/actions/remove-pending-workspace-app.d.ts.map +1 -0
  14. package/dist/actions/remove-pending-workspace-app.js +15 -0
  15. package/dist/actions/remove-pending-workspace-app.js.map +1 -0
  16. package/dist/actions/scaffold-workspace-app.d.ts +3 -0
  17. package/dist/actions/scaffold-workspace-app.d.ts.map +1 -0
  18. package/dist/actions/scaffold-workspace-app.js +27 -0
  19. package/dist/actions/scaffold-workspace-app.js.map +1 -0
  20. package/dist/actions/unarchive-workspace-app.d.ts +3 -0
  21. package/dist/actions/unarchive-workspace-app.d.ts.map +1 -0
  22. package/dist/actions/unarchive-workspace-app.js +15 -0
  23. package/dist/actions/unarchive-workspace-app.js.map +1 -0
  24. package/dist/components/workspace-app-card.d.ts.map +1 -1
  25. package/dist/components/workspace-app-card.js +33 -2
  26. package/dist/components/workspace-app-card.js.map +1 -1
  27. package/dist/lib/workspace-apps.d.ts +1 -0
  28. package/dist/lib/workspace-apps.d.ts.map +1 -1
  29. package/dist/lib/workspace-apps.js.map +1 -1
  30. package/dist/routes/pages/apps.d.ts.map +1 -1
  31. package/dist/routes/pages/apps.js +41 -8
  32. package/dist/routes/pages/apps.js.map +1 -1
  33. package/dist/server/lib/app-creation-store.d.ts +40 -0
  34. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  35. package/dist/server/lib/app-creation-store.js +245 -6
  36. package/dist/server/lib/app-creation-store.js.map +1 -1
  37. package/dist/server/lib/thread-debug-store.d.ts +2 -2
  38. package/package.json +2 -2
  39. package/src/actions/archive-workspace-app.ts +16 -0
  40. package/src/actions/index.ts +10 -0
  41. package/src/actions/list-available-workspace-templates.ts +11 -0
  42. package/src/actions/remove-pending-workspace-app.ts +16 -0
  43. package/src/actions/scaffold-workspace-app.ts +34 -0
  44. package/src/actions/unarchive-workspace-app.ts +16 -0
  45. package/src/components/workspace-app-card.tsx +95 -5
  46. package/src/lib/workspace-apps.ts +1 -0
  47. package/src/routes/pages/apps.tsx +159 -6
  48. package/src/server/lib/app-creation-store.ts +312 -15
@@ -1,3 +1,4 @@
1
+ import { spawn } from "node:child_process";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import { fileURLToPath } from "node:url";
@@ -52,10 +53,27 @@ export interface WorkspaceAppSummary {
52
53
  a2aEndpointUrl?: string | null;
53
54
  agentName?: string | null;
54
55
  agentSkillsCount?: number | null;
56
+ archived?: boolean;
55
57
  }
56
58
 
57
59
  export interface ListWorkspaceAppsOptions {
58
60
  includeAgentCards?: boolean;
61
+ /**
62
+ * Include apps the current viewer has hidden (archived). Defaults to false
63
+ * so polling/UI callers see only the visible set; the apps page passes true
64
+ * when rendering the "Hidden apps" expander.
65
+ */
66
+ includeArchived?: boolean;
67
+ }
68
+
69
+ export interface AvailableWorkspaceTemplate {
70
+ name: string;
71
+ label: string;
72
+ hint: string;
73
+ icon: string;
74
+ color: string;
75
+ colorRgb: string;
76
+ core: boolean;
59
77
  }
60
78
 
61
79
  export interface AppCreationSettings {
@@ -260,6 +278,76 @@ async function listPendingWorkspaceApps(): Promise<PendingWorkspaceApp[]> {
260
278
  return parsePendingWorkspaceApps(raw.pendingApps);
261
279
  }
262
280
 
281
+ function parseArchivedAppIds(value: unknown): string[] {
282
+ if (!Array.isArray(value)) return [];
283
+ const ids = value
284
+ .map((entry) => (typeof entry === "string" ? entry.trim() : ""))
285
+ .filter(Boolean);
286
+ return Array.from(new Set(ids));
287
+ }
288
+
289
+ async function listArchivedAppIds(): Promise<string[]> {
290
+ const raw = await readSettingsRecord();
291
+ return parseArchivedAppIds(raw.archivedAppIds);
292
+ }
293
+
294
+ export async function archiveWorkspaceApp(input: {
295
+ appId: string;
296
+ }): Promise<{ archivedAppIds: string[] }> {
297
+ const appId = input.appId.trim();
298
+ if (!appId) throw new Error("appId is required");
299
+ const raw = await readSettingsRecord();
300
+ const current = parseArchivedAppIds(raw.archivedAppIds);
301
+ if (!current.includes(appId)) current.push(appId);
302
+ await putSetting(scopedSettingsKey(), { ...raw, archivedAppIds: current });
303
+ await recordAudit({
304
+ action: "workspace-app.archived",
305
+ targetType: "workspace-app",
306
+ targetId: appId,
307
+ summary: "Hid workspace app from the Apps list",
308
+ });
309
+ return { archivedAppIds: current };
310
+ }
311
+
312
+ export async function unarchiveWorkspaceApp(input: {
313
+ appId: string;
314
+ }): Promise<{ archivedAppIds: string[] }> {
315
+ const appId = input.appId.trim();
316
+ if (!appId) throw new Error("appId is required");
317
+ const raw = await readSettingsRecord();
318
+ const current = parseArchivedAppIds(raw.archivedAppIds).filter(
319
+ (id) => id !== appId,
320
+ );
321
+ await putSetting(scopedSettingsKey(), { ...raw, archivedAppIds: current });
322
+ await recordAudit({
323
+ action: "workspace-app.unarchived",
324
+ targetType: "workspace-app",
325
+ targetId: appId,
326
+ summary: "Restored workspace app to the Apps list",
327
+ });
328
+ return { archivedAppIds: current };
329
+ }
330
+
331
+ export async function removePendingWorkspaceApp(input: {
332
+ appId: string;
333
+ }): Promise<{ removed: boolean }> {
334
+ const appId = input.appId.trim();
335
+ if (!appId) throw new Error("appId is required");
336
+ const raw = await readSettingsRecord();
337
+ const pending = parsePendingWorkspaceApps(raw.pendingApps);
338
+ const next = pending.filter((app) => app.id !== appId);
339
+ const removed = next.length !== pending.length;
340
+ if (!removed) return { removed: false };
341
+ await putSetting(scopedSettingsKey(), { ...raw, pendingApps: next });
342
+ await recordAudit({
343
+ action: "workspace-app.pending-removed",
344
+ targetType: "workspace-app",
345
+ targetId: appId,
346
+ summary: "Removed pending Builder app from the Apps list",
347
+ });
348
+ return { removed: true };
349
+ }
350
+
263
351
  function pendingAppToSummary(app: PendingWorkspaceApp): WorkspaceAppSummary {
264
352
  return {
265
353
  id: app.id,
@@ -597,13 +685,30 @@ export function getWorkspaceInfo(): WorkspaceInfo {
597
685
  };
598
686
  }
599
687
 
688
+ async function applyArchivedAndPending(
689
+ apps: WorkspaceAppSummary[],
690
+ options: ListWorkspaceAppsOptions,
691
+ ): Promise<WorkspaceAppSummary[]> {
692
+ const [withPending, archivedIds] = await Promise.all([
693
+ appendPendingWorkspaceApps(apps),
694
+ listArchivedAppIds(),
695
+ ]);
696
+ const archivedSet = new Set(archivedIds);
697
+ const annotated = withPending.map((app) =>
698
+ archivedSet.has(app.id) ? { ...app, archived: true } : app,
699
+ );
700
+ return options.includeArchived
701
+ ? annotated
702
+ : annotated.filter((app) => !app.archived);
703
+ }
704
+
600
705
  export async function listWorkspaceApps(
601
706
  options: ListWorkspaceAppsOptions = {},
602
707
  ): Promise<WorkspaceAppSummary[]> {
603
708
  const gatewayApps = await readWorkspaceAppsFromGateway();
604
709
  if (gatewayApps) {
605
710
  return maybeIncludeAgentCards(
606
- await appendPendingWorkspaceApps(gatewayApps),
711
+ await applyArchivedAndPending(gatewayApps, options),
607
712
  options,
608
713
  );
609
714
  }
@@ -615,7 +720,7 @@ export async function listWorkspaceApps(
615
720
  : null;
616
721
  if (localFilesystemApps) {
617
722
  return maybeIncludeAgentCards(
618
- await appendPendingWorkspaceApps(localFilesystemApps),
723
+ await applyArchivedAndPending(localFilesystemApps, options),
619
724
  options,
620
725
  );
621
726
  }
@@ -624,35 +729,227 @@ export async function listWorkspaceApps(
624
729
  readWorkspaceAppsFromEnv() ?? readWorkspaceAppsFromManifestFile();
625
730
  if (manifestApps) {
626
731
  return maybeIncludeAgentCards(
627
- await appendPendingWorkspaceApps(manifestApps),
732
+ await applyArchivedAndPending(manifestApps, options),
628
733
  options,
629
734
  );
630
735
  }
631
736
 
632
737
  if (!workspaceRoot) {
633
738
  return maybeIncludeAgentCards(
634
- await appendPendingWorkspaceApps([
635
- {
636
- id: "dispatch",
637
- name: "Dispatch",
638
- description: "Workspace control plane",
639
- path: "/dispatch",
640
- url: workspaceAppUrl("/dispatch"),
641
- isDispatch: true,
642
- status: "ready",
643
- },
644
- ]),
739
+ await applyArchivedAndPending(
740
+ [
741
+ {
742
+ id: "dispatch",
743
+ name: "Dispatch",
744
+ description: "Workspace control plane",
745
+ path: "/dispatch",
746
+ url: workspaceAppUrl("/dispatch"),
747
+ isDispatch: true,
748
+ status: "ready",
749
+ },
750
+ ],
751
+ options,
752
+ ),
645
753
  options,
646
754
  );
647
755
  }
648
756
 
649
757
  const apps = readWorkspaceAppsFromFilesystem(workspaceRoot) ?? [];
650
758
  return maybeIncludeAgentCards(
651
- await appendPendingWorkspaceApps(apps),
759
+ await applyArchivedAndPending(apps, options),
652
760
  options,
653
761
  );
654
762
  }
655
763
 
764
+ /**
765
+ * First-party templates the user can scaffold into this workspace via the
766
+ * Apps page tiles. Inlined here (rather than importing from
767
+ * `@agent-native/shared-app-config`) because the published `@agent-native/dispatch`
768
+ * package has no `workspace:*` runtime dependencies. Keep in sync with
769
+ * `packages/core/src/cli/templates-meta.ts`.
770
+ */
771
+ const ADDABLE_TEMPLATES: AvailableWorkspaceTemplate[] = [
772
+ {
773
+ name: "mail",
774
+ label: "Mail",
775
+ hint: "Email client with keyboard shortcuts and AI triage",
776
+ icon: "Mail",
777
+ color: "#3B82F6",
778
+ colorRgb: "59 130 246",
779
+ core: true,
780
+ },
781
+ {
782
+ name: "calendar",
783
+ label: "Calendar",
784
+ hint: "Manage events, sync, and public booking",
785
+ icon: "CalendarMonth",
786
+ color: "#8B5CF6",
787
+ colorRgb: "139 92 246",
788
+ core: true,
789
+ },
790
+ {
791
+ name: "content",
792
+ label: "Content",
793
+ hint: "Write and organize with agent assistance",
794
+ icon: "FileText",
795
+ color: "#10B981",
796
+ colorRgb: "16 185 129",
797
+ core: true,
798
+ },
799
+ {
800
+ name: "slides",
801
+ label: "Slides",
802
+ hint: "Generate and edit React presentations",
803
+ icon: "Presentation",
804
+ color: "#EC4899",
805
+ colorRgb: "236 72 153",
806
+ core: true,
807
+ },
808
+ {
809
+ name: "clips",
810
+ label: "Clips",
811
+ hint: "Screen recording, meeting notes, and voice dictation",
812
+ icon: "ScreenShare",
813
+ color: "#625DF5",
814
+ colorRgb: "98 93 245",
815
+ core: true,
816
+ },
817
+ {
818
+ name: "analytics",
819
+ label: "Analytics",
820
+ hint: "Connect data sources, prompt for charts",
821
+ icon: "ChartBar",
822
+ color: "#F59E0B",
823
+ colorRgb: "245 158 11",
824
+ core: true,
825
+ },
826
+ {
827
+ name: "forms",
828
+ label: "Forms",
829
+ hint: "Create, edit, and manage forms",
830
+ icon: "ClipboardList",
831
+ color: "#06B6D4",
832
+ colorRgb: "6 182 212",
833
+ core: true,
834
+ },
835
+ {
836
+ name: "design",
837
+ label: "Design",
838
+ hint: "Create and edit visual designs with agent assistance",
839
+ icon: "Brush",
840
+ color: "#F472B6",
841
+ colorRgb: "244 114 182",
842
+ core: true,
843
+ },
844
+ {
845
+ name: "videos",
846
+ label: "Video",
847
+ hint: "Video editing with Remotion",
848
+ icon: "Video",
849
+ color: "#EF4444",
850
+ colorRgb: "239 68 68",
851
+ core: false,
852
+ },
853
+ ];
854
+
855
+ export async function listAvailableWorkspaceTemplates(): Promise<
856
+ AvailableWorkspaceTemplate[]
857
+ > {
858
+ const installed = new Set(
859
+ (await listWorkspaceApps({ includeArchived: true })).map((app) => app.id),
860
+ );
861
+ return ADDABLE_TEMPLATES.filter((tpl) => !installed.has(tpl.name));
862
+ }
863
+
864
+ const SCAFFOLD_TIMEOUT_MS = 90_000;
865
+
866
+ export async function scaffoldWorkspaceAppFromTemplate(input: {
867
+ template: string;
868
+ appId?: string | null;
869
+ }): Promise<{ appId: string; template: string; output: string }> {
870
+ if (!isLocalAppCreationRuntime()) {
871
+ throw new Error(
872
+ "Scaffolding from Dispatch is only available in local development. " +
873
+ "Use the Builder branch flow on a deployed workspace.",
874
+ );
875
+ }
876
+ const template = input.template.trim();
877
+ if (!template) throw new Error("template is required");
878
+ if (!ADDABLE_TEMPLATES.some((tpl) => tpl.name === template)) {
879
+ throw new Error(`Unknown template "${template}".`);
880
+ }
881
+
882
+ const appId = (input.appId?.trim() || template).toLowerCase();
883
+ assertValidWorkspaceAppId(appId);
884
+
885
+ const workspaceRoot = findWorkspaceRoot();
886
+ if (!workspaceRoot) {
887
+ throw new Error("No agent-native workspace detected for scaffolding.");
888
+ }
889
+ const appDir = path.join(workspaceRoot, "apps", appId);
890
+ if (fs.existsSync(appDir)) {
891
+ throw new Error(`apps/${appId} already exists.`);
892
+ }
893
+
894
+ const output = await runScaffoldCli({
895
+ cwd: workspaceRoot,
896
+ args: ["add-app", appId, "--template", template],
897
+ });
898
+
899
+ await recordAudit({
900
+ action: "workspace-app.scaffolded",
901
+ targetType: "workspace-app",
902
+ targetId: appId,
903
+ summary: `Scaffolded apps/${appId} from ${template}`,
904
+ metadata: { template },
905
+ });
906
+
907
+ return { appId, template, output };
908
+ }
909
+
910
+ function runScaffoldCli(input: {
911
+ cwd: string;
912
+ args: string[];
913
+ }): Promise<string> {
914
+ return new Promise((resolve, reject) => {
915
+ const child = spawn("pnpm", ["exec", "agent-native", ...input.args], {
916
+ cwd: input.cwd,
917
+ stdio: ["ignore", "pipe", "pipe"],
918
+ env: { ...process.env, CI: "1", FORCE_COLOR: "0" },
919
+ });
920
+ let stdout = "";
921
+ let stderr = "";
922
+ child.stdout?.on("data", (chunk) => {
923
+ stdout += String(chunk);
924
+ });
925
+ child.stderr?.on("data", (chunk) => {
926
+ stderr += String(chunk);
927
+ });
928
+ const timer = setTimeout(() => {
929
+ child.kill("SIGTERM");
930
+ reject(
931
+ new Error(
932
+ `Scaffold timed out after ${Math.round(SCAFFOLD_TIMEOUT_MS / 1000)}s`,
933
+ ),
934
+ );
935
+ }, SCAFFOLD_TIMEOUT_MS);
936
+ timer.unref();
937
+ child.on("error", (err) => {
938
+ clearTimeout(timer);
939
+ reject(err);
940
+ });
941
+ child.on("exit", (code) => {
942
+ clearTimeout(timer);
943
+ if (code === 0) {
944
+ resolve([stdout, stderr].filter(Boolean).join("\n").trim());
945
+ return;
946
+ }
947
+ const detail = (stderr || stdout || "").trim() || `exit code ${code}`;
948
+ reject(new Error(`Scaffold failed: ${detail}`));
949
+ });
950
+ });
951
+ }
952
+
656
953
  export async function getAppCreationSettings(): Promise<AppCreationSettings> {
657
954
  const envBuilderProjectId = getEnvBuilderProjectId();
658
955
  const resolvedBuilderProjectId = await resolveBuilderBranchProjectId();