orionfold-relay 0.24.1 → 0.25.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/dist/cli.js +96 -16
- package/package.json +1 -1
- package/src/app/api/chat/conversations/route.ts +4 -1
- 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/chat-session-provider.tsx +10 -1
- package/src/components/settings/license-section.tsx +5 -0
- package/src/components/workflows/shared/workflow-header.tsx +114 -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/tools/table-tools.ts +5 -2
- package/src/lib/environment/workspace-context.ts +6 -0
- package/src/lib/http/self-base-url.ts +42 -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 +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/src/lib/tables/trigger-evaluator.ts +6 -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({
|
|
@@ -4614,6 +4614,19 @@ var init_history = __esm({
|
|
|
4614
4614
|
}
|
|
4615
4615
|
});
|
|
4616
4616
|
|
|
4617
|
+
// src/lib/http/self-base-url.ts
|
|
4618
|
+
function getSelfBaseUrl() {
|
|
4619
|
+
const explicit = process.env.RELAY_SELF_BASE_URL || process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL;
|
|
4620
|
+
if (explicit) return explicit;
|
|
4621
|
+
const port = process.env.PORT || "3000";
|
|
4622
|
+
return `http://127.0.0.1:${port}`;
|
|
4623
|
+
}
|
|
4624
|
+
var init_self_base_url = __esm({
|
|
4625
|
+
"src/lib/http/self-base-url.ts"() {
|
|
4626
|
+
"use strict";
|
|
4627
|
+
}
|
|
4628
|
+
});
|
|
4629
|
+
|
|
4617
4630
|
// src/lib/workflows/delay.ts
|
|
4618
4631
|
function parseDuration(input) {
|
|
4619
4632
|
const match = input.match(DURATION_PATTERN);
|
|
@@ -7135,7 +7148,13 @@ function getWorkspaceContext() {
|
|
|
7135
7148
|
gitBranch = execFileSync2("git", ["branch", "--show-current"], {
|
|
7136
7149
|
cwd,
|
|
7137
7150
|
encoding: "utf-8",
|
|
7138
|
-
timeout: 3e3
|
|
7151
|
+
timeout: 3e3,
|
|
7152
|
+
// Route git's stderr to a pipe (captured on the thrown error, then
|
|
7153
|
+
// discarded) instead of inheriting the console. Without this, a
|
|
7154
|
+
// non-git cwd leaks a raw `fatal: not a git repository` to a
|
|
7155
|
+
// customer's first-run log before the catch swallows the exit code.
|
|
7156
|
+
// Mirrors src/lib/instance/git-ops.ts run().
|
|
7157
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
7139
7158
|
}).trim() || null;
|
|
7140
7159
|
} catch {
|
|
7141
7160
|
}
|
|
@@ -12134,7 +12153,7 @@ async function listPendingApprovalPayloads(limit = 20) {
|
|
|
12134
12153
|
const isBatchProposal = row.type === "context_proposal_batch";
|
|
12135
12154
|
const parsedInput = parseNotificationToolInput(row.toolInput);
|
|
12136
12155
|
let effectiveWorkflowId = row.workflowId;
|
|
12137
|
-
if (!row.taskId && row.toolName === "WorkflowCheckpoint" && row.toolInput) {
|
|
12156
|
+
if (!row.taskId && (row.toolName === "WorkflowCheckpoint" || row.toolName === "AskUserQuestion") && row.toolInput) {
|
|
12138
12157
|
try {
|
|
12139
12158
|
const parsed = typeof row.toolInput === "string" ? JSON.parse(row.toolInput) : row.toolInput;
|
|
12140
12159
|
effectiveWorkflowId = parsed.workflowId ?? null;
|
|
@@ -12963,7 +12982,7 @@ var init_registry6 = __esm({
|
|
|
12963
12982
|
init_registry5();
|
|
12964
12983
|
init_installer();
|
|
12965
12984
|
init_schedule_spec();
|
|
12966
|
-
SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.
|
|
12985
|
+
SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.24"]);
|
|
12967
12986
|
pluginCache = null;
|
|
12968
12987
|
lastLoadedPluginIds = /* @__PURE__ */ new Set();
|
|
12969
12988
|
PluginTableSchema = z16.object({
|
|
@@ -15275,7 +15294,7 @@ Guidelines for schema inference:
|
|
|
15275
15294
|
try {
|
|
15276
15295
|
const table = await getTable(args.tableId);
|
|
15277
15296
|
if (!table) return err("Table not found");
|
|
15278
|
-
const baseUrl =
|
|
15297
|
+
const baseUrl = getSelfBaseUrl();
|
|
15279
15298
|
return ok({
|
|
15280
15299
|
url: `${baseUrl}/api/tables/${args.tableId}/export?format=${args.format}`,
|
|
15281
15300
|
table: table.name,
|
|
@@ -15721,7 +15740,7 @@ Guidelines for schema inference:
|
|
|
15721
15740
|
];
|
|
15722
15741
|
}
|
|
15723
15742
|
function getBaseUrl() {
|
|
15724
|
-
return
|
|
15743
|
+
return getSelfBaseUrl();
|
|
15725
15744
|
}
|
|
15726
15745
|
var init_table_tools = __esm({
|
|
15727
15746
|
"src/lib/chat/tools/table-tools.ts"() {
|
|
@@ -15732,6 +15751,7 @@ var init_table_tools = __esm({
|
|
|
15732
15751
|
init_history();
|
|
15733
15752
|
init_import();
|
|
15734
15753
|
init_enrichment();
|
|
15754
|
+
init_self_base_url();
|
|
15735
15755
|
}
|
|
15736
15756
|
});
|
|
15737
15757
|
|
|
@@ -24164,6 +24184,7 @@ ${step.prompt}`;
|
|
|
24164
24184
|
}
|
|
24165
24185
|
async function executeCheckpoint(workflowId, definition, state, parentTaskId, workflowRuntimeId) {
|
|
24166
24186
|
let previousOutput = "";
|
|
24187
|
+
let userAnswer = "";
|
|
24167
24188
|
for (let i = 0; i < definition.steps.length; i++) {
|
|
24168
24189
|
const step = definition.steps[i];
|
|
24169
24190
|
state.currentStepIndex = i;
|
|
@@ -24177,8 +24198,27 @@ async function executeCheckpoint(workflowId, definition, state, parentTaskId, wo
|
|
|
24177
24198
|
throw new Error(`Step "${step.name}" was denied approval`);
|
|
24178
24199
|
}
|
|
24179
24200
|
}
|
|
24180
|
-
|
|
24181
|
-
|
|
24201
|
+
userAnswer = "";
|
|
24202
|
+
if (step.requiresInput) {
|
|
24203
|
+
state.stepStates[i].status = "waiting_approval";
|
|
24204
|
+
state.status = "paused";
|
|
24205
|
+
await updateWorkflowState(workflowId, state, "paused");
|
|
24206
|
+
const question = step.inputPrompt ?? step.prompt;
|
|
24207
|
+
userAnswer = await waitForInput(workflowId, step.name, question);
|
|
24208
|
+
state.status = "running";
|
|
24209
|
+
state.stepStates[i].status = "running";
|
|
24210
|
+
await updateWorkflowState(workflowId, state, "active");
|
|
24211
|
+
}
|
|
24212
|
+
const contextParts = [];
|
|
24213
|
+
if (previousOutput) {
|
|
24214
|
+
contextParts.push(`Previous step output:
|
|
24215
|
+
${previousOutput}`);
|
|
24216
|
+
}
|
|
24217
|
+
if (userAnswer.trim()) {
|
|
24218
|
+
contextParts.push(`User-provided input:
|
|
24219
|
+
${userAnswer}`);
|
|
24220
|
+
}
|
|
24221
|
+
const contextPrompt = contextParts.length > 0 ? `${contextParts.join("\n\n---\n\n")}
|
|
24182
24222
|
|
|
24183
24223
|
---
|
|
24184
24224
|
|
|
@@ -24200,6 +24240,14 @@ ${step.prompt}` : step.prompt;
|
|
|
24200
24240
|
throw new Error(`Step "${step.name}" failed: ${result.error}`);
|
|
24201
24241
|
}
|
|
24202
24242
|
previousOutput = result.result ?? "";
|
|
24243
|
+
const hasDependents = i < definition.steps.length - 1;
|
|
24244
|
+
if (hasDependents && previousOutput.trim().length === 0) {
|
|
24245
|
+
state.stepStates[i].status = "failed";
|
|
24246
|
+
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.";
|
|
24247
|
+
throw new Error(
|
|
24248
|
+
`Step "${step.name}" produced no usable output \u2014 halting workflow to avoid cascading refusals`
|
|
24249
|
+
);
|
|
24250
|
+
}
|
|
24203
24251
|
}
|
|
24204
24252
|
}
|
|
24205
24253
|
async function executeParallel(workflowId, definition, state, parentTaskId, workflowRuntimeId) {
|
|
@@ -24729,6 +24777,33 @@ ${previousOutput.slice(0, 500)}`,
|
|
|
24729
24777
|
}).where(eq44(notifications.id, notificationId));
|
|
24730
24778
|
return false;
|
|
24731
24779
|
}
|
|
24780
|
+
async function waitForInput(workflowId, stepName, question, options) {
|
|
24781
|
+
const notificationId = crypto.randomUUID();
|
|
24782
|
+
await db.insert(notifications).values({
|
|
24783
|
+
id: notificationId,
|
|
24784
|
+
taskId: null,
|
|
24785
|
+
type: "permission_required",
|
|
24786
|
+
title: `Workflow needs input: ${stepName}`,
|
|
24787
|
+
body: question.slice(0, 500),
|
|
24788
|
+
toolName: "AskUserQuestion",
|
|
24789
|
+
toolInput: JSON.stringify({ question, options, workflowId, stepName }),
|
|
24790
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
24791
|
+
});
|
|
24792
|
+
const pollInterval = 2e3;
|
|
24793
|
+
for (; ; ) {
|
|
24794
|
+
const [notification] = await db.select().from(notifications).where(eq44(notifications.id, notificationId));
|
|
24795
|
+
if (notification?.response) {
|
|
24796
|
+
try {
|
|
24797
|
+
const parsed = JSON.parse(notification.response);
|
|
24798
|
+
const answer = parsed?.updatedInput?.answer;
|
|
24799
|
+
return typeof answer === "string" ? answer : "";
|
|
24800
|
+
} catch {
|
|
24801
|
+
return "";
|
|
24802
|
+
}
|
|
24803
|
+
}
|
|
24804
|
+
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
24805
|
+
}
|
|
24806
|
+
}
|
|
24732
24807
|
async function updateWorkflowState(workflowId, state, status) {
|
|
24733
24808
|
const [workflow] = await db.select().from(workflows).where(eq44(workflows.id, workflowId));
|
|
24734
24809
|
if (!workflow) throw new Error(`Workflow ${workflowId} not found \u2014 cannot update state`);
|
|
@@ -25318,13 +25393,15 @@ async function fireAction(actionType, config, rowData, meta) {
|
|
|
25318
25393
|
Trigger data: ${JSON.stringify(rowData, null, 2)}` : `Triggered by table row change.
|
|
25319
25394
|
|
|
25320
25395
|
Data: ${JSON.stringify(rowData, null, 2)}`;
|
|
25321
|
-
await fetch(`${
|
|
25396
|
+
await fetch(`${getSelfBaseUrl()}/api/tasks`, {
|
|
25322
25397
|
method: "POST",
|
|
25323
25398
|
headers: { "Content-Type": "application/json" },
|
|
25324
25399
|
body: JSON.stringify({
|
|
25325
25400
|
title: config.title ?? "Triggered Task",
|
|
25326
25401
|
description,
|
|
25327
|
-
|
|
25402
|
+
// createTaskSchema wants `string | undefined`, not null — sending null
|
|
25403
|
+
// 400s the self-call and the task is never created. Omit when absent.
|
|
25404
|
+
...config.projectId ? { projectId: config.projectId } : {}
|
|
25328
25405
|
})
|
|
25329
25406
|
});
|
|
25330
25407
|
return;
|
|
@@ -25350,7 +25427,7 @@ Data: ${JSON.stringify(rowData, null, 2)}`;
|
|
|
25350
25427
|
return;
|
|
25351
25428
|
}
|
|
25352
25429
|
if (actionType === "run_workflow" && config.workflowId) {
|
|
25353
|
-
await fetch(`${
|
|
25430
|
+
await fetch(`${getSelfBaseUrl()}/api/workflows/${config.workflowId}/execute`, {
|
|
25354
25431
|
method: "POST",
|
|
25355
25432
|
headers: { "Content-Type": "application/json" },
|
|
25356
25433
|
body: JSON.stringify({
|
|
@@ -25368,14 +25445,12 @@ function deriveAppIdFromBlueprintId(blueprintId) {
|
|
|
25368
25445
|
if (idx <= 0) return null;
|
|
25369
25446
|
return blueprintId.slice(0, idx);
|
|
25370
25447
|
}
|
|
25371
|
-
function getBaseUrl2() {
|
|
25372
|
-
return process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
|
25373
|
-
}
|
|
25374
25448
|
var init_trigger_evaluator = __esm({
|
|
25375
25449
|
"src/lib/tables/trigger-evaluator.ts"() {
|
|
25376
25450
|
"use strict";
|
|
25377
25451
|
init_db();
|
|
25378
25452
|
init_schema();
|
|
25453
|
+
init_self_base_url();
|
|
25379
25454
|
}
|
|
25380
25455
|
});
|
|
25381
25456
|
|
|
@@ -25838,8 +25913,8 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
25838
25913
|
import yaml12 from "js-yaml";
|
|
25839
25914
|
import semver from "semver";
|
|
25840
25915
|
function relayCoreVersion() {
|
|
25841
|
-
if (semver.valid("0.
|
|
25842
|
-
return "0.
|
|
25916
|
+
if (semver.valid("0.25.1")) {
|
|
25917
|
+
return "0.25.1";
|
|
25843
25918
|
}
|
|
25844
25919
|
try {
|
|
25845
25920
|
const root = getAppRoot(import.meta.dirname, 3);
|
|
@@ -27487,6 +27562,11 @@ Falling back to development mode for this run \u2014 Relay still works, but slow
|
|
|
27487
27562
|
RELAY_DATA_DIR: DATA_DIR,
|
|
27488
27563
|
RELAY_LAUNCH_CWD: launchCwd2,
|
|
27489
27564
|
PORT: String(actualPort),
|
|
27565
|
+
// Origin for Relay's internal loopback self-calls (trigger dispatch,
|
|
27566
|
+
// compose table tools). The server always listens on loopback even when
|
|
27567
|
+
// bound to a non-loopback host, so self-calls target 127.0.0.1 + the real
|
|
27568
|
+
// port — never :3000, never the LAN IP. Fixes issue #29.
|
|
27569
|
+
RELAY_SELF_BASE_URL: buildSidecarUrl(actualPort, "127.0.0.1"),
|
|
27490
27570
|
...opts.safeMode ? { RELAY_SAFE_MODE: "true" } : {},
|
|
27491
27571
|
// In dev mode, Next blocks cross-origin /_next/* dev-asset requests from
|
|
27492
27572
|
// the LAN client's IP, breaking the app over the network (issue #13).
|
package/package.json
CHANGED
|
@@ -51,7 +51,10 @@ export async function POST(req: NextRequest) {
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
// "ollama" is first-class: getRuntimeForModel() returns it for local models,
|
|
55
|
+
// engine.ts routes it to sendOllamaMessage. Omitting it here 400'd the
|
|
56
|
+
// "Best privacy (local only)" tier's first chat/compose on a fresh install (#30).
|
|
57
|
+
const validRuntimes = ["claude-code", "openai-codex-app-server", "ollama"];
|
|
55
58
|
if (!validRuntimes.includes(runtimeId)) {
|
|
56
59
|
return NextResponse.json(
|
|
57
60
|
{ error: `Invalid runtimeId. Must be one of: ${validRuntimes.join(", ")}` },
|
|
@@ -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
|
}
|
|
@@ -297,7 +297,16 @@ export function ChatSessionProvider({ children }: { children: ReactNode }) {
|
|
|
297
297
|
...(opts?.title ? { title: opts.title } : {}),
|
|
298
298
|
}),
|
|
299
299
|
});
|
|
300
|
-
if (!res.ok)
|
|
300
|
+
if (!res.ok) {
|
|
301
|
+
// Never swallow a create failure silently again (#30): a rejected
|
|
302
|
+
// runtime/model used to clear the composer with no signal at all.
|
|
303
|
+
const detail = await res
|
|
304
|
+
.json()
|
|
305
|
+
.then((b) => (b as { error?: string })?.error)
|
|
306
|
+
.catch(() => null);
|
|
307
|
+
toast.error(detail || "Couldn't start a chat with this model. Try another model.");
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
301
310
|
const conversation = (await res.json()) as ConversationRow;
|
|
302
311
|
setConversations((prev) => [conversation, ...prev]);
|
|
303
312
|
// Set empty messages BEFORE activating so the conversation has an
|
|
@@ -53,10 +53,15 @@ function daysUntil(iso: string): number {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
function formatDate(iso: string): string {
|
|
56
|
+
// License dates are calendar dates stored as midnight-UTC instants
|
|
57
|
+
// (e.g. "2026-07-03T00:00:00Z"). Format in UTC so a customer in a
|
|
58
|
+
// behind-UTC timezone doesn't see the date shift one day earlier than
|
|
59
|
+
// the license file and their purchase email say.
|
|
56
60
|
return new Date(iso).toLocaleDateString(undefined, {
|
|
57
61
|
year: "numeric",
|
|
58
62
|
month: "short",
|
|
59
63
|
day: "numeric",
|
|
64
|
+
timeZone: "UTC",
|
|
60
65
|
});
|
|
61
66
|
}
|
|
62
67
|
|
|
@@ -11,11 +11,74 @@ import {
|
|
|
11
11
|
RotateCcw,
|
|
12
12
|
Trash2,
|
|
13
13
|
FolderKanban,
|
|
14
|
+
ArrowRight,
|
|
15
|
+
Inbox as InboxIcon,
|
|
16
|
+
Clock3,
|
|
17
|
+
Loader2,
|
|
14
18
|
} from "lucide-react";
|
|
15
19
|
import { workflowStatusVariant, patternLabels } from "@/lib/constants/status-colors";
|
|
16
20
|
import { IconCircle, getWorkflowIconFromName } from "@/lib/constants/card-icons";
|
|
17
21
|
import type { WorkflowStatusResponse } from "@/lib/workflows/types";
|
|
18
22
|
|
|
23
|
+
/**
|
|
24
|
+
* FEAT-7/8: a status-aware signpost telling the user what to do next. After a
|
|
25
|
+
* blueprint instantiates a draft, "Execute" is not obvious — so `draft` gets a
|
|
26
|
+
* "click Execute to start" nudge. Once running/paused, activity lives on other
|
|
27
|
+
* surfaces (Inbox for approvals, the steps below for progress) — so those
|
|
28
|
+
* statuses point there. A paused HITL run must read as "waiting for you", not
|
|
29
|
+
* "stuck".
|
|
30
|
+
*
|
|
31
|
+
* Distinguishing the two kinds of `paused`: a delay pause carries `resumeAt`
|
|
32
|
+
* (it resumes on its own); a HITL pause (BUG-3) sets a step to
|
|
33
|
+
* `waiting_approval` and has no resumeAt (it waits for your answer → Inbox).
|
|
34
|
+
*/
|
|
35
|
+
export type Signpost = {
|
|
36
|
+
tone: "info" | "wait";
|
|
37
|
+
icon: "arrow" | "inbox" | "clock" | "spinner";
|
|
38
|
+
href?: string;
|
|
39
|
+
text: string;
|
|
40
|
+
} | null;
|
|
41
|
+
|
|
42
|
+
export function computeSignpost(data: WorkflowStatusResponse): Signpost {
|
|
43
|
+
const status = data.status;
|
|
44
|
+
if (status === "draft") {
|
|
45
|
+
return {
|
|
46
|
+
tone: "info",
|
|
47
|
+
icon: "arrow",
|
|
48
|
+
text: "Ready to go. Click Execute to start this workflow.",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// A live workflow is `active` at the top level (`running` is a step/run-state
|
|
52
|
+
// value; the loop arm may report it too — accept both).
|
|
53
|
+
if (status === "active" || status === "running") {
|
|
54
|
+
return {
|
|
55
|
+
tone: "info",
|
|
56
|
+
icon: "spinner",
|
|
57
|
+
text: "Working now. Watch the steps below as it goes.",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (status === "paused") {
|
|
61
|
+
// A delay pause resumes on its own; the non-loop arm carries resumeAt.
|
|
62
|
+
const resumeAt =
|
|
63
|
+
"resumeAt" in data ? (data.resumeAt as number | null) : null;
|
|
64
|
+
if (resumeAt != null) {
|
|
65
|
+
return {
|
|
66
|
+
tone: "wait",
|
|
67
|
+
icon: "clock",
|
|
68
|
+
text: "Paused for a scheduled step. It resumes on its own.",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Otherwise it is waiting for your answer (HITL checkpoint → Inbox).
|
|
72
|
+
return {
|
|
73
|
+
tone: "wait",
|
|
74
|
+
icon: "inbox",
|
|
75
|
+
href: "/inbox",
|
|
76
|
+
text: "Waiting for your approval. Answer it in your Inbox.",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
19
82
|
/**
|
|
20
83
|
* Pattern-agnostic header card for the workflow detail page. Renders the
|
|
21
84
|
* workflow name, pattern label, project/run badges, status badge, and the
|
|
@@ -136,6 +199,57 @@ export function WorkflowHeader({
|
|
|
136
199
|
)}
|
|
137
200
|
</div>
|
|
138
201
|
</div>
|
|
202
|
+
<SignpostBanner signpost={computeSignpost(data)} />
|
|
139
203
|
</CardHeader>
|
|
140
204
|
);
|
|
141
205
|
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* FEAT-7/8: renders the status-aware "what to do next" banner. `wait` tone
|
|
209
|
+
* (waiting on the user) is emphasized; `info` tone is quiet. When a signpost
|
|
210
|
+
* has an href, the whole banner is a link (e.g. paused HITL → /inbox).
|
|
211
|
+
*/
|
|
212
|
+
function SignpostBanner({ signpost }: { signpost: Signpost }) {
|
|
213
|
+
const router = useRouter();
|
|
214
|
+
if (!signpost) return null;
|
|
215
|
+
|
|
216
|
+
const Icon =
|
|
217
|
+
signpost.icon === "inbox"
|
|
218
|
+
? InboxIcon
|
|
219
|
+
: signpost.icon === "clock"
|
|
220
|
+
? Clock3
|
|
221
|
+
: signpost.icon === "spinner"
|
|
222
|
+
? Loader2
|
|
223
|
+
: ArrowRight;
|
|
224
|
+
|
|
225
|
+
const tone =
|
|
226
|
+
signpost.tone === "wait"
|
|
227
|
+
? "border-status-warning/40 bg-status-warning/10 text-foreground"
|
|
228
|
+
: "border-border bg-muted/40 text-muted-foreground";
|
|
229
|
+
|
|
230
|
+
const body = (
|
|
231
|
+
<div
|
|
232
|
+
className={`mt-3 flex items-center gap-2 rounded-lg border px-3 py-2 text-sm ${tone} ${
|
|
233
|
+
signpost.href ? "cursor-pointer hover:bg-accent transition-colors" : ""
|
|
234
|
+
}`}
|
|
235
|
+
>
|
|
236
|
+
<Icon
|
|
237
|
+
className={`h-4 w-4 shrink-0 ${
|
|
238
|
+
signpost.icon === "spinner" ? "animate-spin" : ""
|
|
239
|
+
}`}
|
|
240
|
+
/>
|
|
241
|
+
<span>{signpost.text}</span>
|
|
242
|
+
{signpost.href && <ArrowRight className="h-3.5 w-3.5 ml-auto shrink-0" />}
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
if (signpost.href) {
|
|
247
|
+
const href = signpost.href;
|
|
248
|
+
return (
|
|
249
|
+
<div role="link" tabIndex={0} onClick={() => router.push(href)}>
|
|
250
|
+
{body}
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return body;
|
|
255
|
+
}
|