orionfold-relay 0.25.1 → 0.27.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/dist/cli.js +9 -4
- package/package.json +1 -1
- package/src/app/api/data/clear/route.ts +8 -1
- package/src/app/api/data/seed/route.ts +8 -1
- package/src/app/profiles/page.tsx +7 -1
- package/src/app/settings/page.tsx +3 -1
- package/src/app/tables/page.tsx +10 -1
- package/src/components/apps/kit-view/slots/header.tsx +9 -3
- package/src/components/apps/run-now-button.tsx +7 -3
- package/src/components/apps/run-now-sheet.tsx +6 -2
- package/src/components/apps/run-now-toast.ts +27 -0
- package/src/components/profiles/profile-browser.tsx +36 -1
- package/src/components/profiles/profile-card.tsx +9 -2
- package/src/components/schedules/schedule-list.tsx +30 -1
- package/src/components/settings/data-management-section.tsx +56 -9
- package/src/components/shared/pack-pill.tsx +39 -0
- package/src/components/tables/table-browser.tsx +25 -1
- package/src/components/tables/table-grid.tsx +16 -4
- package/src/components/tables/table-list-table.tsx +12 -1
- package/src/components/workflows/blueprint-gallery.tsx +68 -4
- package/src/lib/apps/pack-of.ts +83 -0
- package/src/lib/apps/view-kits/data.ts +12 -1
- package/src/lib/apps/view-kits/header-status.ts +14 -0
- package/src/lib/apps/view-kits/kits/coach.ts +2 -1
- package/src/lib/apps/view-kits/kits/inbox.ts +2 -1
- package/src/lib/apps/view-kits/kits/ledger.ts +2 -1
- package/src/lib/apps/view-kits/kits/placeholder.ts +2 -1
- package/src/lib/apps/view-kits/kits/research.ts +2 -1
- package/src/lib/apps/view-kits/kits/tracker.ts +2 -1
- package/src/lib/apps/view-kits/kits/workflow-hub.ts +2 -1
- package/src/lib/apps/view-kits/types.ts +7 -1
- package/src/lib/chat/files/search.ts +5 -0
- package/src/lib/constants/status-families.ts +13 -1
- package/src/lib/data/seed-data/installed-packs.ts +97 -0
- package/src/lib/data/seed.ts +24 -0
- package/src/lib/environment/git-manager.ts +5 -0
- package/src/lib/packs/install.ts +19 -0
- package/src/lib/packs/templates/relay-agency-pro/base/manifest.yaml +8 -2
- package/src/lib/packs/templates/relay-agency-pro/base/seed/tables/engagements.json +28 -0
- package/src/lib/packs/templates/relay-agency-pro/pack.yaml +5 -1
- package/src/lib/plugins/examples/echo-server/plugin.yaml +1 -1
- package/src/lib/plugins/examples/finance-pack/plugin.yaml +1 -1
- package/src/lib/plugins/examples/reading-radar/plugin.yaml +1 -1
- package/src/lib/plugins/registry.ts +1 -1
- package/src/lib/plugins/sdk/types.ts +1 -1
package/dist/cli.js
CHANGED
|
@@ -1186,7 +1186,7 @@ var CURRENT_PLUGIN_API_VERSION, CAPABILITY_VALUES, ORIGIN_VALUES, PrimitivesBund
|
|
|
1186
1186
|
var init_types = __esm({
|
|
1187
1187
|
"src/lib/plugins/sdk/types.ts"() {
|
|
1188
1188
|
"use strict";
|
|
1189
|
-
CURRENT_PLUGIN_API_VERSION = "0.
|
|
1189
|
+
CURRENT_PLUGIN_API_VERSION = "0.27";
|
|
1190
1190
|
CAPABILITY_VALUES = ["fs", "net", "child_process", "env"];
|
|
1191
1191
|
ORIGIN_VALUES = ["ainative-internal", "third-party"];
|
|
1192
1192
|
PrimitivesBundleManifestSchema = z.object({
|
|
@@ -12982,7 +12982,7 @@ var init_registry6 = __esm({
|
|
|
12982
12982
|
init_registry5();
|
|
12983
12983
|
init_installer();
|
|
12984
12984
|
init_schedule_spec();
|
|
12985
|
-
SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.
|
|
12985
|
+
SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.26"]);
|
|
12986
12986
|
pluginCache = null;
|
|
12987
12987
|
lastLoadedPluginIds = /* @__PURE__ */ new Set();
|
|
12988
12988
|
PluginTableSchema = z16.object({
|
|
@@ -25913,8 +25913,8 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
25913
25913
|
import yaml12 from "js-yaml";
|
|
25914
25914
|
import semver from "semver";
|
|
25915
25915
|
function relayCoreVersion() {
|
|
25916
|
-
if (semver.valid("0.
|
|
25917
|
-
return "0.
|
|
25916
|
+
if (semver.valid("0.27.0")) {
|
|
25917
|
+
return "0.27.0";
|
|
25918
25918
|
}
|
|
25919
25919
|
try {
|
|
25920
25920
|
const root = getAppRoot(import.meta.dirname, 3);
|
|
@@ -26092,6 +26092,11 @@ async function installPack(source, options = {}) {
|
|
|
26092
26092
|
const { reloadBlueprints: reloadBlueprints2 } = await Promise.resolve().then(() => (init_registry3(), registry_exports3));
|
|
26093
26093
|
reloadBlueprints2();
|
|
26094
26094
|
}
|
|
26095
|
+
try {
|
|
26096
|
+
const { revalidateTag } = await import("next/cache");
|
|
26097
|
+
revalidateTag(`app-runtime:${pack.meta.id}`, { expire: 0 });
|
|
26098
|
+
} catch {
|
|
26099
|
+
}
|
|
26095
26100
|
return {
|
|
26096
26101
|
packId: pack.meta.id,
|
|
26097
26102
|
packVersion: pack.meta.version,
|
package/package.json
CHANGED
|
@@ -4,7 +4,14 @@ import { isDataOpsAllowed } from "@/lib/data/staging-gate";
|
|
|
4
4
|
|
|
5
5
|
export async function POST() {
|
|
6
6
|
if (!isDataOpsAllowed()) {
|
|
7
|
-
return NextResponse.json(
|
|
7
|
+
return NextResponse.json(
|
|
8
|
+
{
|
|
9
|
+
success: false,
|
|
10
|
+
error:
|
|
11
|
+
"Clearing data is a staging-only tool and is disabled on this build.",
|
|
12
|
+
},
|
|
13
|
+
{ status: 403 }
|
|
14
|
+
);
|
|
8
15
|
}
|
|
9
16
|
|
|
10
17
|
try {
|
|
@@ -4,7 +4,14 @@ import { isDataOpsAllowed } from "@/lib/data/staging-gate";
|
|
|
4
4
|
|
|
5
5
|
export async function POST() {
|
|
6
6
|
if (!isDataOpsAllowed()) {
|
|
7
|
-
return NextResponse.json(
|
|
7
|
+
return NextResponse.json(
|
|
8
|
+
{
|
|
9
|
+
success: false,
|
|
10
|
+
error:
|
|
11
|
+
"Sample data seeding is a staging-only tool and is disabled on this build.",
|
|
12
|
+
},
|
|
13
|
+
{ status: 403 }
|
|
14
|
+
);
|
|
8
15
|
}
|
|
9
16
|
|
|
10
17
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { listProfiles, isBuiltin } from "@/lib/agents/profiles/registry";
|
|
2
2
|
import { sortProfilesByName } from "@/lib/agents/profiles/sort";
|
|
3
|
+
import { listApps } from "@/lib/apps/registry";
|
|
3
4
|
import { ProfileBrowser } from "@/components/profiles/profile-browser";
|
|
4
5
|
import { PageShell } from "@/components/shared/page-shell";
|
|
5
6
|
|
|
@@ -13,12 +14,17 @@ export default async function ProfilesPage() {
|
|
|
13
14
|
}))
|
|
14
15
|
);
|
|
15
16
|
|
|
17
|
+
// Installed packs — the source-of-truth set the client resolves each
|
|
18
|
+
// profile's pack provenance against (FEAT-8 pill / FEAT-7 filter via
|
|
19
|
+
// packOf). Just {id, name}; the pill renders the name, packOf gates on the id.
|
|
20
|
+
const installedPacks = listApps().map((a) => ({ id: a.id, name: a.name }));
|
|
21
|
+
|
|
16
22
|
return (
|
|
17
23
|
<PageShell
|
|
18
24
|
title="Profiles"
|
|
19
25
|
description="Browse and inspect agent profiles without blur-heavy detail surfaces."
|
|
20
26
|
>
|
|
21
|
-
<ProfileBrowser initialProfiles={profiles} />
|
|
27
|
+
<ProfileBrowser initialProfiles={profiles} installedPacks={installedPacks} />
|
|
22
28
|
</PageShell>
|
|
23
29
|
);
|
|
24
30
|
}
|
|
@@ -14,10 +14,12 @@ import { ChannelsSection } from "@/components/settings/channels-section";
|
|
|
14
14
|
import { InstanceSection } from "@/components/instance/instance-section";
|
|
15
15
|
import { LicenseSection } from "@/components/settings/license-section";
|
|
16
16
|
import { PageShell } from "@/components/shared/page-shell";
|
|
17
|
+
import { isDataOpsAllowed } from "@/lib/data/staging-gate";
|
|
17
18
|
|
|
18
19
|
export const dynamic = "force-dynamic";
|
|
19
20
|
|
|
20
21
|
export default function SettingsPage() {
|
|
22
|
+
const dataOpsAllowed = isDataOpsAllowed();
|
|
21
23
|
return (
|
|
22
24
|
<PageShell
|
|
23
25
|
title="Settings"
|
|
@@ -38,7 +40,7 @@ export default function SettingsPage() {
|
|
|
38
40
|
<BudgetGuardrailsSection />
|
|
39
41
|
<PermissionsSections />
|
|
40
42
|
<DatabaseSnapshotsSection />
|
|
41
|
-
<DataManagementSection />
|
|
43
|
+
<DataManagementSection allowed={dataOpsAllowed} />
|
|
42
44
|
</div>
|
|
43
45
|
</PageShell>
|
|
44
46
|
);
|
package/src/app/tables/page.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { listTables } from "@/lib/data/tables";
|
|
2
2
|
import { db } from "@/lib/db";
|
|
3
3
|
import { projects } from "@/lib/db/schema";
|
|
4
|
+
import { listApps } from "@/lib/apps/registry";
|
|
4
5
|
import { TableBrowser } from "@/components/tables/table-browser";
|
|
5
6
|
import { PageShell } from "@/components/shared/page-shell";
|
|
6
7
|
|
|
@@ -13,9 +14,17 @@ export default async function TablesPage() {
|
|
|
13
14
|
.select({ id: projects.id, name: projects.name })
|
|
14
15
|
.from(projects);
|
|
15
16
|
|
|
17
|
+
// Installed packs — mark tables whose projectId is a pack with a pack pill
|
|
18
|
+
// (FEAT-8) instead of the plain project name.
|
|
19
|
+
const installedPacks = listApps().map((a) => ({ id: a.id, name: a.name }));
|
|
20
|
+
|
|
16
21
|
return (
|
|
17
22
|
<PageShell title="Tables">
|
|
18
|
-
<TableBrowser
|
|
23
|
+
<TableBrowser
|
|
24
|
+
initialTables={tables}
|
|
25
|
+
projects={projectList}
|
|
26
|
+
installedPacks={installedPacks}
|
|
27
|
+
/>
|
|
19
28
|
</PageShell>
|
|
20
29
|
);
|
|
21
30
|
}
|
|
@@ -22,8 +22,8 @@ interface HeaderSlotProps {
|
|
|
22
22
|
export function HeaderSlotView({ slot, manifestPane }: HeaderSlotProps) {
|
|
23
23
|
const { title, description, status, actions, cadenceChip, runNowBlueprintId, runNowVariables, periodChip, triggerSourceChip } = slot;
|
|
24
24
|
return (
|
|
25
|
-
<div className="flex flex-col gap-
|
|
26
|
-
<div className="min-w-0 flex-1">
|
|
25
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between sm:gap-x-4 sm:gap-y-2">
|
|
26
|
+
<div className="min-w-0 flex-1 sm:min-w-[16rem]">
|
|
27
27
|
<h1 className="text-xl font-semibold tracking-tight line-clamp-2" title={title}>
|
|
28
28
|
{title}
|
|
29
29
|
</h1>
|
|
@@ -33,7 +33,13 @@ export function HeaderSlotView({ slot, manifestPane }: HeaderSlotProps) {
|
|
|
33
33
|
</p>
|
|
34
34
|
)}
|
|
35
35
|
</div>
|
|
36
|
-
|
|
36
|
+
{/*
|
|
37
|
+
* Action group stays a single non-wrapping row (chips + Run + manifest
|
|
38
|
+
* never split across lines). When the title can't yield enough inline
|
|
39
|
+
* space, the PARENT's flex-wrap drops this whole group to its own row —
|
|
40
|
+
* an intentional stack, not an accidental two-line break (FEAT-4).
|
|
41
|
+
*/}
|
|
42
|
+
<div className="flex flex-nowrap items-center gap-2 sm:shrink-0">
|
|
37
43
|
{status && <StatusChip status={status} size="md" />}
|
|
38
44
|
{cadenceChip && (
|
|
39
45
|
<ScheduleCadenceChip
|
|
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
|
|
|
5
5
|
import { Play } from "lucide-react";
|
|
6
6
|
import { toast } from "sonner";
|
|
7
7
|
import { RunNowSheet } from "./run-now-sheet";
|
|
8
|
+
import { toastDraftCreated } from "./run-now-toast";
|
|
8
9
|
import type { BlueprintVariable } from "@/lib/workflows/blueprints/types";
|
|
9
10
|
|
|
10
11
|
interface RunNowButtonProps {
|
|
@@ -63,12 +64,15 @@ export function RunNowButton({
|
|
|
63
64
|
});
|
|
64
65
|
if (!res.ok) {
|
|
65
66
|
const err = (await res.json().catch(() => ({}))) as { error?: string };
|
|
66
|
-
toast.error(err.error ?? `Failed to
|
|
67
|
+
toast.error(err.error ?? `Failed to create draft (${res.status})`);
|
|
67
68
|
return;
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
+
const body = (await res.json().catch(() => ({}))) as {
|
|
71
|
+
workflowId?: string;
|
|
72
|
+
};
|
|
73
|
+
toastDraftCreated(body.workflowId);
|
|
70
74
|
} catch (err) {
|
|
71
|
-
toast.error(err instanceof Error ? err.message : "
|
|
75
|
+
toast.error(err instanceof Error ? err.message : "Could not create draft");
|
|
72
76
|
} finally {
|
|
73
77
|
setPending(false);
|
|
74
78
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "@/components/ui/sheet";
|
|
14
14
|
import { VariableInput } from "@/components/workflows/variable-input";
|
|
15
15
|
import { validateVariables } from "@/lib/workflows/blueprints/validate-variables";
|
|
16
|
+
import { toastDraftCreated } from "./run-now-toast";
|
|
16
17
|
import type { BlueprintVariable } from "@/lib/workflows/blueprints/types";
|
|
17
18
|
|
|
18
19
|
interface RunNowSheetProps {
|
|
@@ -74,10 +75,13 @@ export function RunNowSheet({
|
|
|
74
75
|
}
|
|
75
76
|
return;
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
+
const body = (await res.json().catch(() => ({}))) as {
|
|
79
|
+
workflowId?: string;
|
|
80
|
+
};
|
|
81
|
+
toastDraftCreated(body.workflowId);
|
|
78
82
|
setOpen(false);
|
|
79
83
|
} catch (err) {
|
|
80
|
-
toast.error(err instanceof Error ? err.message : "
|
|
84
|
+
toast.error(err instanceof Error ? err.message : "Could not create draft");
|
|
81
85
|
} finally {
|
|
82
86
|
setPending(false);
|
|
83
87
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { toast } from "sonner";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BUG-4: "Run now" POSTs the blueprint instantiate endpoint, which creates a
|
|
5
|
+
* `draft` workflow and does NOT dispatch it. The old toast said "Run started",
|
|
6
|
+
* which is a lie — nothing runs until the user hits Execute on the workflow
|
|
7
|
+
* detail page. Surface the truth: a draft was created, and deep-link to where
|
|
8
|
+
* it can be executed.
|
|
9
|
+
*
|
|
10
|
+
* Shared by `run-now-button.tsx` (direct POST) and `run-now-sheet.tsx`
|
|
11
|
+
* (variable path) so the honest copy can't drift between the two.
|
|
12
|
+
*/
|
|
13
|
+
export function toastDraftCreated(workflowId: string | undefined): void {
|
|
14
|
+
if (!workflowId) {
|
|
15
|
+
// No id to link to — still don't claim it started.
|
|
16
|
+
toast.success("Draft created. Open it in Workflows to Execute.");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
toast.success("Draft created. Open it in Workflows to Execute.", {
|
|
20
|
+
action: {
|
|
21
|
+
label: "Open workflow",
|
|
22
|
+
onClick: () => {
|
|
23
|
+
window.location.assign(`/workflows/${workflowId}`);
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -17,18 +17,52 @@ import { ProfileCard } from "@/components/profiles/profile-card";
|
|
|
17
17
|
import { ProfileImportDialog } from "@/components/profiles/profile-import-dialog";
|
|
18
18
|
import { RepoImportWizard } from "@/components/profiles/repo-import-wizard";
|
|
19
19
|
import type { AgentProfile } from "@/lib/agents/profiles/types";
|
|
20
|
+
import { packOf } from "@/lib/apps/pack-of";
|
|
20
21
|
|
|
21
22
|
interface ProfileWithBuiltin extends AgentProfile {
|
|
22
23
|
isBuiltin?: boolean;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/** The installed-pack identity a profile carries (FEAT-8). {id, name} only. */
|
|
27
|
+
export interface InstalledPackRef {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
interface ProfileBrowserProps {
|
|
26
33
|
initialProfiles: AgentProfile[];
|
|
34
|
+
/** Installed packs — resolves each profile's pack provenance (FEAT-8). */
|
|
35
|
+
installedPacks?: InstalledPackRef[];
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
export function ProfileBrowser({
|
|
38
|
+
export function ProfileBrowser({
|
|
39
|
+
initialProfiles,
|
|
40
|
+
installedPacks = [],
|
|
41
|
+
}: ProfileBrowserProps) {
|
|
30
42
|
const router = useRouter();
|
|
31
43
|
const [profiles, setProfiles] = useState<ProfileWithBuiltin[]>(initialProfiles);
|
|
44
|
+
|
|
45
|
+
// Stable {id → display name} lookup + the gated id-set for packOf. Rebuilt
|
|
46
|
+
// only when the installed packs change (never on a profile refresh), so the
|
|
47
|
+
// pill survives refreshProfiles — which recomputes from the refreshed id.
|
|
48
|
+
const packNameById = useMemo(
|
|
49
|
+
() => new Map(installedPacks.map((p) => [p.id, p.name])),
|
|
50
|
+
[installedPacks]
|
|
51
|
+
);
|
|
52
|
+
const installedPackIds = useMemo(
|
|
53
|
+
() => new Set(installedPacks.map((p) => p.id)),
|
|
54
|
+
[installedPacks]
|
|
55
|
+
);
|
|
56
|
+
const packNameFor = useCallback(
|
|
57
|
+
(profile: AgentProfile): string | null => {
|
|
58
|
+
const packId = packOf(
|
|
59
|
+
{ kind: "profile", id: profile.id },
|
|
60
|
+
installedPackIds
|
|
61
|
+
);
|
|
62
|
+
return packId ? packNameById.get(packId) ?? null : null;
|
|
63
|
+
},
|
|
64
|
+
[installedPackIds, packNameById]
|
|
65
|
+
);
|
|
32
66
|
const [search, setSearch] = useState("");
|
|
33
67
|
const [domainFilter, setDomainFilter] = useState<
|
|
34
68
|
"all" | "work" | "personal"
|
|
@@ -201,6 +235,7 @@ export function ProfileBrowser({ initialProfiles }: ProfileBrowserProps) {
|
|
|
201
235
|
key={profile.id}
|
|
202
236
|
profile={profile}
|
|
203
237
|
isBuiltin={profile.isBuiltin}
|
|
238
|
+
packName={packNameFor(profile)}
|
|
204
239
|
onClick={() => router.push(`/profiles/${profile.id}`)}
|
|
205
240
|
/>
|
|
206
241
|
))}
|
|
@@ -6,11 +6,14 @@ import { Download } from "lucide-react";
|
|
|
6
6
|
import type { AgentRuntimeId } from "@/lib/agents/runtime/catalog";
|
|
7
7
|
import { getSupportedRuntimes } from "@/lib/agents/profiles/compatibility";
|
|
8
8
|
import { IconCircle, getProfileIcon, getDomainColors } from "@/lib/constants/card-icons";
|
|
9
|
+
import { PackPill } from "@/components/shared/pack-pill";
|
|
9
10
|
import type { AgentProfile } from "@/lib/agents/profiles/types";
|
|
10
11
|
|
|
11
12
|
interface ProfileCardProps {
|
|
12
13
|
profile: AgentProfile;
|
|
13
14
|
isBuiltin?: boolean;
|
|
15
|
+
/** Display name of the pack that installed this profile, or null (FEAT-8). */
|
|
16
|
+
packName?: string | null;
|
|
14
17
|
onClick: () => void;
|
|
15
18
|
}
|
|
16
19
|
|
|
@@ -30,7 +33,7 @@ const RUNTIME_SHORT_LABEL: Record<AgentRuntimeId, string> = {
|
|
|
30
33
|
ollama: "Ollama (Local)",
|
|
31
34
|
};
|
|
32
35
|
|
|
33
|
-
export function ProfileCard({ profile, isBuiltin = false, onClick }: ProfileCardProps) {
|
|
36
|
+
export function ProfileCard({ profile, isBuiltin = false, packName = null, onClick }: ProfileCardProps) {
|
|
34
37
|
|
|
35
38
|
return (
|
|
36
39
|
<Card
|
|
@@ -83,7 +86,11 @@ export function ProfileCard({ profile, isBuiltin = false, onClick }: ProfileCard
|
|
|
83
86
|
</div>
|
|
84
87
|
|
|
85
88
|
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
|
86
|
-
{
|
|
89
|
+
{/* Pack provenance outranks every other origin: a pack-installed
|
|
90
|
+
profile is never "Custom"/"Discovered" — it belongs to its pack. */}
|
|
91
|
+
{packName ? (
|
|
92
|
+
<PackPill packName={packName} />
|
|
93
|
+
) : profile.importMeta ? (
|
|
87
94
|
<span className="flex items-center gap-1.5">
|
|
88
95
|
<Badge variant="outline" className="border-purple-200 text-purple-600 dark:border-purple-800 dark:text-purple-400">
|
|
89
96
|
<Download className="mr-1 h-3 w-3" />
|
|
@@ -11,6 +11,8 @@ import { ScheduleStatusBadge } from "./schedule-status-badge";
|
|
|
11
11
|
import { ConfirmDialog } from "@/components/shared/confirm-dialog";
|
|
12
12
|
import { EmptyState } from "@/components/shared/empty-state";
|
|
13
13
|
import { describeCron } from "@/lib/schedules/interval-parser";
|
|
14
|
+
import { PackPill } from "@/components/shared/pack-pill";
|
|
15
|
+
import { packOf } from "@/lib/apps/pack-of";
|
|
14
16
|
import { Clock, Heart, Pause, Play, Trash2 } from "lucide-react";
|
|
15
17
|
import { toast } from "sonner";
|
|
16
18
|
|
|
@@ -39,6 +41,9 @@ interface ScheduleListProps {
|
|
|
39
41
|
|
|
40
42
|
export function ScheduleList({ projects, initialSelectedId }: ScheduleListProps) {
|
|
41
43
|
const [schedules, setSchedules] = useState<Schedule[]>([]);
|
|
44
|
+
const [installedPacks, setInstalledPacks] = useState<
|
|
45
|
+
{ id: string; name: string }[]
|
|
46
|
+
>([]);
|
|
42
47
|
const [loaded, setLoaded] = useState(false);
|
|
43
48
|
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
|
|
44
49
|
const [selectedScheduleId, setSelectedScheduleId] = useState<string | null>(
|
|
@@ -57,6 +62,24 @@ export function ScheduleList({ projects, initialSelectedId }: ScheduleListProps)
|
|
|
57
62
|
refresh();
|
|
58
63
|
}, [refresh]);
|
|
59
64
|
|
|
65
|
+
// Installed packs — a pack schedule's id is `app:<packId>:<sid>`, so packOf
|
|
66
|
+
// resolves provenance from the id alone (FEAT-8).
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
fetch("/api/apps")
|
|
69
|
+
.then((r) => (r.ok ? r.json() : []))
|
|
70
|
+
.then((apps: Array<{ id: string; name: string }>) =>
|
|
71
|
+
setInstalledPacks(apps.map((a) => ({ id: a.id, name: a.name })))
|
|
72
|
+
)
|
|
73
|
+
.catch(() => {});
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const installedPackIds = new Set(installedPacks.map((p) => p.id));
|
|
77
|
+
const packNameById = new Map(installedPacks.map((p) => [p.id, p.name]));
|
|
78
|
+
const packNameForSchedule = (id: string): string | null => {
|
|
79
|
+
const packId = packOf({ kind: "schedule", id }, installedPackIds);
|
|
80
|
+
return packId ? packNameById.get(packId) ?? null : null;
|
|
81
|
+
};
|
|
82
|
+
|
|
60
83
|
async function handlePauseResume(id: string, currentStatus: string) {
|
|
61
84
|
const newStatus = currentStatus === "active" ? "paused" : "active";
|
|
62
85
|
const res = await fetch(`/api/schedules/${id}`, {
|
|
@@ -163,7 +186,13 @@ export function ScheduleList({ projects, initialSelectedId }: ScheduleListProps)
|
|
|
163
186
|
)}
|
|
164
187
|
{sched.name}
|
|
165
188
|
</CardTitle>
|
|
166
|
-
<
|
|
189
|
+
<div className="flex shrink-0 items-center gap-1.5">
|
|
190
|
+
{(() => {
|
|
191
|
+
const packName = packNameForSchedule(sched.id);
|
|
192
|
+
return packName ? <PackPill packName={packName} /> : null;
|
|
193
|
+
})()}
|
|
194
|
+
<ScheduleStatusBadge status={sched.status} />
|
|
195
|
+
</div>
|
|
167
196
|
</div>
|
|
168
197
|
</CardHeader>
|
|
169
198
|
<CardContent>
|
|
@@ -15,7 +15,35 @@ import { ConfirmDialog } from "@/components/shared/confirm-dialog";
|
|
|
15
15
|
import { toast } from "sonner";
|
|
16
16
|
import { Loader2, Trash2, Database } from "lucide-react";
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Reads a JSON body defensively. A gated route returns an explanatory
|
|
20
|
+
* `{success:false,error}` body with a non-2xx status; a genuine network/parse
|
|
21
|
+
* failure has no parseable body. Distinguishing the two is what stops a
|
|
22
|
+
* deliberate gate (403) from being disguised as "Network error" (BUG-5).
|
|
23
|
+
*/
|
|
24
|
+
async function readResult(
|
|
25
|
+
res: Response
|
|
26
|
+
): Promise<{ ok: boolean; body: Record<string, unknown> | null }> {
|
|
27
|
+
let body: Record<string, unknown> | null = null;
|
|
28
|
+
try {
|
|
29
|
+
body = (await res.json()) as Record<string, unknown> | null;
|
|
30
|
+
} catch {
|
|
31
|
+
body = null;
|
|
32
|
+
}
|
|
33
|
+
return { ok: res.ok, body };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Prefer the route's explanatory reason; fall back to the HTTP status. */
|
|
37
|
+
function failureMessage(
|
|
38
|
+
res: Response,
|
|
39
|
+
body: Record<string, unknown> | null,
|
|
40
|
+
fallback: string
|
|
41
|
+
): string {
|
|
42
|
+
if (body && typeof body.error === "string") return body.error;
|
|
43
|
+
return `${fallback} (HTTP ${res.status})`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function DataManagementSection({ allowed = true }: { allowed?: boolean }) {
|
|
19
47
|
const [clearOpen, setClearOpen] = useState(false);
|
|
20
48
|
const [seedOpen, setSeedOpen] = useState(false);
|
|
21
49
|
const [loading, setLoading] = useState(false);
|
|
@@ -24,14 +52,14 @@ export function DataManagementSection() {
|
|
|
24
52
|
setLoading(true);
|
|
25
53
|
try {
|
|
26
54
|
const res = await fetch("/api/data/clear", { method: "POST" });
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
const d =
|
|
55
|
+
const { ok, body } = await readResult(res);
|
|
56
|
+
if (ok && body?.success) {
|
|
57
|
+
const d = body.deleted as Record<string, number>;
|
|
30
58
|
toast.success(
|
|
31
59
|
`Cleared ${d.projects} projects, ${d.tasks} tasks, ${d.workflows} workflows, ${d.schedules} schedules, ${d.documents} documents, ${d.conversations} conversations, ${d.chatMessages} messages, ${d.learnedContext} learned context, ${d.views} views, ${d.usageLedger} usage entries, ${d.agentLogs} logs, ${d.notifications} notifications, ${d.sampleProfiles} sample profiles, ${d.files} files`
|
|
32
60
|
);
|
|
33
61
|
} else {
|
|
34
|
-
toast.error(
|
|
62
|
+
toast.error(failureMessage(res, body, "Clear failed"));
|
|
35
63
|
}
|
|
36
64
|
} catch {
|
|
37
65
|
toast.error("Clear failed. Network error");
|
|
@@ -44,14 +72,14 @@ export function DataManagementSection() {
|
|
|
44
72
|
setLoading(true);
|
|
45
73
|
try {
|
|
46
74
|
const res = await fetch("/api/data/seed", { method: "POST" });
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
const s =
|
|
75
|
+
const { ok, body } = await readResult(res);
|
|
76
|
+
if (ok && body?.success) {
|
|
77
|
+
const s = body.seeded as Record<string, number>;
|
|
50
78
|
toast.success(
|
|
51
79
|
`Seeded ${s.profiles} profiles, ${s.projects} projects, ${s.tasks} tasks, ${s.workflows} workflows, ${s.schedules} schedules, ${s.documents} documents, ${s.userTables} tables (${s.userTableRows} rows, ${s.tableViews} views, ${s.tableTriggers} triggers, ${s.tableRelationships} links), ${s.conversations} conversations, ${s.chatMessages} messages, ${s.agentMemory} memories, ${s.agentMessages} handoffs, ${s.channelConfigs} channels (${s.channelBindings} bindings), ${s.environmentScans} scans (${s.environmentArtifacts} artifacts, ${s.environmentCheckpoints} checkpoints, ${s.environmentTemplates} templates), ${s.workflowExecutionStats} workflow-stats, ${s.scheduleFiringMetrics} firing-metrics, ${s.usageLedger} usage entries, ${s.learnedContext} learned context, ${s.views} views, ${s.profileTestResults} test results, ${s.repoImports} repo imports, ${s.agentLogs} logs, ${s.notifications} notifications`
|
|
52
80
|
);
|
|
53
81
|
} else {
|
|
54
|
-
toast.error(
|
|
82
|
+
toast.error(failureMessage(res, body, "Seed failed"));
|
|
55
83
|
}
|
|
56
84
|
} catch {
|
|
57
85
|
toast.error("Seed failed. Network error");
|
|
@@ -60,6 +88,25 @@ export function DataManagementSection() {
|
|
|
60
88
|
}
|
|
61
89
|
}
|
|
62
90
|
|
|
91
|
+
if (!allowed) {
|
|
92
|
+
return (
|
|
93
|
+
<Card className="surface-card">
|
|
94
|
+
<CardHeader>
|
|
95
|
+
<CardTitle>Data Management</CardTitle>
|
|
96
|
+
<CardDescription>
|
|
97
|
+
Reset or populate your Orionfold Relay instance
|
|
98
|
+
</CardDescription>
|
|
99
|
+
</CardHeader>
|
|
100
|
+
<CardContent>
|
|
101
|
+
<p className="text-sm text-muted-foreground">
|
|
102
|
+
Seeding and clearing sample data are staging-only tools. They are
|
|
103
|
+
turned off on this build so your real data stays safe.
|
|
104
|
+
</p>
|
|
105
|
+
</CardContent>
|
|
106
|
+
</Card>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
63
110
|
return (
|
|
64
111
|
<>
|
|
65
112
|
<Card className="surface-card">
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Package } from "lucide-react";
|
|
2
|
+
import { Badge } from "@/components/ui/badge";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
interface PackPillProps {
|
|
6
|
+
/** The installed pack's display name (e.g. "Relay Agency"), from its manifest. */
|
|
7
|
+
packName: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* PackPill — provenance label marking a primitive (profile, blueprint, table,
|
|
13
|
+
* schedule) as installed by a pack (FEAT-8, spec:
|
|
14
|
+
* features/fix-app-shell-activation-redesign.md).
|
|
15
|
+
*
|
|
16
|
+
* Deliberately NOT a StatusChip / status-family: pack provenance is an open-set
|
|
17
|
+
* identity ("which pack"), not one of the 5 fixed status dimensions. It shares
|
|
18
|
+
* the visual system by building on the same `Badge` primitive StatusChip uses,
|
|
19
|
+
* with its own icon (package) and a distinct amber color family so it reads as
|
|
20
|
+
* a provenance marker — the sibling of the card's existing Imported/Discovered/
|
|
21
|
+
* Built-in badges, and outranks the "Custom" fallback (a pack-installed
|
|
22
|
+
* primitive is never "Custom").
|
|
23
|
+
*/
|
|
24
|
+
export function PackPill({ packName, className }: PackPillProps) {
|
|
25
|
+
return (
|
|
26
|
+
<Badge
|
|
27
|
+
data-testid="pack-pill"
|
|
28
|
+
variant="outline"
|
|
29
|
+
title={`Installed by the ${packName} pack`}
|
|
30
|
+
className={cn(
|
|
31
|
+
"border-amber-200 text-amber-700 dark:border-amber-800 dark:text-amber-400",
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
<Package className="mr-1 h-3 w-3" />
|
|
36
|
+
{packName}
|
|
37
|
+
</Badge>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -19,15 +19,37 @@ import { TableCreateSheet } from "./table-create-sheet";
|
|
|
19
19
|
import { FilterBar } from "@/components/shared/filter-bar";
|
|
20
20
|
import { EmptyState } from "@/components/shared/empty-state";
|
|
21
21
|
import { Table2 } from "lucide-react";
|
|
22
|
+
import { packOf } from "@/lib/apps/pack-of";
|
|
22
23
|
import type { TableWithRelations } from "./types";
|
|
23
24
|
|
|
24
25
|
interface TableBrowserProps {
|
|
25
26
|
initialTables: TableWithRelations[];
|
|
26
27
|
projects: { id: string; name: string }[];
|
|
28
|
+
/** Installed packs — marks tables whose project is a pack (FEAT-8). */
|
|
29
|
+
installedPacks?: { id: string; name: string }[];
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
export function TableBrowser({
|
|
32
|
+
export function TableBrowser({
|
|
33
|
+
initialTables,
|
|
34
|
+
projects,
|
|
35
|
+
installedPacks = [],
|
|
36
|
+
}: TableBrowserProps) {
|
|
30
37
|
const [tables, setTables] = useState(initialTables);
|
|
38
|
+
// {projectId → pack name} for pack-installed projects, via the shared
|
|
39
|
+
// resolver (tables associate to a pack by projectId === packId).
|
|
40
|
+
const installedPackIds = new Set(installedPacks.map((p) => p.id));
|
|
41
|
+
const packNameById = new Map(installedPacks.map((p) => [p.id, p.name]));
|
|
42
|
+
const packNameForProject = useCallback(
|
|
43
|
+
(projectId: string | null | undefined): string | null => {
|
|
44
|
+
const packId = packOf(
|
|
45
|
+
{ kind: "table", id: "", projectId: projectId ?? undefined },
|
|
46
|
+
installedPackIds
|
|
47
|
+
);
|
|
48
|
+
return packId ? packNameById.get(packId) ?? null : null;
|
|
49
|
+
},
|
|
50
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
51
|
+
[installedPacks]
|
|
52
|
+
);
|
|
31
53
|
const [view, setView] = useState<"table" | "grid">("table");
|
|
32
54
|
const [search, setSearch] = useState("");
|
|
33
55
|
const [sourceFilter, setSourceFilter] = useState<string>("all");
|
|
@@ -214,12 +236,14 @@ export function TableBrowser({ initialTables, projects }: TableBrowserProps) {
|
|
|
214
236
|
onToggleSelectAll={toggleSelectAll}
|
|
215
237
|
onSelect={navigate}
|
|
216
238
|
onOpen={navigate}
|
|
239
|
+
packNameForProject={packNameForProject}
|
|
217
240
|
/>
|
|
218
241
|
) : (
|
|
219
242
|
<TableGrid
|
|
220
243
|
tables={filtered}
|
|
221
244
|
onSelect={navigate}
|
|
222
245
|
onOpen={navigate}
|
|
246
|
+
packNameForProject={packNameForProject}
|
|
223
247
|
/>
|
|
224
248
|
)}
|
|
225
249
|
|
|
@@ -4,6 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
4
4
|
import { Badge } from "@/components/ui/badge";
|
|
5
5
|
import { Table2 } from "lucide-react";
|
|
6
6
|
import { tableSourceVariant } from "@/lib/constants/table-status";
|
|
7
|
+
import { PackPill } from "@/components/shared/pack-pill";
|
|
7
8
|
import { formatRowCount, formatColumnCount } from "./utils";
|
|
8
9
|
import type { TableWithRelations } from "./types";
|
|
9
10
|
|
|
@@ -11,9 +12,16 @@ interface TableGridProps {
|
|
|
11
12
|
tables: TableWithRelations[];
|
|
12
13
|
onSelect: (id: string) => void;
|
|
13
14
|
onOpen: (id: string) => void;
|
|
15
|
+
/** Resolves a projectId to its pack display name, or null (FEAT-8). */
|
|
16
|
+
packNameForProject?: (projectId: string | null | undefined) => string | null;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
export function TableGrid({
|
|
19
|
+
export function TableGrid({
|
|
20
|
+
tables,
|
|
21
|
+
onSelect,
|
|
22
|
+
onOpen,
|
|
23
|
+
packNameForProject,
|
|
24
|
+
}: TableGridProps) {
|
|
17
25
|
return (
|
|
18
26
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
19
27
|
{tables.map((t) => (
|
|
@@ -49,9 +57,13 @@ export function TableGrid({ tables, onSelect, onOpen }: TableGridProps) {
|
|
|
49
57
|
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
|
50
58
|
<span>{formatColumnCount(t.columnCount)}</span>
|
|
51
59
|
<span>{formatRowCount(t.rowCount)}</span>
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
{(() => {
|
|
61
|
+
const packName = packNameForProject?.(t.projectId);
|
|
62
|
+
if (packName) return <PackPill packName={packName} />;
|
|
63
|
+
return t.projectName ? (
|
|
64
|
+
<span className="truncate">{t.projectName}</span>
|
|
65
|
+
) : null;
|
|
66
|
+
})()}
|
|
55
67
|
</div>
|
|
56
68
|
</CardContent>
|
|
57
69
|
</Card>
|