orionfold-relay 0.24.0 → 0.25.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 +111 -15
- package/package.json +1 -1
- package/src/app/api/snapshots/[id]/restore/route.ts +6 -0
- package/src/app/api/snapshots/route.ts +6 -0
- package/src/components/apps/app-detail-actions.tsx +10 -23
- package/src/components/apps/kit-view/kit-view.tsx +8 -1
- package/src/components/apps/kit-view/slots/header.tsx +3 -3
- package/src/components/apps/kit-view/slots/manifest-sheet.tsx +2 -2
- package/src/components/apps/last-run-card.tsx +91 -2
- package/src/components/apps/ledger-hero-panel.tsx +2 -1
- package/src/components/chat/app-composer-hero.tsx +1 -1
- package/src/components/chat/chat-empty-state.tsx +3 -3
- package/src/components/dashboard/welcome-landing.tsx +4 -4
- package/src/components/onboarding/runtime-preference-modal.tsx +8 -8
- package/src/components/profiles/profile-card.tsx +18 -8
- package/src/components/settings/license-section.tsx +5 -0
- package/src/components/workflows/shared/workflow-header.tsx +114 -0
- package/src/lib/agents/runtime/ollama-adapter.ts +11 -6
- package/src/lib/agents/runtime/ollama-model-resolver.ts +76 -0
- package/src/lib/apps/view-kits/data.ts +79 -0
- package/src/lib/apps/view-kits/kits/workflow-hub.ts +48 -9
- package/src/lib/apps/view-kits/types.ts +38 -0
- package/src/lib/chat/ollama-engine.ts +17 -4
- package/src/lib/environment/workspace-context.ts +6 -0
- package/src/lib/notifications/actionable.ts +11 -2
- package/src/lib/packs/templates/relay-agency-pro/base/manifest.yaml +8 -2
- package/src/lib/packs/templates/relay-agency-pro/pack.yaml +34 -14
- 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/src/lib/snapshots/snapshot-manager.ts +40 -6
- package/src/lib/workflows/engine.ts +106 -3
- package/src/lib/workflows/types.ts +13 -0
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.25";
|
|
1190
1190
|
CAPABILITY_VALUES = ["fs", "net", "child_process", "env"];
|
|
1191
1191
|
ORIGIN_VALUES = ["ainative-internal", "third-party"];
|
|
1192
1192
|
PrimitivesBundleManifestSchema = z.object({
|
|
@@ -7135,7 +7135,13 @@ function getWorkspaceContext() {
|
|
|
7135
7135
|
gitBranch = execFileSync2("git", ["branch", "--show-current"], {
|
|
7136
7136
|
cwd,
|
|
7137
7137
|
encoding: "utf-8",
|
|
7138
|
-
timeout: 3e3
|
|
7138
|
+
timeout: 3e3,
|
|
7139
|
+
// Route git's stderr to a pipe (captured on the thrown error, then
|
|
7140
|
+
// discarded) instead of inheriting the console. Without this, a
|
|
7141
|
+
// non-git cwd leaks a raw `fatal: not a git repository` to a
|
|
7142
|
+
// customer's first-run log before the catch swallows the exit code.
|
|
7143
|
+
// Mirrors src/lib/instance/git-ops.ts run().
|
|
7144
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
7139
7145
|
}).trim() || null;
|
|
7140
7146
|
} catch {
|
|
7141
7147
|
}
|
|
@@ -12134,7 +12140,7 @@ async function listPendingApprovalPayloads(limit = 20) {
|
|
|
12134
12140
|
const isBatchProposal = row.type === "context_proposal_batch";
|
|
12135
12141
|
const parsedInput = parseNotificationToolInput(row.toolInput);
|
|
12136
12142
|
let effectiveWorkflowId = row.workflowId;
|
|
12137
|
-
if (!row.taskId && row.toolName === "WorkflowCheckpoint" && row.toolInput) {
|
|
12143
|
+
if (!row.taskId && (row.toolName === "WorkflowCheckpoint" || row.toolName === "AskUserQuestion") && row.toolInput) {
|
|
12138
12144
|
try {
|
|
12139
12145
|
const parsed = typeof row.toolInput === "string" ? JSON.parse(row.toolInput) : row.toolInput;
|
|
12140
12146
|
effectiveWorkflowId = parsed.workflowId ?? null;
|
|
@@ -12963,7 +12969,7 @@ var init_registry6 = __esm({
|
|
|
12963
12969
|
init_registry5();
|
|
12964
12970
|
init_installer();
|
|
12965
12971
|
init_schedule_spec();
|
|
12966
|
-
SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.
|
|
12972
|
+
SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.24"]);
|
|
12967
12973
|
pluginCache = null;
|
|
12968
12974
|
lastLoadedPluginIds = /* @__PURE__ */ new Set();
|
|
12969
12975
|
PluginTableSchema = z16.object({
|
|
@@ -21726,6 +21732,41 @@ var init_openai_direct = __esm({
|
|
|
21726
21732
|
}
|
|
21727
21733
|
});
|
|
21728
21734
|
|
|
21735
|
+
// src/lib/agents/runtime/ollama-model-resolver.ts
|
|
21736
|
+
async function listPulledOllamaModels(baseUrl) {
|
|
21737
|
+
try {
|
|
21738
|
+
const response = await fetch(`${baseUrl}/api/tags`, {
|
|
21739
|
+
signal: AbortSignal.timeout(5e3)
|
|
21740
|
+
});
|
|
21741
|
+
if (!response.ok) return [];
|
|
21742
|
+
const data = await response.json();
|
|
21743
|
+
return (data.models ?? []).map((m) => m.name).filter((n) => typeof n === "string" && n.length > 0);
|
|
21744
|
+
} catch {
|
|
21745
|
+
return [];
|
|
21746
|
+
}
|
|
21747
|
+
}
|
|
21748
|
+
async function resolveOllamaModel(baseUrl, requestedModel, defaultModel) {
|
|
21749
|
+
const explicit = requestedModel?.trim() || defaultModel?.trim();
|
|
21750
|
+
if (explicit) return explicit;
|
|
21751
|
+
const pulled = await listPulledOllamaModels(baseUrl);
|
|
21752
|
+
if (pulled.length > 0) return pulled[0];
|
|
21753
|
+
throw new OllamaModelNotConfiguredError();
|
|
21754
|
+
}
|
|
21755
|
+
var OllamaModelNotConfiguredError;
|
|
21756
|
+
var init_ollama_model_resolver = __esm({
|
|
21757
|
+
"src/lib/agents/runtime/ollama-model-resolver.ts"() {
|
|
21758
|
+
"use strict";
|
|
21759
|
+
OllamaModelNotConfiguredError = class extends Error {
|
|
21760
|
+
constructor(message) {
|
|
21761
|
+
super(
|
|
21762
|
+
message ?? "No Ollama model is configured. Pull a model (e.g. `ollama pull llama3.2`) or set a default in Settings \u2192 Ollama."
|
|
21763
|
+
);
|
|
21764
|
+
this.name = "OllamaModelNotConfiguredError";
|
|
21765
|
+
}
|
|
21766
|
+
};
|
|
21767
|
+
}
|
|
21768
|
+
});
|
|
21769
|
+
|
|
21729
21770
|
// src/lib/agents/runtime/ollama-adapter.ts
|
|
21730
21771
|
import { eq as eq38 } from "drizzle-orm";
|
|
21731
21772
|
async function getOllamaBaseUrl() {
|
|
@@ -21734,11 +21775,11 @@ async function getOllamaBaseUrl() {
|
|
|
21734
21775
|
const url = await getSetting2(SETTINGS_KEYS2.OLLAMA_BASE_URL);
|
|
21735
21776
|
return url || DEFAULT_OLLAMA_BASE_URL;
|
|
21736
21777
|
}
|
|
21737
|
-
async function getOllamaModel() {
|
|
21778
|
+
async function getOllamaModel(baseUrl) {
|
|
21738
21779
|
const { getSetting: getSetting2 } = await Promise.resolve().then(() => (init_helpers(), helpers_exports));
|
|
21739
21780
|
const { SETTINGS_KEYS: SETTINGS_KEYS2 } = await Promise.resolve().then(() => (init_settings(), settings_exports));
|
|
21740
|
-
const
|
|
21741
|
-
return
|
|
21781
|
+
const defaultModel = await getSetting2(SETTINGS_KEYS2.OLLAMA_DEFAULT_MODEL);
|
|
21782
|
+
return resolveOllamaModel(baseUrl, null, defaultModel);
|
|
21742
21783
|
}
|
|
21743
21784
|
async function streamOllamaChat(baseUrl, model, messages, signal) {
|
|
21744
21785
|
const response = await fetch(`${baseUrl}/api/chat`, {
|
|
@@ -21833,7 +21874,7 @@ async function executeOllamaTask(taskId) {
|
|
|
21833
21874
|
await db.update(tasks).set({ status: "running", updatedAt: /* @__PURE__ */ new Date() }).where(eq38(tasks.id, taskId));
|
|
21834
21875
|
const ctx = await buildTaskQueryContext(task, agentProfileId);
|
|
21835
21876
|
const baseUrl = await getOllamaBaseUrl();
|
|
21836
|
-
const modelId = await getOllamaModel();
|
|
21877
|
+
const modelId = await getOllamaModel(baseUrl);
|
|
21837
21878
|
const messages = [];
|
|
21838
21879
|
if (ctx.systemInstructions) {
|
|
21839
21880
|
messages.push({ role: "system", content: ctx.systemInstructions });
|
|
@@ -21924,7 +21965,7 @@ async function executeOllamaTask(taskId) {
|
|
|
21924
21965
|
}
|
|
21925
21966
|
async function runOllamaTaskAssist(input) {
|
|
21926
21967
|
const baseUrl = await getOllamaBaseUrl();
|
|
21927
|
-
const modelId = await getOllamaModel();
|
|
21968
|
+
const modelId = await getOllamaModel(baseUrl);
|
|
21928
21969
|
const profileIds = listProfiles().map((p) => p.id);
|
|
21929
21970
|
const profileList = profileIds.length > 0 ? `Available agent profiles: ${profileIds.join(", ")}` : "No explicit profiles available.";
|
|
21930
21971
|
const systemPrompt = `You are an AI task definition assistant. Analyze the given task and return ONLY a JSON object (no markdown) with:
|
|
@@ -21980,7 +22021,7 @@ async function testOllamaConnection() {
|
|
|
21980
22021
|
};
|
|
21981
22022
|
}
|
|
21982
22023
|
}
|
|
21983
|
-
var DEFAULT_OLLAMA_BASE_URL,
|
|
22024
|
+
var DEFAULT_OLLAMA_BASE_URL, ollamaRuntimeAdapter;
|
|
21984
22025
|
var init_ollama_adapter = __esm({
|
|
21985
22026
|
"src/lib/agents/runtime/ollama-adapter.ts"() {
|
|
21986
22027
|
"use strict";
|
|
@@ -21989,10 +22030,10 @@ var init_ollama_adapter = __esm({
|
|
|
21989
22030
|
init_execution_manager();
|
|
21990
22031
|
init_claude_agent();
|
|
21991
22032
|
init_catalog2();
|
|
22033
|
+
init_ollama_model_resolver();
|
|
21992
22034
|
init_registry2();
|
|
21993
22035
|
init_ledger();
|
|
21994
22036
|
DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434";
|
|
21995
|
-
DEFAULT_OLLAMA_MODEL = "llama3.2";
|
|
21996
22037
|
ollamaRuntimeAdapter = {
|
|
21997
22038
|
metadata: getRuntimeCatalogEntry("ollama"),
|
|
21998
22039
|
async executeTask(taskId) {
|
|
@@ -24129,6 +24170,7 @@ ${step.prompt}`;
|
|
|
24129
24170
|
}
|
|
24130
24171
|
async function executeCheckpoint(workflowId, definition, state, parentTaskId, workflowRuntimeId) {
|
|
24131
24172
|
let previousOutput = "";
|
|
24173
|
+
let userAnswer = "";
|
|
24132
24174
|
for (let i = 0; i < definition.steps.length; i++) {
|
|
24133
24175
|
const step = definition.steps[i];
|
|
24134
24176
|
state.currentStepIndex = i;
|
|
@@ -24142,8 +24184,27 @@ async function executeCheckpoint(workflowId, definition, state, parentTaskId, wo
|
|
|
24142
24184
|
throw new Error(`Step "${step.name}" was denied approval`);
|
|
24143
24185
|
}
|
|
24144
24186
|
}
|
|
24145
|
-
|
|
24146
|
-
|
|
24187
|
+
userAnswer = "";
|
|
24188
|
+
if (step.requiresInput) {
|
|
24189
|
+
state.stepStates[i].status = "waiting_approval";
|
|
24190
|
+
state.status = "paused";
|
|
24191
|
+
await updateWorkflowState(workflowId, state, "paused");
|
|
24192
|
+
const question = step.inputPrompt ?? step.prompt;
|
|
24193
|
+
userAnswer = await waitForInput(workflowId, step.name, question);
|
|
24194
|
+
state.status = "running";
|
|
24195
|
+
state.stepStates[i].status = "running";
|
|
24196
|
+
await updateWorkflowState(workflowId, state, "active");
|
|
24197
|
+
}
|
|
24198
|
+
const contextParts = [];
|
|
24199
|
+
if (previousOutput) {
|
|
24200
|
+
contextParts.push(`Previous step output:
|
|
24201
|
+
${previousOutput}`);
|
|
24202
|
+
}
|
|
24203
|
+
if (userAnswer.trim()) {
|
|
24204
|
+
contextParts.push(`User-provided input:
|
|
24205
|
+
${userAnswer}`);
|
|
24206
|
+
}
|
|
24207
|
+
const contextPrompt = contextParts.length > 0 ? `${contextParts.join("\n\n---\n\n")}
|
|
24147
24208
|
|
|
24148
24209
|
---
|
|
24149
24210
|
|
|
@@ -24165,6 +24226,14 @@ ${step.prompt}` : step.prompt;
|
|
|
24165
24226
|
throw new Error(`Step "${step.name}" failed: ${result.error}`);
|
|
24166
24227
|
}
|
|
24167
24228
|
previousOutput = result.result ?? "";
|
|
24229
|
+
const hasDependents = i < definition.steps.length - 1;
|
|
24230
|
+
if (hasDependents && previousOutput.trim().length === 0) {
|
|
24231
|
+
state.stepStates[i].status = "failed";
|
|
24232
|
+
state.stepStates[i].error = "Step produced no usable output; halting dependent steps to avoid a false completion. The agent may have refused for missing context \u2014 check its output and re-run with the needed input.";
|
|
24233
|
+
throw new Error(
|
|
24234
|
+
`Step "${step.name}" produced no usable output \u2014 halting workflow to avoid cascading refusals`
|
|
24235
|
+
);
|
|
24236
|
+
}
|
|
24168
24237
|
}
|
|
24169
24238
|
}
|
|
24170
24239
|
async function executeParallel(workflowId, definition, state, parentTaskId, workflowRuntimeId) {
|
|
@@ -24694,6 +24763,33 @@ ${previousOutput.slice(0, 500)}`,
|
|
|
24694
24763
|
}).where(eq44(notifications.id, notificationId));
|
|
24695
24764
|
return false;
|
|
24696
24765
|
}
|
|
24766
|
+
async function waitForInput(workflowId, stepName, question, options) {
|
|
24767
|
+
const notificationId = crypto.randomUUID();
|
|
24768
|
+
await db.insert(notifications).values({
|
|
24769
|
+
id: notificationId,
|
|
24770
|
+
taskId: null,
|
|
24771
|
+
type: "permission_required",
|
|
24772
|
+
title: `Workflow needs input: ${stepName}`,
|
|
24773
|
+
body: question.slice(0, 500),
|
|
24774
|
+
toolName: "AskUserQuestion",
|
|
24775
|
+
toolInput: JSON.stringify({ question, options, workflowId, stepName }),
|
|
24776
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
24777
|
+
});
|
|
24778
|
+
const pollInterval = 2e3;
|
|
24779
|
+
for (; ; ) {
|
|
24780
|
+
const [notification] = await db.select().from(notifications).where(eq44(notifications.id, notificationId));
|
|
24781
|
+
if (notification?.response) {
|
|
24782
|
+
try {
|
|
24783
|
+
const parsed = JSON.parse(notification.response);
|
|
24784
|
+
const answer = parsed?.updatedInput?.answer;
|
|
24785
|
+
return typeof answer === "string" ? answer : "";
|
|
24786
|
+
} catch {
|
|
24787
|
+
return "";
|
|
24788
|
+
}
|
|
24789
|
+
}
|
|
24790
|
+
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
24791
|
+
}
|
|
24792
|
+
}
|
|
24697
24793
|
async function updateWorkflowState(workflowId, state, status) {
|
|
24698
24794
|
const [workflow] = await db.select().from(workflows).where(eq44(workflows.id, workflowId));
|
|
24699
24795
|
if (!workflow) throw new Error(`Workflow ${workflowId} not found \u2014 cannot update state`);
|
|
@@ -25803,8 +25899,8 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
25803
25899
|
import yaml12 from "js-yaml";
|
|
25804
25900
|
import semver from "semver";
|
|
25805
25901
|
function relayCoreVersion() {
|
|
25806
|
-
if (semver.valid("0.
|
|
25807
|
-
return "0.
|
|
25902
|
+
if (semver.valid("0.25.0")) {
|
|
25903
|
+
return "0.25.0";
|
|
25808
25904
|
}
|
|
25809
25905
|
try {
|
|
25810
25906
|
const root = getAppRoot(import.meta.dirname, 3);
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
2
2
|
import {
|
|
3
3
|
restoreFromSnapshot,
|
|
4
4
|
isSnapshotLocked,
|
|
5
|
+
SnapshotBusyError,
|
|
5
6
|
} from "@/lib/snapshots/snapshot-manager";
|
|
6
7
|
import { db } from "@/lib/db";
|
|
7
8
|
import { tasks } from "@/lib/db/schema";
|
|
@@ -51,6 +52,11 @@ export async function POST(
|
|
|
51
52
|
"Restore complete. Please restart the server to load the restored database.",
|
|
52
53
|
});
|
|
53
54
|
} catch (error) {
|
|
55
|
+
// Lock contention is a 409, not a server error — never conflate it with a
|
|
56
|
+
// genuine failure (issue #24).
|
|
57
|
+
if (error instanceof SnapshotBusyError) {
|
|
58
|
+
return NextResponse.json({ error: error.message }, { status: 409 });
|
|
59
|
+
}
|
|
54
60
|
return NextResponse.json(
|
|
55
61
|
{
|
|
56
62
|
error:
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
listSnapshots,
|
|
5
5
|
getSnapshotsSize,
|
|
6
6
|
isSnapshotLocked,
|
|
7
|
+
SnapshotBusyError,
|
|
7
8
|
} from "@/lib/snapshots/snapshot-manager";
|
|
8
9
|
|
|
9
10
|
/** GET /api/snapshots — list all snapshots with disk usage */
|
|
@@ -46,6 +47,11 @@ export async function POST(req: NextRequest) {
|
|
|
46
47
|
|
|
47
48
|
return NextResponse.json(snapshot, { status: 201 });
|
|
48
49
|
} catch (error) {
|
|
50
|
+
// A lock grabbed between the isSnapshotLocked() check and createSnapshot is
|
|
51
|
+
// still contention, not a server error (issue #24).
|
|
52
|
+
if (error instanceof SnapshotBusyError) {
|
|
53
|
+
return NextResponse.json({ error: error.message }, { status: 409 });
|
|
54
|
+
}
|
|
49
55
|
return NextResponse.json(
|
|
50
56
|
{ error: error instanceof Error ? error.message : "Failed to create snapshot" },
|
|
51
57
|
{ status: 500 }
|
|
@@ -2,15 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useTransition } from "react";
|
|
4
4
|
import { useRouter } from "next/navigation";
|
|
5
|
-
import {
|
|
5
|
+
import { Trash2 } from "lucide-react";
|
|
6
6
|
import { toast } from "sonner";
|
|
7
7
|
import { Button } from "@/components/ui/button";
|
|
8
|
-
import {
|
|
9
|
-
DropdownMenu,
|
|
10
|
-
DropdownMenuContent,
|
|
11
|
-
DropdownMenuItem,
|
|
12
|
-
DropdownMenuTrigger,
|
|
13
|
-
} from "@/components/ui/dropdown-menu";
|
|
14
8
|
import { ConfirmDialog } from "@/components/shared/confirm-dialog";
|
|
15
9
|
|
|
16
10
|
interface AppDetailActionsProps {
|
|
@@ -76,22 +70,15 @@ export function AppDetailActions({
|
|
|
76
70
|
|
|
77
71
|
return (
|
|
78
72
|
<>
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
className="text-destructive focus:text-destructive"
|
|
89
|
-
>
|
|
90
|
-
<Trash2 className="h-3.5 w-3.5 mr-2" aria-hidden="true" />
|
|
91
|
-
Delete app
|
|
92
|
-
</DropdownMenuItem>
|
|
93
|
-
</DropdownMenuContent>
|
|
94
|
-
</DropdownMenu>
|
|
73
|
+
<Button
|
|
74
|
+
variant="outline"
|
|
75
|
+
size="sm"
|
|
76
|
+
onClick={() => setConfirmOpen(true)}
|
|
77
|
+
className="text-destructive hover:text-destructive"
|
|
78
|
+
>
|
|
79
|
+
<Trash2 className="h-3.5 w-3.5 mr-1.5" aria-hidden="true" />
|
|
80
|
+
Delete app
|
|
81
|
+
</Button>
|
|
95
82
|
|
|
96
83
|
<ConfirmDialog
|
|
97
84
|
open={confirmOpen}
|
|
@@ -25,7 +25,14 @@ export function KitView({ model }: KitViewProps) {
|
|
|
25
25
|
{model.kpis && <KpisSlotView tiles={model.kpis} />}
|
|
26
26
|
{model.hero && <HeroSlotView slot={model.hero} />}
|
|
27
27
|
{model.secondary && model.secondary.length > 0 && (
|
|
28
|
-
<
|
|
28
|
+
<div className="space-y-3">
|
|
29
|
+
{model.secondaryLead && (
|
|
30
|
+
<p className="text-sm text-muted-foreground">
|
|
31
|
+
{model.secondaryLead}
|
|
32
|
+
</p>
|
|
33
|
+
)}
|
|
34
|
+
<SecondarySlotView slots={model.secondary} />
|
|
35
|
+
</div>
|
|
29
36
|
)}
|
|
30
37
|
{model.activity && <ActivitySlotView slot={model.activity} />}
|
|
31
38
|
{model.footer && <FooterSlotView slot={model.footer} />}
|
|
@@ -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-1 sm:flex-row sm:items-center sm:justify-between">
|
|
26
|
-
<div className="min-w-0">
|
|
25
|
+
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
|
|
26
|
+
<div className="min-w-0 flex-1">
|
|
27
27
|
<h1 className="text-xl font-semibold tracking-tight line-clamp-2" title={title}>
|
|
28
28
|
{title}
|
|
29
29
|
</h1>
|
|
@@ -33,7 +33,7 @@ export function HeaderSlotView({ slot, manifestPane }: HeaderSlotProps) {
|
|
|
33
33
|
</p>
|
|
34
34
|
)}
|
|
35
35
|
</div>
|
|
36
|
-
<div className="flex flex-wrap items-center gap-2 mt-2 sm:mt-0">
|
|
36
|
+
<div className="flex flex-wrap items-center gap-2 mt-2 sm:mt-0 sm:flex-nowrap sm:shrink-0">
|
|
37
37
|
{status && <StatusChip status={status} size="md" />}
|
|
38
38
|
{cadenceChip && (
|
|
39
39
|
<ScheduleCadenceChip
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
4
|
import { useState } from "react";
|
|
5
|
-
import {
|
|
5
|
+
import { ChevronRight, FileText } from "lucide-react";
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
7
|
import {
|
|
8
8
|
Sheet,
|
|
@@ -36,7 +36,7 @@ export function ManifestSheet({ appName, body }: ManifestSheetProps) {
|
|
|
36
36
|
>
|
|
37
37
|
<FileText className="h-3.5 w-3.5 mr-1.5" aria-hidden="true" />
|
|
38
38
|
View manifest
|
|
39
|
-
<
|
|
39
|
+
<ChevronRight className="h-3.5 w-3.5 ml-1" aria-hidden="true" />
|
|
40
40
|
</Button>
|
|
41
41
|
<Sheet open={open} onOpenChange={setOpen}>
|
|
42
42
|
<SheetContent side="right" className="w-full sm:max-w-2xl overflow-hidden flex flex-col">
|
|
@@ -13,10 +13,11 @@ import {
|
|
|
13
13
|
SheetTitle,
|
|
14
14
|
SheetTrigger,
|
|
15
15
|
} from "@/components/ui/sheet";
|
|
16
|
-
import { ChevronDown, AlertCircle } from "lucide-react";
|
|
16
|
+
import { ChevronDown, AlertCircle, Sparkles, RefreshCw } from "lucide-react";
|
|
17
17
|
import { ErrorBoundary } from "@/components/shared/error-boundary";
|
|
18
|
+
import { RunNowButton } from "@/components/apps/run-now-button";
|
|
18
19
|
import type { TaskStatus } from "@/lib/constants/task-status";
|
|
19
|
-
import type { RuntimeTaskSummary } from "@/lib/apps/view-kits/types";
|
|
20
|
+
import type { BlueprintCard, RuntimeTaskSummary } from "@/lib/apps/view-kits/types";
|
|
20
21
|
|
|
21
22
|
interface LastRunSummary {
|
|
22
23
|
id: string;
|
|
@@ -94,6 +95,94 @@ function CompactVariant({ blueprintLabel, lastRun, runCount30d }: CompactProps)
|
|
|
94
95
|
);
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
interface RunnableBlueprintCardProps {
|
|
99
|
+
card: BlueprintCard;
|
|
100
|
+
lastRun: LastRunSummary | null;
|
|
101
|
+
runCount30d: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* FEAT-5/6: the runnable blueprint card on the app home. Renders the
|
|
106
|
+
* blueprint's name + one-line description + last-run status + a Run action.
|
|
107
|
+
* The "Start here" card is highlighted. Row-insert blueprints label their
|
|
108
|
+
* automatic trigger instead of offering a manual Run that fights the contract.
|
|
109
|
+
*/
|
|
110
|
+
export function RunnableBlueprintCard({
|
|
111
|
+
card,
|
|
112
|
+
lastRun,
|
|
113
|
+
runCount30d,
|
|
114
|
+
}: RunnableBlueprintCardProps) {
|
|
115
|
+
const isRowInsert = card.trigger?.kind === "row-insert";
|
|
116
|
+
return (
|
|
117
|
+
<Card
|
|
118
|
+
className={
|
|
119
|
+
card.isPrimary
|
|
120
|
+
? "surface-card border-primary ring-1 ring-primary/30"
|
|
121
|
+
: "surface-card"
|
|
122
|
+
}
|
|
123
|
+
>
|
|
124
|
+
<CardHeader className="pb-2 space-y-1.5">
|
|
125
|
+
{card.isPrimary && (
|
|
126
|
+
<Badge className="w-fit gap-1">
|
|
127
|
+
<Sparkles className="h-3 w-3" />
|
|
128
|
+
Start here
|
|
129
|
+
</Badge>
|
|
130
|
+
)}
|
|
131
|
+
<CardTitle className="text-sm font-medium">{card.name}</CardTitle>
|
|
132
|
+
{card.description && (
|
|
133
|
+
<p className="text-xs text-muted-foreground line-clamp-2">
|
|
134
|
+
{card.description}
|
|
135
|
+
</p>
|
|
136
|
+
)}
|
|
137
|
+
</CardHeader>
|
|
138
|
+
<CardContent className="space-y-3">
|
|
139
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
140
|
+
{lastRun ? (
|
|
141
|
+
<>
|
|
142
|
+
<Badge variant={statusVariant[lastRun.status]}>
|
|
143
|
+
{lastRun.status}
|
|
144
|
+
</Badge>
|
|
145
|
+
<span className="text-xs text-muted-foreground">
|
|
146
|
+
{formatAgo(lastRun.createdAt)}
|
|
147
|
+
</span>
|
|
148
|
+
</>
|
|
149
|
+
) : (
|
|
150
|
+
<span className="text-xs text-muted-foreground">never run</span>
|
|
151
|
+
)}
|
|
152
|
+
<span className="text-xs text-muted-foreground">
|
|
153
|
+
· {runCount30d} {runCount30d === 1 ? "run" : "runs"} · 30d
|
|
154
|
+
</span>
|
|
155
|
+
</div>
|
|
156
|
+
{isRowInsert ? (
|
|
157
|
+
<p className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
158
|
+
<RefreshCw className="h-3.5 w-3.5" />
|
|
159
|
+
Runs on its own when you add a row to the{" "}
|
|
160
|
+
{card.trigger?.tableName ?? "linked"} table
|
|
161
|
+
</p>
|
|
162
|
+
) : (
|
|
163
|
+
<div className="space-y-1.5">
|
|
164
|
+
<RunNowButton
|
|
165
|
+
blueprintId={card.id}
|
|
166
|
+
variables={card.variables}
|
|
167
|
+
label="Run"
|
|
168
|
+
/>
|
|
169
|
+
{/* FEAT-5.2: on the "Start here" card, tell a first-time user what
|
|
170
|
+
Run does. Only the primary card carries this hint, so the grid
|
|
171
|
+
stays scannable (progressive disclosure). */}
|
|
172
|
+
{card.isPrimary && (
|
|
173
|
+
<p className="text-xs text-muted-foreground">
|
|
174
|
+
{card.variables.length > 0
|
|
175
|
+
? "Run asks a few questions, then starts a workflow you can watch."
|
|
176
|
+
: "Run starts a workflow you can watch as it works."}
|
|
177
|
+
</p>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
</CardContent>
|
|
182
|
+
</Card>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
97
186
|
function HeroVariant({ task, previousRuns }: HeroProps) {
|
|
98
187
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
99
188
|
|
|
@@ -15,7 +15,8 @@ export function LedgerHeroPanel({ series, categories, period }: LedgerHeroPanelP
|
|
|
15
15
|
if (series.length === 0 && categories.length === 0) {
|
|
16
16
|
return (
|
|
17
17
|
<div className="surface-card rounded-xl p-12 text-center text-muted-foreground border">
|
|
18
|
-
No data yet.
|
|
18
|
+
No data yet. Click <strong>Run now</strong> to start this app's workflow.
|
|
19
|
+
Results show up here once it finishes.
|
|
19
20
|
</div>
|
|
20
21
|
);
|
|
21
22
|
}
|
|
@@ -16,7 +16,7 @@ const EXAMPLE_PROMPTS: { label: string; prompt: string }[] = [
|
|
|
16
16
|
{
|
|
17
17
|
label: "Build me a reading log…",
|
|
18
18
|
prompt:
|
|
19
|
-
"Build me a reading log. Track each book with title, author, date finished, and a 1
|
|
19
|
+
"Build me a reading log. Track each book with title, author, date finished, and a rating from 1 to 5. Every Friday at 5pm, summarize what I read this week.",
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
label: "Build me an expense tracker for my contractors…",
|
|
@@ -97,10 +97,10 @@ export function ChatEmptyState({
|
|
|
97
97
|
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
|
|
98
98
|
<Bot className="h-6 w-6 text-primary" />
|
|
99
99
|
</div>
|
|
100
|
-
<h2 className="text-lg font-semibold">Describe an app.
|
|
100
|
+
<h2 className="text-lg font-semibold">Describe an app. Relay builds it.</h2>
|
|
101
101
|
<p className="text-sm text-muted-foreground text-center max-w-md">
|
|
102
|
-
|
|
103
|
-
Or ask anything about your workspace.
|
|
102
|
+
One prompt builds the whole app: profiles, blueprints, tables, and
|
|
103
|
+
schedules. Or ask anything about your workspace.
|
|
104
104
|
</p>
|
|
105
105
|
</div>
|
|
106
106
|
|
|
@@ -9,17 +9,17 @@ const pillars = [
|
|
|
9
9
|
{
|
|
10
10
|
icon: Sparkles,
|
|
11
11
|
title: "Apps from a sentence",
|
|
12
|
-
description: "
|
|
12
|
+
description: "Tell Relay what you do each week. It builds a running app for you. No code needed.",
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
icon: Shield,
|
|
16
16
|
title: "Your rules, enforced",
|
|
17
|
-
description: "
|
|
17
|
+
description: "Agents follow your rules on every task. You get a full record of what they did.",
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
icon: Wallet,
|
|
21
21
|
title: "Know what you spend",
|
|
22
|
-
description: "
|
|
22
|
+
description: "See the cost of each task. Set a budget so you never get a surprise bill.",
|
|
23
23
|
},
|
|
24
24
|
];
|
|
25
25
|
|
|
@@ -44,7 +44,7 @@ export function WelcomeLanding({ starters = [] }: WelcomeLandingProps) {
|
|
|
44
44
|
Welcome
|
|
45
45
|
</h1>
|
|
46
46
|
<p className="text-base text-muted-foreground mb-8 max-w-lg">
|
|
47
|
-
|
|
47
|
+
Relay is your AI business operating system. Describe an app. Relay builds it and runs it on your rules, your budget, and your data.
|
|
48
48
|
</p>
|
|
49
49
|
|
|
50
50
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 w-full mb-8">
|
|
@@ -42,7 +42,7 @@ const STATIC_OPTIONS: readonly PreferenceOption[] = [
|
|
|
42
42
|
id: "quality",
|
|
43
43
|
label: "Best quality",
|
|
44
44
|
capabilityNote:
|
|
45
|
-
"
|
|
45
|
+
"Our smartest model (Opus). It can use every tool and read files. Costs the most.",
|
|
46
46
|
recommendedModel: "opus",
|
|
47
47
|
icon: Sparkles,
|
|
48
48
|
},
|
|
@@ -50,7 +50,7 @@ const STATIC_OPTIONS: readonly PreferenceOption[] = [
|
|
|
50
50
|
id: "balanced",
|
|
51
51
|
label: "Balanced (recommended)",
|
|
52
52
|
capabilityNote:
|
|
53
|
-
"
|
|
53
|
+
"Great quality for less (Sonnet). It can do everything Opus can.",
|
|
54
54
|
recommendedModel: "sonnet",
|
|
55
55
|
icon: Scale,
|
|
56
56
|
},
|
|
@@ -58,7 +58,7 @@ const STATIC_OPTIONS: readonly PreferenceOption[] = [
|
|
|
58
58
|
id: "cost",
|
|
59
59
|
label: "Lowest cost",
|
|
60
60
|
capabilityNote:
|
|
61
|
-
"
|
|
61
|
+
"Our fastest, cheapest model (Haiku). It uses the same tools as Sonnet.",
|
|
62
62
|
recommendedModel: "haiku",
|
|
63
63
|
icon: DollarSign,
|
|
64
64
|
},
|
|
@@ -66,7 +66,7 @@ const STATIC_OPTIONS: readonly PreferenceOption[] = [
|
|
|
66
66
|
id: "privacy",
|
|
67
67
|
label: "Best privacy (local only)",
|
|
68
68
|
capabilityNote:
|
|
69
|
-
"Runs
|
|
69
|
+
"Runs on your own computer with Ollama. Nothing leaves your machine. It skips cloud tools and holds less at once.",
|
|
70
70
|
recommendedModel: "", // resolved against the Ollama discovery list at submit time
|
|
71
71
|
icon: Lock,
|
|
72
72
|
},
|
|
@@ -168,7 +168,7 @@ export function RuntimePreferenceModal({
|
|
|
168
168
|
return {
|
|
169
169
|
modelId: BALANCED_FALLBACK_MODEL,
|
|
170
170
|
fallbackNote:
|
|
171
|
-
"
|
|
171
|
+
"We could not find a local model. We will use the balanced model for now. To use your own, point Relay at Ollama in Settings.",
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
174
|
return {
|
|
@@ -248,8 +248,8 @@ export function RuntimePreferenceModal({
|
|
|
248
248
|
<DialogHeader>
|
|
249
249
|
<DialogTitle>Pick your default chat model</DialogTitle>
|
|
250
250
|
<DialogDescription>
|
|
251
|
-
|
|
252
|
-
|
|
251
|
+
Tell us what matters most and we will set a good default. You can
|
|
252
|
+
change it anytime in Settings.
|
|
253
253
|
</DialogDescription>
|
|
254
254
|
</DialogHeader>
|
|
255
255
|
|
|
@@ -311,7 +311,7 @@ export function RuntimePreferenceModal({
|
|
|
311
311
|
onClick={handleSkip}
|
|
312
312
|
disabled={submitting}
|
|
313
313
|
>
|
|
314
|
-
Skip
|
|
314
|
+
Skip, use default
|
|
315
315
|
</Button>
|
|
316
316
|
<Button onClick={handleConfirm} disabled={submitting}>
|
|
317
317
|
{submitting ? "Saving…" : "Continue"}
|