@agent-native/dispatch 0.6.1 → 0.7.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 (146) hide show
  1. package/README.md +1 -1
  2. package/dist/actions/create-pylon-ticket.d.ts +3 -0
  3. package/dist/actions/create-pylon-ticket.d.ts.map +1 -0
  4. package/dist/actions/create-pylon-ticket.js +94 -0
  5. package/dist/actions/create-pylon-ticket.js.map +1 -0
  6. package/dist/actions/create-vault-grant.js +1 -1
  7. package/dist/actions/create-vault-grant.js.map +1 -1
  8. package/dist/actions/create-vault-secret.d.ts.map +1 -1
  9. package/dist/actions/create-vault-secret.js +4 -3
  10. package/dist/actions/create-vault-secret.js.map +1 -1
  11. package/dist/actions/get-vault-access-settings.d.ts +3 -0
  12. package/dist/actions/get-vault-access-settings.d.ts.map +1 -0
  13. package/dist/actions/get-vault-access-settings.js +10 -0
  14. package/dist/actions/get-vault-access-settings.js.map +1 -0
  15. package/dist/actions/grant-vault-secrets-to-app.js +1 -1
  16. package/dist/actions/grant-vault-secrets-to-app.js.map +1 -1
  17. package/dist/actions/index.d.ts.map +1 -1
  18. package/dist/actions/index.js +8 -0
  19. package/dist/actions/index.js.map +1 -1
  20. package/dist/actions/list-integrations-catalog.js +1 -1
  21. package/dist/actions/list-integrations-catalog.js.map +1 -1
  22. package/dist/actions/list-vault-grants.js +1 -1
  23. package/dist/actions/list-vault-grants.js.map +1 -1
  24. package/dist/actions/list-workspace-apps.d.ts.map +1 -1
  25. package/dist/actions/list-workspace-apps.js +5 -1
  26. package/dist/actions/list-workspace-apps.js.map +1 -1
  27. package/dist/actions/set-vault-access-settings.d.ts +3 -0
  28. package/dist/actions/set-vault-access-settings.d.ts.map +1 -0
  29. package/dist/actions/set-vault-access-settings.js +13 -0
  30. package/dist/actions/set-vault-access-settings.js.map +1 -0
  31. package/dist/actions/start-workspace-app-creation.d.ts.map +1 -1
  32. package/dist/actions/start-workspace-app-creation.js +6 -0
  33. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  34. package/dist/actions/sync-vault-to-app.js +1 -1
  35. package/dist/actions/sync-vault-to-app.js.map +1 -1
  36. package/dist/actions/update-workspace-app-metadata.d.ts +3 -0
  37. package/dist/actions/update-workspace-app-metadata.d.ts.map +1 -0
  38. package/dist/actions/update-workspace-app-metadata.js +30 -0
  39. package/dist/actions/update-workspace-app-metadata.js.map +1 -0
  40. package/dist/actions/view-screen.d.ts.map +1 -1
  41. package/dist/actions/view-screen.js +4 -2
  42. package/dist/actions/view-screen.js.map +1 -1
  43. package/dist/components/app-keys-popover.js +16 -5
  44. package/dist/components/app-keys-popover.js.map +1 -1
  45. package/dist/components/create-app-popover.d.ts.map +1 -1
  46. package/dist/components/create-app-popover.js +38 -14
  47. package/dist/components/create-app-popover.js.map +1 -1
  48. package/dist/components/dispatch-shell.d.ts +4 -4
  49. package/dist/components/dispatch-shell.d.ts.map +1 -1
  50. package/dist/components/dispatch-shell.js +6 -6
  51. package/dist/components/dispatch-shell.js.map +1 -1
  52. package/dist/components/layout/Layout.d.ts.map +1 -1
  53. package/dist/components/layout/Layout.js +10 -3
  54. package/dist/components/layout/Layout.js.map +1 -1
  55. package/dist/components/messaging-setup-panel.d.ts.map +1 -1
  56. package/dist/components/messaging-setup-panel.js +2 -2
  57. package/dist/components/messaging-setup-panel.js.map +1 -1
  58. package/dist/components/workspace-app-card.d.ts.map +1 -1
  59. package/dist/components/workspace-app-card.js +41 -2
  60. package/dist/components/workspace-app-card.js.map +1 -1
  61. package/dist/hooks/use-navigation-state.js +12 -5
  62. package/dist/hooks/use-navigation-state.js.map +1 -1
  63. package/dist/lib/catch-all-target.d.ts +2 -0
  64. package/dist/lib/catch-all-target.d.ts.map +1 -0
  65. package/dist/lib/catch-all-target.js +95 -0
  66. package/dist/lib/catch-all-target.js.map +1 -0
  67. package/dist/lib/workspace-apps.d.ts +9 -0
  68. package/dist/lib/workspace-apps.d.ts.map +1 -1
  69. package/dist/lib/workspace-apps.js.map +1 -1
  70. package/dist/routes/pages/$appId.d.ts +2 -2
  71. package/dist/routes/pages/$appId.d.ts.map +1 -1
  72. package/dist/routes/pages/$appId.js +17 -8
  73. package/dist/routes/pages/$appId.js.map +1 -1
  74. package/dist/routes/pages/integrations.d.ts.map +1 -1
  75. package/dist/routes/pages/integrations.js +20 -15
  76. package/dist/routes/pages/integrations.js.map +1 -1
  77. package/dist/routes/pages/new-app.js +1 -1
  78. package/dist/routes/pages/new-app.js.map +1 -1
  79. package/dist/routes/pages/overview.d.ts.map +1 -1
  80. package/dist/routes/pages/overview.js +5 -1
  81. package/dist/routes/pages/overview.js.map +1 -1
  82. package/dist/routes/pages/vault.d.ts.map +1 -1
  83. package/dist/routes/pages/vault.js +23 -5
  84. package/dist/routes/pages/vault.js.map +1 -1
  85. package/dist/server/lib/app-creation-store.d.ts +13 -0
  86. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  87. package/dist/server/lib/app-creation-store.js +295 -9
  88. package/dist/server/lib/app-creation-store.js.map +1 -1
  89. package/dist/server/lib/env-config.d.ts.map +1 -1
  90. package/dist/server/lib/env-config.js +5 -0
  91. package/dist/server/lib/env-config.js.map +1 -1
  92. package/dist/server/lib/onboarding-steps.d.ts +12 -0
  93. package/dist/server/lib/onboarding-steps.d.ts.map +1 -0
  94. package/dist/server/lib/onboarding-steps.js +47 -0
  95. package/dist/server/lib/onboarding-steps.js.map +1 -0
  96. package/dist/server/lib/vault-store.d.ts +55 -0
  97. package/dist/server/lib/vault-store.d.ts.map +1 -1
  98. package/dist/server/lib/vault-store.js +210 -41
  99. package/dist/server/lib/vault-store.js.map +1 -1
  100. package/dist/server/plugins/agent-chat.d.ts.map +1 -1
  101. package/dist/server/plugins/agent-chat.js +2 -1
  102. package/dist/server/plugins/agent-chat.js.map +1 -1
  103. package/dist/server/plugins/core-routes.d.ts.map +1 -1
  104. package/dist/server/plugins/core-routes.js +4 -0
  105. package/dist/server/plugins/core-routes.js.map +1 -1
  106. package/dist/server/plugins/integrations.js +2 -2
  107. package/dist/server/plugins/integrations.js.map +1 -1
  108. package/package.json +13 -11
  109. package/src/actions/create-pylon-ticket.ts +109 -0
  110. package/src/actions/create-vault-grant.ts +1 -1
  111. package/src/actions/create-vault-secret.ts +4 -3
  112. package/src/actions/get-vault-access-settings.ts +11 -0
  113. package/src/actions/grant-vault-secrets-to-app.ts +1 -1
  114. package/src/actions/index.ts +8 -0
  115. package/src/actions/list-integrations-catalog.ts +1 -1
  116. package/src/actions/list-vault-grants.ts +1 -1
  117. package/src/actions/list-workspace-apps.ts +5 -1
  118. package/src/actions/set-vault-access-settings.ts +16 -0
  119. package/src/actions/start-workspace-app-creation.ts +8 -0
  120. package/src/actions/sync-vault-to-app.ts +1 -1
  121. package/src/actions/update-workspace-app-metadata.ts +32 -0
  122. package/src/actions/view-screen.ts +4 -1
  123. package/src/components/app-keys-popover.tsx +23 -7
  124. package/src/components/create-app-popover.tsx +47 -14
  125. package/src/components/dispatch-shell.tsx +16 -15
  126. package/src/components/layout/Layout.tsx +11 -5
  127. package/src/components/messaging-setup-panel.tsx +54 -39
  128. package/src/components/workspace-app-card.tsx +102 -0
  129. package/src/hooks/use-navigation-state.ts +10 -4
  130. package/src/lib/catch-all-target.spec.ts +218 -0
  131. package/src/lib/catch-all-target.ts +99 -0
  132. package/src/lib/workspace-apps.ts +9 -0
  133. package/src/routes/pages/$appId.tsx +21 -8
  134. package/src/routes/pages/integrations.tsx +57 -18
  135. package/src/routes/pages/new-app.tsx +1 -1
  136. package/src/routes/pages/overview.tsx +11 -3
  137. package/src/routes/pages/vault.tsx +76 -9
  138. package/src/server/lib/app-creation-store.spec.ts +61 -2
  139. package/src/server/lib/app-creation-store.ts +386 -11
  140. package/src/server/lib/env-config.ts +5 -0
  141. package/src/server/lib/onboarding-steps.ts +49 -0
  142. package/src/server/lib/vault-store.spec.ts +69 -0
  143. package/src/server/lib/vault-store.ts +266 -49
  144. package/src/server/plugins/agent-chat.ts +2 -1
  145. package/src/server/plugins/core-routes.ts +5 -0
  146. package/src/server/plugins/integrations.ts +2 -2
