@agent-native/dispatch 0.7.0 → 0.8.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/README.md +37 -0
- package/dist/actions/apply-dream-proposal.d.ts +3 -0
- package/dist/actions/apply-dream-proposal.d.ts.map +1 -0
- package/dist/actions/apply-dream-proposal.js +11 -0
- package/dist/actions/apply-dream-proposal.js.map +1 -0
- package/dist/actions/create-dream-report.d.ts +3 -0
- package/dist/actions/create-dream-report.d.ts.map +1 -0
- package/dist/actions/create-dream-report.js +67 -0
- package/dist/actions/create-dream-report.js.map +1 -0
- package/dist/actions/create-workspace-resource.js +3 -3
- package/dist/actions/create-workspace-resource.js.map +1 -1
- package/dist/actions/delete-workspace-resource.js +1 -1
- package/dist/actions/delete-workspace-resource.js.map +1 -1
- package/dist/actions/ensure-dream-job.d.ts +3 -0
- package/dist/actions/ensure-dream-job.d.ts.map +1 -0
- package/dist/actions/ensure-dream-job.js +73 -0
- package/dist/actions/ensure-dream-job.js.map +1 -0
- package/dist/actions/get-dream-settings.d.ts +3 -0
- package/dist/actions/get-dream-settings.d.ts.map +1 -0
- package/dist/actions/get-dream-settings.js +11 -0
- package/dist/actions/get-dream-settings.js.map +1 -0
- package/dist/actions/get-dream.d.ts +3 -0
- package/dist/actions/get-dream.d.ts.map +1 -0
- package/dist/actions/get-dream.js +13 -0
- package/dist/actions/get-dream.js.map +1 -0
- package/dist/actions/get-workspace-resource-effective-context.d.ts +3 -0
- package/dist/actions/get-workspace-resource-effective-context.d.ts.map +1 -0
- package/dist/actions/get-workspace-resource-effective-context.js +27 -0
- package/dist/actions/get-workspace-resource-effective-context.js.map +1 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +30 -4
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-dream-candidates.d.ts +3 -0
- package/dist/actions/list-dream-candidates.d.ts.map +1 -0
- package/dist/actions/list-dream-candidates.js +68 -0
- package/dist/actions/list-dream-candidates.js.map +1 -0
- package/dist/actions/list-dreams.d.ts +3 -0
- package/dist/actions/list-dreams.d.ts.map +1 -0
- package/dist/actions/list-dreams.js +17 -0
- package/dist/actions/list-dreams.js.map +1 -0
- package/dist/actions/list-workspace-resources-for-app.d.ts +3 -0
- package/dist/actions/list-workspace-resources-for-app.d.ts.map +1 -0
- package/dist/actions/list-workspace-resources-for-app.js +12 -0
- package/dist/actions/list-workspace-resources-for-app.js.map +1 -0
- package/dist/actions/list-workspace-resources.js +1 -1
- package/dist/actions/list-workspace-resources.js.map +1 -1
- package/dist/actions/navigate.d.ts +1 -0
- package/dist/actions/navigate.d.ts.map +1 -1
- package/dist/actions/navigate.js +2 -1
- package/dist/actions/navigate.js.map +1 -1
- package/dist/actions/preview-dream-proposal.d.ts +3 -0
- package/dist/actions/preview-dream-proposal.d.ts.map +1 -0
- package/dist/actions/preview-dream-proposal.js +13 -0
- package/dist/actions/preview-dream-proposal.js.map +1 -0
- package/dist/actions/preview-workspace-resource-change.d.ts +3 -0
- package/dist/actions/preview-workspace-resource-change.d.ts.map +1 -0
- package/dist/actions/preview-workspace-resource-change.js +24 -0
- package/dist/actions/preview-workspace-resource-change.js.map +1 -0
- package/dist/actions/reject-dream-proposal.d.ts +3 -0
- package/dist/actions/reject-dream-proposal.d.ts.map +1 -0
- package/dist/actions/reject-dream-proposal.js +12 -0
- package/dist/actions/reject-dream-proposal.js.map +1 -0
- package/dist/actions/restore-starter-workspace-resources.d.ts +3 -0
- package/dist/actions/restore-starter-workspace-resources.d.ts.map +1 -0
- package/dist/actions/restore-starter-workspace-resources.js +14 -0
- package/dist/actions/restore-starter-workspace-resources.js.map +1 -0
- package/dist/actions/send-code-agent-remote-command.d.ts +3 -0
- package/dist/actions/send-code-agent-remote-command.d.ts.map +1 -0
- package/dist/actions/send-code-agent-remote-command.js +53 -0
- package/dist/actions/send-code-agent-remote-command.js.map +1 -0
- package/dist/actions/set-dream-settings.d.ts +3 -0
- package/dist/actions/set-dream-settings.d.ts.map +1 -0
- package/dist/actions/set-dream-settings.js +41 -0
- package/dist/actions/set-dream-settings.js.map +1 -0
- package/dist/actions/update-workspace-resource.js +1 -1
- package/dist/actions/update-workspace-resource.js.map +1 -1
- package/dist/actions/view-screen.d.ts.map +1 -1
- package/dist/actions/view-screen.js +73 -2
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/approval-value-block.d.ts +7 -0
- package/dist/components/approval-value-block.d.ts.map +1 -0
- package/dist/components/approval-value-block.js +22 -0
- package/dist/components/approval-value-block.js.map +1 -0
- package/dist/components/create-app-popover.d.ts.map +1 -1
- package/dist/components/create-app-popover.js +3 -2
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +8 -1
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/ui/chart.d.ts +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +25 -4
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-resource-effective-stack.d.ts +11 -0
- package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -0
- package/dist/components/workspace-resource-effective-stack.js +59 -0
- package/dist/components/workspace-resource-effective-stack.js.map +1 -0
- package/dist/components/workspace-resource-impact-preview.d.ts +9 -0
- package/dist/components/workspace-resource-impact-preview.d.ts.map +1 -0
- package/dist/components/workspace-resource-impact-preview.js +39 -0
- package/dist/components/workspace-resource-impact-preview.js.map +1 -0
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +59 -0
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/schema.d.ts +714 -0
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +44 -2
- package/dist/db/schema.js.map +1 -1
- package/dist/hooks/use-navigation-state.d.ts +3 -0
- package/dist/hooks/use-navigation-state.d.ts.map +1 -1
- package/dist/hooks/use-navigation-state.js +23 -3
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/lib/utils.d.ts +2 -1
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +5 -1
- package/dist/lib/utils.js.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +1 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/pages/approval.d.ts.map +1 -1
- package/dist/routes/pages/approval.js +4 -1
- package/dist/routes/pages/approval.js.map +1 -1
- package/dist/routes/pages/approvals.js +1 -1
- package/dist/routes/pages/approvals.js.map +1 -1
- package/dist/routes/pages/dream-settings.d.ts +34 -0
- package/dist/routes/pages/dream-settings.d.ts.map +1 -0
- package/dist/routes/pages/dream-settings.js +68 -0
- package/dist/routes/pages/dream-settings.js.map +1 -0
- package/dist/routes/pages/dreams.d.ts +5 -0
- package/dist/routes/pages/dreams.d.ts.map +1 -0
- package/dist/routes/pages/dreams.js +435 -0
- package/dist/routes/pages/dreams.js.map +1 -0
- package/dist/routes/pages/workspace.d.ts.map +1 -1
- package/dist/routes/pages/workspace.js +187 -35
- package/dist/routes/pages/workspace.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +3 -2
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/dispatch-integrations.d.ts +1 -1
- package/dist/server/lib/dispatch-integrations.d.ts.map +1 -1
- package/dist/server/lib/dispatch-integrations.js +9 -4
- package/dist/server/lib/dispatch-integrations.js.map +1 -1
- package/dist/server/lib/dispatch-remote-commands.d.ts +83 -0
- package/dist/server/lib/dispatch-remote-commands.d.ts.map +1 -0
- package/dist/server/lib/dispatch-remote-commands.js +256 -0
- package/dist/server/lib/dispatch-remote-commands.js.map +1 -0
- package/dist/server/lib/dispatch-store.d.ts +26 -0
- package/dist/server/lib/dispatch-store.d.ts.map +1 -1
- package/dist/server/lib/dispatch-store.js +17 -1
- package/dist/server/lib/dispatch-store.js.map +1 -1
- package/dist/server/lib/dreams-store.d.ts +398 -0
- package/dist/server/lib/dreams-store.d.ts.map +1 -0
- package/dist/server/lib/dreams-store.js +2330 -0
- package/dist/server/lib/dreams-store.js.map +1 -0
- package/dist/server/lib/thread-debug-store.d.ts +2 -2
- package/dist/server/lib/vault-store.d.ts +1 -1
- package/dist/server/lib/workspace-resources-store.d.ts +181 -17
- package/dist/server/lib/workspace-resources-store.d.ts.map +1 -1
- package/dist/server/lib/workspace-resources-store.js +737 -108
- package/dist/server/lib/workspace-resources-store.js.map +1 -1
- package/package.json +4 -2
- package/src/actions/apply-dream-proposal.ts +12 -0
- package/src/actions/create-dream-report.ts +76 -0
- package/src/actions/create-workspace-resource.ts +3 -3
- package/src/actions/delete-workspace-resource.ts +1 -1
- package/src/actions/ensure-dream-job.ts +76 -0
- package/src/actions/get-dream-settings.ts +12 -0
- package/src/actions/get-dream.ts +14 -0
- package/src/actions/get-workspace-resource-effective-context.ts +34 -0
- package/src/actions/index.spec.ts +26 -0
- package/src/actions/index.ts +31 -4
- package/src/actions/list-dream-candidates.ts +77 -0
- package/src/actions/list-dreams.ts +17 -0
- package/src/actions/list-workspace-resources-for-app.ts +13 -0
- package/src/actions/list-workspace-resources.ts +1 -1
- package/src/actions/navigate.ts +2 -1
- package/src/actions/preview-dream-proposal.ts +14 -0
- package/src/actions/preview-workspace-resource-change.ts +25 -0
- package/src/actions/reject-dream-proposal.ts +12 -0
- package/src/actions/restore-starter-workspace-resources.ts +17 -0
- package/src/actions/send-code-agent-remote-command.ts +59 -0
- package/src/actions/set-dream-settings.spec.ts +81 -0
- package/src/actions/set-dream-settings.ts +44 -0
- package/src/actions/update-workspace-resource.ts +1 -1
- package/src/actions/view-screen.ts +90 -2
- package/src/components/approval-value-block.spec.tsx +59 -0
- package/src/components/approval-value-block.tsx +33 -0
- package/src/components/create-app-popover.tsx +3 -2
- package/src/components/layout/Layout.tsx +8 -0
- package/src/components/workspace-app-card.tsx +166 -1
- package/src/components/workspace-resource-effective-stack.spec.tsx +125 -0
- package/src/components/workspace-resource-effective-stack.tsx +141 -0
- package/src/components/workspace-resource-impact-preview.spec.tsx +147 -0
- package/src/components/workspace-resource-impact-preview.tsx +116 -0
- package/src/db/migrations.ts +59 -0
- package/src/db/schema.ts +46 -2
- package/src/hooks/use-navigation-state.ts +24 -5
- package/src/lib/utils.ts +6 -1
- package/src/routes/index.ts +1 -0
- package/src/routes/pages/approval.tsx +14 -1
- package/src/routes/pages/approvals.tsx +1 -1
- package/src/routes/pages/dream-settings.spec.ts +130 -0
- package/src/routes/pages/dream-settings.ts +103 -0
- package/src/routes/pages/dreams.tsx +1828 -0
- package/src/routes/pages/workspace.tsx +577 -97
- package/src/server/lib/app-creation-store.ts +3 -2
- package/src/server/lib/dispatch-integrations.ts +10 -3
- package/src/server/lib/dispatch-remote-commands.spec.ts +167 -0
- package/src/server/lib/dispatch-remote-commands.ts +375 -0
- package/src/server/lib/dispatch-store.ts +37 -1
- package/src/server/lib/dreams-store.spec.ts +1492 -0
- package/src/server/lib/dreams-store.ts +3168 -0
- package/src/server/lib/workspace-resource-approval-lifecycle.spec.ts +236 -0
- package/src/server/lib/workspace-resources-store.spec.ts +1106 -0
- package/src/server/lib/workspace-resources-store.ts +1001 -134
- package/dist/actions/sync-workspace-resources-to-all.d.ts +0 -3
- package/dist/actions/sync-workspace-resources-to-all.d.ts.map +0 -1
- package/dist/actions/sync-workspace-resources-to-all.js +0 -9
- package/dist/actions/sync-workspace-resources-to-all.js.map +0 -1
- package/dist/actions/sync-workspace-resources-to-app.d.ts +0 -3
- package/dist/actions/sync-workspace-resources-to-app.d.ts.map +0 -1
- package/dist/actions/sync-workspace-resources-to-app.js +0 -11
- package/dist/actions/sync-workspace-resources-to-app.js.map +0 -1
- package/src/actions/sync-workspace-resources-to-all.ts +0 -10
- package/src/actions/sync-workspace-resources-to-app.ts +0 -12
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import React, { act } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
ApprovalValueBlock,
|
|
7
|
+
approvalValuePreview,
|
|
8
|
+
parseApprovalValue,
|
|
9
|
+
} from "./approval-value-block";
|
|
10
|
+
|
|
11
|
+
describe("approval value display", () => {
|
|
12
|
+
let container: HTMLDivElement;
|
|
13
|
+
let root: Root;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
17
|
+
container = document.createElement("div");
|
|
18
|
+
document.body.appendChild(container);
|
|
19
|
+
root = createRoot(container);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
act(() => root.unmount());
|
|
24
|
+
container.remove();
|
|
25
|
+
vi.unstubAllGlobals();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("parses serialized approval values and preserves plain strings", () => {
|
|
29
|
+
expect(
|
|
30
|
+
parseApprovalValue('{"scope":"all","path":"context/brand.md"}'),
|
|
31
|
+
).toEqual({
|
|
32
|
+
scope: "all",
|
|
33
|
+
path: "context/brand.md",
|
|
34
|
+
});
|
|
35
|
+
expect(parseApprovalValue("plain text")).toBe("plain text");
|
|
36
|
+
expect(parseApprovalValue(null)).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("formats before/after payloads for readable approval review", () => {
|
|
40
|
+
expect(approvalValuePreview(null)).toBe("None");
|
|
41
|
+
expect(approvalValuePreview("plain text")).toBe("plain text");
|
|
42
|
+
expect(approvalValuePreview({ scope: "all" })).toBe(
|
|
43
|
+
JSON.stringify({ scope: "all" }, null, 2),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
act(() => {
|
|
47
|
+
root.render(
|
|
48
|
+
<ApprovalValueBlock
|
|
49
|
+
label="After"
|
|
50
|
+
value={{ path: "context/brand.md", scope: "all" }}
|
|
51
|
+
/>,
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(container.textContent).toContain("After");
|
|
56
|
+
expect(container.textContent).toContain('"path": "context/brand.md"');
|
|
57
|
+
expect(container.textContent).toContain('"scope": "all"');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function parseApprovalValue(value: string | null | undefined): unknown {
|
|
2
|
+
if (!value) return null;
|
|
3
|
+
try {
|
|
4
|
+
return JSON.parse(value);
|
|
5
|
+
} catch {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function approvalValuePreview(value: unknown): string {
|
|
11
|
+
if (value === null || value === undefined) return "None";
|
|
12
|
+
if (typeof value === "string") return value;
|
|
13
|
+
return JSON.stringify(value, null, 2);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ApprovalValueBlock({
|
|
17
|
+
label,
|
|
18
|
+
value,
|
|
19
|
+
}: {
|
|
20
|
+
label: string;
|
|
21
|
+
value: unknown;
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
<div className="space-y-1.5">
|
|
25
|
+
<div className="text-[11px] font-medium uppercase text-muted-foreground">
|
|
26
|
+
{label}
|
|
27
|
+
</div>
|
|
28
|
+
<pre className="max-h-40 overflow-auto rounded-lg border bg-background p-2 text-[11px] leading-relaxed text-foreground">
|
|
29
|
+
{approvalValuePreview(value)}
|
|
30
|
+
</pre>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -108,6 +108,7 @@ function buildAppCreationPrompt(input: {
|
|
|
108
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.`,
|
|
109
109
|
grantRequest,
|
|
110
110
|
`Requested Dispatch workspace resources for this app:\n${resourceList}`,
|
|
111
|
+
`Dispatch workspace resources with scope=all are inherited workspace context. Do not copy or sync them into the new app; every workspace app reads them at runtime and may override with app shared or personal resources.`,
|
|
111
112
|
``,
|
|
112
113
|
`Pick a starter template that fits the user's prompt — analytics, calendar, content, design, dispatch, forms, mail, slides, clips, or starter when none of the others fit.`,
|
|
113
114
|
`Use the workspace app layout: create it under apps/${input.appId}, mount it at /${input.appId}, keep it on the shared workspace database/hosting model, and avoid table-name collisions by namespacing any new domain tables to the app.`,
|
|
@@ -125,8 +126,8 @@ function buildAppCreationPrompt(input: {
|
|
|
125
126
|
? `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
127
|
: `Do not grant any Dispatch vault keys unless the user asks later.`,
|
|
127
128
|
input.selectedResources.length
|
|
128
|
-
? `After the app exists, grant the selected Dispatch workspace resources to appId "${input.appId}"
|
|
129
|
-
: `Do not grant any Dispatch workspace resources unless the user asks later.`,
|
|
129
|
+
? `After the app exists, grant the selected Dispatch workspace resources to appId "${input.appId}". Do not sync All-app workspace resources; they are inherited.`
|
|
130
|
+
: `Do not grant any selected-only Dispatch workspace resources unless the user asks later.`,
|
|
130
131
|
``,
|
|
131
132
|
`App readiness requirements before handing off:`,
|
|
132
133
|
`- 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.`,
|
|
@@ -12,6 +12,7 @@ import { InvitationBanner, OrgSwitcher } from "@agent-native/core/client/org";
|
|
|
12
12
|
import {
|
|
13
13
|
IconArrowUpRight,
|
|
14
14
|
IconApps,
|
|
15
|
+
IconBrain,
|
|
15
16
|
IconChartBar,
|
|
16
17
|
IconBrandTelegram,
|
|
17
18
|
IconKey,
|
|
@@ -151,6 +152,13 @@ const OPERATIONS_NAV_ITEMS = [
|
|
|
151
152
|
icon: IconHistory,
|
|
152
153
|
section: "operations",
|
|
153
154
|
},
|
|
155
|
+
{
|
|
156
|
+
id: "dreams",
|
|
157
|
+
to: "/dreams",
|
|
158
|
+
label: "Dreams",
|
|
159
|
+
icon: IconBrain,
|
|
160
|
+
section: "operations",
|
|
161
|
+
},
|
|
154
162
|
{
|
|
155
163
|
id: "thread-debug",
|
|
156
164
|
to: "/thread-debug",
|
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
import { useEffect, useState, type FormEvent } from "react";
|
|
2
|
-
import { useActionMutation } from "@agent-native/core/client";
|
|
2
|
+
import { useActionMutation, useActionQuery } from "@agent-native/core/client";
|
|
3
3
|
import {
|
|
4
4
|
IconArrowUpRight,
|
|
5
|
+
IconChevronDown,
|
|
6
|
+
IconChevronRight,
|
|
5
7
|
IconClockHour4,
|
|
6
8
|
IconDots,
|
|
7
9
|
IconEdit,
|
|
8
10
|
IconEye,
|
|
9
11
|
IconEyeOff,
|
|
12
|
+
IconFileText,
|
|
10
13
|
IconWorld,
|
|
11
14
|
IconTrash,
|
|
12
15
|
} from "@tabler/icons-react";
|
|
13
16
|
import { toast } from "sonner";
|
|
14
17
|
import { AppKeysPopover } from "@/components/app-keys-popover";
|
|
18
|
+
import { AppResourceEffectiveStack } from "@/components/workspace-resource-effective-stack";
|
|
15
19
|
import { Badge } from "@/components/ui/badge";
|
|
16
20
|
import { Button } from "@/components/ui/button";
|
|
17
21
|
import {
|
|
18
22
|
Dialog,
|
|
19
23
|
DialogContent,
|
|
24
|
+
DialogDescription,
|
|
20
25
|
DialogFooter,
|
|
21
26
|
DialogHeader,
|
|
22
27
|
DialogTitle,
|
|
28
|
+
DialogTrigger,
|
|
23
29
|
} from "@/components/ui/dialog";
|
|
24
30
|
import {
|
|
25
31
|
DropdownMenu,
|
|
@@ -170,6 +176,11 @@ export function WorkspaceAppCard({
|
|
|
170
176
|
) : null}
|
|
171
177
|
</div>
|
|
172
178
|
<div className="flex shrink-0 items-center gap-1">
|
|
179
|
+
{!isPending && !isArchived ? (
|
|
180
|
+
<div className="pointer-events-auto">
|
|
181
|
+
<AppResourcesDialog app={app} />
|
|
182
|
+
</div>
|
|
183
|
+
) : null}
|
|
173
184
|
{!isPending && !isArchived ? (
|
|
174
185
|
<div className="pointer-events-auto">
|
|
175
186
|
<AppKeysPopover appId={app.id} appName={app.name} />
|
|
@@ -271,6 +282,160 @@ export function WorkspaceAppCard({
|
|
|
271
282
|
);
|
|
272
283
|
}
|
|
273
284
|
|
|
285
|
+
function AppResourcesDialog({ app }: { app: WorkspaceAppSummary }) {
|
|
286
|
+
const [open, setOpen] = useState(false);
|
|
287
|
+
const [inspectedResourceId, setInspectedResourceId] = useState<string | null>(
|
|
288
|
+
null,
|
|
289
|
+
);
|
|
290
|
+
const { data, isLoading } = useActionQuery(
|
|
291
|
+
"list-workspace-resources-for-app",
|
|
292
|
+
{ appId: app.id },
|
|
293
|
+
{ enabled: open },
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const resources = ((data as any)?.resources ?? []) as any[];
|
|
297
|
+
const counts = (data as any)?.counts;
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<Dialog
|
|
301
|
+
open={open}
|
|
302
|
+
onOpenChange={(nextOpen) => {
|
|
303
|
+
setOpen(nextOpen);
|
|
304
|
+
if (!nextOpen) setInspectedResourceId(null);
|
|
305
|
+
}}
|
|
306
|
+
>
|
|
307
|
+
<DialogTrigger asChild>
|
|
308
|
+
<Button
|
|
309
|
+
type="button"
|
|
310
|
+
variant="ghost"
|
|
311
|
+
size="sm"
|
|
312
|
+
className="h-7 px-2 text-xs"
|
|
313
|
+
onClick={(e) => e.stopPropagation()}
|
|
314
|
+
>
|
|
315
|
+
<IconFileText size={14} className="mr-1" />
|
|
316
|
+
Context
|
|
317
|
+
</Button>
|
|
318
|
+
</DialogTrigger>
|
|
319
|
+
<DialogContent className="max-w-2xl">
|
|
320
|
+
<DialogHeader>
|
|
321
|
+
<DialogTitle>{app.name} workspace resources</DialogTitle>
|
|
322
|
+
<DialogDescription>
|
|
323
|
+
Workspace-level resources are inherited at runtime. App shared and
|
|
324
|
+
personal resources can override them locally.
|
|
325
|
+
</DialogDescription>
|
|
326
|
+
</DialogHeader>
|
|
327
|
+
<div className="space-y-4">
|
|
328
|
+
<div className="rounded-lg border bg-muted/30 px-3 py-2 text-xs leading-relaxed text-muted-foreground">
|
|
329
|
+
All-app resources live once at workspace scope and are read by each
|
|
330
|
+
app agent when it builds context. Nothing is copied into this app.
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
334
|
+
<Badge variant="secondary">{counts?.total ?? 0} total</Badge>
|
|
335
|
+
<Badge variant="outline">
|
|
336
|
+
{counts?.workspace ?? counts?.global ?? 0} workspace
|
|
337
|
+
</Badge>
|
|
338
|
+
<Badge variant="outline">{counts?.granted ?? 0} granted</Badge>
|
|
339
|
+
<Badge variant="outline">
|
|
340
|
+
{counts?.autoLoaded ?? 0} auto-loaded
|
|
341
|
+
</Badge>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
{isLoading ? (
|
|
345
|
+
<div className="space-y-2">
|
|
346
|
+
<div className="h-14 rounded-lg border bg-muted/30" />
|
|
347
|
+
<div className="h-14 rounded-lg border bg-muted/30" />
|
|
348
|
+
<div className="h-14 rounded-lg border bg-muted/30" />
|
|
349
|
+
</div>
|
|
350
|
+
) : resources.length === 0 ? (
|
|
351
|
+
<div className="rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground">
|
|
352
|
+
No workspace or granted resources are visible to this app yet.
|
|
353
|
+
</div>
|
|
354
|
+
) : (
|
|
355
|
+
<div className="max-h-[420px] space-y-2 overflow-y-auto pr-1">
|
|
356
|
+
{resources.map((resource) => {
|
|
357
|
+
const inspected = inspectedResourceId === resource.id;
|
|
358
|
+
return (
|
|
359
|
+
<div
|
|
360
|
+
key={resource.id}
|
|
361
|
+
className="rounded-lg border px-3 py-3"
|
|
362
|
+
>
|
|
363
|
+
<div className="flex items-start justify-between gap-3">
|
|
364
|
+
<div className="min-w-0">
|
|
365
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
366
|
+
<span className="text-sm font-medium text-foreground">
|
|
367
|
+
{resource.name}
|
|
368
|
+
</span>
|
|
369
|
+
<Badge variant="secondary">{resource.kind}</Badge>
|
|
370
|
+
<Badge variant="outline">
|
|
371
|
+
{resource.source === "workspace"
|
|
372
|
+
? "All apps"
|
|
373
|
+
: "Granted"}
|
|
374
|
+
</Badge>
|
|
375
|
+
{resource.autoLoaded ? (
|
|
376
|
+
<Badge variant="outline">Auto-loaded</Badge>
|
|
377
|
+
) : null}
|
|
378
|
+
</div>
|
|
379
|
+
<div className="mt-1 truncate font-mono text-xs text-muted-foreground">
|
|
380
|
+
{resource.path}
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
<div className="flex shrink-0 flex-col items-end gap-2">
|
|
384
|
+
{resource.source === "grant" ? (
|
|
385
|
+
<div className="text-right text-[11px] text-muted-foreground">
|
|
386
|
+
Selected grant
|
|
387
|
+
</div>
|
|
388
|
+
) : null}
|
|
389
|
+
<Button
|
|
390
|
+
type="button"
|
|
391
|
+
variant="ghost"
|
|
392
|
+
size="sm"
|
|
393
|
+
className="h-7 px-2 text-xs"
|
|
394
|
+
onClick={(event) => {
|
|
395
|
+
event.stopPropagation();
|
|
396
|
+
setInspectedResourceId(
|
|
397
|
+
inspected ? null : resource.id,
|
|
398
|
+
);
|
|
399
|
+
}}
|
|
400
|
+
>
|
|
401
|
+
{inspected ? (
|
|
402
|
+
<IconChevronDown size={14} className="mr-1" />
|
|
403
|
+
) : (
|
|
404
|
+
<IconChevronRight size={14} className="mr-1" />
|
|
405
|
+
)}
|
|
406
|
+
Stack
|
|
407
|
+
</Button>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
{resource.description ? (
|
|
412
|
+
<p className="mt-2 line-clamp-2 text-xs text-muted-foreground">
|
|
413
|
+
{resource.description}
|
|
414
|
+
</p>
|
|
415
|
+
) : null}
|
|
416
|
+
|
|
417
|
+
{inspected ? (
|
|
418
|
+
<AppResourceEffectiveStack
|
|
419
|
+
appId={app.id}
|
|
420
|
+
resource={resource}
|
|
421
|
+
/>
|
|
422
|
+
) : null}
|
|
423
|
+
</div>
|
|
424
|
+
);
|
|
425
|
+
})}
|
|
426
|
+
</div>
|
|
427
|
+
)}
|
|
428
|
+
</div>
|
|
429
|
+
<DialogFooter>
|
|
430
|
+
<Button type="button" onClick={() => setOpen(false)}>
|
|
431
|
+
Done
|
|
432
|
+
</Button>
|
|
433
|
+
</DialogFooter>
|
|
434
|
+
</DialogContent>
|
|
435
|
+
</Dialog>
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
274
439
|
function stringifyError(err: unknown): string {
|
|
275
440
|
if (err instanceof Error) return err.message;
|
|
276
441
|
return String(err);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import React, { act } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
AppResourceEffectiveStack,
|
|
7
|
+
appAvailabilityLabel,
|
|
8
|
+
appLayerState,
|
|
9
|
+
} from "./workspace-resource-effective-stack";
|
|
10
|
+
|
|
11
|
+
const queryState = vi.hoisted(() => ({
|
|
12
|
+
result: { data: null as any, isLoading: false },
|
|
13
|
+
calls: [] as any[],
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("@agent-native/core/client", () => ({
|
|
17
|
+
useActionQuery: (...args: any[]) => {
|
|
18
|
+
queryState.calls.push(args);
|
|
19
|
+
return queryState.result;
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
describe("AppResourceEffectiveStack", () => {
|
|
24
|
+
let container: HTMLDivElement;
|
|
25
|
+
let root: Root;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
29
|
+
queryState.calls = [];
|
|
30
|
+
queryState.result = { data: null, isLoading: false };
|
|
31
|
+
container = document.createElement("div");
|
|
32
|
+
document.body.appendChild(container);
|
|
33
|
+
root = createRoot(container);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
act(() => root.unmount());
|
|
38
|
+
container.remove();
|
|
39
|
+
vi.unstubAllGlobals();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("renders the effective layer stack and winning override", () => {
|
|
43
|
+
queryState.result = {
|
|
44
|
+
isLoading: false,
|
|
45
|
+
data: {
|
|
46
|
+
availability: "all-apps",
|
|
47
|
+
effectiveResource: {
|
|
48
|
+
owner: "person@example.test",
|
|
49
|
+
path: "context/brand.md",
|
|
50
|
+
},
|
|
51
|
+
layers: [
|
|
52
|
+
{
|
|
53
|
+
scope: "workspace",
|
|
54
|
+
label: "Workspace default",
|
|
55
|
+
owner: "__workspace__",
|
|
56
|
+
resource: {
|
|
57
|
+
path: "context/brand.md",
|
|
58
|
+
updatedAt: 1,
|
|
59
|
+
},
|
|
60
|
+
exists: true,
|
|
61
|
+
effective: false,
|
|
62
|
+
overridden: true,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
scope: "shared",
|
|
66
|
+
label: "Organization/app override",
|
|
67
|
+
owner: "__shared__",
|
|
68
|
+
resource: null,
|
|
69
|
+
exists: false,
|
|
70
|
+
effective: false,
|
|
71
|
+
overridden: false,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
scope: "personal",
|
|
75
|
+
label: "Personal override",
|
|
76
|
+
owner: "person@example.test",
|
|
77
|
+
resource: {
|
|
78
|
+
path: "context/brand.md",
|
|
79
|
+
updatedAt: 2,
|
|
80
|
+
},
|
|
81
|
+
exists: true,
|
|
82
|
+
effective: true,
|
|
83
|
+
overridden: false,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
act(() => {
|
|
90
|
+
root.render(
|
|
91
|
+
<AppResourceEffectiveStack
|
|
92
|
+
appId="analytics"
|
|
93
|
+
resource={{ id: "resource_1", path: "context/brand.md" }}
|
|
94
|
+
/>,
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(queryState.calls[0]).toEqual([
|
|
99
|
+
"get-workspace-resource-effective-context",
|
|
100
|
+
{ resourceId: "resource_1", appId: "analytics" },
|
|
101
|
+
{ enabled: true },
|
|
102
|
+
]);
|
|
103
|
+
expect(container.textContent).toContain("Effective context stack");
|
|
104
|
+
expect(container.textContent).toContain("Inherited by all apps");
|
|
105
|
+
expect(container.textContent).toContain("Workspace default");
|
|
106
|
+
expect(container.textContent).toContain("Organization/app override");
|
|
107
|
+
expect(container.textContent).toContain("Personal override");
|
|
108
|
+
expect(container.textContent).toContain("Overridden");
|
|
109
|
+
expect(container.textContent).toContain("Wins");
|
|
110
|
+
expect(container.textContent).toContain("No file at this layer");
|
|
111
|
+
expect(container.textContent).toContain(
|
|
112
|
+
"person@example.test/context/brand.md",
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("keeps availability and layer-state labels stable", () => {
|
|
117
|
+
expect(appAvailabilityLabel("selected-granted")).toBe(
|
|
118
|
+
"Granted to this app",
|
|
119
|
+
);
|
|
120
|
+
expect(appAvailabilityLabel("selected-not-granted")).toBe("Not granted");
|
|
121
|
+
expect(appLayerState({ effective: true }).label).toBe("Wins");
|
|
122
|
+
expect(appLayerState({ overridden: true }).label).toBe("Overridden");
|
|
123
|
+
expect(appLayerState({}).label).toBe("Missing");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useActionQuery } from "@agent-native/core/client";
|
|
2
|
+
import { Badge } from "@/components/ui/badge";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export function appAvailabilityLabel(value?: string) {
|
|
6
|
+
switch (value) {
|
|
7
|
+
case "all-apps":
|
|
8
|
+
return "Inherited by all apps";
|
|
9
|
+
case "selected-granted":
|
|
10
|
+
return "Granted to this app";
|
|
11
|
+
case "selected-not-granted":
|
|
12
|
+
return "Not granted";
|
|
13
|
+
case "selected-no-app":
|
|
14
|
+
return "Select app";
|
|
15
|
+
case "path-not-managed":
|
|
16
|
+
return "Not managed";
|
|
17
|
+
default:
|
|
18
|
+
return "Checking";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function appLayerState(layer: any): {
|
|
23
|
+
label: string;
|
|
24
|
+
className: string;
|
|
25
|
+
} {
|
|
26
|
+
if (layer.effective) {
|
|
27
|
+
return {
|
|
28
|
+
label: "Wins",
|
|
29
|
+
className: "border-green-500/30 bg-green-500/10 text-green-700",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (layer.overridden) {
|
|
33
|
+
return {
|
|
34
|
+
label: "Overridden",
|
|
35
|
+
className: "border-amber-500/30 bg-amber-500/10 text-amber-700",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
label: "Missing",
|
|
40
|
+
className: "text-muted-foreground",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function formatResourceTimestamp(value?: number | null): string {
|
|
45
|
+
if (!value) return "not present";
|
|
46
|
+
return new Date(value).toLocaleString();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function AppResourceEffectiveStack({
|
|
50
|
+
appId,
|
|
51
|
+
resource,
|
|
52
|
+
}: {
|
|
53
|
+
appId: string;
|
|
54
|
+
resource: any;
|
|
55
|
+
}) {
|
|
56
|
+
const { data: context, isLoading } = useActionQuery(
|
|
57
|
+
"get-workspace-resource-effective-context",
|
|
58
|
+
{ resourceId: resource.id, appId },
|
|
59
|
+
{ enabled: !!resource.id },
|
|
60
|
+
);
|
|
61
|
+
const layers = ((context as any)?.layers ?? []) as any[];
|
|
62
|
+
const active = (context as any)?.effectiveResource;
|
|
63
|
+
const availability = (context as any)?.availability;
|
|
64
|
+
|
|
65
|
+
if (isLoading && !context) {
|
|
66
|
+
return (
|
|
67
|
+
<div className="mt-3 rounded-lg border bg-muted/20 p-3">
|
|
68
|
+
<div className="h-3 w-44 animate-pulse rounded bg-muted-foreground/10" />
|
|
69
|
+
<div className="mt-3 grid gap-2 sm:grid-cols-3">
|
|
70
|
+
<div className="h-20 animate-pulse rounded-md bg-muted-foreground/10" />
|
|
71
|
+
<div className="h-20 animate-pulse rounded-md bg-muted-foreground/10" />
|
|
72
|
+
<div className="h-20 animate-pulse rounded-md bg-muted-foreground/10" />
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="mt-3 rounded-lg border bg-muted/20 p-3">
|
|
80
|
+
<div className="flex flex-wrap items-start justify-between gap-2">
|
|
81
|
+
<div className="min-w-0">
|
|
82
|
+
<div className="text-xs font-semibold uppercase text-muted-foreground">
|
|
83
|
+
Effective context stack
|
|
84
|
+
</div>
|
|
85
|
+
<div className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
|
|
86
|
+
{resource.path}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<Badge variant="outline">{appAvailabilityLabel(availability)}</Badge>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div className="mt-3 grid gap-2 sm:grid-cols-3">
|
|
93
|
+
{layers.map((layer) => {
|
|
94
|
+
const state = appLayerState(layer);
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
key={layer.scope}
|
|
98
|
+
className={cn("rounded-md border bg-background/70 p-2", {
|
|
99
|
+
"border-green-500/30 bg-green-500/10": layer.effective,
|
|
100
|
+
})}
|
|
101
|
+
>
|
|
102
|
+
<div className="flex items-start justify-between gap-2">
|
|
103
|
+
<span className="text-xs font-medium text-foreground">
|
|
104
|
+
{layer.label}
|
|
105
|
+
</span>
|
|
106
|
+
<Badge variant="outline" className={state.className}>
|
|
107
|
+
{state.label}
|
|
108
|
+
</Badge>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="mt-1 truncate font-mono text-[10px] text-muted-foreground">
|
|
111
|
+
{layer.owner}
|
|
112
|
+
</div>
|
|
113
|
+
{layer.resource ? (
|
|
114
|
+
<div className="mt-2 text-[11px] text-muted-foreground">
|
|
115
|
+
Updated {formatResourceTimestamp(layer.resource.updatedAt)}
|
|
116
|
+
</div>
|
|
117
|
+
) : (
|
|
118
|
+
<div className="mt-2 text-[11px] text-muted-foreground">
|
|
119
|
+
No file at this layer
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
})}
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div className="mt-3 rounded-md bg-background/70 px-3 py-2 text-xs text-muted-foreground">
|
|
128
|
+
{active ? (
|
|
129
|
+
<>
|
|
130
|
+
Winning layer:{" "}
|
|
131
|
+
<span className="font-mono text-foreground">
|
|
132
|
+
{active.owner}/{active.path}
|
|
133
|
+
</span>
|
|
134
|
+
</>
|
|
135
|
+
) : (
|
|
136
|
+
"No active resource exists for this path yet."
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|