@agent-native/dispatch 0.41.1-nightly-20261104110058 → 0.42.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.
- package/CHANGELOG.md +26 -55
- package/dist/actions/get-hosted-apps-settings.d.ts +5 -0
- package/dist/actions/get-hosted-apps-settings.d.ts.map +1 -0
- package/dist/actions/get-hosted-apps-settings.js +25 -0
- package/dist/actions/get-hosted-apps-settings.js.map +1 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +4 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-connected-agents.d.ts +1 -0
- package/dist/actions/list-connected-agents.js +11 -10
- package/dist/actions/list-connected-agents.js.map +1 -1
- package/dist/actions/list_apps.js +3 -2
- package/dist/actions/list_apps.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +10 -10
- package/dist/actions/set-hosted-apps-settings.d.ts +7 -0
- package/dist/actions/set-hosted-apps-settings.d.ts.map +1 -0
- package/dist/actions/set-hosted-apps-settings.js +35 -0
- package/dist/actions/set-hosted-apps-settings.js.map +1 -0
- package/dist/components/app-card-tiers.d.ts +13 -0
- package/dist/components/app-card-tiers.d.ts.map +1 -0
- package/dist/components/app-card-tiers.js +22 -0
- package/dist/components/app-card-tiers.js.map +1 -0
- package/dist/components/app-list-row.d.ts +12 -4
- package/dist/components/app-list-row.d.ts.map +1 -1
- package/dist/components/app-list-row.js +19 -4
- package/dist/components/app-list-row.js.map +1 -1
- package/dist/components/app-open-actions.d.ts +4 -0
- package/dist/components/app-open-actions.d.ts.map +1 -1
- package/dist/components/app-open-actions.js +9 -0
- package/dist/components/app-open-actions.js.map +1 -1
- package/dist/components/app-pin-button.d.ts +9 -0
- package/dist/components/app-pin-button.d.ts.map +1 -0
- package/dist/components/app-pin-button.js +29 -0
- package/dist/components/app-pin-button.js.map +1 -0
- package/dist/components/app-tier-sections.d.ts +27 -0
- package/dist/components/app-tier-sections.d.ts.map +1 -0
- package/dist/components/app-tier-sections.js +44 -0
- package/dist/components/app-tier-sections.js.map +1 -0
- package/dist/components/chat-home-apps.d.ts.map +1 -1
- package/dist/components/chat-home-apps.js +19 -26
- package/dist/components/chat-home-apps.js.map +1 -1
- package/dist/components/connected-app-card.d.ts +3 -1
- package/dist/components/connected-app-card.d.ts.map +1 -1
- package/dist/components/connected-app-card.js +6 -3
- package/dist/components/connected-app-card.js.map +1 -1
- package/dist/components/create-app-popover.d.ts +4 -0
- package/dist/components/create-app-popover.d.ts.map +1 -1
- package/dist/components/create-app-popover.js +6 -0
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +25 -35
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/hosted-apps-settings-group.d.ts +6 -0
- package/dist/components/hosted-apps-settings-group.d.ts.map +1 -0
- package/dist/components/hosted-apps-settings-group.js +76 -0
- package/dist/components/hosted-apps-settings-group.js.map +1 -0
- package/dist/components/layout/Layout.d.ts +6 -4
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +97 -80
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +9 -20
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-app-layout-notice.d.ts +9 -0
- package/dist/components/workspace-app-layout-notice.d.ts.map +1 -0
- package/dist/components/workspace-app-layout-notice.js +27 -0
- package/dist/components/workspace-app-layout-notice.js.map +1 -0
- package/dist/components/workspace-app-search.d.ts.map +1 -1
- package/dist/components/workspace-app-search.js +11 -5
- package/dist/components/workspace-app-search.js.map +1 -1
- package/dist/components/workspace-template-card.d.ts.map +1 -1
- package/dist/components/workspace-template-card.js +7 -2
- package/dist/components/workspace-template-card.js.map +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -0
- package/dist/db/index.js.map +1 -1
- package/dist/lib/other-apps.d.ts +10 -1
- package/dist/lib/other-apps.d.ts.map +1 -1
- package/dist/lib/other-apps.js +7 -1
- package/dist/lib/other-apps.js.map +1 -1
- package/dist/lib/workspace-app-layout.d.ts +62 -6
- package/dist/lib/workspace-app-layout.d.ts.map +1 -1
- package/dist/lib/workspace-app-layout.js +196 -41
- package/dist/lib/workspace-app-layout.js.map +1 -1
- package/dist/lib/workspace-app-lists.d.ts +17 -0
- package/dist/lib/workspace-app-lists.d.ts.map +1 -0
- package/dist/lib/workspace-app-lists.js +21 -0
- package/dist/lib/workspace-app-lists.js.map +1 -0
- package/dist/lib/workspace-apps.d.ts +3 -1
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +5 -1
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +23 -35
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/settings-page.d.ts.map +1 -1
- package/dist/routes/pages/settings-page.js +24 -3
- package/dist/routes/pages/settings-page.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +13 -18
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/app-roles.d.ts +2 -0
- package/dist/server/lib/app-roles.d.ts.map +1 -1
- package/dist/server/lib/app-roles.js +26 -7
- package/dist/server/lib/app-roles.js.map +1 -1
- package/dist/server/lib/hosted-apps-settings-schema.d.ts +6 -0
- package/dist/server/lib/hosted-apps-settings-schema.d.ts.map +1 -0
- package/dist/server/lib/hosted-apps-settings-schema.js +6 -0
- package/dist/server/lib/hosted-apps-settings-schema.js.map +1 -0
- package/dist/server/lib/hosted-apps-settings.d.ts +27 -0
- package/dist/server/lib/hosted-apps-settings.d.ts.map +1 -0
- package/dist/server/lib/hosted-apps-settings.js +72 -0
- package/dist/server/lib/hosted-apps-settings.js.map +1 -0
- package/dist/server/plugins/agent-chat.js +6 -3
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/dist/server/plugins/integrations.js +3 -3
- package/dist/server/plugins/integrations.js.map +1 -1
- package/dist/shared/hosted-apps-settings.d.ts +3 -0
- package/dist/shared/hosted-apps-settings.d.ts.map +1 -0
- package/dist/shared/hosted-apps-settings.js +3 -0
- package/dist/shared/hosted-apps-settings.js.map +1 -0
- package/package.json +4 -4
- package/src/actions/get-hosted-apps-settings.ts +29 -0
- package/src/actions/hosted-apps-settings.spec.ts +100 -0
- package/src/actions/index.ts +4 -0
- package/src/actions/list-connected-agents.spec.ts +67 -0
- package/src/actions/list-connected-agents.ts +14 -15
- package/src/actions/list_apps.spec.ts +16 -0
- package/src/actions/list_apps.ts +5 -2
- package/src/actions/set-hosted-apps-settings.ts +42 -0
- package/src/components/app-card-tiers.tsx +85 -0
- package/src/components/app-list-row.spec.tsx +54 -0
- package/src/components/app-list-row.tsx +50 -8
- package/src/components/app-open-actions.tsx +16 -0
- package/src/components/app-pin-button.spec.tsx +68 -0
- package/src/components/app-pin-button.tsx +66 -0
- package/src/components/app-tier-sections.spec.tsx +155 -0
- package/src/components/app-tier-sections.tsx +158 -0
- package/src/components/chat-home-apps.spec.tsx +135 -29
- package/src/components/chat-home-apps.tsx +115 -111
- package/src/components/connected-app-card.spec.tsx +18 -2
- package/src/components/connected-app-card.tsx +18 -1
- package/src/components/create-app-popover.tsx +22 -0
- package/src/components/dispatch-control-plane.spec.tsx +89 -20
- package/src/components/dispatch-control-plane.tsx +91 -125
- package/src/components/hosted-apps-settings-group.spec.tsx +218 -0
- package/src/components/hosted-apps-settings-group.tsx +189 -0
- package/src/components/layout/Layout.spec.tsx +356 -49
- package/src/components/layout/Layout.tsx +189 -142
- package/src/components/workspace-app-card.spec.tsx +33 -29
- package/src/components/workspace-app-card.tsx +13 -31
- package/src/components/workspace-app-layout-notice.spec.tsx +106 -0
- package/src/components/workspace-app-layout-notice.tsx +65 -0
- package/src/components/workspace-app-search.spec.tsx +53 -0
- package/src/components/workspace-app-search.tsx +39 -32
- package/src/components/workspace-template-card.spec.tsx +10 -0
- package/src/components/workspace-template-card.tsx +7 -2
- package/src/db/index.spec.ts +9 -0
- package/src/db/index.ts +1 -0
- package/src/lib/other-apps.spec.ts +25 -0
- package/src/lib/other-apps.ts +9 -0
- package/src/lib/workspace-app-layout.spec.ts +61 -3
- package/src/lib/workspace-app-layout.store.spec.tsx +261 -0
- package/src/lib/workspace-app-layout.ts +265 -48
- package/src/lib/workspace-app-lists.ts +32 -0
- package/src/lib/workspace-apps.ts +10 -2
- package/src/routes/pages/apps.spec.tsx +133 -5
- package/src/routes/pages/apps.tsx +79 -140
- package/src/routes/pages/chat.spec.tsx +66 -49
- package/src/routes/pages/settings-page.tsx +31 -3
- package/src/routes/pages/settings.spec.tsx +37 -0
- package/src/server/lib/app-creation-store.spec.ts +14 -0
- package/src/server/lib/app-creation-store.ts +16 -29
- package/src/server/lib/app-creation-store.write-observation.spec.ts +216 -0
- package/src/server/lib/app-roles.spec.ts +48 -1
- package/src/server/lib/app-roles.ts +44 -12
- package/src/server/lib/hosted-apps-settings-schema.ts +6 -0
- package/src/server/lib/hosted-apps-settings.spec.ts +154 -0
- package/src/server/lib/hosted-apps-settings.ts +108 -0
- package/src/server/plugins/agent-chat.spec.ts +16 -0
- package/src/server/plugins/agent-chat.ts +7 -2
- package/src/server/plugins/integrations.ts +3 -3
- package/src/shared/hosted-apps-settings.ts +3 -0
- package/src/styles/dispatch.css +40 -11
- package/dist/components/available-apps-section.d.ts +0 -9
- package/dist/components/available-apps-section.d.ts.map +0 -1
- package/dist/components/available-apps-section.js +0 -97
- package/dist/components/available-apps-section.js.map +0 -1
- package/dist/components/other-apps-section.d.ts +0 -35
- package/dist/components/other-apps-section.d.ts.map +0 -1
- package/dist/components/other-apps-section.js +0 -79
- package/dist/components/other-apps-section.js.map +0 -1
- package/src/components/available-apps-section.tsx +0 -239
- package/src/components/other-apps-section.spec.ts +0 -50
- package/src/components/other-apps-section.tsx +0 -254
|
@@ -35,7 +35,10 @@ vi.mock("@agent-native/core/server", () => ({
|
|
|
35
35
|
|
|
36
36
|
import { ForbiddenError } from "@agent-native/core/sharing";
|
|
37
37
|
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
authorizeDispatchAdmin,
|
|
40
|
+
authorizeOrgOwnerOrAdmin,
|
|
41
|
+
} from "./app-roles.js";
|
|
39
42
|
|
|
40
43
|
const context = {
|
|
41
44
|
caller: "http" as const,
|
|
@@ -164,3 +167,47 @@ describe("authorizeDispatchAdmin", () => {
|
|
|
164
167
|
).rejects.toBeInstanceOf(ForbiddenError);
|
|
165
168
|
});
|
|
166
169
|
});
|
|
170
|
+
|
|
171
|
+
describe("authorizeOrgOwnerOrAdmin", () => {
|
|
172
|
+
beforeEach(() => {
|
|
173
|
+
vi.clearAllMocks();
|
|
174
|
+
mocks.isStandaloneDispatchRuntime.mockReturnValue(false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("denies a Dispatch admin who is only an organization member", async () => {
|
|
178
|
+
mocks.roles = ["admin"];
|
|
179
|
+
mocks.validateFederatedOrganizationMembershipForCurrentRequest.mockResolvedValue(
|
|
180
|
+
{ active: true, role: "member" },
|
|
181
|
+
);
|
|
182
|
+
await expect(authorizeOrgOwnerOrAdmin({}, context)).rejects.toThrow(
|
|
183
|
+
"Only organization owners and admins",
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it.each(["owner", "admin"])("allows an organization %s", async (role) => {
|
|
188
|
+
mocks.validateFederatedOrganizationMembershipForCurrentRequest.mockResolvedValue(
|
|
189
|
+
{ active: true, role },
|
|
190
|
+
);
|
|
191
|
+
await expect(
|
|
192
|
+
authorizeOrgOwnerOrAdmin({}, context),
|
|
193
|
+
).resolves.toBeUndefined();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("denies an inactive membership", async () => {
|
|
197
|
+
mocks.validateFederatedOrganizationMembershipForCurrentRequest.mockResolvedValue(
|
|
198
|
+
{ active: false, role: null },
|
|
199
|
+
);
|
|
200
|
+
await expect(authorizeOrgOwnerOrAdmin({}, context)).rejects.toThrow(
|
|
201
|
+
"requires active organization membership",
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("lets a signed-in user without an organization manage their own scope", async () => {
|
|
206
|
+
await expect(
|
|
207
|
+
authorizeOrgOwnerOrAdmin({}, { ...context, orgId: null } as never),
|
|
208
|
+
).resolves.toBeUndefined();
|
|
209
|
+
await expect(
|
|
210
|
+
authorizeOrgOwnerOrAdmin({}, { ...context, userEmail: "" }),
|
|
211
|
+
).rejects.toBeInstanceOf(ForbiddenError);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
isMissingOrganizationTableError,
|
|
5
5
|
isStandaloneDispatchRuntime,
|
|
6
6
|
type AppRoles,
|
|
7
|
+
type OrgRole,
|
|
7
8
|
validateFederatedOrganizationMembershipForCurrentRequest,
|
|
8
9
|
} from "@agent-native/core/org";
|
|
9
10
|
import {
|
|
@@ -22,19 +23,21 @@ function getDispatchAccess(): AppRoles<"admin", "administer"> {
|
|
|
22
23
|
}));
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The caller's active membership in the request org, or null when there is no
|
|
28
|
+
* org to check (a user without one, or standalone Dispatch with no org tables).
|
|
29
|
+
*/
|
|
30
|
+
async function requireActiveMembership(
|
|
31
|
+
ctx: ActionRunContext | undefined,
|
|
32
|
+
action: string,
|
|
33
|
+
): Promise<{ email: string; orgId: string; role: OrgRole } | null> {
|
|
29
34
|
const email =
|
|
30
35
|
ctx?.userEmail !== undefined ? ctx.userEmail : getRequestUserEmail();
|
|
31
36
|
const orgId = ctx?.orgId !== undefined ? ctx.orgId : getRequestOrgId();
|
|
32
37
|
if (!email?.trim()) {
|
|
33
|
-
throw new ForbiddenError(
|
|
34
|
-
"Dispatch administration requires an authenticated user.",
|
|
35
|
-
);
|
|
38
|
+
throw new ForbiddenError(`${action} requires an authenticated user.`);
|
|
36
39
|
}
|
|
37
|
-
if (!orgId?.trim()) return;
|
|
40
|
+
if (!orgId?.trim()) return null;
|
|
38
41
|
let membership;
|
|
39
42
|
try {
|
|
40
43
|
membership = await validateFederatedOrganizationMembershipForCurrentRequest(
|
|
@@ -48,17 +51,46 @@ export async function authorizeDispatchAdmin(
|
|
|
48
51
|
isMissingOrganizationTableError(error) &&
|
|
49
52
|
isStandaloneDispatchRuntime()
|
|
50
53
|
) {
|
|
51
|
-
return;
|
|
54
|
+
return null;
|
|
52
55
|
}
|
|
53
56
|
throw error;
|
|
54
57
|
}
|
|
55
58
|
if (!membership.active) {
|
|
56
59
|
throw new ForbiddenError(
|
|
57
|
-
|
|
60
|
+
`${action} requires active organization membership.`,
|
|
58
61
|
);
|
|
59
62
|
}
|
|
63
|
+
return { email, orgId, role: membership.role };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function authorizeDispatchAdmin(
|
|
67
|
+
_args: unknown,
|
|
68
|
+
ctx?: ActionRunContext,
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
const membership = await requireActiveMembership(
|
|
71
|
+
ctx,
|
|
72
|
+
"Dispatch administration",
|
|
73
|
+
);
|
|
74
|
+
if (!membership) return;
|
|
60
75
|
await getDispatchAccess().assertPermission(["administer"], {
|
|
61
|
-
userEmail: email,
|
|
62
|
-
orgId,
|
|
76
|
+
userEmail: membership.email,
|
|
77
|
+
orgId: membership.orgId,
|
|
63
78
|
});
|
|
64
79
|
}
|
|
80
|
+
|
|
81
|
+
/** Organization owners and admins only; Dispatch admin roles do not count. */
|
|
82
|
+
export async function authorizeOrgOwnerOrAdmin(
|
|
83
|
+
_args: unknown,
|
|
84
|
+
ctx?: ActionRunContext,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
const membership = await requireActiveMembership(
|
|
87
|
+
ctx,
|
|
88
|
+
"This organization setting",
|
|
89
|
+
);
|
|
90
|
+
if (!membership) return;
|
|
91
|
+
if (membership.role !== "owner" && membership.role !== "admin") {
|
|
92
|
+
throw new ForbiddenError(
|
|
93
|
+
"Only organization owners and admins can change this setting.",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { isActionContractError } from "@agent-native/core/action";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const mocks = vi.hoisted(() => ({
|
|
5
|
+
orgId: vi.fn<() => string | undefined>(() => "org-1"),
|
|
6
|
+
email: vi.fn<() => string | undefined>(() => "owner@example.test"),
|
|
7
|
+
getOrgSetting: vi.fn(),
|
|
8
|
+
getUserSetting: vi.fn(),
|
|
9
|
+
putOrgSetting: vi.fn(),
|
|
10
|
+
putUserSetting: vi.fn(),
|
|
11
|
+
getBuiltinAgents: vi.fn(),
|
|
12
|
+
loadWorkspaceAppsManifest: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("@agent-native/core/server", () => ({
|
|
16
|
+
getRequestOrgId: () => mocks.orgId(),
|
|
17
|
+
getRequestUserEmail: () => mocks.email(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("@agent-native/core/settings", () => ({
|
|
21
|
+
getOrgSetting: mocks.getOrgSetting,
|
|
22
|
+
getUserSetting: mocks.getUserSetting,
|
|
23
|
+
putOrgSetting: mocks.putOrgSetting,
|
|
24
|
+
putUserSetting: mocks.putUserSetting,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock("@agent-native/core/server/agent-discovery", () => ({
|
|
28
|
+
getBuiltinAgents: mocks.getBuiltinAgents,
|
|
29
|
+
loadWorkspaceAppsManifest: mocks.loadWorkspaceAppsManifest,
|
|
30
|
+
normalizeAgentId: (id: string) => id.trim().toLowerCase(),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
hideHostedAppsWhenOff,
|
|
35
|
+
HOSTED_APPS_SETTINGS_KEY,
|
|
36
|
+
InvalidHostedAppsSettingsError,
|
|
37
|
+
listHostedApps,
|
|
38
|
+
readHostedAppsSettings,
|
|
39
|
+
setHostedAppsSettings,
|
|
40
|
+
} from "./hosted-apps-settings.js";
|
|
41
|
+
|
|
42
|
+
const agent = (id: string) => ({
|
|
43
|
+
id,
|
|
44
|
+
name: id,
|
|
45
|
+
description: "",
|
|
46
|
+
url: `https://${id}.example.test`,
|
|
47
|
+
color: "#000000",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("hosted apps settings", () => {
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
vi.clearAllMocks();
|
|
53
|
+
mocks.orgId.mockReturnValue("org-1");
|
|
54
|
+
mocks.email.mockReturnValue("owner@example.test");
|
|
55
|
+
mocks.getBuiltinAgents.mockReturnValue([agent("mail"), agent("slides")]);
|
|
56
|
+
mocks.loadWorkspaceAppsManifest.mockResolvedValue([{ id: "Slides" }]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("shows hosted apps when the org has never set it", async () => {
|
|
60
|
+
mocks.getOrgSetting.mockResolvedValue(null);
|
|
61
|
+
await expect(readHostedAppsSettings()).resolves.toEqual({
|
|
62
|
+
showHostedApps: true,
|
|
63
|
+
invalid: false,
|
|
64
|
+
});
|
|
65
|
+
expect(mocks.getOrgSetting).toHaveBeenCalledWith(
|
|
66
|
+
"org-1",
|
|
67
|
+
HOSTED_APPS_SETTINGS_KEY,
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("reads the stored org value", async () => {
|
|
72
|
+
mocks.getOrgSetting.mockResolvedValue({ showHostedApps: false });
|
|
73
|
+
await expect(readHostedAppsSettings()).resolves.toEqual({
|
|
74
|
+
showHostedApps: false,
|
|
75
|
+
invalid: false,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("falls back to the user's setting without an org", async () => {
|
|
80
|
+
mocks.orgId.mockReturnValue(undefined);
|
|
81
|
+
mocks.getUserSetting.mockResolvedValue({ showHostedApps: false });
|
|
82
|
+
await expect(readHostedAppsSettings()).resolves.toEqual({
|
|
83
|
+
showHostedApps: false,
|
|
84
|
+
invalid: false,
|
|
85
|
+
});
|
|
86
|
+
expect(mocks.getUserSetting).toHaveBeenCalledWith(
|
|
87
|
+
"owner@example.test",
|
|
88
|
+
HOSTED_APPS_SETTINGS_KEY,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
await setHostedAppsSettings({ showHostedApps: true });
|
|
92
|
+
expect(mocks.putUserSetting).toHaveBeenCalledWith(
|
|
93
|
+
"owner@example.test",
|
|
94
|
+
HOSTED_APPS_SETTINGS_KEY,
|
|
95
|
+
{ showHostedApps: true },
|
|
96
|
+
);
|
|
97
|
+
expect(mocks.putOrgSetting).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("reports an invalid stored value and hides hosted apps for it", async () => {
|
|
101
|
+
mocks.getOrgSetting.mockResolvedValue({ showHostedApps: "no" });
|
|
102
|
+
await expect(readHostedAppsSettings()).resolves.toEqual({
|
|
103
|
+
showHostedApps: false,
|
|
104
|
+
invalid: true,
|
|
105
|
+
});
|
|
106
|
+
expect(
|
|
107
|
+
(await hideHostedAppsWhenOff([agent("mail"), agent("partner")])).map(
|
|
108
|
+
(a) => a.id,
|
|
109
|
+
),
|
|
110
|
+
).toEqual(["partner"]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("types the invalid-setting error so Settings can offer the repair", () => {
|
|
114
|
+
const error = new InvalidHostedAppsSettingsError();
|
|
115
|
+
expect(isActionContractError(error)).toBe(true);
|
|
116
|
+
expect(error).toMatchObject({
|
|
117
|
+
errorCode: "dispatch_hosted_apps_settings_invalid",
|
|
118
|
+
statusCode: 409,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("propagates a settings read failure", async () => {
|
|
123
|
+
mocks.getOrgSetting.mockRejectedValue(new Error("db down"));
|
|
124
|
+
await expect(readHostedAppsSettings()).rejects.toThrow("db down");
|
|
125
|
+
await expect(hideHostedAppsWhenOff([agent("mail")])).rejects.toThrow(
|
|
126
|
+
"db down",
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("writes the org setting", async () => {
|
|
131
|
+
await setHostedAppsSettings({ showHostedApps: false });
|
|
132
|
+
expect(mocks.putOrgSetting).toHaveBeenCalledWith(
|
|
133
|
+
"org-1",
|
|
134
|
+
HOSTED_APPS_SETTINGS_KEY,
|
|
135
|
+
{ showHostedApps: false },
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("treats a mounted workspace app with a built-in id as not hosted", async () => {
|
|
140
|
+
expect((await listHostedApps()).map((app) => app.id)).toEqual(["mail"]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("removes only hosted apps from agent context when off", async () => {
|
|
144
|
+
const agents = [agent("mail"), agent("slides"), agent("partner")];
|
|
145
|
+
mocks.getOrgSetting.mockResolvedValue({ showHostedApps: true });
|
|
146
|
+
expect(await hideHostedAppsWhenOff(agents)).toEqual(agents);
|
|
147
|
+
|
|
148
|
+
mocks.getOrgSetting.mockResolvedValue({ showHostedApps: false });
|
|
149
|
+
expect((await hideHostedAppsWhenOff(agents)).map((a) => a.id)).toEqual([
|
|
150
|
+
"slides",
|
|
151
|
+
"partner",
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ActionContractError } from "@agent-native/core/action";
|
|
2
|
+
import {
|
|
3
|
+
getRequestOrgId,
|
|
4
|
+
getRequestUserEmail,
|
|
5
|
+
} from "@agent-native/core/server";
|
|
6
|
+
import {
|
|
7
|
+
getBuiltinAgents,
|
|
8
|
+
loadWorkspaceAppsManifest,
|
|
9
|
+
normalizeAgentId,
|
|
10
|
+
type DiscoveredAgent,
|
|
11
|
+
} from "@agent-native/core/server/agent-discovery";
|
|
12
|
+
import {
|
|
13
|
+
getOrgSetting,
|
|
14
|
+
getUserSetting,
|
|
15
|
+
putOrgSetting,
|
|
16
|
+
putUserSetting,
|
|
17
|
+
} from "@agent-native/core/settings";
|
|
18
|
+
|
|
19
|
+
import { HOSTED_APPS_SETTINGS_INVALID_ERROR_CODE } from "../../shared/hosted-apps-settings.js";
|
|
20
|
+
|
|
21
|
+
export const HOSTED_APPS_SETTINGS_KEY = "dispatch-hosted-apps-settings";
|
|
22
|
+
|
|
23
|
+
export interface HostedAppsSettings {
|
|
24
|
+
showHostedApps: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A contract error, so its code reaches Settings, which offers the save that
|
|
28
|
+
// repairs it; a bare error would arrive as an anonymous 500.
|
|
29
|
+
export class InvalidHostedAppsSettingsError extends ActionContractError {
|
|
30
|
+
constructor() {
|
|
31
|
+
super(
|
|
32
|
+
`Stored ${HOSTED_APPS_SETTINGS_KEY} is not { showHostedApps: boolean }. Save the Agent-Native.com hosted apps setting again to repair it.`,
|
|
33
|
+
{ errorCode: HOSTED_APPS_SETTINGS_INVALID_ERROR_CODE, statusCode: 409 },
|
|
34
|
+
);
|
|
35
|
+
this.name = "InvalidHostedAppsSettingsError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Built-in catalog apps this workspace offers. A mounted workspace app with a
|
|
41
|
+
* built-in's id is a workspace app; the built-in config never applies to it.
|
|
42
|
+
*/
|
|
43
|
+
export async function listHostedApps(): Promise<DiscoveredAgent[]> {
|
|
44
|
+
const workspaceApps = await loadWorkspaceAppsManifest();
|
|
45
|
+
const workspaceIds = new Set(
|
|
46
|
+
(workspaceApps ?? []).map((app) => normalizeAgentId(app.id)),
|
|
47
|
+
);
|
|
48
|
+
return getBuiltinAgents("dispatch").filter(
|
|
49
|
+
(agent) => !workspaceIds.has(agent.id),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function hostedAppsSettingsScope() {
|
|
54
|
+
const orgId = getRequestOrgId();
|
|
55
|
+
if (orgId) return { scope: "org" as const, scopeId: orgId };
|
|
56
|
+
const email = getRequestUserEmail();
|
|
57
|
+
return email ? { scope: "user" as const, scopeId: email } : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The stored setting. An unreadable value is `invalid` and hides hosted apps,
|
|
62
|
+
* so it cannot fail the app lists or agent context; Settings reports it.
|
|
63
|
+
*/
|
|
64
|
+
export async function readHostedAppsSettings(): Promise<
|
|
65
|
+
HostedAppsSettings & { invalid: boolean }
|
|
66
|
+
> {
|
|
67
|
+
const scope = hostedAppsSettingsScope();
|
|
68
|
+
// No org and no user means there is nowhere the setting could be stored.
|
|
69
|
+
if (!scope) return { showHostedApps: true, invalid: false };
|
|
70
|
+
const raw =
|
|
71
|
+
scope.scope === "org"
|
|
72
|
+
? await getOrgSetting(scope.scopeId, HOSTED_APPS_SETTINGS_KEY)
|
|
73
|
+
: await getUserSetting(scope.scopeId, HOSTED_APPS_SETTINGS_KEY);
|
|
74
|
+
if (raw === null) return { showHostedApps: true, invalid: false };
|
|
75
|
+
if (typeof raw.showHostedApps !== "boolean") {
|
|
76
|
+
return { showHostedApps: false, invalid: true };
|
|
77
|
+
}
|
|
78
|
+
return { showHostedApps: raw.showHostedApps, invalid: false };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function setHostedAppsSettings(
|
|
82
|
+
next: HostedAppsSettings,
|
|
83
|
+
): Promise<HostedAppsSettings> {
|
|
84
|
+
const scope = hostedAppsSettingsScope();
|
|
85
|
+
if (!scope) {
|
|
86
|
+
throw Object.assign(
|
|
87
|
+
new Error("Sign in to change the Agent-Native.com hosted apps setting."),
|
|
88
|
+
{ statusCode: 401 },
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const value = { showHostedApps: next.showHostedApps };
|
|
92
|
+
if (scope.scope === "org") {
|
|
93
|
+
await putOrgSetting(scope.scopeId, HOSTED_APPS_SETTINGS_KEY, value);
|
|
94
|
+
} else {
|
|
95
|
+
await putUserSetting(scope.scopeId, HOSTED_APPS_SETTINGS_KEY, value);
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Hides hosted apps from Dispatch's lists and agent context when off. */
|
|
101
|
+
export async function hideHostedAppsWhenOff<T extends { id: string }>(
|
|
102
|
+
agents: T[],
|
|
103
|
+
): Promise<T[]> {
|
|
104
|
+
const { showHostedApps } = await readHostedAppsSettings();
|
|
105
|
+
if (showHostedApps) return agents;
|
|
106
|
+
const hostedIds = new Set((await listHostedApps()).map((app) => app.id));
|
|
107
|
+
return agents.filter((agent) => !hostedIds.has(agent.id));
|
|
108
|
+
}
|
|
@@ -2,11 +2,18 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
const mocks = vi.hoisted(() => ({
|
|
4
4
|
createAgentChatPlugin: vi.fn((options: Record<string, unknown>) => options),
|
|
5
|
+
registerAgentContextAgentFilter: vi.fn(),
|
|
6
|
+
hideHostedAppsWhenOff: vi.fn(),
|
|
5
7
|
}));
|
|
6
8
|
|
|
7
9
|
vi.mock("@agent-native/core/server", () => ({
|
|
8
10
|
createAgentChatPlugin: mocks.createAgentChatPlugin,
|
|
9
11
|
getBuilderWorkspaceUrl: () => null,
|
|
12
|
+
registerAgentContextAgentFilter: mocks.registerAgentContextAgentFilter,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("../lib/hosted-apps-settings.js", () => ({
|
|
16
|
+
hideHostedAppsWhenOff: mocks.hideHostedAppsWhenOff,
|
|
10
17
|
}));
|
|
11
18
|
|
|
12
19
|
vi.mock("@agent-native/core/org", () => ({
|
|
@@ -48,6 +55,15 @@ describe("Dispatch agent chat plugin", () => {
|
|
|
48
55
|
);
|
|
49
56
|
});
|
|
50
57
|
|
|
58
|
+
it("hides hosted apps from its own agent context through the setting", async () => {
|
|
59
|
+
await import("./agent-chat.js");
|
|
60
|
+
|
|
61
|
+
expect(mocks.registerAgentContextAgentFilter).toHaveBeenCalledWith(
|
|
62
|
+
"dispatch",
|
|
63
|
+
mocks.hideHostedAppsWhenOff,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
51
67
|
it("routes customer credit questions to the owning workspace app", async () => {
|
|
52
68
|
await import("./agent-chat.js");
|
|
53
69
|
|
|
@@ -2,9 +2,11 @@ import { getOrgContext } from "@agent-native/core/org";
|
|
|
2
2
|
import {
|
|
3
3
|
createAgentChatPlugin,
|
|
4
4
|
getBuilderWorkspaceUrl,
|
|
5
|
+
registerAgentContextAgentFilter,
|
|
5
6
|
} from "@agent-native/core/server";
|
|
6
7
|
|
|
7
8
|
import { dispatchActions } from "../../actions/index.js";
|
|
9
|
+
import { hideHostedAppsWhenOff } from "../lib/hosted-apps-settings.js";
|
|
8
10
|
import {
|
|
9
11
|
workspaceAppActionRouteAuth,
|
|
10
12
|
WORKSPACE_APP_CLAIM_ACTION_PATH,
|
|
@@ -41,6 +43,8 @@ const INITIAL_TOOL_NAMES = [
|
|
|
41
43
|
"navigate",
|
|
42
44
|
];
|
|
43
45
|
|
|
46
|
+
registerAgentContextAgentFilter("dispatch", hideHostedAppsWhenOff);
|
|
47
|
+
|
|
44
48
|
export default createAgentChatPlugin({
|
|
45
49
|
appId: "dispatch",
|
|
46
50
|
connectApps: true,
|
|
@@ -89,11 +93,12 @@ Use the standard workspace primitives:
|
|
|
89
93
|
- You receive a compact available-apps block with sibling workspace app names and descriptions. Use it to pick the right A2A target, and call list-connected-agents or tool-search only when you need fresh details.
|
|
90
94
|
- Hosted/connected A2A neighbors such as Analytics and Content come from the available-apps context or list-connected-agents. list-workspace-apps only inventories apps mounted inside this workspace deployment; never use a missing row there to conclude that a connected agent is unavailable.
|
|
91
95
|
- When answering whether a mounted workspace app exposes an agent card or A2A endpoint, call list-workspace-apps with includeAgentCards=true. If you have not requested that probe, absence of agent-card fields means unchecked, not unavailable.
|
|
92
|
-
- When creating a new workspace app, create a separate app under apps/<app-id> with apps/<app-id>/package.json including a concise generated description, mount it at /<app-id>, use relative /<app-id> links, never hardcode localhost or dev ports, use shadcn/ui with @tabler/icons-react rather than lucide-react, and
|
|
96
|
+
- When creating a new workspace app, create a separate app under apps/<app-id> with apps/<app-id>/package.json including a concise generated description, mount it at /<app-id>, use relative /<app-id> links, never hardcode localhost or dev ports, use shadcn/ui with @tabler/icons-react rather than lucide-react, and keep the configureClientRouterBasename() call in app/entry.client.tsx so the app hydrates at /<app-id>. There is no separate workspace app registry to edit.
|
|
93
97
|
- When a user asks to rename an existing workspace app or change its Dispatch title/description, call update-workspace-app-metadata with the existing appId from list-workspace-apps. This is a metadata-only edit — never call start-workspace-app-creation, which creates a new app and Builder branch.
|
|
94
98
|
- When an explicit app-creation request already includes a source brief or a concrete repeatable workflow, call start-workspace-app-creation without asking non-blocking product or UX questions. Choose recommended defaults, let the Builder handoff record assumptions, and ask only for authorization, credentials, a destructive action, an ambiguous target workspace, or a genuinely missing workflow.
|
|
95
99
|
- If the chat template is used, treat it as scaffolding only: the finished app must be branded as the requested app with its own home screen/navigation/package metadata/manifest, and must not leave visible "Chat", "Starter", "Blank app", or "New app" UI behind.
|
|
96
|
-
- Treat
|
|
100
|
+
- Treat the apps in the available-apps context as existing neighbors available through links and A2A. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.
|
|
101
|
+
- Owners and admins can hide Agent-Native.com hosted apps for the organization with set-hosted-apps-settings; get-hosted-apps-settings reads it. When hidden they are left out of available-apps and list-connected-agents marks them hiddenFromLaunchers, but call-agent by name still reaches them.
|
|
97
102
|
- Integration grants are not provider capability limits. For ad hoc provider inspection, querying, reporting, or troubleshooting, call provider-api-catalog/provider-api-docs, then provider-api-request against the provider's real HTTP API. Use connectionId for a specific shared grant and accountId for a specific OAuth account. Never expose secret values or silently widen app access while doing this.
|
|
98
103
|
- For broad provider searches, joins, classification, corpus counts, or absence claims, fetch every relevant page or an explicitly bounded cohort, stage/save large responses with stageAs/saveToFile/fetchAllPages, and reduce them with query-staged-dataset or run-code. Report source, filters, row counts, pagination, truncation, failed pages, and uncovered gaps.
|
|
99
104
|
- Use list-dispatch-usage-metrics only for this workspace's own members' agent/LLM usage and spend, or aggregate adoption of an accessible workspace app. For agent/LLM usage or spend, call it with scope=workspace and the requested sinceDays; it reports workspace-member usage only. For app adoption, call it with scope=app and appId; app creators can see aggregate daily/weekly active users and tracked actions for their own app, while organization owners/admins can inspect any accessible app. App scope omits individual users and prompt previews. External customer, account, or company usage, credits, activity, or product data belongs to the relevant app: use its identity in <relevant-apps> when clear, or call describe-workspace-apps with a query when ownership is unclear. Do not use this action for external customer/account/company data.
|
|
@@ -23,7 +23,7 @@ Default posture:
|
|
|
23
23
|
- Use relevant-apps prompt profiles first, then list-connected-agents when you need fresh details. If that context is absent or does not list the needed capability, search with describe-workspace-apps query="<capability>" before assuming a request must be handled locally.
|
|
24
24
|
- Hosted/connected A2A neighbors such as Analytics and Content come from the relevant-apps context or list-connected-agents. If neither identifies the needed capability, use describe-workspace-apps before concluding a connected agent is unavailable. list-workspace-apps only inventories apps mounted inside this workspace deployment; never use a missing row there to conclude that a connected agent is unavailable.
|
|
25
25
|
- When asked whether a mounted workspace app exposes an agent card or A2A endpoint, call list-workspace-apps with includeAgentCards=true. Without that probe, missing agent-card fields mean unchecked, not unavailable.
|
|
26
|
-
- Treat
|
|
26
|
+
- Treat the apps in the relevant-apps context as existing neighbors available through links and A2A. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.
|
|
27
27
|
- Integration grants are not provider capability limits. For ad hoc provider inspection, querying, reporting, or troubleshooting, call provider-api-catalog/provider-api-docs, then provider-api-request against the provider's real HTTP API. Use connectionId for a specific shared grant and accountId for a specific OAuth account. Never expose secret values or silently widen app access while doing this.
|
|
28
28
|
- Keep durable memory and operating instructions in resources rather than ephemeral chat.
|
|
29
29
|
- Reply in the originating thread unless the user explicitly asks you to send to a saved destination.
|
|
@@ -38,9 +38,9 @@ When a user asks for something:
|
|
|
38
38
|
- Exception: if the downstream agent reports a missing model/provider credential, do not name exact env vars, Vault keys, tokens, or secrets. Say the target app needs an LLM connection and recommend using Builder/managed LLM for that app; keep bring-your-own provider keys as a secondary option only if the user asks.
|
|
39
39
|
- If the user asks to create, build, make, scaffold, or generate an "agent" from Dispatch chat or by tagging @agent-native in Slack, email, or Telegram, first classify the ask. If it is a simple Dispatch-native behavior like a reminder, digest, monitor, routing rule, saved instruction, or recurring workflow, create or update the recurring job/resource/destination in Dispatch. If it is a robust unique product or teammate that needs its own UI, data model, actions, integrations, or domain workflow, treat it as a new workspace app and call start-workspace-app-creation.
|
|
40
40
|
- If a user asks to rename an existing workspace app or change its Dispatch title/description, call update-workspace-app-metadata with the existing appId from list-workspace-apps. Do not call start-workspace-app-creation for a metadata-only edit because that starts a new app and Builder branch.
|
|
41
|
-
- If a new-app prompt asks for access to Mail, Calendar, Analytics, Brain, Assets, or similar first-party app data/agents, keep using the existing
|
|
41
|
+
- If a new-app prompt asks for access to Mail, Calendar, Analytics, Brain, Assets, or similar first-party app data/agents, keep using the existing app through A2A. Do not ask Builder to scaffold those apps as children of the new app unless the user explicitly asks for a customized app from that template.
|
|
42
42
|
- If the chat template is used, treat it as scaffolding only: the finished app must be branded as the requested app with its own home screen/navigation/package metadata/manifest, and must not leave visible "Chat", "Starter", "Blank app", or "New app" UI behind.
|
|
43
|
-
- If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt and include a concise generated description by default. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/chat or another existing app unless the user explicitly asks to modify that existing app. When the prompt contains a source brief or concrete repeatable workflow, choose recommended defaults and do not ask non-blocking product or UX questions; ask only for authorization, credentials, a destructive action, an ambiguous target workspace, or a genuinely missing workflow. If the request is too vague to classify, ask one concise follow-up. If the action returns mode "builder", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists with name/displayName and description so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and
|
|
43
|
+
- If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt and include a concise generated description by default. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/chat or another existing app unless the user explicitly asks to modify that existing app. When the prompt contains a source brief or concrete repeatable workflow, choose recommended defaults and do not ask non-blocking product or UX questions; ask only for authorization, credentials, a destructive action, an ambiguous target workspace, or a genuinely missing workflow. If the request is too vague to classify, ask one concise follow-up. If the action returns mode "builder", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists with name/displayName and description so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and keeping the configureClientRouterBasename() call in app/entry.client.tsx. The new app lives at the workspace root /<app-id>, NOT under /dispatch/<app-id>, /apps/<app-id>, or any other Dispatch tab — when telling the user where to find it, link to /<app-id> only. There is no separate workspace app registry to edit. If it returns mode "local-agent", tell the user it is ready for the local code agent and include the returned app path/prompt summary. Dispatch reuses the connected workspace Builder project or provisions it through the Builder Projects API when an organization owner/admin starts the first app; members need an owner/admin to configure the project first. Do not ask the user to use Builder org/beta settings. If it returns mode "builder-unavailable", the action also returns a \`reason\` code plus a user-facing \`message\` and (for some reasons) an operator-facing \`detail\`. Relay the \`message\` to the user as-is; only surface \`detail\` when the user is clearly an operator debugging the deployment (e.g. troubleshooting a broken Builder connection for the workspace), never as routine detail for an end user. If it returns mode "app-id-taken", the app id is already in use: \`conflict\` is "registered" for a live app or "pending" for a creation in flight, and \`owner\` names who started it. Relay the \`message\` and propose a different app id; never retry the same id.
|
|
44
44
|
- For digests, reminders, or saved behavior, prefer recurring jobs, resources, or destinations over chat replies.
|
|
45
45
|
- Keep responses concise and operational — messaging platforms have character limits.
|
|
46
46
|
- Use markdown sparingly (bold and lists are fine, avoid complex formatting).
|
package/src/styles/dispatch.css
CHANGED
|
@@ -17,22 +17,51 @@
|
|
|
17
17
|
display: none;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
/* Chat history uses the shadcn sidebar metrics the apps rail below it gets
|
|
21
|
+
from SidebarGroupLabel: 32px labels and rows, 8px inner padding, 4px gaps. */
|
|
22
|
+
.dispatch-chat-first-chats .an-chat-history__section-label {
|
|
21
23
|
display: flex;
|
|
22
24
|
align-items: center;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
background: hsl(var(--muted) / 0.35);
|
|
27
|
-
padding: 0.5rem 0.75rem;
|
|
28
|
-
color: hsl(var(--muted-foreground));
|
|
25
|
+
min-height: 2rem;
|
|
26
|
+
padding: 0 0.5rem;
|
|
27
|
+
color: hsl(var(--sidebar-foreground) / 0.7);
|
|
29
28
|
font-size: 0.75rem;
|
|
29
|
+
font-weight: 500;
|
|
30
|
+
letter-spacing: 0;
|
|
31
|
+
text-transform: none;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
.dispatch-chat-first-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
.dispatch-chat-first-chats .an-chat-history--rail .an-chat-history__list {
|
|
35
|
+
padding-block: 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.dispatch-chat-first-chats .an-chat-history--rail .an-chat-history__section {
|
|
39
|
+
gap: 0.25rem;
|
|
40
|
+
padding-block: 0.5rem;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.dispatch-chat-first-chats .an-chat-history--rail .an-chat-history-row,
|
|
44
|
+
.dispatch-chat-first-chats
|
|
45
|
+
.an-chat-history--rail
|
|
46
|
+
.an-chat-history__skeleton-row {
|
|
47
|
+
margin-inline: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.dispatch-chat-first-chats .an-chat-history--rail .an-chat-history-row__button,
|
|
51
|
+
.dispatch-chat-first-chats
|
|
52
|
+
.an-chat-history--rail
|
|
53
|
+
.an-chat-history__skeleton-row {
|
|
54
|
+
min-height: 2rem;
|
|
55
|
+
padding-inline: 0.5rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.dispatch-chat-first-chats .an-chat-history-rail__section-footer {
|
|
59
|
+
padding: 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.dispatch-chat-first-chats .an-chat-history-rail__section-toggle {
|
|
63
|
+
min-height: 2rem;
|
|
64
|
+
padding: 0 0.5rem;
|
|
36
65
|
}
|
|
37
66
|
|
|
38
67
|
@media (max-width: 767px) {
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { ConnectedAppSummary, WorkspaceAppId } from "../lib/other-apps.js";
|
|
2
|
-
export declare function AvailableAppsSection({ connectedApps, workspaceApps, query, onConnected, className, }: {
|
|
3
|
-
connectedApps: ConnectedAppSummary[];
|
|
4
|
-
workspaceApps: WorkspaceAppId[];
|
|
5
|
-
query?: string;
|
|
6
|
-
onConnected?: () => void;
|
|
7
|
-
className?: string;
|
|
8
|
-
}): import("node_modules/@types/react").JSX.Element | null;
|
|
9
|
-
//# sourceMappingURL=available-apps-section.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"available-apps-section.d.ts","sourceRoot":"","sources":["../../src/components/available-apps-section.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAwB7E,wBAAgB,oBAAoB,CAAC,EACnC,aAAa,EACb,aAAa,EACb,KAAU,EACV,WAAW,EACX,SAAS,GACV,EAAE;IACD,aAAa,EAAE,mBAAmB,EAAE,CAAC;IACrC,aAAa,EAAE,cAAc,EAAE,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,0DAqCA"}
|