@@ -3,6 +3,7 @@ import approveDispatchChange from "./approve-dispatch-change.js";
3
3
  import approveVaultRequest from "./approve-vault-request.js";
4
4
  import archiveWorkspaceApp from "./archive-workspace-app.js";
5
5
  import createLinkToken from "./create-link-token.js";
6
+ import createPylonTicket from "./create-pylon-ticket.js";
6
7
  import createVaultGrant from "./create-vault-grant.js";
7
8
  import createVaultSecret from "./create-vault-secret.js";
8
9
  import createWorkspaceResourceGrant from "./create-workspace-resource-grant.js";
@@ -14,6 +15,7 @@ import denyVaultRequest from "./deny-vault-request.js";
14
15
  import getAppCreationSettings from "./get-app-creation-settings.js";
15
16
  import getAgentThreadDebug from "./get-agent-thread-debug.js";
16
17
  import getDispatchSettings from "./get-dispatch-settings.js";
18
+ import getVaultAccessSettings from "./get-vault-access-settings.js";
17
19
  import getWorkspaceInfo from "./get-workspace-info.js";
18
20
  import grantWorkspaceResourcesToApp from "./grant-workspace-resources-to-app.js";
19
21
  import grantVaultSecretsToApp from "./grant-vault-secrets-to-app.js";
