@agent-native/dispatch 0.7.0 → 0.8.1
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 +56 -3
- 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/start-workspace-app-creation.js +1 -1
- package/dist/actions/start-workspace-app-creation.js.map +1 -1
- 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 +6 -5
- 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/dist/server/plugins/agent-chat.js +1 -1
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/dist/server/plugins/integrations.js +2 -2
- package/dist/server/plugins/integrations.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/start-workspace-app-creation.ts +1 -1
- 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 +6 -5
- 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.spec.ts +79 -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 +226 -0
- package/src/server/lib/workspace-resources-store.spec.ts +1106 -0
- package/src/server/lib/workspace-resources-store.ts +1001 -134
- package/src/server/plugins/agent-chat.ts +1 -1
- package/src/server/plugins/integrations.ts +2 -2
- 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
|
@@ -20,13 +20,36 @@ import {
|
|
|
20
20
|
} from "../server/lib/vault-store.js";
|
|
21
21
|
import { listWorkspaceApps } from "../server/lib/app-creation-store.js";
|
|
22
22
|
import { listDispatchUsageMetrics } from "../server/lib/usage-metrics-store.js";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
listWorkspaceResourceOptions,
|
|
25
|
+
listWorkspaceResourcesForApp,
|
|
26
|
+
} from "../server/lib/workspace-resources-store.js";
|
|
24
27
|
import {
|
|
25
28
|
getAgentThreadDebug,
|
|
26
29
|
listThreadDebugSources,
|
|
27
30
|
searchAgentThreads,
|
|
28
31
|
} from "../server/lib/thread-debug-store.js";
|
|
29
32
|
|
|
33
|
+
async function runLocalDispatchAction(
|
|
34
|
+
name: string,
|
|
35
|
+
args: Record<string, unknown>,
|
|
36
|
+
) {
|
|
37
|
+
const modulePath = `./${name}.js`;
|
|
38
|
+
const module = (await import(/* @vite-ignore */ modulePath)) as {
|
|
39
|
+
default?: {
|
|
40
|
+
run: (args: Record<string, unknown>) => unknown | Promise<unknown>;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
if (!module.default) throw new Error(`Dispatch action not found: ${name}`);
|
|
44
|
+
return module.default.run(stripUndefined(args));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function stripUndefined(args: Record<string, unknown>) {
|
|
48
|
+
return Object.fromEntries(
|
|
49
|
+
Object.entries(args).filter(([, value]) => value !== undefined),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
30
53
|
export default defineAction({
|
|
31
54
|
description:
|
|
32
55
|
"See what the user is currently looking at in the dispatch UI, including navigation state and a compact operational summary.",
|
|
@@ -57,9 +80,32 @@ export default defineAction({
|
|
|
57
80
|
navigation?.view === "apps" ||
|
|
58
81
|
navigation?.view === "new-app"
|
|
59
82
|
) {
|
|
60
|
-
|
|
83
|
+
const workspaceApps = await listWorkspaceApps({
|
|
61
84
|
includeAgentCards: true,
|
|
62
85
|
});
|
|
86
|
+
screen.workspaceApps = workspaceApps;
|
|
87
|
+
if (navigation?.view === "apps") {
|
|
88
|
+
screen.workspaceAppResources = await Promise.all(
|
|
89
|
+
workspaceApps
|
|
90
|
+
.filter((app) => !app.isDispatch)
|
|
91
|
+
.slice(0, 12)
|
|
92
|
+
.map(async (app) => {
|
|
93
|
+
const result = await listWorkspaceResourcesForApp(app.id);
|
|
94
|
+
return {
|
|
95
|
+
appId: app.id,
|
|
96
|
+
appName: app.name,
|
|
97
|
+
counts: result.counts,
|
|
98
|
+
resources: result.resources.map((resource) => ({
|
|
99
|
+
name: resource.name,
|
|
100
|
+
path: resource.path,
|
|
101
|
+
kind: resource.kind,
|
|
102
|
+
source: resource.source,
|
|
103
|
+
autoLoaded: resource.autoLoaded,
|
|
104
|
+
})),
|
|
105
|
+
};
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
63
109
|
}
|
|
64
110
|
if (navigation?.view === "metrics") {
|
|
65
111
|
try {
|
|
@@ -99,6 +145,16 @@ export default defineAction({
|
|
|
99
145
|
}
|
|
100
146
|
if (navigation?.view === "workspace" || navigation?.view === "new-app") {
|
|
101
147
|
screen.workspaceResources = await listWorkspaceResourceOptions();
|
|
148
|
+
screen.workspaceResourceEffectiveContext = {
|
|
149
|
+
action: "get-workspace-resource-effective-context",
|
|
150
|
+
description:
|
|
151
|
+
"Preview workspace -> organization/app -> personal precedence for a resource path and optional app/user. All-app resources are inherited at runtime; selected resources are app-specific exceptions.",
|
|
152
|
+
};
|
|
153
|
+
screen.workspaceResourceImpactPreview = {
|
|
154
|
+
action: "preview-workspace-resource-change",
|
|
155
|
+
description:
|
|
156
|
+
"Preview All-app reach, overrides, and approval behavior before creating, updating, or deleting a workspace resource.",
|
|
157
|
+
};
|
|
102
158
|
}
|
|
103
159
|
if (navigation?.view === "thread-debug") {
|
|
104
160
|
try {
|
|
@@ -136,6 +192,38 @@ export default defineAction({
|
|
|
136
192
|
error instanceof Error ? error.message : String(error);
|
|
137
193
|
}
|
|
138
194
|
}
|
|
195
|
+
if (navigation?.view === "dreams") {
|
|
196
|
+
try {
|
|
197
|
+
const nav = navigation as Record<string, any>;
|
|
198
|
+
const [sources, candidates, dreams, settings] = await Promise.all([
|
|
199
|
+
listThreadDebugSources(),
|
|
200
|
+
runLocalDispatchAction("list-dream-candidates", {
|
|
201
|
+
sourceId: nav.sourceId,
|
|
202
|
+
ownerEmail: nav.ownerEmail,
|
|
203
|
+
limit: 10,
|
|
204
|
+
}),
|
|
205
|
+
runLocalDispatchAction("list-dreams", {
|
|
206
|
+
status: nav.status,
|
|
207
|
+
limit: 10,
|
|
208
|
+
}),
|
|
209
|
+
runLocalDispatchAction("get-dream-settings", {}),
|
|
210
|
+
]);
|
|
211
|
+
screen.dreamSources = sources;
|
|
212
|
+
screen.dreamCandidates = candidates;
|
|
213
|
+
screen.latestDreams = dreams;
|
|
214
|
+
screen.dreamSettings = settings;
|
|
215
|
+
|
|
216
|
+
const dreamId = nav.dreamId ?? nav.id;
|
|
217
|
+
if (dreamId) {
|
|
218
|
+
screen.dreamDetail = await runLocalDispatchAction("get-dream", {
|
|
219
|
+
id: dreamId,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
screen.dreamsError =
|
|
224
|
+
error instanceof Error ? error.message : String(error);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
139
227
|
|
|
140
228
|
if (Object.keys(screen).length === 0) {
|
|
141
229
|
return "No application state found. Is the app running?";
|
|
@@ -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,15 +108,16 @@ 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
|
-
`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
|
+
`Pick a starter template that fits the user's prompt — analytics, brain, 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.`,
|
|
114
115
|
`Important routing rule: from outside the app, link to /${input.appId}; inside apps/${input.appId}, React Router routes are app-local. Use <Link to="/review"> and navigate("/review"), not "/${input.appId}/review"; APP_BASE_PATH supplies the mounted prefix, and hardcoding it causes doubled URLs like /${input.appId}/${input.appId}/review.`,
|
|
115
116
|
`Prefer useActionQuery/useActionMutation for actions. If you must raw-fetch framework endpoints, wrap them with agentNativePath("/_agent-native/actions/<name>") so mounted apps call the right URL.`,
|
|
116
117
|
`Use relative workspace links like /${input.appId}. Do not hardcode localhost, 127.0.0.1, 8080, 8100, or any dev port; the active workspace gateway/browser origin owns the port.`,
|
|
117
118
|
`Use the framework/template UI stack: shadcn/ui components and @tabler/icons-react. Do not add lucide-react or another icon library for standard UI.`,
|
|
118
|
-
`Existing first-party apps are neighbors, not implementation details for this app. If the user's prompt mentions Mail, Calendar, Analytics, Dispatch, or other templates, treat them as existing hosted/connected apps that this app can link to or call through A2A/default connected agents. For example, Mail, Calendar, and
|
|
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.`,
|
|
119
|
+
`Existing first-party apps are neighbors, not implementation details for this app. If the user's prompt mentions Mail, Calendar, Analytics, Brain, Dispatch, or other templates, treat them as existing hosted/connected apps that this app can link to or call through A2A/default connected agents. For example, Mail, Calendar, Analytics, and Brain already exist at https://mail.agent-native.com, https://calendar.agent-native.com, https://analytics.agent-native.com, and https://brain.agent-native.com.`,
|
|
120
|
+
`Do not clone first-party templates, create wrapper apps, or scaffold child apps/routes for Mail, Calendar, Analytics, Brain, 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.`,
|
|
120
121
|
`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.`,
|
|
121
122
|
`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.`,
|
|
122
123
|
input.vaultAccessMode === "all-apps"
|
|
@@ -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
|
+
});
|