@tokagent/tokagentos 2.0.29 → 2.0.31
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/package.json +1 -1
- package/scaffold-patches/packages/agent/src/api/plugin-routes.ts +1889 -0
- package/scaffold-patches/packages/agent/src/api/server.ts +4509 -0
- package/scaffold-patches/packages/agent/src/api/trigger-routes.ts +942 -0
- package/scaffold-patches/packages/agent/src/runtime/core-plugins.ts +4 -0
- package/scaffold-patches/packages/agent/src/triggers/runtime.ts +955 -0
- package/scaffold-patches/packages/app-core/src/api/client-agent.ts +2755 -0
- package/scaffold-patches/packages/app-core/src/components/pages/AutomationsView.tsx +446 -26
- package/scaffold-patches/packages/app-core/src/components/pages/SettingsView.tsx +155 -0
- package/scaffold-patches/packages/shared/src/onboarding-presets.characters.ts +16 -16
- package/templates/fullstack-app/.env.example +5 -1
- package/templates/fullstack-app/package.json +9 -5
- package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/routes/messages-proxy-routes.ts +114 -3
- package/templates/fullstack-app/plugins/plugin-web-fetch/build.ts +35 -0
- package/templates/fullstack-app/plugins/plugin-web-fetch/package.json +37 -0
- package/templates/fullstack-app/plugins/plugin-web-fetch/src/index.ts +471 -0
- package/templates/fullstack-app/plugins/plugin-web-fetch/tsconfig.json +20 -0
- package/templates/fullstack-app/scripts/ensure-plugin-builds.mjs +1 -0
- package/templates/fullstack-app/scripts/verify-llm-plugins.mjs +122 -0
- package/templates-manifest.json +1 -1
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from "@elizaos/ui";
|
|
24
24
|
import {
|
|
25
25
|
Calendar,
|
|
26
|
+
Check,
|
|
26
27
|
CheckCircle2,
|
|
27
28
|
ChevronDown,
|
|
28
29
|
ChevronRight,
|
|
@@ -3045,6 +3046,356 @@ function WorkflowDataFlowStrip({
|
|
|
3045
3046
|
);
|
|
3046
3047
|
}
|
|
3047
3048
|
|
|
3049
|
+
/**
|
|
3050
|
+
* Single row in the Run history list. Click to expand → lazy-fetches the
|
|
3051
|
+
* agent output for this run via /api/triggers/:triggerId/runs/:runId/output
|
|
3052
|
+
* and renders it inline. Caches per-row, so toggling collapse/expand
|
|
3053
|
+
* doesn't re-fetch. Errors are rendered in place, never crash the list.
|
|
3054
|
+
*/
|
|
3055
|
+
function TriggerRunRow({
|
|
3056
|
+
run,
|
|
3057
|
+
triggerId,
|
|
3058
|
+
triggerInstructions,
|
|
3059
|
+
expanded,
|
|
3060
|
+
onToggle,
|
|
3061
|
+
uiLanguage,
|
|
3062
|
+
t,
|
|
3063
|
+
}: {
|
|
3064
|
+
run: import("../../api").TriggerRunRecord;
|
|
3065
|
+
triggerId: string;
|
|
3066
|
+
triggerInstructions: string;
|
|
3067
|
+
expanded: boolean;
|
|
3068
|
+
onToggle: () => void;
|
|
3069
|
+
uiLanguage: string;
|
|
3070
|
+
t: (key: string, opts?: Record<string, unknown>) => string;
|
|
3071
|
+
}) {
|
|
3072
|
+
type OutputState =
|
|
3073
|
+
| { phase: "idle" }
|
|
3074
|
+
| { phase: "loading" }
|
|
3075
|
+
| {
|
|
3076
|
+
phase: "ready";
|
|
3077
|
+
text: string;
|
|
3078
|
+
truncated: boolean;
|
|
3079
|
+
messageCount?: number;
|
|
3080
|
+
segments?: Array<{
|
|
3081
|
+
stage: "action" | "final";
|
|
3082
|
+
action?: string;
|
|
3083
|
+
text: string;
|
|
3084
|
+
}>;
|
|
3085
|
+
}
|
|
3086
|
+
| {
|
|
3087
|
+
phase: "pending";
|
|
3088
|
+
kind: "still_processing" | "skipped" | "no_output" | "no_autonomy_room";
|
|
3089
|
+
message?: string;
|
|
3090
|
+
diagnostics?: {
|
|
3091
|
+
memoriesInWindow: number;
|
|
3092
|
+
agentId: string;
|
|
3093
|
+
roomId: string;
|
|
3094
|
+
windowStart: number;
|
|
3095
|
+
windowEnd: number;
|
|
3096
|
+
peek: Array<{
|
|
3097
|
+
entityIdMatches: boolean;
|
|
3098
|
+
source: string | null;
|
|
3099
|
+
metadataType: unknown;
|
|
3100
|
+
textPreview: string | null;
|
|
3101
|
+
createdAt: number | null;
|
|
3102
|
+
}>;
|
|
3103
|
+
};
|
|
3104
|
+
}
|
|
3105
|
+
| { phase: "error"; message: string };
|
|
3106
|
+
|
|
3107
|
+
const [output, setOutput] = useState<OutputState>({ phase: "idle" });
|
|
3108
|
+
|
|
3109
|
+
const fetchOutput = useCallback(async () => {
|
|
3110
|
+
setOutput({ phase: "loading" });
|
|
3111
|
+
try {
|
|
3112
|
+
const res = await client.getTriggerRunOutput(triggerId, run.triggerRunId);
|
|
3113
|
+
if (res.output && res.status === "ready") {
|
|
3114
|
+
setOutput({
|
|
3115
|
+
phase: "ready",
|
|
3116
|
+
text: res.output.text,
|
|
3117
|
+
truncated: res.output.truncated,
|
|
3118
|
+
messageCount: res.messageCount,
|
|
3119
|
+
segments: res.segments,
|
|
3120
|
+
});
|
|
3121
|
+
return;
|
|
3122
|
+
}
|
|
3123
|
+
if (
|
|
3124
|
+
res.status === "still_processing" ||
|
|
3125
|
+
res.status === "skipped" ||
|
|
3126
|
+
res.status === "no_output" ||
|
|
3127
|
+
res.status === "no_autonomy_room"
|
|
3128
|
+
) {
|
|
3129
|
+
setOutput({
|
|
3130
|
+
phase: "pending",
|
|
3131
|
+
kind: res.status,
|
|
3132
|
+
message: res.message,
|
|
3133
|
+
diagnostics: res.diagnostics,
|
|
3134
|
+
});
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
setOutput({
|
|
3138
|
+
phase: "error",
|
|
3139
|
+
message: res.message ?? `Lookup returned status: ${res.status}`,
|
|
3140
|
+
});
|
|
3141
|
+
} catch (err) {
|
|
3142
|
+
setOutput({
|
|
3143
|
+
phase: "error",
|
|
3144
|
+
message: err instanceof Error ? err.message : String(err),
|
|
3145
|
+
});
|
|
3146
|
+
}
|
|
3147
|
+
}, [triggerId, run.triggerRunId]);
|
|
3148
|
+
|
|
3149
|
+
// Lazy: fetch the first time the row is expanded, not on render.
|
|
3150
|
+
useEffect(() => {
|
|
3151
|
+
if (expanded && output.phase === "idle") {
|
|
3152
|
+
void fetchOutput();
|
|
3153
|
+
}
|
|
3154
|
+
}, [expanded, output.phase, fetchOutput]);
|
|
3155
|
+
|
|
3156
|
+
const ChevronIcon = expanded ? ChevronDown : ChevronRight;
|
|
3157
|
+
const startedAt = new Date(run.startedAt);
|
|
3158
|
+
const finishedAt = new Date(run.finishedAt);
|
|
3159
|
+
const fullStarted = `${startedAt.toLocaleString(uiLanguage || undefined)} (${startedAt.toISOString()})`;
|
|
3160
|
+
const fullFinished = finishedAt.toISOString();
|
|
3161
|
+
|
|
3162
|
+
const pendingCopy =
|
|
3163
|
+
output.phase === "pending"
|
|
3164
|
+
? output.kind === "still_processing"
|
|
3165
|
+
? "Agent is still working on this run. Try refresh in a moment."
|
|
3166
|
+
: output.kind === "skipped"
|
|
3167
|
+
? "This run was skipped — no output to show."
|
|
3168
|
+
: output.kind === "no_output"
|
|
3169
|
+
? "No agent reply was recorded in the autonomy room for this run's time window."
|
|
3170
|
+
: output.kind === "no_autonomy_room"
|
|
3171
|
+
? "Autonomy room not configured — cannot resolve outputs for past runs."
|
|
3172
|
+
: null
|
|
3173
|
+
: null;
|
|
3174
|
+
|
|
3175
|
+
return (
|
|
3176
|
+
<div className="border-b border-border/20 last:border-b-0">
|
|
3177
|
+
<button
|
|
3178
|
+
type="button"
|
|
3179
|
+
onClick={onToggle}
|
|
3180
|
+
aria-expanded={expanded}
|
|
3181
|
+
className={`flex w-full flex-wrap items-center gap-2 px-3 py-2 text-left text-xs-tight transition-colors hover:bg-muted/30 ${
|
|
3182
|
+
expanded ? "bg-muted/20" : ""
|
|
3183
|
+
}`}
|
|
3184
|
+
>
|
|
3185
|
+
<ChevronIcon className="h-3 w-3 shrink-0 text-muted/60" />
|
|
3186
|
+
<StatusBadge
|
|
3187
|
+
label={localizedExecutionStatus(run.status, t)}
|
|
3188
|
+
variant={toneForLastStatus(run.status)}
|
|
3189
|
+
/>
|
|
3190
|
+
<span className="text-muted/70 tabular-nums">
|
|
3191
|
+
{formatDateTime(run.startedAt, { locale: uiLanguage })}
|
|
3192
|
+
</span>
|
|
3193
|
+
<span className="text-muted/60">
|
|
3194
|
+
{formatDurationMs(run.latencyMs, { t })}
|
|
3195
|
+
</span>
|
|
3196
|
+
<span className="ml-auto rounded bg-bg/40 px-1 py-0.5 font-mono text-[10px] text-muted/60">
|
|
3197
|
+
{run.source}
|
|
3198
|
+
</span>
|
|
3199
|
+
</button>
|
|
3200
|
+
|
|
3201
|
+
{expanded ? (
|
|
3202
|
+
<div className="border-t border-border/20 bg-bg/20 px-3 py-3 text-xs-tight space-y-3">
|
|
3203
|
+
{/* Timestamps + IDs */}
|
|
3204
|
+
<div className="grid grid-cols-1 gap-1 text-[11px] text-muted/70 sm:grid-cols-2">
|
|
3205
|
+
<div>
|
|
3206
|
+
<span className="text-muted/50">Started:</span> {fullStarted}
|
|
3207
|
+
</div>
|
|
3208
|
+
<div>
|
|
3209
|
+
<span className="text-muted/50">Finished:</span> {fullFinished}
|
|
3210
|
+
</div>
|
|
3211
|
+
<div className="font-mono text-[10px] text-muted/50 truncate">
|
|
3212
|
+
run {run.triggerRunId}
|
|
3213
|
+
</div>
|
|
3214
|
+
<div className="font-mono text-[10px] text-muted/50 truncate">
|
|
3215
|
+
task {run.taskId}
|
|
3216
|
+
</div>
|
|
3217
|
+
</div>
|
|
3218
|
+
|
|
3219
|
+
{/* Instruction */}
|
|
3220
|
+
{triggerInstructions ? (
|
|
3221
|
+
<div>
|
|
3222
|
+
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted/60">
|
|
3223
|
+
Instruction
|
|
3224
|
+
</div>
|
|
3225
|
+
<div className="whitespace-pre-wrap rounded border border-border/30 bg-bg/40 px-2 py-1.5 text-[11px] text-muted/90">
|
|
3226
|
+
{triggerInstructions}
|
|
3227
|
+
</div>
|
|
3228
|
+
</div>
|
|
3229
|
+
) : null}
|
|
3230
|
+
|
|
3231
|
+
{/* Output */}
|
|
3232
|
+
<div>
|
|
3233
|
+
<div className="mb-1 flex items-center justify-between gap-2">
|
|
3234
|
+
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted/60">
|
|
3235
|
+
Output
|
|
3236
|
+
</span>
|
|
3237
|
+
{output.phase === "ready" || output.phase === "pending" || output.phase === "error" ? (
|
|
3238
|
+
<button
|
|
3239
|
+
type="button"
|
|
3240
|
+
className="text-[10px] text-muted/60 underline-offset-2 hover:text-txt hover:underline"
|
|
3241
|
+
onClick={(e) => {
|
|
3242
|
+
e.stopPropagation();
|
|
3243
|
+
void fetchOutput();
|
|
3244
|
+
}}
|
|
3245
|
+
>
|
|
3246
|
+
{t("common.refresh")}
|
|
3247
|
+
</button>
|
|
3248
|
+
) : null}
|
|
3249
|
+
</div>
|
|
3250
|
+
{output.phase === "idle" || output.phase === "loading" ? (
|
|
3251
|
+
<div className="flex items-center gap-2 rounded border border-border/30 bg-bg/40 px-2 py-2 text-[11px] text-muted/60">
|
|
3252
|
+
<div className="h-3 w-3 animate-spin rounded-full border-2 border-muted/30 border-t-muted/80" />
|
|
3253
|
+
Loading output…
|
|
3254
|
+
</div>
|
|
3255
|
+
) : output.phase === "ready" ? (
|
|
3256
|
+
<div className="space-y-2">
|
|
3257
|
+
{output.segments && output.segments.length > 0 ? (
|
|
3258
|
+
output.segments.map((seg, i) => {
|
|
3259
|
+
const isAction = seg.stage === "action";
|
|
3260
|
+
return (
|
|
3261
|
+
<div
|
|
3262
|
+
key={`${seg.stage}-${i}`}
|
|
3263
|
+
className={`rounded border px-2 py-1.5 text-[11px] ${
|
|
3264
|
+
isAction
|
|
3265
|
+
? "border-accent/30 bg-accent/5"
|
|
3266
|
+
: "border-border/30 bg-bg/40 text-txt/85"
|
|
3267
|
+
}`}
|
|
3268
|
+
>
|
|
3269
|
+
<div
|
|
3270
|
+
className={`mb-1 text-[9px] font-semibold uppercase tracking-wider ${
|
|
3271
|
+
isAction ? "text-accent" : "text-muted/60"
|
|
3272
|
+
}`}
|
|
3273
|
+
>
|
|
3274
|
+
{isAction
|
|
3275
|
+
? `Tool · ${seg.action ?? "action"}`
|
|
3276
|
+
: "Agent reply"}
|
|
3277
|
+
</div>
|
|
3278
|
+
<div className="whitespace-pre-wrap font-mono text-[11px]">
|
|
3279
|
+
{seg.text}
|
|
3280
|
+
</div>
|
|
3281
|
+
</div>
|
|
3282
|
+
);
|
|
3283
|
+
})
|
|
3284
|
+
) : (
|
|
3285
|
+
<div className="whitespace-pre-wrap rounded border border-border/30 bg-bg/40 px-2 py-1.5 text-[11px] text-txt/85">
|
|
3286
|
+
{output.text}
|
|
3287
|
+
</div>
|
|
3288
|
+
)}
|
|
3289
|
+
{output.truncated ? (
|
|
3290
|
+
<div className="text-[10px] text-muted/50">
|
|
3291
|
+
Output truncated — see autonomy room for full reply.
|
|
3292
|
+
</div>
|
|
3293
|
+
) : null}
|
|
3294
|
+
</div>
|
|
3295
|
+
) : output.phase === "pending" ? (
|
|
3296
|
+
<div className="space-y-2">
|
|
3297
|
+
<div className="rounded border border-border/30 bg-bg/40 px-2 py-1.5 text-[11px] italic text-muted/70">
|
|
3298
|
+
{pendingCopy}
|
|
3299
|
+
</div>
|
|
3300
|
+
{output.diagnostics ? (
|
|
3301
|
+
<details className="text-[10px] text-muted/60">
|
|
3302
|
+
<summary className="cursor-pointer select-none hover:text-txt">
|
|
3303
|
+
Diagnostics ({output.diagnostics.memoriesInWindow}{" "}
|
|
3304
|
+
memor{output.diagnostics.memoriesInWindow === 1 ? "y" : "ies"}{" "}
|
|
3305
|
+
in window)
|
|
3306
|
+
</summary>
|
|
3307
|
+
<div className="mt-1.5 space-y-1.5 rounded border border-border/30 bg-bg/40 px-2 py-1.5 font-mono">
|
|
3308
|
+
<div>
|
|
3309
|
+
agentId:{" "}
|
|
3310
|
+
<span className="text-muted/80">
|
|
3311
|
+
{output.diagnostics.agentId}
|
|
3312
|
+
</span>
|
|
3313
|
+
</div>
|
|
3314
|
+
<div>
|
|
3315
|
+
roomId:{" "}
|
|
3316
|
+
<span className="text-muted/80">
|
|
3317
|
+
{output.diagnostics.roomId}
|
|
3318
|
+
</span>
|
|
3319
|
+
</div>
|
|
3320
|
+
<div>
|
|
3321
|
+
window:{" "}
|
|
3322
|
+
<span className="text-muted/80">
|
|
3323
|
+
{new Date(output.diagnostics.windowStart).toISOString()}{" "}
|
|
3324
|
+
→ {new Date(output.diagnostics.windowEnd).toISOString()}
|
|
3325
|
+
</span>
|
|
3326
|
+
</div>
|
|
3327
|
+
{output.diagnostics.peek.length === 0 ? (
|
|
3328
|
+
<div className="italic">
|
|
3329
|
+
No memories at all in window — autonomy loop never
|
|
3330
|
+
wrote to the room.
|
|
3331
|
+
</div>
|
|
3332
|
+
) : (
|
|
3333
|
+
<div>
|
|
3334
|
+
<div className="mb-1">First {output.diagnostics.peek.length} memories:</div>
|
|
3335
|
+
<ul className="space-y-1 pl-3">
|
|
3336
|
+
{output.diagnostics.peek.map((p, i) => (
|
|
3337
|
+
<li
|
|
3338
|
+
key={`${p.createdAt ?? ""}-${i}`}
|
|
3339
|
+
className="border-l border-border/40 pl-2"
|
|
3340
|
+
>
|
|
3341
|
+
<div>
|
|
3342
|
+
entityIdMatches:{" "}
|
|
3343
|
+
<span
|
|
3344
|
+
className={
|
|
3345
|
+
p.entityIdMatches
|
|
3346
|
+
? "text-ok/80"
|
|
3347
|
+
: "text-warning/80"
|
|
3348
|
+
}
|
|
3349
|
+
>
|
|
3350
|
+
{String(p.entityIdMatches)}
|
|
3351
|
+
</span>
|
|
3352
|
+
</div>
|
|
3353
|
+
<div>
|
|
3354
|
+
source: {p.source ?? "(none)"}
|
|
3355
|
+
</div>
|
|
3356
|
+
<div>
|
|
3357
|
+
metadata.type: {String(p.metadataType ?? "(none)")}
|
|
3358
|
+
</div>
|
|
3359
|
+
{p.textPreview ? (
|
|
3360
|
+
<div className="text-muted/80">
|
|
3361
|
+
text: "{p.textPreview}"
|
|
3362
|
+
</div>
|
|
3363
|
+
) : (
|
|
3364
|
+
<div className="italic">no text</div>
|
|
3365
|
+
)}
|
|
3366
|
+
</li>
|
|
3367
|
+
))}
|
|
3368
|
+
</ul>
|
|
3369
|
+
</div>
|
|
3370
|
+
)}
|
|
3371
|
+
</div>
|
|
3372
|
+
</details>
|
|
3373
|
+
) : null}
|
|
3374
|
+
</div>
|
|
3375
|
+
) : (
|
|
3376
|
+
<div className="whitespace-pre-wrap rounded border border-danger/20 bg-danger/10 px-2 py-1.5 font-mono text-[11px] text-danger/90">
|
|
3377
|
+
Couldn't load output: {output.message}
|
|
3378
|
+
</div>
|
|
3379
|
+
)}
|
|
3380
|
+
</div>
|
|
3381
|
+
|
|
3382
|
+
{/* Error trace */}
|
|
3383
|
+
{run.error ? (
|
|
3384
|
+
<div>
|
|
3385
|
+
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-danger/70">
|
|
3386
|
+
Error
|
|
3387
|
+
</div>
|
|
3388
|
+
<div className="whitespace-pre-wrap rounded border border-danger/20 bg-danger/10 px-2 py-1.5 font-mono text-[11px] text-danger/90">
|
|
3389
|
+
{run.error}
|
|
3390
|
+
</div>
|
|
3391
|
+
</div>
|
|
3392
|
+
) : null}
|
|
3393
|
+
</div>
|
|
3394
|
+
) : null}
|
|
3395
|
+
</div>
|
|
3396
|
+
);
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3048
3399
|
function TriggerAutomationDetailPane({
|
|
3049
3400
|
automation,
|
|
3050
3401
|
onPromoteToWorkflow,
|
|
@@ -3073,6 +3424,67 @@ function TriggerAutomationDetailPane({
|
|
|
3073
3424
|
? Object.hasOwn(triggerRunsById, triggerId)
|
|
3074
3425
|
: false;
|
|
3075
3426
|
|
|
3427
|
+
// "Run now" UX — local state so the button gives feedback instead of
|
|
3428
|
+
// looking like a static link. While the dispatch is in flight: button
|
|
3429
|
+
// disabled + spinner. After it resolves: brief "Triggered" green tick,
|
|
3430
|
+
// toast in the global notice tray, and the run-history list refreshes
|
|
3431
|
+
// so the new run row appears immediately (no manual refresh needed).
|
|
3432
|
+
const { setActionNotice } = useApp();
|
|
3433
|
+
const [runNowState, setRunNowState] = useState<
|
|
3434
|
+
"idle" | "running" | "fired"
|
|
3435
|
+
>("idle");
|
|
3436
|
+
const runNowFiredTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
3437
|
+
null,
|
|
3438
|
+
);
|
|
3439
|
+
useEffect(() => {
|
|
3440
|
+
return () => {
|
|
3441
|
+
if (runNowFiredTimeoutRef.current) {
|
|
3442
|
+
clearTimeout(runNowFiredTimeoutRef.current);
|
|
3443
|
+
}
|
|
3444
|
+
};
|
|
3445
|
+
}, []);
|
|
3446
|
+
const handleRunNow = useCallback(async () => {
|
|
3447
|
+
if (!triggerId || runNowState === "running") return;
|
|
3448
|
+
setRunNowState("running");
|
|
3449
|
+
try {
|
|
3450
|
+
await onRunSelectedTrigger(triggerId);
|
|
3451
|
+
setActionNotice?.(
|
|
3452
|
+
"Trigger fired. Watch the Run history for the agent's output.",
|
|
3453
|
+
"success",
|
|
3454
|
+
4000,
|
|
3455
|
+
);
|
|
3456
|
+
// Refresh runs so the new row pops in without a manual refresh.
|
|
3457
|
+
void loadTriggerRuns(triggerId);
|
|
3458
|
+
setRunNowState("fired");
|
|
3459
|
+
runNowFiredTimeoutRef.current = setTimeout(() => {
|
|
3460
|
+
setRunNowState("idle");
|
|
3461
|
+
}, 2500);
|
|
3462
|
+
} catch (err) {
|
|
3463
|
+
setActionNotice?.(
|
|
3464
|
+
err instanceof Error
|
|
3465
|
+
? `Trigger dispatch failed: ${err.message}`
|
|
3466
|
+
: "Trigger dispatch failed.",
|
|
3467
|
+
"error",
|
|
3468
|
+
5000,
|
|
3469
|
+
);
|
|
3470
|
+
setRunNowState("idle");
|
|
3471
|
+
}
|
|
3472
|
+
}, [
|
|
3473
|
+
triggerId,
|
|
3474
|
+
runNowState,
|
|
3475
|
+
onRunSelectedTrigger,
|
|
3476
|
+
setActionNotice,
|
|
3477
|
+
loadTriggerRuns,
|
|
3478
|
+
]);
|
|
3479
|
+
|
|
3480
|
+
// Accordion: at most one run row is open at a time. Reset when the
|
|
3481
|
+
// user switches to a different trigger (different triggerId) so the
|
|
3482
|
+
// state doesn't leak across automations.
|
|
3483
|
+
const [openRunId, setOpenRunId] = useState<string | null>(null);
|
|
3484
|
+
useEffect(() => {
|
|
3485
|
+
setOpenRunId(null);
|
|
3486
|
+
}, [triggerId]);
|
|
3487
|
+
|
|
3076
3488
|
useEffect(() => {
|
|
3077
3489
|
if (triggerId && !hasLoadedRuns) {
|
|
3078
3490
|
void loadTriggerRuns(triggerId);
|
|
@@ -3132,9 +3544,26 @@ function TriggerAutomationDetailPane({
|
|
|
3132
3544
|
tone={trigger.enabled ? "warning" : "ok"}
|
|
3133
3545
|
/>
|
|
3134
3546
|
<IconAction
|
|
3135
|
-
label={
|
|
3136
|
-
|
|
3137
|
-
|
|
3547
|
+
label={
|
|
3548
|
+
runNowState === "running"
|
|
3549
|
+
? "Firing trigger…"
|
|
3550
|
+
: runNowState === "fired"
|
|
3551
|
+
? "Trigger fired"
|
|
3552
|
+
: t("triggersview.RunNow")
|
|
3553
|
+
}
|
|
3554
|
+
onClick={() => void handleRunNow()}
|
|
3555
|
+
disabled={runNowState === "running"}
|
|
3556
|
+
ariaBusy={runNowState === "running"}
|
|
3557
|
+
tone={runNowState === "fired" ? "ok" : undefined}
|
|
3558
|
+
icon={
|
|
3559
|
+
runNowState === "running" ? (
|
|
3560
|
+
<div className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
|
3561
|
+
) : runNowState === "fired" ? (
|
|
3562
|
+
<Check className="h-3.5 w-3.5" />
|
|
3563
|
+
) : (
|
|
3564
|
+
<Zap className="h-3.5 w-3.5" />
|
|
3565
|
+
)
|
|
3566
|
+
}
|
|
3138
3567
|
/>
|
|
3139
3568
|
<IconAction
|
|
3140
3569
|
label={t("triggersview.Edit")}
|
|
@@ -3241,31 +3670,22 @@ function TriggerAutomationDetailPane({
|
|
|
3241
3670
|
No runs yet.
|
|
3242
3671
|
</div>
|
|
3243
3672
|
) : (
|
|
3244
|
-
<div
|
|
3673
|
+
<div>
|
|
3245
3674
|
{selectedRuns.map((run) => (
|
|
3246
|
-
<
|
|
3675
|
+
<TriggerRunRow
|
|
3247
3676
|
key={run.triggerRunId}
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
<span className="ml-auto rounded bg-bg/40 px-1 py-0.5 font-mono text-[10px] text-muted/60">
|
|
3261
|
-
{run.source}
|
|
3262
|
-
</span>
|
|
3263
|
-
{run.error ? (
|
|
3264
|
-
<div className="basis-full whitespace-pre-wrap rounded border border-danger/20 bg-danger/10 px-2 py-1 font-mono text-[11px] text-danger/90">
|
|
3265
|
-
{run.error}
|
|
3266
|
-
</div>
|
|
3267
|
-
) : null}
|
|
3268
|
-
</div>
|
|
3677
|
+
run={run}
|
|
3678
|
+
triggerId={trigger.id}
|
|
3679
|
+
triggerInstructions={trigger.instructions ?? ""}
|
|
3680
|
+
expanded={openRunId === run.triggerRunId}
|
|
3681
|
+
onToggle={() =>
|
|
3682
|
+
setOpenRunId((current) =>
|
|
3683
|
+
current === run.triggerRunId ? null : run.triggerRunId,
|
|
3684
|
+
)
|
|
3685
|
+
}
|
|
3686
|
+
uiLanguage={uiLanguage}
|
|
3687
|
+
t={t}
|
|
3688
|
+
/>
|
|
3269
3689
|
))}
|
|
3270
3690
|
</div>
|
|
3271
3691
|
)}
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
useRef,
|
|
56
56
|
useState,
|
|
57
57
|
} from "react";
|
|
58
|
+
import { client } from "../../api/client";
|
|
58
59
|
import { CodingAgentSettingsSection } from "../../app-shell/task-coordinator-slots.js";
|
|
59
60
|
import { useApp } from "../../state";
|
|
60
61
|
import { WidgetHost } from "../../widgets";
|
|
@@ -174,6 +175,25 @@ const SETTINGS_SECTIONS: SettingsSectionDef[] = [
|
|
|
174
175
|
keywordKeys: ["settings.keyword.voice"],
|
|
175
176
|
level: "simple",
|
|
176
177
|
},
|
|
178
|
+
{
|
|
179
|
+
// Tavily API key for the WEB_SEARCH action (plugin-web-fetch). Lives
|
|
180
|
+
// outside ai-model because Tavily isn't a model provider — it's a
|
|
181
|
+
// search backend the agent calls when a trigger or chat instruction
|
|
182
|
+
// asks for web search.
|
|
183
|
+
id: "web-search",
|
|
184
|
+
label: "settings.sections.webSearch.label",
|
|
185
|
+
description: "settings.sections.webSearch.desc",
|
|
186
|
+
keywords: [
|
|
187
|
+
"web search",
|
|
188
|
+
"tavily",
|
|
189
|
+
"search",
|
|
190
|
+
"internet",
|
|
191
|
+
"online",
|
|
192
|
+
"trends",
|
|
193
|
+
"api key",
|
|
194
|
+
],
|
|
195
|
+
level: "simple",
|
|
196
|
+
},
|
|
177
197
|
{
|
|
178
198
|
// Cloud and direct-provider model routing. Local model runtime controls are
|
|
179
199
|
// split into the advanced Local Models section below.
|
|
@@ -665,6 +685,125 @@ function AdvancedSection() {
|
|
|
665
685
|
);
|
|
666
686
|
}
|
|
667
687
|
|
|
688
|
+
/* ── WebSearchKeySection ──────────────────────────────────────────────── */
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Single-purpose Tavily API key input. Calls a dedicated endpoint
|
|
692
|
+
* (`PUT /api/integrations/tavily-key`) instead of the plugin-config-save
|
|
693
|
+
* pipeline because the web-fetch plugin is a workspace plugin not in the
|
|
694
|
+
* bundled plugin-discovery manifest — its `TAVILY_API_KEY` parameter is
|
|
695
|
+
* invisible to the generic plugin-config validator, which rejects with
|
|
696
|
+
* 422 ("not a declared config key").
|
|
697
|
+
*
|
|
698
|
+
* The endpoint writes to config.env, mirrors to process.env (so the next
|
|
699
|
+
* WEB_SEARCH call sees the key immediately), and triggers a runtime
|
|
700
|
+
* restart so any cached `runtime.getSetting` value refreshes.
|
|
701
|
+
*/
|
|
702
|
+
function WebSearchKeySection() {
|
|
703
|
+
const app = useApp();
|
|
704
|
+
const setActionNotice = app.setActionNotice;
|
|
705
|
+
const [value, setValue] = useState("");
|
|
706
|
+
const [revealed, setRevealed] = useState(false);
|
|
707
|
+
const [saving, setSaving] = useState(false);
|
|
708
|
+
const [savedAt, setSavedAt] = useState<number | null>(null);
|
|
709
|
+
const saved = savedAt != null && Date.now() - savedAt < 4000;
|
|
710
|
+
|
|
711
|
+
const trimmed = value.trim();
|
|
712
|
+
const canSave =
|
|
713
|
+
!saving && /^tvly-[A-Za-z0-9_-]{8,}$/.test(trimmed);
|
|
714
|
+
|
|
715
|
+
const onSave = useCallback(async () => {
|
|
716
|
+
if (!canSave) return;
|
|
717
|
+
setSaving(true);
|
|
718
|
+
try {
|
|
719
|
+
const result = await client.setTavilyApiKey(trimmed);
|
|
720
|
+
setActionNotice?.(
|
|
721
|
+
result.restartScheduled
|
|
722
|
+
? "Tavily key saved. Web search is active; agent restart scheduled."
|
|
723
|
+
: "Tavily key saved. Web search is active.",
|
|
724
|
+
"success",
|
|
725
|
+
4000,
|
|
726
|
+
);
|
|
727
|
+
setValue("");
|
|
728
|
+
setSavedAt(Date.now());
|
|
729
|
+
} catch (err) {
|
|
730
|
+
setActionNotice?.(
|
|
731
|
+
err instanceof Error ? `Save failed: ${err.message}` : "Save failed.",
|
|
732
|
+
"error",
|
|
733
|
+
5000,
|
|
734
|
+
);
|
|
735
|
+
} finally {
|
|
736
|
+
setSaving(false);
|
|
737
|
+
}
|
|
738
|
+
}, [canSave, setActionNotice, trimmed]);
|
|
739
|
+
|
|
740
|
+
const formatHint =
|
|
741
|
+
value.length > 0 && !canSave && !saving
|
|
742
|
+
? "Expected format: tvly- followed by 8+ alphanumeric characters."
|
|
743
|
+
: null;
|
|
744
|
+
|
|
745
|
+
return (
|
|
746
|
+
<div className="space-y-3">
|
|
747
|
+
<p className="text-sm text-muted">
|
|
748
|
+
Tavily powers the agent's <code className="text-txt">WEB_SEARCH</code>{" "}
|
|
749
|
+
action — needed for triggers like "fetch daily AI trends" and any
|
|
750
|
+
chat instruction that asks the agent to search the web. Get a free
|
|
751
|
+
key (1,000 searches/month, no credit card) at{" "}
|
|
752
|
+
<a
|
|
753
|
+
href="https://app.tavily.com/sign-in"
|
|
754
|
+
target="_blank"
|
|
755
|
+
rel="noopener noreferrer"
|
|
756
|
+
className="text-accent underline-offset-2 hover:underline"
|
|
757
|
+
>
|
|
758
|
+
app.tavily.com
|
|
759
|
+
</a>
|
|
760
|
+
. Saving here writes to <code className="text-txt">config.env</code>{" "}
|
|
761
|
+
and restarts the runtime so the key takes effect.
|
|
762
|
+
</p>
|
|
763
|
+
|
|
764
|
+
<div className="space-y-2">
|
|
765
|
+
<Label htmlFor="settings-tavily-key" className="text-txt-strong">
|
|
766
|
+
Tavily API key
|
|
767
|
+
</Label>
|
|
768
|
+
<div className="flex gap-2">
|
|
769
|
+
<Input
|
|
770
|
+
id="settings-tavily-key"
|
|
771
|
+
type={revealed ? "text" : "password"}
|
|
772
|
+
value={value}
|
|
773
|
+
onChange={(e) => setValue(e.target.value)}
|
|
774
|
+
placeholder="tvly-…"
|
|
775
|
+
autoComplete="off"
|
|
776
|
+
spellCheck={false}
|
|
777
|
+
className="rounded-lg bg-bg font-mono text-sm"
|
|
778
|
+
/>
|
|
779
|
+
<Button
|
|
780
|
+
type="button"
|
|
781
|
+
variant="outline"
|
|
782
|
+
size="sm"
|
|
783
|
+
onClick={() => setRevealed((v) => !v)}
|
|
784
|
+
aria-label={revealed ? "Hide key" : "Show key"}
|
|
785
|
+
>
|
|
786
|
+
{revealed ? "Hide" : "Show"}
|
|
787
|
+
</Button>
|
|
788
|
+
<Button
|
|
789
|
+
type="button"
|
|
790
|
+
variant="default"
|
|
791
|
+
size="sm"
|
|
792
|
+
onClick={() => void onSave()}
|
|
793
|
+
disabled={!canSave}
|
|
794
|
+
>
|
|
795
|
+
{saving && <Spinner size={14} />}
|
|
796
|
+
{saving ? "Saving" : saved ? "Saved" : "Save"}
|
|
797
|
+
</Button>
|
|
798
|
+
</div>
|
|
799
|
+
{formatHint && (
|
|
800
|
+
<p className="text-xs text-danger">{formatHint}</p>
|
|
801
|
+
)}
|
|
802
|
+
</div>
|
|
803
|
+
</div>
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
|
|
668
807
|
/* ── SettingsView ─────────────────────────────────────────────────────── */
|
|
669
808
|
|
|
670
809
|
export function SettingsView({
|
|
@@ -951,6 +1090,22 @@ export function SettingsView({
|
|
|
951
1090
|
</SettingsSection>
|
|
952
1091
|
)}
|
|
953
1092
|
|
|
1093
|
+
{visibleSectionIds.has("web-search") && (
|
|
1094
|
+
<SettingsSection
|
|
1095
|
+
id="web-search"
|
|
1096
|
+
title={t("settings.sections.webSearch.label", {
|
|
1097
|
+
defaultValue: "Web search",
|
|
1098
|
+
})}
|
|
1099
|
+
description={t("settings.sections.webSearch.desc", {
|
|
1100
|
+
defaultValue:
|
|
1101
|
+
"Configure the Tavily API key the agent uses to search the web.",
|
|
1102
|
+
})}
|
|
1103
|
+
ref={registerContentItem("web-search")}
|
|
1104
|
+
>
|
|
1105
|
+
<WebSearchKeySection />
|
|
1106
|
+
</SettingsSection>
|
|
1107
|
+
)}
|
|
1108
|
+
|
|
954
1109
|
{visibleSectionIds.has("ai-model") && (
|
|
955
1110
|
<div className="grid gap-5 xl:grid-cols-2 items-start">
|
|
956
1111
|
<div className="flex flex-col gap-5 min-w-0">
|