@@ -47,11 +49,13 @@ import searchAgentThreads from "./search-agent-threads.js";
47
49
  import sendPlatformMessage from "./send-platform-message.js";
48
50
  import setAppCreationSettings from "./set-app-creation-settings.js";
49
51
  import setDispatchApprovalPolicy from "./set-dispatch-approval-policy.js";
52
+ import setVaultAccessSettings from "./set-vault-access-settings.js";
50
53
  import startWorkspaceAppCreation from "./start-workspace-app-creation.js";
51
54
  import syncVaultToApp from "./sync-vault-to-app.js";
52
55
  import syncWorkspaceResourcesToAll from "./sync-workspace-resources-to-all.js";
53
56
  import syncWorkspaceResourcesToApp from "./sync-workspace-resources-to-app.js";
54
57
  import unarchiveWorkspaceApp from "./unarchive-workspace-app.js";
58
+ import updateWorkspaceAppMetadata from "./update-workspace-app-metadata.js";
55
59
  import updateVaultSecret from "./update-vault-secret.js";
56
60
  import updateWorkspaceResource from "./update-workspace-resource.js";
57
61
  import upsertDestination from "./upsert-destination.js";
@@ -68,6 +72,7 @@ export const dispatchActions: Record<string, ActionEntry> = {
68
72
  "approve-vault-request": approveVaultRequest,
69
73
  "archive-workspace-app": archiveWorkspaceApp,
70
74
  "create-link-token": createLinkToken,
75
+ "create-pylon-ticket": createPylonTicket,
71
76
  "create-vault-grant": createVaultGrant,
72
77
  "create-vault-secret": createVaultSecret,
73
78
  "create-workspace-resource-grant": createWorkspaceResourceGrant,
@@ -79,6 +84,7 @@ export const dispatchActions: Record<string, ActionEntry> = {
79
84
  "get-app-creation-settings": getAppCreationSettings,
80
85
  "get-agent-thread-debug": getAgentThreadDebug,
81
86
  "get-dispatch-settings": getDispatchSettings,
87
+ "get-vault-access-settings": getVaultAccessSettings,
82
88
  "get-workspace-info": getWorkspaceInfo,
83
89
  "grant-workspace-resources-to-app": grantWorkspaceResourcesToApp,
84
90
  "grant-vault-secrets-to-app": grantVaultSecretsToApp,
@@ -112,11 +118,13 @@ export const dispatchActions: Record<string, ActionEntry> = {
112
118
  "send-platform-message": sendPlatformMessage,
113
119
  "set-app-creation-settings": setAppCreationSettings,
114
120
  "set-dispatch-approval-policy": setDispatchApprovalPolicy,
121
+ "set-vault-access-settings": setVaultAccessSettings,
115
122
  "start-workspace-app-creation": startWorkspaceAppCreation,
116
123
  "sync-vault-to-app": syncVaultToApp,
117
124
  "sync-workspace-resources-to-all": syncWorkspaceResourcesToAll,
118
125
  "sync-workspace-resources-to-app": syncWorkspaceResourcesToApp,
119
126
  "unarchive-workspace-app": unarchiveWorkspaceApp,
127
+ "update-workspace-app-metadata": updateWorkspaceAppMetadata,
120
128
  "update-vault-secret": updateVaultSecret,
121
129
  "update-workspace-resource": updateWorkspaceResource,
122
130
  "upsert-destination": upsertDestination,
@@ -4,7 +4,7 @@ import { listIntegrationsCatalog } from "../server/lib/vault-store.js";
4
4
 
5
5
  export default defineAction({
6
6
  description:
7
- "List all workspace apps and their credential/integration requirements. Shows which credentials are configured, which are granted from the vault, and which are missing.",
7
+ "List all workspace apps and their credential/integration requirements. Shows configured credentials and effective Dispatch vault access.",
8
8
  schema: z.object({}),
9
9
  http: { method: "GET" },
10
10
  run: async () => listIntegrationsCatalog(),
@@ -4,7 +4,7 @@ import { listGrants } from "../server/lib/vault-store.js";
4
4
 
5
5
  export default defineAction({
6
6
  description:
7
- "List vault grants which apps have access to which secrets. Optionally filter by app or secret.",
7
+ "List explicit vault grants used by manual vault access mode. Optionally filter by app or secret.",
8
8
  schema: z.object({
9
9
  appId: z.string().optional().describe("Filter by app ID"),
10
10
  secretId: z.string().optional().describe("Filter by secret ID"),
@@ -12,13 +12,17 @@ const httpBoolean = z.preprocess((value) => {
12
12
 
13
13
  export default defineAction({
14
14
  description:
15
- "List apps installed in this workspace, including mounted paths, absolute URLs, and agent-card/A2A metadata for ready apps by default. UI polling callers can pass includeAgentCards=false to skip network probes.",
15
+ "List apps installed in this workspace, including mounted paths, absolute URLs, audience (internal/public), page route access overrides, and agent-card/A2A metadata for ready apps by default. UI polling callers can pass includeAgentCards=false to skip network probes.",
16
16
  schema: z.object({
17
17
  includeAgentCards: httpBoolean
18
18
  .default(true)
19
19
  .describe(
20
20
  "Fetch each ready app's /.well-known/agent-card.json with a short non-throwing timeout and include agentCardUrl, agentCardReachable, a2aEndpointUrl, agentName, and agentSkillsCount. Defaults to true for agent calls; UI polling should pass false. Pending Builder apps are not probed.",
21
21
  ),
22
+ audience: z
23
+ .enum(["all", "internal", "public"])
24
+ .default("all")
25
+ .describe("Filter by workspace app audience."),
22
26
  }),
23
27
  http: { method: "GET" },
24
28
  run: async (input) => listWorkspaceApps(input),
@@ -0,0 +1,16 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { z } from "zod";
3
+ import { setVaultAccessSettings } from "../server/lib/vault-store.js";
4
+
5
+ export default defineAction({
6
+ description:
7
+ "Set the Dispatch vault access mode. Use all-apps for the default workspace-wide mode or manual to require explicit per-app grants.",
8
+ schema: z.object({
9
+ mode: z
10
+ .enum(["all-apps", "manual"])
11
+ .describe(
12
+ "all-apps shares every vault key with every app; manual requires grants",
13
+ ),
14
+ }),
15
+ run: async (args) => setVaultAccessSettings(args),
16
+ });
@@ -23,6 +23,14 @@ export default defineAction({
23
23
  .optional()
24
24
  .nullable()
25
25
  .describe("Template to start from"),
26
+ description: z
27
+ .string()
28
+ .max(500)
29
+ .optional()
30
+ .nullable()
31
+ .describe(
32
+ "Concise AI-generated description of the app based on the user's prompt. Dispatch saves this while the app is being created.",
33
+ ),
26
34
  secretIds: z
27
35
  .array(z.string())
28
36
  .max(100)
@@ -4,7 +4,7 @@ import { syncGrantsToApp } from "../server/lib/vault-store.js";
4
4
 
5
5
  export default defineAction({
6
6
  description:
7
- "Push all granted secrets to an app by calling its env-vars endpoint. Returns the list of synced credential keys.",
7
+ "Push vault secrets to an app by calling its env-vars endpoint. In all-apps mode this syncs every vault key; in manual mode it syncs active grants.",
8
8
  schema: z.object({
9
9
  appId: z
10
10
  .string()
@@ -0,0 +1,32 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { getWorkspaceAppIdValidationError } from "@agent-native/core/shared";
3
+ import { z } from "zod";
4
+ import { updateWorkspaceAppMetadata } from "../server/lib/app-creation-store.js";
5
+
6
+ export default defineAction({
7
+ description:
8
+ "Update the human-editable display name and description Dispatch uses for a workspace app. These details are also used as connected-agent/A2A context.",
9
+ schema: z.object({
10
+ appId: z
11
+ .string()
12
+ .max(64)
13
+ .refine((appId) => !getWorkspaceAppIdValidationError(appId), {
14
+ message:
15
+ "Use a non-reserved app id with lowercase letters, numbers, and hyphens.",
16
+ })
17
+ .describe("Workspace app id, matching the apps/<id> folder"),
18
+ name: z
19
+ .string()
20
+ .max(120)
21
+ .optional()
22
+ .nullable()
23
+ .describe("Human-readable app name"),
24
+ description: z
25
+ .string()
26
+ .max(500)
27
+ .optional()
28
+ .nullable()
29
+ .describe("Human-readable app description"),
30
+ }),
31
+ run: async (args) => updateWorkspaceAppMetadata(args),
32
+ });
@@ -16,6 +16,7 @@ import {
16
16
  listSecrets,
17
17
  listGrants,
18
18
  listRequests,
19
+ getVaultAccessSettings,
19
20
  } from "../server/lib/vault-store.js";
20
21
  import { listWorkspaceApps } from "../server/lib/app-creation-store.js";
21
22
  import { listDispatchUsageMetrics } from "../server/lib/usage-metrics-store.js";
@@ -78,11 +79,13 @@ export default defineAction({
78
79
  }
79
80
  }
80
81
  if (navigation?.view === "vault" || navigation?.view === "new-app") {
81
- const [secrets, grants, requests] = await Promise.all([
82
+ const [secrets, grants, requests, access] = await Promise.all([
82
83
  listSecrets(),
83
84
  listGrants(),
84
85
  listRequests({ status: "pending" }),
86
+ getVaultAccessSettings(),
85
87
  ]);
88
+ screen.vaultAccessMode = access.mode;
86
89
  screen.vaultSecrets = secrets.map((s) => ({
87
90
  id: s.id,
88
91
  name: s.name,
@@ -89,6 +89,12 @@ function AppKeysPanel({ appId, appName }: { appId: string; appName: string }) {
89
89
  isLoading: grantsLoading,
90
90
  refetch: refetchGrants,
91
91
  } = useActionQuery("list-vault-grants", { appId });
92
+ const { data: accessSettings, isLoading: accessLoading } = useActionQuery(
93
+ "get-vault-access-settings",
94
+ {},
95
+ );
96
+ const accessMode =
97
+ (accessSettings as any)?.mode === "manual" ? "manual" : "all-apps";
92
98
 
93
99
  const grantBySecretId = useMemo(() => {
94
100
  const map = new Map<string, VaultGrant>();
@@ -135,11 +141,13 @@ function AppKeysPanel({ appId, appName }: { appId: string; appName: string }) {
135
141
  onError: (err) => toast.error(`Sync failed: ${String(err)}`),
136
142
  });
137
143
 
138
- const isLoading = secretsLoading || grantsLoading;
144
+ const isLoading = secretsLoading || grantsLoading || accessLoading;
139
145
  const grantedCount = grantBySecretId.size;
140
146
  const typedSecrets = secrets as VaultSecret[];
147
+ const allApps = accessMode !== "manual";
141
148
 
142
149
  const toggleSecret = (secret: VaultSecret) => {
150
+ if (allApps) return;
143
151
  if (pendingSecretIds.has(secret.id)) return;
144
152
  const existing = grantBySecretId.get(secret.id);
145
153
  markPending(secret.id, true);
@@ -159,14 +167,20 @@ function AppKeysPanel({ appId, appName }: { appId: string; appName: string }) {
159
167
  Keys for {appName}
160
168
  </p>
161
169
  <p className="text-[11px] text-muted-foreground">
162
- {grantedCount} of {typedSecrets.length} granted
170
+ {allApps
171
+ ? `${typedSecrets.length} available`
172
+ : `${grantedCount} of ${typedSecrets.length} granted`}
163
173
  </p>
164
174
  </div>
165
175
  <Button
166
176
  type="button"
167
177
  variant="outline"
168
178
  size="sm"
169
- disabled={syncMutation.isPending || grantedCount === 0}
179
+ disabled={
180
+ syncMutation.isPending ||
181
+ typedSecrets.length === 0 ||
182
+ (!allApps && grantedCount === 0)
183
+ }
170
184
  onClick={() => syncMutation.mutate({ appId })}
171
185
  className="h-7 px-2"
172
186
  >
@@ -201,17 +215,17 @@ function AppKeysPanel({ appId, appName }: { appId: string; appName: string }) {
201
215
  </p>
202
216
  ) : (
203
217
  typedSecrets.map((secret) => {
204
- const granted = grantBySecretId.has(secret.id);
218
+ const granted = allApps || grantBySecretId.has(secret.id);
205
219
  const pending = pendingSecretIds.has(secret.id);
206
220
  return (
207
221
  <button
208
222
  key={secret.id}
209
223
  type="button"
210
224
  aria-pressed={granted}
211
- disabled={pending}
225
+ disabled={pending || allApps}
212
226
  onClick={() => toggleSecret(secret)}
213
227
  className={`flex w-full items-start gap-3 rounded-md px-2.5 py-2 text-left text-sm disabled:cursor-not-allowed disabled:opacity-60 ${
214
- pending ? "" : "cursor-pointer"
228
+ pending || allApps ? "" : "cursor-pointer"
215
229
  } ${
216
230
  granted
217
231
  ? "border border-primary/45 bg-primary/5"
@@ -232,7 +246,9 @@ function AppKeysPanel({ appId, appName }: { appId: string; appName: string }) {
232
246
  {secret.credentialKey}
233
247
  </span>
234
248
  <span className="block truncate text-xs text-muted-foreground/70">
235
- {secret.provider || secret.name || "Vault secret"}
249
+ {allApps
250
+ ? "Available to this app"
251
+ : secret.provider || secret.name || "Vault secret"}
236
252
  </span>
237
253
  </span>
238
254
  </button>
@@ -44,6 +44,8 @@ interface WorkspaceResourceOption {
44
44
  updatedAt?: number;
45
45
  }
46
46
 
47
+ type VaultAccessMode = "all-apps" | "manual";
48
+
47
49
  interface CreateAppPopoverProps {
48
50
  /**
49
51
  * Custom trigger element. Defaults to a dashed-border tile that matches the
@@ -78,11 +80,15 @@ function buildAppCreationPrompt(input: {
78
80
  prompt: string;
79
81
  selectedKeys: string[];
80
82
  selectedResources: WorkspaceResourceOption[];
83
+ vaultAccessMode: VaultAccessMode;
81
84
  }): string {
82
85
  const keyList = input.selectedKeys.join(", ");
83
- const grantRequest = keyList
84
- ? `Requested Dispatch vault key grants for this app: ${keyList}`
85
- : `Requested Dispatch vault key grants for this app: none`;
86
+ const grantRequest =
87
+ input.vaultAccessMode === "all-apps"
88
+ ? `Dispatch vault access: all saved vault keys are available to every workspace app by default. No per-app vault grants are needed.`
89
+ : keyList
90
+ ? `Requested Dispatch vault key grants for this app: ${keyList}`
91
+ : `Requested Dispatch vault key grants for this app: none`;
86
92
  const resourceList = input.selectedResources.length
87
93
  ? input.selectedResources
88
94
  .map(
@@ -98,6 +104,7 @@ function buildAppCreationPrompt(input: {
98
104
  ``,
99
105
  `Suggested app name: ${input.appId} (you may adjust the slug if it conflicts)`,
100
106
  `User prompt: ${input.prompt.trim()}`,
107
+ `Generate a concise one-sentence app description from the user prompt before coding; save it in apps/${input.appId}/package.json "description" so Dispatch and A2A can describe the app.`,
101
108
  `If the user mentions a product or company such as Granola, Loom, Superhuman, Linear, or Notion, treat it as product inspiration unless they explicitly ask to connect to that service. Do not invent or require third-party API keys like GRANOLA_API_KEY just because a product is named.`,
102
109
  grantRequest,
103
110
  `Requested Dispatch workspace resources for this app:\n${resourceList}`,
@@ -112,18 +119,20 @@ function buildAppCreationPrompt(input: {
112
119
  `Do not clone first-party templates, create wrapper apps, or scaffold child apps/routes for Mail, Calendar, Analytics, etc. inside apps/${input.appId} just so this app can access them. If the request is a cross-app dashboard or overview, build only the new dashboard/overview app and delegate to the existing apps for domain work.`,
113
120
  `Only create another first-party app copy when the user explicitly asks for a customized fork/copy of that app; otherwise keep using the hosted/shared app so improvements to the base template keep flowing to users.`,
114
121
  `Do not satisfy this by adding a route, page, component, or file inside apps/starter or another existing app unless the user explicitly asks to modify that existing app.`,
115
- keyList
116
- ? `After the app exists, grant the selected Dispatch vault keys to appId "${input.appId}" and sync them once the app server is available. Treat these as requested grants, not active grants before creation succeeds.`
117
- : `Do not grant any Dispatch vault keys unless the user asks later.`,
122
+ input.vaultAccessMode === "all-apps"
123
+ ? `Do not create per-app Dispatch vault grants unless the workspace switches vault access to manual or the user explicitly asks for manual grants.`
124
+ : keyList
125
+ ? `After the app exists, grant the selected Dispatch vault keys to appId "${input.appId}" and sync them once the app server is available. Treat these as requested grants, not active grants before creation succeeds.`
126
+ : `Do not grant any Dispatch vault keys unless the user asks later.`,
118
127
  input.selectedResources.length
119
128
  ? `After the app exists, grant the selected Dispatch workspace resources to appId "${input.appId}" and sync them once the app server is available. Add a short note to apps/${input.appId}/AGENTS.md telling the app agent to read relevant shared resources under context/ or the selected resource paths before doing GTM/domain work.`
120
129
  : `Do not grant any Dispatch workspace resources unless the user asks later.`,
121
130
  ``,
122
131
  `App readiness requirements before handing off:`,
123
- `- Ensure apps/${input.appId}/package.json exists; Dispatch discovers workspace apps from apps/<app-id>/package.json, not a separate app registry.`,
132
+ `- Ensure apps/${input.appId}/package.json exists with displayName/name and a concise description; Dispatch discovers workspace apps from apps/<app-id>/package.json, not a separate app registry.`,
124
133
  `- Update the app manifest/package/deploy metadata needed by the existing workspace deployment model.`,
125
134
  `- Ensure the React Router client entry preserves APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() so /${input.appId} hydrates correctly.`,
126
- `- Verify the app's agent card/A2A metadata is ready so Dispatch can discover and delegate to the app after deployment.`,
135
+ `- Verify the app's agent card/A2A metadata is ready so Dispatch can discover and delegate to the app after deployment. Every sibling workspace app is available over A2A by default through call-agent, with names and descriptions from the workspace app registry.`,
127
136
  `When it is ready, start or update the workspace dev server and navigate the user to the absolute path /${input.appId} on the workspace origin. Do not prefix with /dispatch/, /apps/, /workspace/, or any other Dispatch tab — the new app is mounted at the workspace root, not under Dispatch. If you have a navigate tool available, pass /${input.appId} verbatim; if you only have a window.location-style escape hatch, set it to /${input.appId}.`,
128
137
  ].join("\n");
129
138
  }
@@ -170,6 +179,8 @@ export function CreateAppFlow({
170
179
  const [selectedResourceIds, setSelectedResourceIds] = useState<string[]>([]);
171
180
  const [secrets, setSecrets] = useState<VaultSecretOption[]>([]);
172
181
  const [resources, setResources] = useState<WorkspaceResourceOption[]>([]);
182
+ const [vaultAccessMode, setVaultAccessMode] =
183
+ useState<VaultAccessMode>("all-apps");
173
184
  const [secretsError, setSecretsError] = useState<string | null>(null);
174
185
  const [resourcesError, setResourcesError] = useState<string | null>(null);
175
186
  const [statusMessage, setStatusMessage] = useState<string | null>(null);
@@ -193,6 +204,15 @@ export function CreateAppFlow({
193
204
  setSecrets([]);
194
205
  setSecretsError(err?.message || "Could not load Dispatch keys");
195
206
  });
207
+ fetchJson(actionUrl(basePath, "get-vault-access-settings"))
208
+ .then((data) => {
209
+ if (cancelled) return;
210
+ setVaultAccessMode(data?.mode === "manual" ? "manual" : "all-apps");
211
+ })
212
+ .catch(() => {
213
+ if (cancelled) return;
214
+ setVaultAccessMode("manual");
215
+ });
196
216
  fetchJson(actionUrl(basePath, "list-workspace-resource-options"))
197
217
  .then((data) => {
198
218
  if (cancelled) return;
@@ -218,9 +238,11 @@ export function CreateAppFlow({
218
238
  [resources, selectedResourceIds],
219
239
  );
220
240
  const selectedSecretLabel =
221
- selectedSecretIds.length === 0
222
- ? "no keys"
223
- : `${selectedSecretIds.length} key${selectedSecretIds.length === 1 ? "" : "s"}`;
241
+ vaultAccessMode === "all-apps"
242
+ ? "all keys"
243
+ : selectedSecretIds.length === 0
244
+ ? "no keys"
245
+ : `${selectedSecretIds.length} key${selectedSecretIds.length === 1 ? "" : "s"}`;
224
246
  const selectedResourceLabel =
225
247
  selectedResourceIds.length === 0
226
248
  ? "no resources"
@@ -254,8 +276,12 @@ export function CreateAppFlow({
254
276
  const message = buildAppCreationPrompt({
255
277
  appId,
256
278
  prompt: trimmed,
257
- selectedKeys: selectedSecrets.map((s) => s.credentialKey),
279
+ selectedKeys:
280
+ vaultAccessMode === "manual"
281
+ ? selectedSecrets.map((s) => s.credentialKey)
282
+ : [],
258
283
  selectedResources,
284
+ vaultAccessMode,
259
285
  });
260
286
  setIsSubmitting(true);
261
287
  setStatusMessage(null);
@@ -279,7 +305,10 @@ export function CreateAppFlow({
279
305
  body: JSON.stringify({
280
306
  prompt: trimmed,
281
307
  appId,
282
- secretIds: selectedSecretIds.length > 0 ? selectedSecretIds : [],
308
+ secretIds:
309
+ vaultAccessMode === "manual" && selectedSecretIds.length > 0
310
+ ? selectedSecretIds
311
+ : [],
283
312
  resourceIds:
284
313
  selectedResourceIds.length > 0 ? selectedResourceIds : [],
285
314
  }),
@@ -351,7 +380,11 @@ export function CreateAppFlow({
351
380
  <IconKey size={12} />
352
381
  Dispatch keys
353
382
  </div>
354
- {secretsError ? (
383
+ {vaultAccessMode === "all-apps" ? (
384
+ <p className="rounded-md border border-dashed border-border px-3 py-3 text-xs text-muted-foreground">
385
+ Every saved Dispatch vault key is available to new apps.
386
+ </p>
387
+ ) : secretsError ? (
355
388
  <p className="rounded-md border border-dashed border-border px-3 py-3 text-xs text-muted-foreground">
356
389
  {secretsError}
357
390
  </p>
@@ -1,17 +1,17 @@
1
1
  import { type ReactNode } from "react";
2
2
  import {
3
- Tooltip,
4
- TooltipContent,
5
- TooltipTrigger,
6
- } from "@/components/ui/tooltip";
3
+ Popover,
4
+ PopoverContent,
5
+ PopoverTrigger,
6
+ } from "@/components/ui/popover";
7
7
  import { IconInfoCircle } from "@tabler/icons-react";
8
8
  import { useSetPageTitle } from "@/components/layout/HeaderActions";
9
9
 
10
10
  /**
11
- * DispatchShell renders the per-page title (with optional description tooltip)
12
- * into the global header via the HeaderActions store. The actual chrome
13
- * (sidebar, AgentSidebar, header bar with AgentToggleButton) is provided by
14
- * `Layout` mounted in `root.tsx`.
11
+ * DispatchShell renders the per-page title (with an optional click-to-open
12
+ * description popover) into the global header via the HeaderActions store.
13
+ * The actual chrome (sidebar, AgentSidebar, header bar with AgentToggleButton)
14
+ * is provided by `Layout` mounted in `root.tsx`.
15
15
  */
16
16
  export function DispatchShell({
17
17
  title,
@@ -28,23 +28,24 @@ export function DispatchShell({
28
28
  {title}
29
29
  </h1>
30
30
  {description ? (
31
- <Tooltip>
32
- <TooltipTrigger asChild>
31
+ <Popover>
32
+ <PopoverTrigger asChild>
33
33
  <button
34
34
  type="button"
35
- className="text-muted-foreground/60 hover:text-foreground cursor-pointer"
35
+ className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground/70 hover:bg-accent hover:text-foreground cursor-pointer"
36
36
  aria-label={`About ${title}`}
37
37
  >
38
38
  <IconInfoCircle className="h-3.5 w-3.5" />
39
39
  </button>
40
- </TooltipTrigger>
41
- <TooltipContent
40
+ </PopoverTrigger>
41
+ <PopoverContent
42
42
  side="bottom"
43
+ align="start"
43
44
  className="max-w-72 text-xs leading-relaxed"
44
45
  >
45
46
  {description}
46
- </TooltipContent>
47
- </Tooltip>
47
+ </PopoverContent>
48
+ </Popover>
48
49
  ) : null}
49
50
  </div>,
50
51
  );
@@ -229,10 +229,16 @@ function dispatchNavLinkTarget(path: string): string {
229
229
  if (typeof window === "undefined") return path;
230
230
  const basePath = appBasePath();
231
231
  if (!basePath) return path;
232
- const context = (
233
- window as Window & { __reactRouterContext?: { basename?: string } }
234
- ).__reactRouterContext;
235
- return context?.basename === basePath ? path : appPath(path);
232
+ // Mirror the basename calculation entry.client.tsx uses to configure the
233
+ // router (basePath iff the current URL is under that mount, "" otherwise).
234
+ // Reading the live URL directly avoids races with the previous check on
235
+ // `__reactRouterContext.basename`, which could read undefined before the
236
+ // entry script set it — that race produced /dispatch/dispatch/<route>
237
+ // history entries that 404'd on back-button navigation.
238
+ const pathname = window.location.pathname;
239
+ const routerHasBasename =
240
+ pathname === basePath || pathname.startsWith(`${basePath}/`);
241
+ return routerHasBasename ? path : appPath(path);
236
242
  }
237
243
 
238
244
  export function NavContent({
@@ -424,7 +430,7 @@ export function Layout({
424
430
  <AgentSidebar
425
431
  position="right"
426
432
  defaultOpen={false}
427
- emptyStateText="Create apps, grant keys, and route work across the workspace."
433
+ emptyStateText="Create apps, manage vault keys, and route work across the workspace."
428
434
  suggestions={SIDEBAR_SUGGESTIONS}
429
435
  >
430
436
  {appContent}
@@ -449,23 +449,34 @@ export function MessagingSetupPanel() {
449
449
  </p>
450
450
  </div>
451
451
  </div>
452
- <div className="flex items-center gap-3">
453
- <a
454
- href={platform.docsUrl}
455
- className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
452
+ <div className="flex shrink-0 items-center gap-1">
453
+ <Button
454
+ asChild
455
+ variant="ghost"
456
+ size="sm"
457
+ className="h-7 px-2 text-xs text-muted-foreground"
456
458
  >
457
- Docs
458
- </a>
459
+ <a href={platform.docsUrl} target="_blank" rel="noreferrer">
460
+ Docs
461
+ <IconExternalLink className="ml-1 h-3 w-3" />
462
+ </a>
463
+ </Button>
459
464
  {platform.externalUrl ? (
460
- <a
461
- href={platform.externalUrl}
462
- target="_blank"
463
- rel="noreferrer"
464
- className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
465
+ <Button
466
+ asChild
467
+ variant="ghost"
468
+ size="sm"
469
+ className="h-7 px-2 text-xs text-muted-foreground"
465
470
  >
466
- {platform.externalLabel ?? "Open"}
467
- <IconExternalLink className="h-3.5 w-3.5" />
468
- </a>
471
+ <a
472
+ href={platform.externalUrl}
473
+ target="_blank"
474
+ rel="noreferrer"
475
+ >
476
+ {platform.externalLabel ?? "Open"}
477
+ <IconExternalLink className="ml-1 h-3 w-3" />
478
+ </a>
479
+ </Button>
469
480
  ) : null}
470
481
  </div>
471
482
  </div>
@@ -609,7 +620,7 @@ export function MessagingSetupPanel() {
609
620
  </div>
610
621
  ) : null}
611
622
 
612
- <div className="mt-5 flex flex-wrap gap-2">
623
+ <div className="mt-5 flex flex-wrap items-center justify-end gap-2 border-t border-border pt-4">
613
624
  {platform.id === "telegram" && configured ? (
614
625
  <Button
615
626
  variant="outline"
@@ -626,31 +637,35 @@ export function MessagingSetupPanel() {
626
637
  )}
627
638
  </Button>
628
639
  ) : null}
629
- <Button
630
- onClick={() => togglePlatform(platform, enabled)}
631
- disabled={
632
- togglingPlatform === platform.id || (!enabled && !canEnable)
633
- }
634
- >
635
- {togglingPlatform === platform.id ? (
636
- <>
637
- <IconLoader2 className="mr-2 h-4 w-4 animate-spin" />
638
- Saving...
639
- </>
640
- ) : enabled ? (
641
- "Disable"
642
- ) : (
643
- "Enable"
644
- )}
645
- </Button>
640
+ {!configured && !enabled ? (
641
+ <Tooltip>
642
+ <TooltipTrigger asChild>
643
+ <span tabIndex={0}>
644
+ <Button disabled>Enable</Button>
645
+ </span>
646
+ </TooltipTrigger>
647
+ <TooltipContent>
648
+ Save the required credentials first.
649
+ </TooltipContent>
650
+ </Tooltip>
651
+ ) : (
652
+ <Button
653
+ onClick={() => togglePlatform(platform, enabled)}
654
+ disabled={togglingPlatform === platform.id}
655
+ >
656
+ {togglingPlatform === platform.id ? (
657
+ <>
658
+ <IconLoader2 className="mr-2 h-4 w-4 animate-spin" />
659
+ Saving...
660
+ </>
661
+ ) : enabled ? (
662
+ "Disable"
663
+ ) : (
664
+ "Enable"
665
+ )}
666
+ </Button>
667
+ )}
646
668
  </div>
647
-
648
- {!configured ? (
649
- <p className="mt-3 text-xs text-muted-foreground">
650
- Save the required credentials before enabling {platform.label}
651
- .
652
- </p>
653
- ) : null}
654
669
  </section>
655
670
  );
656
671
  })}