clementine-agent 1.18.30 → 1.18.32
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/dashboard.js +1842 -268
- package/dist/dashboard/builder/dry-run.js +10 -0
- package/dist/dashboard/builder/runner.js +82 -2
- package/dist/dashboard/builder/serializer.js +14 -0
- package/dist/dashboard/builder/validation.js +35 -0
- package/dist/gateway/episodic-consolidation.d.ts +11 -16
- package/dist/gateway/episodic-consolidation.js +86 -3
- package/dist/gateway/router.js +23 -0
- package/dist/memory/store.d.ts +63 -0
- package/dist/memory/store.js +160 -0
- package/dist/tools/builder-tools.js +5 -1
- package/dist/types.d.ts +14 -1
- package/package.json +1 -1
package/dist/cli/dashboard.js
CHANGED
|
@@ -3363,6 +3363,8 @@ export async function cmdDashboard(opts) {
|
|
|
3363
3363
|
const folderFilter = typeof req.query.folder === 'string' ? req.query.folder : '';
|
|
3364
3364
|
const search = typeof req.query.q === 'string' ? req.query.q.toLowerCase() : '';
|
|
3365
3365
|
const includeAuto = req.query.includeAuto === '1';
|
|
3366
|
+
const typeFilter = typeof req.query.type === 'string' ? req.query.type : '';
|
|
3367
|
+
const tagFilter = typeof req.query.tag === 'string' ? req.query.tag : '';
|
|
3366
3368
|
const cutoffMs = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
|
|
3367
3369
|
const vaultRoot = path.join(BASE_DIR, 'vault');
|
|
3368
3370
|
const matter = (await import('gray-matter')).default;
|
|
@@ -3413,6 +3415,8 @@ export async function cmdDashboard(opts) {
|
|
|
3413
3415
|
// Skip system housekeeping files (their author will surface via mtime in agent's own dir)
|
|
3414
3416
|
let title = path.basename(rel, '.md');
|
|
3415
3417
|
let typeTag = null;
|
|
3418
|
+
let categoryTag = null;
|
|
3419
|
+
let tags = [];
|
|
3416
3420
|
try {
|
|
3417
3421
|
const head = readFileSync(full, 'utf-8').slice(0, 4000);
|
|
3418
3422
|
const parsed = matter(head);
|
|
@@ -3428,6 +3432,14 @@ export async function cmdDashboard(opts) {
|
|
|
3428
3432
|
}
|
|
3429
3433
|
if (typeof data.type === 'string')
|
|
3430
3434
|
typeTag = data.type;
|
|
3435
|
+
if (typeof data.category === 'string')
|
|
3436
|
+
categoryTag = data.category;
|
|
3437
|
+
if (Array.isArray(data.tags)) {
|
|
3438
|
+
tags = data.tags.filter((t) => typeof t === 'string');
|
|
3439
|
+
}
|
|
3440
|
+
else if (typeof data.tags === 'string') {
|
|
3441
|
+
tags = data.tags.split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
|
|
3442
|
+
}
|
|
3431
3443
|
}
|
|
3432
3444
|
catch { /* */ }
|
|
3433
3445
|
files.push({
|
|
@@ -3439,6 +3451,8 @@ export async function cmdDashboard(opts) {
|
|
|
3439
3451
|
mtime: new Date(stat.mtimeMs).toISOString(),
|
|
3440
3452
|
sizeBytes: stat.size,
|
|
3441
3453
|
type: typeTag,
|
|
3454
|
+
category: categoryTag,
|
|
3455
|
+
tags,
|
|
3442
3456
|
});
|
|
3443
3457
|
}
|
|
3444
3458
|
}
|
|
@@ -3452,19 +3466,33 @@ export async function cmdDashboard(opts) {
|
|
|
3452
3466
|
return false;
|
|
3453
3467
|
if (folderFilter && f.folder !== folderFilter)
|
|
3454
3468
|
return false;
|
|
3469
|
+
if (typeFilter && f.type !== typeFilter)
|
|
3470
|
+
return false;
|
|
3471
|
+
if (tagFilter && !f.tags.includes(tagFilter))
|
|
3472
|
+
return false;
|
|
3455
3473
|
if (search) {
|
|
3456
|
-
const hay = (f.title + ' ' + f.relPath
|
|
3474
|
+
const hay = (f.title + ' ' + f.relPath + ' ' +
|
|
3475
|
+
(f.type || '') + ' ' + (f.category || '') + ' ' +
|
|
3476
|
+
f.tags.join(' ')).toLowerCase();
|
|
3457
3477
|
if (!hay.includes(search))
|
|
3458
3478
|
return false;
|
|
3459
3479
|
}
|
|
3460
3480
|
return true;
|
|
3461
3481
|
})
|
|
3462
3482
|
.slice(0, limit);
|
|
3463
|
-
//
|
|
3483
|
+
// Facet counts reflect the unfiltered set so the rail keeps the full
|
|
3484
|
+
// vocabulary visible after a filter is applied.
|
|
3464
3485
|
const folderCounts = {};
|
|
3465
|
-
|
|
3486
|
+
const typeCounts = {};
|
|
3487
|
+
const tagCounts = {};
|
|
3488
|
+
for (const f of files) {
|
|
3466
3489
|
folderCounts[f.folder] = (folderCounts[f.folder] || 0) + 1;
|
|
3467
|
-
|
|
3490
|
+
if (f.type)
|
|
3491
|
+
typeCounts[f.type] = (typeCounts[f.type] || 0) + 1;
|
|
3492
|
+
for (const t of f.tags)
|
|
3493
|
+
tagCounts[t] = (tagCounts[t] || 0) + 1;
|
|
3494
|
+
}
|
|
3495
|
+
res.json({ files: filtered, total: files.length, folderCounts, typeCounts, tagCounts });
|
|
3468
3496
|
}
|
|
3469
3497
|
catch (err) {
|
|
3470
3498
|
res.status(500).json({ error: String(err) });
|
|
@@ -3482,6 +3510,22 @@ export async function cmdDashboard(opts) {
|
|
|
3482
3510
|
res.status(404).json({ error: 'Not found' });
|
|
3483
3511
|
return;
|
|
3484
3512
|
}
|
|
3513
|
+
const headOnly = req.query.head === '1';
|
|
3514
|
+
if (headOnly) {
|
|
3515
|
+
const stat = statSync(full);
|
|
3516
|
+
const head = readFileSync(full, 'utf-8').slice(0, 4000);
|
|
3517
|
+
const matter = (await import('gray-matter')).default;
|
|
3518
|
+
const parsed = matter(head);
|
|
3519
|
+
const snippet = (parsed.content || '').replace(/^#+\s.*$/m, '').trim().slice(0, 400);
|
|
3520
|
+
res.json({
|
|
3521
|
+
path: relPath,
|
|
3522
|
+
frontmatter: parsed.data,
|
|
3523
|
+
snippet,
|
|
3524
|
+
sizeBytes: stat.size,
|
|
3525
|
+
mtime: new Date(stat.mtimeMs).toISOString(),
|
|
3526
|
+
});
|
|
3527
|
+
return;
|
|
3528
|
+
}
|
|
3485
3529
|
const content = readFileSync(full, 'utf-8');
|
|
3486
3530
|
res.json({ path: relPath, content });
|
|
3487
3531
|
}
|
|
@@ -3989,6 +4033,395 @@ export async function cmdDashboard(opts) {
|
|
|
3989
4033
|
res.status(500).json({ error: 'Create failed', detail: String(err) });
|
|
3990
4034
|
}
|
|
3991
4035
|
});
|
|
4036
|
+
// ── Routines API (canonical surface for the Build tab) ─────────
|
|
4037
|
+
// The "Routines" UI uses this surface exclusively. Workflows + cron
|
|
4038
|
+
// jobs both flow through here as a single Routine concept; legacy
|
|
4039
|
+
// /api/builder/* and /api/cron/* endpoints remain for one minor
|
|
4040
|
+
// version, then are removed.
|
|
4041
|
+
app.get('/api/routines', async (_req, res) => {
|
|
4042
|
+
try {
|
|
4043
|
+
const { listAllForBuilder } = await import('../dashboard/builder/serializer.js');
|
|
4044
|
+
res.json({ routines: listAllForBuilder() });
|
|
4045
|
+
}
|
|
4046
|
+
catch (err) {
|
|
4047
|
+
res.status(500).json({ error: 'list failed', detail: String(err) });
|
|
4048
|
+
}
|
|
4049
|
+
});
|
|
4050
|
+
app.get('/api/routines/mcp-tools', async (_req, res) => {
|
|
4051
|
+
try {
|
|
4052
|
+
const { discoverMcpServers, loadToolInventory } = await import('../agent/mcp-bridge.js');
|
|
4053
|
+
const servers = discoverMcpServers();
|
|
4054
|
+
const inv = loadToolInventory();
|
|
4055
|
+
const allTools = inv?.tools ?? [];
|
|
4056
|
+
// Group flat tool names of shape `mcp__<server>__<tool>` (server may
|
|
4057
|
+
// contain underscores — split on the first `__` after the prefix).
|
|
4058
|
+
const grouped = {};
|
|
4059
|
+
for (const s of servers) {
|
|
4060
|
+
grouped[s.name] = { name: s.name, enabled: s.enabled !== false, tools: [] };
|
|
4061
|
+
}
|
|
4062
|
+
for (const t of allTools) {
|
|
4063
|
+
if (!t.startsWith('mcp__'))
|
|
4064
|
+
continue;
|
|
4065
|
+
const rest = t.slice(5);
|
|
4066
|
+
const idx = rest.indexOf('__');
|
|
4067
|
+
if (idx < 0)
|
|
4068
|
+
continue;
|
|
4069
|
+
const server = rest.slice(0, idx);
|
|
4070
|
+
const tool = rest.slice(idx + 2);
|
|
4071
|
+
if (!grouped[server])
|
|
4072
|
+
grouped[server] = { name: server, enabled: true, tools: [] };
|
|
4073
|
+
if (!grouped[server].tools.includes(tool))
|
|
4074
|
+
grouped[server].tools.push(tool);
|
|
4075
|
+
}
|
|
4076
|
+
const out = Object.values(grouped)
|
|
4077
|
+
.filter(s => s.tools.length > 0 || s.enabled)
|
|
4078
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
4079
|
+
res.json({ servers: out });
|
|
4080
|
+
}
|
|
4081
|
+
catch (err) {
|
|
4082
|
+
res.status(500).json({ error: 'mcp-tools failed', detail: String(err) });
|
|
4083
|
+
}
|
|
4084
|
+
});
|
|
4085
|
+
app.get('/api/routines/cli-tools', async (_req, res) => {
|
|
4086
|
+
try {
|
|
4087
|
+
// Reuse discoverCliTools() defined elsewhere in this file.
|
|
4088
|
+
const tools = discoverCliTools().filter(t => t.installed && !t.blocked);
|
|
4089
|
+
res.json({ tools: tools.map(t => ({ cmd: t.name, description: t.description, userDefined: !!t.userDefined })) });
|
|
4090
|
+
}
|
|
4091
|
+
catch (err) {
|
|
4092
|
+
res.status(500).json({ error: 'cli-tools failed', detail: String(err) });
|
|
4093
|
+
}
|
|
4094
|
+
});
|
|
4095
|
+
app.get('/api/routines/:id', async (req, res) => {
|
|
4096
|
+
try {
|
|
4097
|
+
const id = decodeURIComponent(req.params.id);
|
|
4098
|
+
const { readWorkflow } = await import('../dashboard/builder/serializer.js');
|
|
4099
|
+
const { validateWorkflow } = await import('../dashboard/builder/validation.js');
|
|
4100
|
+
const wf = readWorkflow(id);
|
|
4101
|
+
if (!wf) {
|
|
4102
|
+
res.status(404).json({ error: 'Not found' });
|
|
4103
|
+
return;
|
|
4104
|
+
}
|
|
4105
|
+
res.json({ id, routine: wf, validation: validateWorkflow(wf) });
|
|
4106
|
+
}
|
|
4107
|
+
catch (err) {
|
|
4108
|
+
res.status(500).json({ error: 'read failed', detail: String(err) });
|
|
4109
|
+
}
|
|
4110
|
+
});
|
|
4111
|
+
app.post('/api/routines', async (req, res) => {
|
|
4112
|
+
try {
|
|
4113
|
+
const body = req.body;
|
|
4114
|
+
if (!body || !body.name) {
|
|
4115
|
+
res.status(400).json({ error: 'name required' });
|
|
4116
|
+
return;
|
|
4117
|
+
}
|
|
4118
|
+
const [{ saveWorkflow, workflowId: makeId }, { emitBuilderEvent }, yamlMod] = await Promise.all([
|
|
4119
|
+
import('../dashboard/builder/serializer.js'),
|
|
4120
|
+
import('../dashboard/builder/events.js'),
|
|
4121
|
+
import('js-yaml'),
|
|
4122
|
+
]);
|
|
4123
|
+
const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64) || 'routine';
|
|
4124
|
+
const agentSlug = body.agent ? (String(body.agent).trim() || undefined) : undefined;
|
|
4125
|
+
// Parse the agent's draft if present; on any parse error fall back to the
|
|
4126
|
+
// single-step stub so the user lands in the editor with something usable.
|
|
4127
|
+
let steps = null;
|
|
4128
|
+
if (body.draftYaml && typeof body.draftYaml === 'string') {
|
|
4129
|
+
try {
|
|
4130
|
+
const parsed = yamlMod.load(body.draftYaml);
|
|
4131
|
+
if (parsed && typeof parsed === 'object') {
|
|
4132
|
+
steps = Object.entries(parsed).map(([id, raw]) => {
|
|
4133
|
+
const r = (raw && typeof raw === 'object') ? raw : {};
|
|
4134
|
+
const dependsOn = Array.isArray(r.dependsOn)
|
|
4135
|
+
? r.dependsOn.map(String)
|
|
4136
|
+
: (typeof r.dependsOn === 'string' ? r.dependsOn.split(',').map(s => s.trim()).filter(Boolean) : []);
|
|
4137
|
+
return {
|
|
4138
|
+
id: String(id),
|
|
4139
|
+
prompt: String(r.prompt ?? ''),
|
|
4140
|
+
dependsOn,
|
|
4141
|
+
tier: typeof r.tier === 'number' ? r.tier : 1,
|
|
4142
|
+
maxTurns: typeof r.maxTurns === 'number' ? r.maxTurns : 15,
|
|
4143
|
+
...(typeof r.model === 'string' ? { model: r.model } : {}),
|
|
4144
|
+
...(typeof r.workDir === 'string' ? { workDir: r.workDir } : {}),
|
|
4145
|
+
...(typeof r.kind === 'string' && r.kind !== 'prompt' ? { kind: r.kind } : {}),
|
|
4146
|
+
};
|
|
4147
|
+
}).filter(s => s.id);
|
|
4148
|
+
if (steps.length === 0)
|
|
4149
|
+
steps = null;
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
catch {
|
|
4153
|
+
steps = null;
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
const wf = {
|
|
4157
|
+
name: body.name,
|
|
4158
|
+
description: body.description ?? '',
|
|
4159
|
+
enabled: true,
|
|
4160
|
+
trigger: body.schedule ? { schedule: body.schedule, manual: false } : { manual: true },
|
|
4161
|
+
inputs: {},
|
|
4162
|
+
steps: (steps ?? [{
|
|
4163
|
+
id: 's1',
|
|
4164
|
+
prompt: body.initialPrompt ?? 'Describe what this trick should do.',
|
|
4165
|
+
dependsOn: [],
|
|
4166
|
+
tier: 1,
|
|
4167
|
+
maxTurns: 15,
|
|
4168
|
+
}]),
|
|
4169
|
+
sourceFile: '',
|
|
4170
|
+
agentSlug,
|
|
4171
|
+
...(body.model ? { model: body.model } : {}),
|
|
4172
|
+
};
|
|
4173
|
+
const id = makeId(slug, agentSlug);
|
|
4174
|
+
const result = saveWorkflow(id, wf);
|
|
4175
|
+
if (!result.ok) {
|
|
4176
|
+
res.status(400).json({ error: result.error });
|
|
4177
|
+
return;
|
|
4178
|
+
}
|
|
4179
|
+
emitBuilderEvent({ type: 'workflow:created', workflowId: id, payload: { workflow: wf } });
|
|
4180
|
+
res.json({ ok: true, id });
|
|
4181
|
+
}
|
|
4182
|
+
catch (err) {
|
|
4183
|
+
res.status(500).json({ error: 'create failed', detail: String(err) });
|
|
4184
|
+
}
|
|
4185
|
+
});
|
|
4186
|
+
app.put('/api/routines/:id', async (req, res) => {
|
|
4187
|
+
try {
|
|
4188
|
+
const id = decodeURIComponent(req.params.id);
|
|
4189
|
+
const body = req.body;
|
|
4190
|
+
if (!body || typeof body.routine !== 'object') {
|
|
4191
|
+
res.status(400).json({ error: 'Missing routine body' });
|
|
4192
|
+
return;
|
|
4193
|
+
}
|
|
4194
|
+
const [{ readWorkflow, saveWorkflow }, { validateWorkflow }, { emitBuilderEvent }] = await Promise.all([
|
|
4195
|
+
import('../dashboard/builder/serializer.js'),
|
|
4196
|
+
import('../dashboard/builder/validation.js'),
|
|
4197
|
+
import('../dashboard/builder/events.js'),
|
|
4198
|
+
]);
|
|
4199
|
+
const existing = readWorkflow(id);
|
|
4200
|
+
if (!existing) {
|
|
4201
|
+
res.status(404).json({ error: 'Not found' });
|
|
4202
|
+
return;
|
|
4203
|
+
}
|
|
4204
|
+
const incoming = body.routine;
|
|
4205
|
+
const next = { ...incoming, sourceFile: existing.sourceFile };
|
|
4206
|
+
const v = validateWorkflow(next);
|
|
4207
|
+
if (!v.ok && !body.force) {
|
|
4208
|
+
res.status(400).json({ error: 'validation', validation: v });
|
|
4209
|
+
return;
|
|
4210
|
+
}
|
|
4211
|
+
const result = saveWorkflow(id, next);
|
|
4212
|
+
if (!result.ok) {
|
|
4213
|
+
res.status(400).json({ error: result.error });
|
|
4214
|
+
return;
|
|
4215
|
+
}
|
|
4216
|
+
emitBuilderEvent({ type: 'workflow:patched', workflowId: id, payload: { workflow: next } });
|
|
4217
|
+
res.json({ ok: true, validation: v });
|
|
4218
|
+
}
|
|
4219
|
+
catch (err) {
|
|
4220
|
+
res.status(500).json({ error: 'save failed', detail: String(err) });
|
|
4221
|
+
}
|
|
4222
|
+
});
|
|
4223
|
+
app.delete('/api/routines/:id', async (req, res) => {
|
|
4224
|
+
try {
|
|
4225
|
+
const id = decodeURIComponent(req.params.id);
|
|
4226
|
+
const [{ readWorkflow, parseBuilderId }, { emitBuilderEvent }] = await Promise.all([
|
|
4227
|
+
import('../dashboard/builder/serializer.js'),
|
|
4228
|
+
import('../dashboard/builder/events.js'),
|
|
4229
|
+
]);
|
|
4230
|
+
const parsed = parseBuilderId(id);
|
|
4231
|
+
if (!parsed) {
|
|
4232
|
+
res.status(400).json({ error: 'Bad id' });
|
|
4233
|
+
return;
|
|
4234
|
+
}
|
|
4235
|
+
if (parsed.origin === 'cron') {
|
|
4236
|
+
res.status(400).json({ error: 'This trick came from a legacy cron entry — disable it instead, or edit CRON.md directly.' });
|
|
4237
|
+
return;
|
|
4238
|
+
}
|
|
4239
|
+
const wf = readWorkflow(id);
|
|
4240
|
+
if (!wf) {
|
|
4241
|
+
res.status(404).json({ error: 'Not found' });
|
|
4242
|
+
return;
|
|
4243
|
+
}
|
|
4244
|
+
if (wf.sourceFile && existsSync(wf.sourceFile))
|
|
4245
|
+
unlinkSync(wf.sourceFile);
|
|
4246
|
+
emitBuilderEvent({ type: 'workflow:deleted', workflowId: id });
|
|
4247
|
+
res.json({ ok: true });
|
|
4248
|
+
}
|
|
4249
|
+
catch (err) {
|
|
4250
|
+
res.status(500).json({ error: String(err) });
|
|
4251
|
+
}
|
|
4252
|
+
});
|
|
4253
|
+
app.post('/api/routines/:id/toggle', async (req, res) => {
|
|
4254
|
+
try {
|
|
4255
|
+
const id = decodeURIComponent(req.params.id);
|
|
4256
|
+
const { readWorkflow, saveWorkflow } = await import('../dashboard/builder/serializer.js');
|
|
4257
|
+
const wf = readWorkflow(id);
|
|
4258
|
+
if (!wf) {
|
|
4259
|
+
res.status(404).json({ error: 'Not found' });
|
|
4260
|
+
return;
|
|
4261
|
+
}
|
|
4262
|
+
wf.enabled = !wf.enabled;
|
|
4263
|
+
const result = saveWorkflow(id, wf);
|
|
4264
|
+
if (!result.ok) {
|
|
4265
|
+
res.status(400).json({ error: result.error });
|
|
4266
|
+
return;
|
|
4267
|
+
}
|
|
4268
|
+
res.json({ ok: true, enabled: wf.enabled });
|
|
4269
|
+
}
|
|
4270
|
+
catch (err) {
|
|
4271
|
+
res.status(500).json({ error: 'toggle failed', detail: String(err) });
|
|
4272
|
+
}
|
|
4273
|
+
});
|
|
4274
|
+
app.post('/api/routines/:id/run', async (req, res) => {
|
|
4275
|
+
try {
|
|
4276
|
+
const id = decodeURIComponent(req.params.id);
|
|
4277
|
+
const { readWorkflow, parseBuilderId } = await import('../dashboard/builder/serializer.js');
|
|
4278
|
+
const wf = readWorkflow(id);
|
|
4279
|
+
if (!wf) {
|
|
4280
|
+
res.status(404).json({ error: 'Not found' });
|
|
4281
|
+
return;
|
|
4282
|
+
}
|
|
4283
|
+
const parsed = parseBuilderId(id);
|
|
4284
|
+
const body = (req.body ?? {});
|
|
4285
|
+
// Cron-origin routines: spawn the cli `cron run <name>` (single-step prompt path).
|
|
4286
|
+
if (parsed?.origin === 'cron') {
|
|
4287
|
+
const child = spawn('node', [DIST_ENTRY, 'cron', 'run', wf.name], {
|
|
4288
|
+
detached: true,
|
|
4289
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4290
|
+
cwd: BASE_DIR,
|
|
4291
|
+
env: { ...process.env, CLEMENTINE_HOME: BASE_DIR },
|
|
4292
|
+
});
|
|
4293
|
+
child.on('exit', (code) => {
|
|
4294
|
+
broadcastEvent({ type: 'cron_complete', data: { job: wf.name, code } });
|
|
4295
|
+
responseCache.delete('activity:');
|
|
4296
|
+
});
|
|
4297
|
+
child.unref();
|
|
4298
|
+
broadcastEvent({ type: 'cron_triggered', data: { job: wf.name } });
|
|
4299
|
+
res.json({ ok: true, message: `Triggered trick: ${wf.name}` });
|
|
4300
|
+
return;
|
|
4301
|
+
}
|
|
4302
|
+
// Workflow-origin routines: side-effect approval gate, then route through gateway.handleWorkflow.
|
|
4303
|
+
const sideEffects = wf.steps
|
|
4304
|
+
.filter(step => {
|
|
4305
|
+
const kind = step.kind ?? 'prompt';
|
|
4306
|
+
if (kind === 'channel' || kind === 'mcp' || kind === 'cli')
|
|
4307
|
+
return true;
|
|
4308
|
+
return /\b(send|post|publish|email|webhook|delete|write|update|create)\b/i.test(step.prompt || '');
|
|
4309
|
+
})
|
|
4310
|
+
.map(step => ({
|
|
4311
|
+
id: step.id,
|
|
4312
|
+
kind: step.kind ?? 'prompt',
|
|
4313
|
+
label: step.channel
|
|
4314
|
+
? `${step.channel.channel}:${step.channel.target}`
|
|
4315
|
+
: step.mcp
|
|
4316
|
+
? `${step.mcp.server}.${step.mcp.tool}`
|
|
4317
|
+
: step.cli
|
|
4318
|
+
? `${step.cli.cmd}${step.cli.args?.length ? ' ' + step.cli.args.join(' ') : ''}`
|
|
4319
|
+
: step.prompt.slice(0, 80),
|
|
4320
|
+
}));
|
|
4321
|
+
if (sideEffects.length > 0 && body.approvedSideEffects !== true) {
|
|
4322
|
+
res.status(409).json({
|
|
4323
|
+
ok: false,
|
|
4324
|
+
error: 'approval_required',
|
|
4325
|
+
message: 'This trick may send, write, post, or call external tools. Approve side effects before running it.',
|
|
4326
|
+
sideEffects,
|
|
4327
|
+
});
|
|
4328
|
+
return;
|
|
4329
|
+
}
|
|
4330
|
+
res.json({ ok: true, message: `Trick "${wf.name}" triggered` });
|
|
4331
|
+
broadcastEvent({ type: 'workflow_triggered', data: { id, name: wf.name } });
|
|
4332
|
+
getGateway().then(gw => gw.handleWorkflow(wf, body.inputs || {})).then(result => {
|
|
4333
|
+
broadcastEvent({ type: 'workflow_complete', data: { id, name: wf.name, status: 'ok', preview: (result || '').slice(0, 300) } });
|
|
4334
|
+
}).catch(err => {
|
|
4335
|
+
broadcastEvent({ type: 'workflow_complete', data: { id, name: wf.name, status: 'error', error: String(err) } });
|
|
4336
|
+
});
|
|
4337
|
+
}
|
|
4338
|
+
catch (err) {
|
|
4339
|
+
res.status(500).json({ error: 'run failed', detail: String(err) });
|
|
4340
|
+
}
|
|
4341
|
+
});
|
|
4342
|
+
app.post('/api/routines/:id/dry-run', async (req, res) => {
|
|
4343
|
+
try {
|
|
4344
|
+
const id = decodeURIComponent(req.params.id);
|
|
4345
|
+
const [{ readWorkflow }, { dryRunWorkflow }] = await Promise.all([
|
|
4346
|
+
import('../dashboard/builder/serializer.js'),
|
|
4347
|
+
import('../dashboard/builder/dry-run.js'),
|
|
4348
|
+
]);
|
|
4349
|
+
const wf = readWorkflow(id);
|
|
4350
|
+
if (!wf) {
|
|
4351
|
+
res.status(404).json({ error: 'Not found' });
|
|
4352
|
+
return;
|
|
4353
|
+
}
|
|
4354
|
+
res.json(dryRunWorkflow(wf));
|
|
4355
|
+
}
|
|
4356
|
+
catch (err) {
|
|
4357
|
+
res.status(500).json({ error: 'dry-run failed', detail: String(err) });
|
|
4358
|
+
}
|
|
4359
|
+
});
|
|
4360
|
+
app.post('/api/routines/:id/test', async (req, res) => {
|
|
4361
|
+
try {
|
|
4362
|
+
const id = decodeURIComponent(req.params.id);
|
|
4363
|
+
const body = (req.body ?? {});
|
|
4364
|
+
const [{ readWorkflow }, { runWorkflowTest }] = await Promise.all([
|
|
4365
|
+
import('../dashboard/builder/serializer.js'),
|
|
4366
|
+
import('../dashboard/builder/runner.js'),
|
|
4367
|
+
]);
|
|
4368
|
+
const wf = readWorkflow(id);
|
|
4369
|
+
if (!wf) {
|
|
4370
|
+
res.status(404).json({ error: 'Not found' });
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
4373
|
+
const runId = (await import('node:crypto')).randomUUID();
|
|
4374
|
+
res.json({ ok: true, runId });
|
|
4375
|
+
runWorkflowTest(wf, {
|
|
4376
|
+
workflowId: id,
|
|
4377
|
+
runId,
|
|
4378
|
+
mode: body.mode ?? 'mock',
|
|
4379
|
+
perStepTimeoutMs: body.perStepTimeoutMs,
|
|
4380
|
+
totalBudgetMs: body.totalBudgetMs,
|
|
4381
|
+
}).catch(() => { });
|
|
4382
|
+
}
|
|
4383
|
+
catch (err) {
|
|
4384
|
+
res.status(500).json({ error: 'test failed to start', detail: String(err) });
|
|
4385
|
+
}
|
|
4386
|
+
});
|
|
4387
|
+
app.get('/api/routines/:id/runs', async (req, res) => {
|
|
4388
|
+
try {
|
|
4389
|
+
const id = decodeURIComponent(req.params.id);
|
|
4390
|
+
const { readWorkflow } = await import('../dashboard/builder/serializer.js');
|
|
4391
|
+
const wf = readWorkflow(id);
|
|
4392
|
+
if (!wf) {
|
|
4393
|
+
res.status(404).json({ error: 'Not found' });
|
|
4394
|
+
return;
|
|
4395
|
+
}
|
|
4396
|
+
const safe = wf.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
4397
|
+
const cronLogPath = path.join(BASE_DIR, 'cron-logs', `${safe}.jsonl`);
|
|
4398
|
+
const workflowLogPath = path.join(BASE_DIR, 'workflows', 'runs', `${safe}.jsonl`);
|
|
4399
|
+
const runs = [];
|
|
4400
|
+
for (const file of [cronLogPath, workflowLogPath]) {
|
|
4401
|
+
if (!existsSync(file))
|
|
4402
|
+
continue;
|
|
4403
|
+
try {
|
|
4404
|
+
const lines = readFileSync(file, 'utf-8').split('\n').filter(l => l.trim());
|
|
4405
|
+
for (const line of lines.slice(-50)) {
|
|
4406
|
+
try {
|
|
4407
|
+
runs.push(JSON.parse(line));
|
|
4408
|
+
}
|
|
4409
|
+
catch { /* skip malformed */ }
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
catch { /* skip unreadable */ }
|
|
4413
|
+
}
|
|
4414
|
+
runs.sort((a, b) => {
|
|
4415
|
+
const at = String(a.startedAt || a.timestamp || '');
|
|
4416
|
+
const bt = String(b.startedAt || b.timestamp || '');
|
|
4417
|
+
return bt.localeCompare(at);
|
|
4418
|
+
});
|
|
4419
|
+
res.json({ runs: runs.slice(0, 50) });
|
|
4420
|
+
}
|
|
4421
|
+
catch (err) {
|
|
4422
|
+
res.status(500).json({ error: 'runs read failed', detail: String(err) });
|
|
4423
|
+
}
|
|
4424
|
+
});
|
|
3992
4425
|
// SSE events handler moved before auth middleware (see above)
|
|
3993
4426
|
// ── POST routes (actions) ──────────────────────────────────────
|
|
3994
4427
|
app.post('/api/cron/run/:job', (req, res) => {
|
|
@@ -7044,6 +7477,55 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
7044
7477
|
res.status(500).json({ error: String(err) });
|
|
7045
7478
|
}
|
|
7046
7479
|
});
|
|
7480
|
+
// Learned facts — durable cross-session beliefs with supersession lineage.
|
|
7481
|
+
app.get('/api/memory/learnings', async (req, res) => {
|
|
7482
|
+
try {
|
|
7483
|
+
const gateway = await getGateway();
|
|
7484
|
+
const store = gateway.assistant?.memoryStore;
|
|
7485
|
+
if (!store || typeof store.listAllLearnedFacts !== 'function') {
|
|
7486
|
+
res.status(503).json({ error: 'Learnings store not available' });
|
|
7487
|
+
return;
|
|
7488
|
+
}
|
|
7489
|
+
const limit = Math.min(parseInt(String(req.query.limit ?? '100'), 10) || 100, 1000);
|
|
7490
|
+
const showAll = String(req.query.all ?? '') === '1';
|
|
7491
|
+
const facts = showAll
|
|
7492
|
+
? store.listAllLearnedFacts({ limit })
|
|
7493
|
+
: store.listActiveLearnedFacts({ limit });
|
|
7494
|
+
res.json({ ok: true, facts });
|
|
7495
|
+
}
|
|
7496
|
+
catch (err) {
|
|
7497
|
+
res.status(500).json({ error: String(err) });
|
|
7498
|
+
}
|
|
7499
|
+
});
|
|
7500
|
+
app.post('/api/memory/learnings/action', async (req, res) => {
|
|
7501
|
+
try {
|
|
7502
|
+
const gateway = await getGateway();
|
|
7503
|
+
const store = gateway.assistant?.memoryStore;
|
|
7504
|
+
if (!store || typeof store.setLearnedFactStatus !== 'function') {
|
|
7505
|
+
res.status(503).json({ error: 'Learnings store not available' });
|
|
7506
|
+
return;
|
|
7507
|
+
}
|
|
7508
|
+
const id = Number(req.body?.id);
|
|
7509
|
+
const action = String(req.body?.action ?? '');
|
|
7510
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
7511
|
+
res.status(400).json({ error: 'id required' });
|
|
7512
|
+
return;
|
|
7513
|
+
}
|
|
7514
|
+
let updated = false;
|
|
7515
|
+
if (action === 'cancel')
|
|
7516
|
+
updated = store.setLearnedFactStatus(id, 'cancelled');
|
|
7517
|
+
else if (action === 'reinstate')
|
|
7518
|
+
updated = store.setLearnedFactStatus(id, 'active');
|
|
7519
|
+
else {
|
|
7520
|
+
res.status(400).json({ error: 'invalid action' });
|
|
7521
|
+
return;
|
|
7522
|
+
}
|
|
7523
|
+
res.json({ ok: updated });
|
|
7524
|
+
}
|
|
7525
|
+
catch (err) {
|
|
7526
|
+
res.status(500).json({ error: String(err) });
|
|
7527
|
+
}
|
|
7528
|
+
});
|
|
7047
7529
|
// Commitments — durable promises tracked across sessions.
|
|
7048
7530
|
app.get('/api/memory/commitments', async (req, res) => {
|
|
7049
7531
|
try {
|
|
@@ -7537,10 +8019,14 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
7537
8019
|
`Help the user think about what makes a good agent: clear role, specific tools, focused personality. Keep it conversational — one question at a time.\n` +
|
|
7538
8020
|
`When the user says "save" or approves, output the final artifact block.]\n\n`
|
|
7539
8021
|
: type === 'workflow'
|
|
7540
|
-
? `[BUILDER MODE: You are helping build a multi-step
|
|
7541
|
-
'```json-artifact\n{"type":"workflow","name":"...","description":"...","schedule":"","steps":"step1:\\n prompt: ...\\nstep2:\\n prompt: ...\\n dependsOn: step1"}\n```\n' +
|
|
7542
|
-
`Update this block in EVERY response
|
|
7543
|
-
`
|
|
8022
|
+
? `[BUILDER MODE: You are helping the user build a "trick" — a (possibly multi-step) thing Clementine can do on a schedule or on demand. As you develop it, output the current state as a JSON block:\n` +
|
|
8023
|
+
'```json-artifact\n{"type":"workflow","name":"...","description":"...","schedule":"","model":"","steps":"step1:\\n prompt: ...\\nstep2:\\n prompt: ...\\n dependsOn: step1"}\n```\n' +
|
|
8024
|
+
`Update this block in EVERY response. Keep it conversational — one question at a time. Ask about (in roughly this order):\n` +
|
|
8025
|
+
` 1. The goal (one sentence is fine).\n` +
|
|
8026
|
+
` 2. When it should run — natural language is fine ("every weekday at 9"); convert to a cron expression in the schedule field. Empty schedule = manual.\n` +
|
|
8027
|
+
` 3. Which tools, projects, or channels she'll need (MCP servers, local CLIs like sf/gh/gcloud, Slack/Discord targets).\n` +
|
|
8028
|
+
` 4. Which model — claude-opus-4-7 (most capable), claude-sonnet-4-6 (balanced), or claude-haiku-4-5-20251001 (fastest). Leave model empty if the user doesn't care.\n` +
|
|
8029
|
+
`Most tricks need only one prompt step. Add steps only when the user explicitly wants a multi-step pipeline.\n` +
|
|
7544
8030
|
`When the user says "save" or approves, output the final artifact block.]\n\n`
|
|
7545
8031
|
: `[BUILDER MODE: You are helping configure an artifact. Output structured JSON blocks as you build.]\n\n`;
|
|
7546
8032
|
enrichedMessage = builderPrefix + fileContext + toolContext + artifactContext + message;
|
|
@@ -13751,6 +14237,75 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
13751
14237
|
font-weight: 500;
|
|
13752
14238
|
}
|
|
13753
14239
|
|
|
14240
|
+
/* Unified Memory tab — facet rail */
|
|
14241
|
+
.vault-facet-list { display: flex; flex-direction: column; gap: 3px; }
|
|
14242
|
+
.vault-facet-row {
|
|
14243
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
14244
|
+
padding: 5px 8px; border-radius: 6px; cursor: pointer;
|
|
14245
|
+
color: var(--text-secondary); font-size: 12px;
|
|
14246
|
+
user-select: none; transition: background var(--motion);
|
|
14247
|
+
}
|
|
14248
|
+
.vault-facet-row:hover { background: var(--bg-hover); color: var(--text-primary); }
|
|
14249
|
+
.vault-facet-row.active { background: var(--clementine-bg); color: var(--clementine); font-weight: 500; }
|
|
14250
|
+
.vault-facet-row .vault-facet-count { font-size: 11px; opacity: 0.6; margin-left: 8px; }
|
|
14251
|
+
.vault-facet-row.active .vault-facet-count { opacity: 0.85; }
|
|
14252
|
+
|
|
14253
|
+
/* Unified Memory tab — file list rows */
|
|
14254
|
+
.vault-mem-row {
|
|
14255
|
+
display: flex; flex-direction: column; gap: 3px;
|
|
14256
|
+
padding: 10px 14px; border-bottom: 1px solid var(--border-light);
|
|
14257
|
+
cursor: pointer; transition: background var(--motion);
|
|
14258
|
+
}
|
|
14259
|
+
.vault-mem-row:hover { background: var(--bg-hover); }
|
|
14260
|
+
.vault-mem-row.active { background: var(--clementine-bg); }
|
|
14261
|
+
.vault-mem-row.active .vault-mem-row-title { color: var(--clementine); }
|
|
14262
|
+
.vault-mem-row-title {
|
|
14263
|
+
font-weight: 500; color: var(--text-primary); font-size: 13px;
|
|
14264
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
14265
|
+
}
|
|
14266
|
+
.vault-mem-row-meta {
|
|
14267
|
+
font-size: 11px; color: var(--text-muted);
|
|
14268
|
+
display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
|
|
14269
|
+
}
|
|
14270
|
+
.vault-mem-row-path {
|
|
14271
|
+
font-family: 'JetBrains Mono', monospace; font-size: 10px;
|
|
14272
|
+
color: var(--text-muted); opacity: 0.75;
|
|
14273
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
14274
|
+
}
|
|
14275
|
+
.vault-pill {
|
|
14276
|
+
display: inline-block; padding: 1px 7px; border-radius: 10px;
|
|
14277
|
+
font-size: 10px; line-height: 1.5; background: var(--bg-tertiary); color: var(--text-secondary);
|
|
14278
|
+
}
|
|
14279
|
+
.vault-pill.type { background: var(--clementine-bg); color: var(--clementine); }
|
|
14280
|
+
.vault-pill.tag { background: var(--bg-tertiary); color: var(--text-secondary); }
|
|
14281
|
+
.vault-pill.match { background: rgba(245,158,11,0.15); color: #d97706; }
|
|
14282
|
+
|
|
14283
|
+
/* Unified Memory tab — reader */
|
|
14284
|
+
.vault-reader-fm {
|
|
14285
|
+
background: var(--bg-secondary); border: 1px solid var(--border-light);
|
|
14286
|
+
border-radius: var(--radius-sm); padding: 10px 14px; margin-bottom: 14px;
|
|
14287
|
+
font-size: 12px; display: grid; grid-template-columns: max-content 1fr;
|
|
14288
|
+
gap: 4px 12px; align-items: baseline;
|
|
14289
|
+
}
|
|
14290
|
+
.vault-reader-fm .k { color: var(--text-muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
14291
|
+
.vault-reader-fm .v { color: var(--text-primary); word-break: break-word; }
|
|
14292
|
+
.vault-reader-body { max-width: 760px; }
|
|
14293
|
+
.vault-reader-body h1 { font-size: 22px; margin: 0 0 12px; }
|
|
14294
|
+
.vault-reader-body h2 { font-size: 17px; margin: 22px 0 8px; padding-top: 8px; border-top: 1px solid var(--border-light); }
|
|
14295
|
+
.vault-reader-body h3 { font-size: 14px; margin: 16px 0 6px; color: var(--text-secondary); }
|
|
14296
|
+
.vault-reader-body p, .vault-reader-body li { font-size: 14px; line-height: 1.65; }
|
|
14297
|
+
.vault-reader-body code { background: var(--bg-tertiary); padding: 1px 5px; border-radius: 3px; font-size: 12px; }
|
|
14298
|
+
.vault-reader-body pre { background: var(--bg-tertiary); padding: 10px 12px; border-radius: 6px; overflow-x: auto; font-size: 12px; }
|
|
14299
|
+
.vault-reader-toc {
|
|
14300
|
+
margin-top: 18px; padding: 10px 14px;
|
|
14301
|
+
background: var(--bg-secondary); border: 1px solid var(--border-light);
|
|
14302
|
+
border-radius: var(--radius-sm); font-size: 12px;
|
|
14303
|
+
}
|
|
14304
|
+
.vault-reader-toc-title { font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); margin-bottom: 6px; }
|
|
14305
|
+
.vault-reader-toc a { display: block; color: var(--text-secondary); text-decoration: none; padding: 2px 0; }
|
|
14306
|
+
.vault-reader-toc a:hover { color: var(--clementine); }
|
|
14307
|
+
.vault-reader-toc a.lvl-3 { padding-left: 14px; font-size: 11px; }
|
|
14308
|
+
|
|
13754
14309
|
/* ── Task Cards ─────────────────────────── */
|
|
13755
14310
|
.task-grid {
|
|
13756
14311
|
display: grid;
|
|
@@ -14477,7 +15032,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
14477
15032
|
<div class="nav-item active" data-page="home" data-icon="home" title="Chat, today, activity">
|
|
14478
15033
|
<span class="nav-icon"></span> Home
|
|
14479
15034
|
</div>
|
|
14480
|
-
<div class="nav-item" data-page="build" data-icon="workflow" title="
|
|
15035
|
+
<div class="nav-item" data-page="build" data-icon="workflow" title="Tricks Clementine knows">
|
|
14481
15036
|
<span class="nav-icon"></span> Build
|
|
14482
15037
|
<span class="nav-badge" id="nav-cron-count" style="display:none">0</span>
|
|
14483
15038
|
</div>
|
|
@@ -14679,189 +15234,876 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
14679
15234
|
</div>
|
|
14680
15235
|
</div>
|
|
14681
15236
|
|
|
14682
|
-
<!-- ═══
|
|
15237
|
+
<!-- ═══ Build Page — Routines (single unified surface) ═══ -->
|
|
14683
15238
|
<div class="page" id="page-build">
|
|
14684
|
-
|
|
14685
|
-
|
|
14686
|
-
<
|
|
14687
|
-
<
|
|
14688
|
-
<
|
|
14689
|
-
</div>
|
|
14690
|
-
<!-- Builder header strip — persists across tabs (except Templates) -->
|
|
14691
|
-
<div id="build-header-strip" style="display:flex;align-items:center;gap:12px;padding:10px 18px;border-bottom:1px solid var(--border)">
|
|
14692
|
-
<select id="builder-type" onchange="resetBuilder();updateBuilderMode()" style="display:none">
|
|
14693
|
-
<option value="skill">skill</option>
|
|
14694
|
-
<option value="cron">cron</option>
|
|
14695
|
-
<option value="agent">agent</option>
|
|
14696
|
-
<option value="workflow">workflow</option>
|
|
14697
|
-
</select>
|
|
14698
|
-
<label style="font-size:11px;color:var(--text-muted);font-weight:500;letter-spacing:0.04em;text-transform:uppercase">Owner</label>
|
|
14699
|
-
<select id="builder-owner" onchange="onBuilderOwnerChange()" title="Filter and create scoped to this owner" style="padding:4px 8px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:12px;min-width:160px">
|
|
14700
|
-
<option value="__all__">All agents</option>
|
|
14701
|
-
<option value="">Clementine (global)</option>
|
|
14702
|
-
</select>
|
|
14703
|
-
<span id="builder-agent-label" style="padding:0;font-size:13px;color:var(--text-secondary);font-weight:500"></span>
|
|
14704
|
-
<input type="hidden" id="builder-agent" value="">
|
|
15239
|
+
<!-- Toolbar -->
|
|
15240
|
+
<div id="routines-toolbar" style="display:flex;align-items:center;gap:12px;padding:14px 18px;border-bottom:1px solid var(--border);background:var(--bg-secondary);flex-wrap:wrap">
|
|
15241
|
+
<h2 style="margin:0;font-size:18px;font-weight:600;color:var(--text-primary);display:flex;align-items:center;gap:8px"><span data-icon="workflow" class="icon-slot"></span> Tricks</h2>
|
|
15242
|
+
<span id="routines-count" style="font-size:11px;color:var(--text-muted)"></span>
|
|
15243
|
+
<span id="routines-editor-breadcrumb" style="display:none;font-size:12px;color:var(--text-muted)"> › <span id="routines-editor-name" style="color:var(--text-primary);font-weight:500"></span></span>
|
|
14705
15244
|
<span style="flex:1"></span>
|
|
14706
|
-
<
|
|
14707
|
-
<
|
|
14708
|
-
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
<
|
|
15245
|
+
<label id="routines-owner-label" style="font-size:11px;color:var(--text-muted);font-weight:500;letter-spacing:0.04em;text-transform:uppercase">Owner</label>
|
|
15246
|
+
<select id="routines-owner-filter" onchange="window.RoutinesUI && RoutinesUI.renderList()" style="padding:5px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:12px;min-width:160px">
|
|
15247
|
+
<option value="__all__">All</option>
|
|
15248
|
+
<option value="__global__">Clementine (global)</option>
|
|
15249
|
+
</select>
|
|
15250
|
+
<button id="routines-back-btn" style="display:none;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary)" onclick="window.RoutinesUI && RoutinesUI.closeEditor()">← Back to list</button>
|
|
15251
|
+
<button id="routines-assist-btn" class="btn-sm" onclick="window.RoutinesUI && RoutinesUI.openCreate()" title="Skip the chat and fill out a form yourself" style="padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary)">Build manually</button>
|
|
15252
|
+
<button id="routines-create-btn" class="btn-sm btn-primary" onclick="window.RoutinesUI && RoutinesUI.openChat()" style="padding:6px 14px;border-radius:6px;cursor:pointer;font-size:12px">+ New Trick</button>
|
|
14712
15253
|
</div>
|
|
14713
|
-
<!--
|
|
14714
|
-
<div id="
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
<div
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
|
|
14721
|
-
|
|
14722
|
-
<button class="btn btn-sm quick-pill" onclick="builderQuick('Create a workflow that reviews open PRs, summarizes risk, and sends a digest')">PR review digest</button>
|
|
14723
|
-
<button class="btn btn-sm quick-pill" onclick="builderQuick('Create a workflow that collects leads, scores them against my ICP, and prepares outreach drafts')">Lead research flow</button>
|
|
14724
|
-
<button class="btn btn-sm quick-pill" onclick="builderQuick('Create a workflow that reviews recent notes, extracts open items, and drafts next priorities')">Weekly review workflow</button>
|
|
14725
|
-
</div>
|
|
14726
|
-
</div>
|
|
14727
|
-
</div>
|
|
14728
|
-
<div id="builder-file-area" style="padding:8px 16px;border-top:1px solid var(--border);background:var(--bg-secondary)">
|
|
14729
|
-
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
|
|
14730
|
-
<span style="font-size:11px;font-weight:600;color:var(--text-secondary)">Reference Files</span>
|
|
14731
|
-
<label style="cursor:pointer;display:inline-flex;align-items:center;gap:4px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:4px;padding:2px 8px;font-size:11px;color:var(--text-primary)">
|
|
14732
|
-
+ Add
|
|
14733
|
-
<input type="file" multiple accept=".csv,.md,.txt,.json,.docx,.xlsx,.yaml,.yml,.xml,.html,.py,.js,.ts" style="display:none" onchange="handleBuilderFileUpload(event)">
|
|
14734
|
-
</label>
|
|
14735
|
-
<span style="font-size:10px;color:var(--text-muted)">Sent with each message so the AI can reference them</span>
|
|
14736
|
-
</div>
|
|
14737
|
-
<div id="builder-attachments-list"></div>
|
|
15254
|
+
<!-- List view (default) -->
|
|
15255
|
+
<div id="routines-list-pane" style="flex:1;min-height:0;overflow-y:auto;padding:18px;background:var(--bg-primary)">
|
|
15256
|
+
<div id="routines-list-empty" class="empty-state" style="display:none;padding:64px 18px;text-align:center;color:var(--text-muted)">
|
|
15257
|
+
<div style="font-size:38px;opacity:0.4;margin-bottom:14px">⚙</div>
|
|
15258
|
+
<div style="font-size:15px;font-weight:500;color:var(--text-secondary);margin-bottom:6px">No tricks yet</div>
|
|
15259
|
+
<div style="font-size:12px;line-height:1.5;max-width:380px;margin:0 auto 16px">A Trick is a sequence of steps Clementine performs on cue — call MCP tools, run local CLIs, prompt the agent, branch on results — that runs on a schedule or on demand. Example: “at 8am check email; if anything urgent, summarize and Slack me.”</div>
|
|
15260
|
+
<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap">
|
|
15261
|
+
<button class="btn-sm btn-primary" onclick="window.RoutinesUI && RoutinesUI.openChat()" style="padding:6px 14px">+ New Trick</button>
|
|
15262
|
+
<button class="btn-sm" onclick="window.RoutinesUI && RoutinesUI.openCreate()" style="padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary)">Build manually</button>
|
|
14738
15263
|
</div>
|
|
14739
|
-
|
|
14740
|
-
|
|
14741
|
-
|
|
14742
|
-
|
|
14743
|
-
|
|
14744
|
-
|
|
15264
|
+
</div>
|
|
15265
|
+
<div id="routines-list-wrap" style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden">
|
|
15266
|
+
<table style="width:100%;border-collapse:collapse;font-size:13px">
|
|
15267
|
+
<thead style="text-align:left;color:var(--text-muted);font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.04em;background:var(--bg-tertiary)">
|
|
15268
|
+
<tr>
|
|
15269
|
+
<th style="padding:11px 14px">Name</th>
|
|
15270
|
+
<th style="padding:11px 14px">Owner</th>
|
|
15271
|
+
<th style="padding:11px 14px">Schedule</th>
|
|
15272
|
+
<th style="padding:11px 14px">Steps</th>
|
|
15273
|
+
<th style="padding:11px 14px">Last run</th>
|
|
15274
|
+
<th style="padding:11px 14px;text-align:center">Enabled</th>
|
|
15275
|
+
<th style="padding:11px 14px;text-align:right">Actions</th>
|
|
15276
|
+
</tr>
|
|
15277
|
+
</thead>
|
|
15278
|
+
<tbody id="routines-list-body"></tbody>
|
|
15279
|
+
</table>
|
|
15280
|
+
</div>
|
|
15281
|
+
</div>
|
|
15282
|
+
<!-- Editor pane (hidden by default; replaces list when a routine is opened) -->
|
|
15283
|
+
<div id="routines-editor-pane" style="display:none;flex:1;min-height:0;overflow-y:auto;background:var(--bg-primary);padding:18px"></div>
|
|
15284
|
+
<!-- Run history drawer (slide-out from right) -->
|
|
15285
|
+
<div id="routines-runs-drawer" style="display:none;position:fixed;top:var(--header-h, 56px);right:0;bottom:0;width:520px;max-width:100vw;background:var(--bg-secondary);border-left:1px solid var(--border);box-shadow:-4px 0 24px rgba(0,0,0,0.18);z-index:120;overflow-y:auto"></div>
|
|
15286
|
+
<!-- Create modal -->
|
|
15287
|
+
<div id="routines-create-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:200;align-items:center;justify-content:center">
|
|
15288
|
+
<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:22px;width:480px;max-width:92vw;display:flex;flex-direction:column;gap:12px">
|
|
15289
|
+
<h3 style="margin:0;font-size:16px;font-weight:600;color:var(--text-primary)">New trick</h3>
|
|
15290
|
+
<label style="font-size:11px;color:var(--text-muted);font-weight:500;text-transform:uppercase;letter-spacing:0.04em">Name</label>
|
|
15291
|
+
<input type="text" id="routines-create-name" placeholder="e.g. 8am Email Triage" style="padding:8px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px">
|
|
15292
|
+
<label style="font-size:11px;color:var(--text-muted);font-weight:500;text-transform:uppercase;letter-spacing:0.04em">Description (optional)</label>
|
|
15293
|
+
<input type="text" id="routines-create-description" placeholder="What does it do?" style="padding:8px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px">
|
|
15294
|
+
<label style="font-size:11px;color:var(--text-muted);font-weight:500;text-transform:uppercase;letter-spacing:0.04em">Schedule (optional cron expression)</label>
|
|
15295
|
+
<input type="text" id="routines-create-schedule" placeholder="e.g. 0 8 * * * (leave blank for manual)" style="padding:8px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px;font-family:'JetBrains Mono',monospace">
|
|
15296
|
+
<label style="font-size:11px;color:var(--text-muted);font-weight:500;text-transform:uppercase;letter-spacing:0.04em">Owner</label>
|
|
15297
|
+
<select id="routines-create-owner" style="padding:8px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px">
|
|
15298
|
+
<option value="">Clementine (global)</option>
|
|
15299
|
+
</select>
|
|
15300
|
+
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:6px">
|
|
15301
|
+
<button class="btn-sm" onclick="window.RoutinesUI && RoutinesUI.closeCreate()" style="padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary)">Cancel</button>
|
|
15302
|
+
<button class="btn-sm btn-primary" onclick="window.RoutinesUI && RoutinesUI.submitCreate()" style="padding:6px 14px">Create</button>
|
|
14745
15303
|
</div>
|
|
14746
15304
|
</div>
|
|
14747
|
-
|
|
14748
|
-
|
|
14749
|
-
|
|
14750
|
-
|
|
14751
|
-
|
|
15305
|
+
</div>
|
|
15306
|
+
<!-- Chat-first builder modal — multi-turn conversation that drafts a trick spec live -->
|
|
15307
|
+
<div id="routines-chat-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:200;align-items:center;justify-content:center">
|
|
15308
|
+
<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);width:760px;max-width:96vw;max-height:88vh;display:flex;flex-direction:column;overflow:hidden">
|
|
15309
|
+
<div style="padding:14px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px">
|
|
15310
|
+
<h3 style="margin:0;font-size:15px;font-weight:600;color:var(--text-primary)">Build a trick with Clementine</h3>
|
|
14752
15311
|
<span style="flex:1"></span>
|
|
14753
|
-
<
|
|
14754
|
-
<option value="">— pick a workflow —</option>
|
|
14755
|
-
</select>
|
|
14756
|
-
<button id="builder-canvas-validate-btn" onclick="validateBuilderCanvas()" title="Static checks (cycles, missing fields, deps)" style="display:none;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);padding:3px 8px;border-radius:4px;cursor:pointer;font-size:11px">Validate</button>
|
|
14757
|
-
<button id="builder-canvas-dryrun-btn" onclick="dryRunBuilderCanvas()" title="Describe what each step would do (no execution)" style="display:none;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);padding:3px 8px;border-radius:4px;cursor:pointer;font-size:11px">Dry-run</button>
|
|
14758
|
-
<button id="builder-canvas-test-btn" onclick="testBuilderCanvas()" title="Mock-safe validation run; prompt steps are stubbed" style="display:none;background:var(--clementine);border:none;color:#fff;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:11px">Mock Test</button>
|
|
14759
|
-
<button id="builder-canvas-real-run-btn" onclick="runBuilderCanvasReal()" title="Run through the real workflow engine; side effects require approval" style="display:none;background:var(--green);border:none;color:#fff;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:11px">Run Real</button>
|
|
14760
|
-
<button id="builder-canvas-cancel-btn" onclick="cancelBuilderTest()" title="Cancel test run" style="display:none;background:var(--red);border:none;color:#fff;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:11px">Cancel</button>
|
|
15312
|
+
<button class="btn-sm" onclick="window.RoutinesUI && RoutinesUI.closeChat()" style="padding:4px 10px;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary)">×</button>
|
|
14761
15313
|
</div>
|
|
14762
|
-
|
|
14763
|
-
|
|
14764
|
-
|
|
14765
|
-
<div id="
|
|
14766
|
-
|
|
14767
|
-
|
|
14768
|
-
|
|
14769
|
-
|
|
14770
|
-
|
|
14771
|
-
<div onclick="_builderAddNodeOfKind('prompt')" class="builder-palette-item" data-kind="prompt">prompt</div>
|
|
14772
|
-
<div onclick="_builderAddNodeOfKind('mcp')" class="builder-palette-item" data-kind="mcp">mcp tool</div>
|
|
14773
|
-
<div onclick="_builderAddNodeOfKind('channel')" class="builder-palette-item" data-kind="channel">channel</div>
|
|
14774
|
-
<div onclick="_builderAddNodeOfKind('transform')" class="builder-palette-item" data-kind="transform">transform</div>
|
|
14775
|
-
</div>
|
|
14776
|
-
<!-- Slide-out config panel -->
|
|
14777
|
-
<div id="builder-config-panel" style="display:none;position:absolute;right:0;top:0;bottom:0;width:340px;background:var(--bg-secondary);border-left:1px solid var(--border);box-shadow:-4px 0 16px rgba(0,0,0,0.15);z-index:12;flex-direction:column"></div>
|
|
14778
|
-
<!-- Empty-state CTA — visible when no workflow is open on the canvas -->
|
|
14779
|
-
<div id="builder-canvas-empty" style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:14px;color:var(--text-muted);text-align:center;padding:32px;pointer-events:none">
|
|
14780
|
-
<div style="font-size:38px;opacity:0.4">🔗</div>
|
|
14781
|
-
<div style="font-size:14px;font-weight:500;color:var(--text-secondary)">No workflow open</div>
|
|
14782
|
-
<div style="font-size:12px;line-height:1.5;max-width:280px">
|
|
14783
|
-
Pick one from the dropdown above — or click <strong>New</strong> in the header to create one from scratch, or open the <strong>Templates</strong> tab for starter patterns.
|
|
14784
|
-
</div>
|
|
14785
|
-
</div>
|
|
14786
|
-
<div id="builder-canvas-footer" style="padding:6px 14px;border-top:1px solid var(--border);font-size:11px;color:var(--text-muted);display:flex;gap:14px;align-items:center">
|
|
14787
|
-
<span id="builder-canvas-status"></span>
|
|
14788
|
-
<span style="flex:1"></span>
|
|
14789
|
-
<button id="builder-delete-btn" onclick="deleteCurrentBuilderWorkflow()" title="Delete this workflow" style="display:none;background:none;border:1px solid transparent;color:var(--red);font-size:11px;cursor:pointer;padding:2px 8px;border-radius:var(--radius-xs)">Delete</button>
|
|
14790
|
-
<span id="builder-canvas-id" style="font-family:'JetBrains Mono',monospace;opacity:0.6"></span>
|
|
15314
|
+
<!-- Live spec preview -->
|
|
15315
|
+
<div id="routines-chat-preview" style="display:none;padding:10px 18px;border-bottom:1px solid var(--border);background:var(--bg-tertiary);font-size:12px;color:var(--text-secondary)"></div>
|
|
15316
|
+
<!-- Messages -->
|
|
15317
|
+
<div id="routines-chat-messages" style="flex:1;min-height:240px;overflow-y:auto;padding:14px 18px;display:flex;flex-direction:column;gap:10px"></div>
|
|
15318
|
+
<!-- Composer -->
|
|
15319
|
+
<div style="padding:12px 18px;border-top:1px solid var(--border);background:var(--bg-secondary)">
|
|
15320
|
+
<div style="display:flex;gap:8px;align-items:flex-end">
|
|
15321
|
+
<textarea id="routines-chat-input" rows="2" placeholder="Tell Clementine what you want her to do…" style="flex:1;padding:8px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px;font-family:inherit;resize:vertical;box-sizing:border-box" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();window.RoutinesUI&&RoutinesUI.sendChat();}"></textarea>
|
|
15322
|
+
<button id="routines-chat-send" class="btn-sm btn-primary" onclick="window.RoutinesUI && RoutinesUI.sendChat()" style="padding:8px 16px;align-self:flex-end">Send</button>
|
|
14791
15323
|
</div>
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
<span style="font-size:12px;font-weight:600;color:var(--text-secondary)">Existing Skills</span>
|
|
14797
|
-
<span id="builder-skills-count" style="font-size:10px;color:var(--text-muted)"></span>
|
|
15324
|
+
<div style="display:flex;align-items:center;gap:10px;margin-top:8px;font-size:11px">
|
|
15325
|
+
<span id="routines-chat-status" style="color:var(--text-muted);flex:1;min-height:14px"></span>
|
|
15326
|
+
<button id="routines-chat-save" class="btn-sm btn-primary" onclick="window.RoutinesUI && RoutinesUI.saveChatDraft()" style="display:none;padding:5px 12px;font-size:11px">Save trick</button>
|
|
15327
|
+
<button class="btn-sm" onclick="window.RoutinesUI && RoutinesUI.closeChat(true);RoutinesUI.openCreate();" style="padding:4px 10px;background:transparent;border:none;color:var(--text-muted);font-size:11px;cursor:pointer;text-decoration:underline">Build manually instead</button>
|
|
14798
15328
|
</div>
|
|
14799
|
-
<div id="builder-skills-list" style="padding:0 12px 12px"></div>
|
|
14800
|
-
</div>
|
|
14801
|
-
</div>
|
|
14802
|
-
</div>
|
|
14803
|
-
|
|
14804
|
-
<!-- Scheduled Tasks tab — simple recurring-task management. The visual canvas is for workflows. -->
|
|
14805
|
-
<div id="build-tab-crons" data-build-tabpane="crons" style="display:block;flex:1;min-height:0;overflow-y:auto;padding:18px;background:var(--bg-primary)">
|
|
14806
|
-
<div style="display:flex;align-items:flex-start;gap:14px;margin-bottom:16px;flex-wrap:wrap">
|
|
14807
|
-
<div style="flex:1;min-width:260px">
|
|
14808
|
-
<h2 style="font-size:18px;font-weight:600;margin:0 0 4px;color:var(--text-primary)">Build Operations</h2>
|
|
14809
|
-
<p style="font-size:13px;color:var(--text-muted);margin:0;line-height:1.45">Scheduled tasks, scheduled workflows, and live runtime work. Cards here are the turn-on, turn-off, run-now surface.</p>
|
|
14810
|
-
</div>
|
|
14811
|
-
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
14812
|
-
<button class="btn-sm btn-primary" onclick="openCreateCronModal(getBuildCreateOwner())" style="padding:7px 14px;border-radius:6px;cursor:pointer;font-size:12px">New Scheduled Task</button>
|
|
14813
|
-
<button class="btn-sm" onclick="createScheduledWorkflowFromBuild()" style="padding:7px 14px;border-radius:6px;cursor:pointer;font-size:12px">New Scheduled Workflow</button>
|
|
14814
15329
|
</div>
|
|
14815
15330
|
</div>
|
|
14816
|
-
<div id="panel-cron"></div>
|
|
14817
15331
|
</div>
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
|
|
14821
|
-
|
|
14822
|
-
|
|
14823
|
-
|
|
14824
|
-
|
|
14825
|
-
<div class="card clickable-row" onclick="forkBuildTemplate('daily-news-digest')" style="padding:18px">
|
|
14826
|
-
<div style="font-size:24px;margin-bottom:8px">📰</div>
|
|
14827
|
-
<div style="font-weight:600;font-size:14px;margin-bottom:4px">Daily news digest</div>
|
|
14828
|
-
<div style="font-size:12px;color:var(--text-muted);line-height:1.4">Scheduled 7am: pull RSS sources, summarize, send to Slack/email.</div>
|
|
14829
|
-
<div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">scheduled workflow · 4 steps</div>
|
|
14830
|
-
</div>
|
|
14831
|
-
<div class="card clickable-row" onclick="forkBuildTemplate('lead-picker')" style="padding:18px">
|
|
14832
|
-
<div style="font-size:24px;margin-bottom:8px">📊</div>
|
|
14833
|
-
<div style="font-weight:600;font-size:14px;margin-bottom:4px">Lead picker → Salesforce</div>
|
|
14834
|
-
<div style="font-size:12px;color:var(--text-muted);line-height:1.4">Manual workflow: search leads by ICP, review, push selected to SF.</div>
|
|
14835
|
-
<div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">manual · 3 steps</div>
|
|
14836
|
-
</div>
|
|
14837
|
-
<div class="card clickable-row" onclick="forkBuildTemplate('pr-review-queue')" style="padding:18px">
|
|
14838
|
-
<div style="font-size:24px;margin-bottom:8px">📝</div>
|
|
14839
|
-
<div style="font-weight:600;font-size:14px;margin-bottom:4px">PR review queue</div>
|
|
14840
|
-
<div style="font-size:12px;color:var(--text-muted);line-height:1.4">Scheduled 9am M-F: list open PRs, summarize risk, message to Slack.</div>
|
|
14841
|
-
<div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">scheduled workflow · 3 steps</div>
|
|
14842
|
-
</div>
|
|
14843
|
-
<div class="card clickable-row" onclick="forkBuildTemplate('email-triage')" style="padding:18px">
|
|
14844
|
-
<div style="font-size:24px;margin-bottom:8px">📧</div>
|
|
14845
|
-
<div style="font-weight:600;font-size:14px;margin-bottom:4px">Email triage</div>
|
|
14846
|
-
<div style="font-size:12px;color:var(--text-muted);line-height:1.4">Scheduled 8am: list unread emails, classify by intent, draft replies for review.</div>
|
|
14847
|
-
<div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">scheduled workflow · 4 steps</div>
|
|
14848
|
-
</div>
|
|
14849
|
-
<div class="card clickable-row" onclick="forkBuildTemplate('weekly-review')" style="padding:18px">
|
|
14850
|
-
<div style="font-size:24px;margin-bottom:8px">📅</div>
|
|
14851
|
-
<div style="font-weight:600;font-size:14px;margin-bottom:4px">Weekly review</div>
|
|
14852
|
-
<div style="font-size:12px;color:var(--text-muted);line-height:1.4">Scheduled Fri 6pm: review the week's daily notes, generate review note.</div>
|
|
14853
|
-
<div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">scheduled workflow · 3 steps</div>
|
|
14854
|
-
</div>
|
|
14855
|
-
<div class="card clickable-row" onclick="forkBuildTemplate('blank-workflow')" style="padding:18px;border-style:dashed">
|
|
14856
|
-
<div style="font-size:24px;margin-bottom:8px">➕</div>
|
|
14857
|
-
<div style="font-weight:600;font-size:14px;margin-bottom:4px">Blank workflow</div>
|
|
14858
|
-
<div style="font-size:12px;color:var(--text-muted);line-height:1.4">Start from scratch with a single prompt step.</div>
|
|
14859
|
-
<div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">manual · 1 step</div>
|
|
14860
|
-
</div>
|
|
15332
|
+
<!-- Step picker modal -->
|
|
15333
|
+
<div id="routines-step-picker" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:200;align-items:center;justify-content:center">
|
|
15334
|
+
<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:0;width:640px;max-width:94vw;max-height:80vh;display:flex;flex-direction:column;overflow:hidden">
|
|
15335
|
+
<div style="padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px">
|
|
15336
|
+
<h3 style="margin:0;font-size:15px;font-weight:600;color:var(--text-primary)">Add step</h3>
|
|
15337
|
+
<span style="flex:1"></span>
|
|
15338
|
+
<button class="btn-sm" onclick="window.RoutinesUI && RoutinesUI.closeStepPicker()" style="padding:4px 10px;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary)">×</button>
|
|
14861
15339
|
</div>
|
|
15340
|
+
<div id="routines-step-picker-body" style="padding:16px 20px;overflow-y:auto;flex:1"></div>
|
|
14862
15341
|
</div>
|
|
14863
15342
|
</div>
|
|
14864
15343
|
</div>
|
|
15344
|
+
<script>
|
|
15345
|
+
// ── Routines UI ─────────────────────────────────────────────────
|
|
15346
|
+
// Vanilla JS module that drives the new Routines surface. State-light,
|
|
15347
|
+
// re-fetches from /api/routines on most actions to stay correct without
|
|
15348
|
+
// a client-side data store. Renders linear step lists; no canvas.
|
|
15349
|
+
(function() {
|
|
15350
|
+
var R = {
|
|
15351
|
+
state: {
|
|
15352
|
+
list: [],
|
|
15353
|
+
owners: [],
|
|
15354
|
+
mcpTools: null, // { servers: [{name,enabled,tools:[]}, ...] }
|
|
15355
|
+
cliTools: null, // [{cmd,description,userDefined}, ...]
|
|
15356
|
+
editing: null, // { id, routine, dirty }
|
|
15357
|
+
assistBusy: false,
|
|
15358
|
+
},
|
|
15359
|
+
init: function() {
|
|
15360
|
+
// Load reference data lazily; trigger list render immediately.
|
|
15361
|
+
this.loadOwners();
|
|
15362
|
+
this.refreshList();
|
|
15363
|
+
this.loadMcpTools();
|
|
15364
|
+
this.loadCliTools();
|
|
15365
|
+
},
|
|
15366
|
+
// ── data ────────────────────────────────────────────────────
|
|
15367
|
+
loadOwners: function() {
|
|
15368
|
+
// Reuse the agent registry the rest of the dashboard uses.
|
|
15369
|
+
apiFetch('/api/agents').then(function(r){ return r.json(); }).then(function(data){
|
|
15370
|
+
R.state.owners = (data.agents || []).map(function(a){ return { slug: a.slug, name: a.name || a.slug }; });
|
|
15371
|
+
R.populateOwnerSelects();
|
|
15372
|
+
}).catch(function(){ /* non-fatal */ });
|
|
15373
|
+
},
|
|
15374
|
+
populateOwnerSelects: function() {
|
|
15375
|
+
var filter = document.getElementById('routines-owner-filter');
|
|
15376
|
+
var creator = document.getElementById('routines-create-owner');
|
|
15377
|
+
var keepFilter = filter && filter.value;
|
|
15378
|
+
if (filter) {
|
|
15379
|
+
filter.innerHTML = '<option value="__all__">All</option><option value="__global__">Clementine (global)</option>'
|
|
15380
|
+
+ R.state.owners.map(function(o){ return '<option value="' + R.esc(o.slug) + '">@' + R.esc(o.name) + '</option>'; }).join('');
|
|
15381
|
+
if (keepFilter) filter.value = keepFilter;
|
|
15382
|
+
}
|
|
15383
|
+
if (creator) {
|
|
15384
|
+
creator.innerHTML = '<option value="">Clementine (global)</option>'
|
|
15385
|
+
+ R.state.owners.map(function(o){ return '<option value="' + R.esc(o.slug) + '">@' + R.esc(o.name) + '</option>'; }).join('');
|
|
15386
|
+
}
|
|
15387
|
+
},
|
|
15388
|
+
loadMcpTools: function() {
|
|
15389
|
+
apiFetch('/api/routines/mcp-tools').then(function(r){ return r.json(); }).then(function(data){
|
|
15390
|
+
R.state.mcpTools = data && data.servers ? data : { servers: [] };
|
|
15391
|
+
}).catch(function(){ R.state.mcpTools = { servers: [] }; });
|
|
15392
|
+
},
|
|
15393
|
+
loadCliTools: function() {
|
|
15394
|
+
apiFetch('/api/routines/cli-tools').then(function(r){ return r.json(); }).then(function(data){
|
|
15395
|
+
R.state.cliTools = (data && data.tools) || [];
|
|
15396
|
+
}).catch(function(){ R.state.cliTools = []; });
|
|
15397
|
+
},
|
|
15398
|
+
refreshList: function() {
|
|
15399
|
+
apiFetch('/api/routines').then(function(r){ return r.json(); }).then(function(data){
|
|
15400
|
+
R.state.list = (data && data.routines) || [];
|
|
15401
|
+
R.renderList();
|
|
15402
|
+
}).catch(function(){ R.state.list = []; R.renderList(); });
|
|
15403
|
+
},
|
|
15404
|
+
// ── list view ───────────────────────────────────────────────
|
|
15405
|
+
renderList: function() {
|
|
15406
|
+
var body = document.getElementById('routines-list-body');
|
|
15407
|
+
var empty = document.getElementById('routines-list-empty');
|
|
15408
|
+
var wrap = document.getElementById('routines-list-wrap');
|
|
15409
|
+
var count = document.getElementById('routines-count');
|
|
15410
|
+
if (!body || !empty || !wrap) return;
|
|
15411
|
+
var filter = (document.getElementById('routines-owner-filter') || {}).value || '__all__';
|
|
15412
|
+
var rows = R.state.list.filter(function(r){
|
|
15413
|
+
if (filter === '__all__') return true;
|
|
15414
|
+
if (filter === '__global__') return r.scope === 'global';
|
|
15415
|
+
return r.scope === 'agent' && r.agentSlug === filter;
|
|
15416
|
+
});
|
|
15417
|
+
if (count) count.textContent = rows.length === 0 ? '' : rows.length + (rows.length === 1 ? ' trick' : ' tricks');
|
|
15418
|
+
if (rows.length === 0) {
|
|
15419
|
+
empty.style.display = 'block';
|
|
15420
|
+
wrap.style.display = 'none';
|
|
15421
|
+
return;
|
|
15422
|
+
}
|
|
15423
|
+
empty.style.display = 'none';
|
|
15424
|
+
wrap.style.display = 'block';
|
|
15425
|
+
body.innerHTML = rows.map(function(r){
|
|
15426
|
+
var owner = r.scope === 'agent' ? '@' + R.esc(r.agentSlug || '?') : 'Clementine';
|
|
15427
|
+
var schedule = r.schedule ? '<code style="font-family:\\x27JetBrains Mono\\x27,monospace;font-size:11px">' + R.esc(r.schedule) + '</code>' : '<span style="color:var(--text-muted)">manual</span>';
|
|
15428
|
+
var enabledBadge = '<input type="checkbox" ' + (r.enabled ? 'checked' : '') + ' onchange="event.stopPropagation();window.RoutinesUI&&RoutinesUI.toggle(\\x27' + R.esc(r.id) + '\\x27)" style="cursor:pointer">';
|
|
15429
|
+
var origin = r.origin === 'cron' ? '<span title="Legacy cron entry — single prompt step" style="font-size:10px;color:var(--text-muted);margin-left:6px">[cron]</span>' : '';
|
|
15430
|
+
return '<tr style="border-top:1px solid var(--border);cursor:pointer" onclick="window.RoutinesUI&&RoutinesUI.openEditor(\\x27' + R.esc(r.id) + '\\x27)">'
|
|
15431
|
+
+ '<td style="padding:11px 14px;color:var(--text-primary);font-weight:500">' + R.esc(r.name) + origin + '</td>'
|
|
15432
|
+
+ '<td style="padding:11px 14px;color:var(--text-secondary);font-size:12px">' + R.esc(owner) + '</td>'
|
|
15433
|
+
+ '<td style="padding:11px 14px">' + schedule + '</td>'
|
|
15434
|
+
+ '<td style="padding:11px 14px;color:var(--text-secondary);font-size:12px">' + r.stepCount + '</td>'
|
|
15435
|
+
+ '<td style="padding:11px 14px;color:var(--text-muted);font-size:12px">—</td>'
|
|
15436
|
+
+ '<td style="padding:11px 14px;text-align:center" onclick="event.stopPropagation()">' + enabledBadge + '</td>'
|
|
15437
|
+
+ '<td style="padding:11px 14px;text-align:right;white-space:nowrap" onclick="event.stopPropagation()">'
|
|
15438
|
+
+ '<button class="btn-sm" title="Run now" onclick="window.RoutinesUI&&RoutinesUI.run(\\x27' + R.esc(r.id) + '\\x27)" style="padding:4px 10px;border:1px solid var(--border);background:var(--bg-tertiary);color:var(--text-primary);border-radius:4px;cursor:pointer;font-size:11px;margin-right:4px">▶ Run</button>'
|
|
15439
|
+
+ '<button class="btn-sm" title="Run history" onclick="window.RoutinesUI&&RoutinesUI.openRuns(\\x27' + R.esc(r.id) + '\\x27)" style="padding:4px 10px;border:1px solid var(--border);background:var(--bg-tertiary);color:var(--text-secondary);border-radius:4px;cursor:pointer;font-size:11px">History</button>'
|
|
15440
|
+
+ '</td></tr>';
|
|
15441
|
+
}).join('');
|
|
15442
|
+
},
|
|
15443
|
+
toggle: function(id) {
|
|
15444
|
+
apiFetch('/api/routines/' + encodeURIComponent(id) + '/toggle', { method: 'POST' })
|
|
15445
|
+
.then(function(r){ return r.json(); })
|
|
15446
|
+
.then(function(){ R.refreshList(); })
|
|
15447
|
+
.catch(function(err){ alert('Toggle failed: ' + err); });
|
|
15448
|
+
},
|
|
15449
|
+
run: function(id, approvedSideEffects) {
|
|
15450
|
+
apiFetch('/api/routines/' + encodeURIComponent(id) + '/run', {
|
|
15451
|
+
method: 'POST',
|
|
15452
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15453
|
+
body: JSON.stringify({ approvedSideEffects: approvedSideEffects === true })
|
|
15454
|
+
}).then(function(r){
|
|
15455
|
+
if (r.status === 409) {
|
|
15456
|
+
return r.json().then(function(j){
|
|
15457
|
+
var lines = (j.sideEffects || []).map(function(s){ return ' • ' + s.kind + ': ' + s.label; }).join('\\n');
|
|
15458
|
+
if (confirm('This trick has side effects:\\n\\n' + lines + '\\n\\nProceed?')) R.run(id, true);
|
|
15459
|
+
});
|
|
15460
|
+
}
|
|
15461
|
+
return r.json().then(function(j){
|
|
15462
|
+
if (j.ok) R.flash('Triggered.');
|
|
15463
|
+
else alert('Run failed: ' + (j.error || 'unknown'));
|
|
15464
|
+
});
|
|
15465
|
+
}).catch(function(err){ alert('Run failed: ' + err); });
|
|
15466
|
+
},
|
|
15467
|
+
// ── editor ──────────────────────────────────────────────────
|
|
15468
|
+
openEditor: function(id) {
|
|
15469
|
+
apiFetch('/api/routines/' + encodeURIComponent(id))
|
|
15470
|
+
.then(function(r){ return r.json(); })
|
|
15471
|
+
.then(function(data){
|
|
15472
|
+
if (!data || !data.routine) { alert('Failed to load trick'); return; }
|
|
15473
|
+
R.state.editing = { id: data.id, routine: data.routine, dirty: false, validation: data.validation };
|
|
15474
|
+
R.showEditor();
|
|
15475
|
+
}).catch(function(err){ alert('Open failed: ' + err); });
|
|
15476
|
+
},
|
|
15477
|
+
showEditor: function() {
|
|
15478
|
+
document.getElementById('routines-list-pane').style.display = 'none';
|
|
15479
|
+
document.getElementById('routines-editor-pane').style.display = 'block';
|
|
15480
|
+
document.getElementById('routines-back-btn').style.display = 'inline-block';
|
|
15481
|
+
document.getElementById('routines-create-btn').style.display = 'none';
|
|
15482
|
+
document.getElementById('routines-assist-btn').style.display = 'none';
|
|
15483
|
+
document.getElementById('routines-owner-filter').style.display = 'none';
|
|
15484
|
+
document.getElementById('routines-owner-label').style.display = 'none';
|
|
15485
|
+
document.getElementById('routines-editor-breadcrumb').style.display = 'inline';
|
|
15486
|
+
document.getElementById('routines-editor-name').textContent = R.state.editing.routine.name;
|
|
15487
|
+
R.renderEditor();
|
|
15488
|
+
},
|
|
15489
|
+
closeEditor: function() {
|
|
15490
|
+
if (R.state.editing && R.state.editing.dirty && !confirm('Discard unsaved changes?')) return;
|
|
15491
|
+
R.state.editing = null;
|
|
15492
|
+
document.getElementById('routines-list-pane').style.display = 'block';
|
|
15493
|
+
document.getElementById('routines-editor-pane').style.display = 'none';
|
|
15494
|
+
document.getElementById('routines-back-btn').style.display = 'none';
|
|
15495
|
+
document.getElementById('routines-create-btn').style.display = 'inline-block';
|
|
15496
|
+
document.getElementById('routines-assist-btn').style.display = 'inline-block';
|
|
15497
|
+
document.getElementById('routines-owner-filter').style.display = 'inline-block';
|
|
15498
|
+
document.getElementById('routines-owner-label').style.display = 'inline';
|
|
15499
|
+
document.getElementById('routines-editor-breadcrumb').style.display = 'none';
|
|
15500
|
+
R.refreshList();
|
|
15501
|
+
},
|
|
15502
|
+
renderEditor: function() {
|
|
15503
|
+
var pane = document.getElementById('routines-editor-pane');
|
|
15504
|
+
if (!pane || !R.state.editing) return;
|
|
15505
|
+
var wf = R.state.editing.routine;
|
|
15506
|
+
var html = '<div style="max-width:920px;margin:0 auto">';
|
|
15507
|
+
html += '<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px;margin-bottom:14px">'
|
|
15508
|
+
+ '<div style="display:flex;align-items:center;gap:12px;margin-bottom:10px">'
|
|
15509
|
+
+ '<input type="text" id="re-name" value="' + R.esc(wf.name) + '" oninput="window.RoutinesUI&&RoutinesUI.markDirty()" style="flex:1;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:14px;font-weight:600">'
|
|
15510
|
+
+ '<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-secondary)"><input type="checkbox" id="re-enabled" ' + (wf.enabled ? 'checked' : '') + ' onchange="window.RoutinesUI&&RoutinesUI.markDirty()"> Enabled</label>'
|
|
15511
|
+
+ '</div>'
|
|
15512
|
+
+ '<input type="text" id="re-description" value="' + R.esc(wf.description || '') + '" placeholder="Description (optional)" oninput="window.RoutinesUI&&RoutinesUI.markDirty()" style="width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px;margin-bottom:10px;box-sizing:border-box">'
|
|
15513
|
+
+ '<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px"><label style="font-size:11px;color:var(--text-muted);font-weight:500;text-transform:uppercase;letter-spacing:0.04em;min-width:62px">Schedule</label>'
|
|
15514
|
+
+ '<input type="text" id="re-schedule" value="' + R.esc(wf.trigger && wf.trigger.schedule || '') + '" placeholder="cron e.g. 0 8 * * * (blank = manual)" oninput="window.RoutinesUI&&RoutinesUI.markDirty()" style="flex:1;min-width:240px;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:12px;font-family:\\x27JetBrains Mono\\x27,monospace">'
|
|
15515
|
+
+ '</div>'
|
|
15516
|
+
+ '<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap"><label style="font-size:11px;color:var(--text-muted);font-weight:500;text-transform:uppercase;letter-spacing:0.04em;min-width:62px">Model</label>'
|
|
15517
|
+
+ R.modelSelect('re-model', wf.model || '', 'Default for prompt steps that don\\x27t override')
|
|
15518
|
+
+ '</div></div>';
|
|
15519
|
+
// Steps
|
|
15520
|
+
html += '<div style="font-size:11px;color:var(--text-muted);font-weight:600;text-transform:uppercase;letter-spacing:0.04em;margin:18px 0 8px">Steps</div>';
|
|
15521
|
+
html += '<div id="re-steps-list">' + (wf.steps || []).map(function(s, i){ return R.renderStepCard(s, i); }).join('') + '</div>';
|
|
15522
|
+
html += '<button class="btn-sm" onclick="window.RoutinesUI&&RoutinesUI.openStepPicker()" style="margin-top:8px;padding:8px 14px;border:1px dashed var(--border);background:transparent;color:var(--text-secondary);border-radius:6px;cursor:pointer;font-size:12px;width:100%">+ Add step</button>';
|
|
15523
|
+
// Action bar
|
|
15524
|
+
html += '<div id="re-action-bar" style="position:sticky;bottom:0;background:var(--bg-primary);border-top:1px solid var(--border);padding:14px 0;margin-top:24px;display:flex;gap:8px;align-items:center">'
|
|
15525
|
+
+ '<button class="btn-sm" onclick="window.RoutinesUI&&RoutinesUI.dryRunCurrent()" style="padding:6px 12px;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);border-radius:6px;cursor:pointer;font-size:12px">Dry-run</button>'
|
|
15526
|
+
+ '<button class="btn-sm" onclick="window.RoutinesUI&&RoutinesUI.testCurrent()" style="padding:6px 12px;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);border-radius:6px;cursor:pointer;font-size:12px">Mock test</button>'
|
|
15527
|
+
+ '<button class="btn-sm" onclick="window.RoutinesUI&&RoutinesUI.openRuns(R.state && R.state.editing && R.state.editing.id)" style="padding:6px 12px;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);border-radius:6px;cursor:pointer;font-size:12px">History</button>'
|
|
15528
|
+
+ '<span style="flex:1"></span>'
|
|
15529
|
+
+ (R.state.editing.routine.sourceFile && R.state.editing.id.indexOf("cron:") !== 0 ? '<button class="btn-sm" onclick="window.RoutinesUI&&RoutinesUI.deleteCurrent()" style="padding:6px 12px;background:transparent;border:1px solid var(--red);color:var(--red);border-radius:6px;cursor:pointer;font-size:12px">Delete</button>' : '')
|
|
15530
|
+
+ '<button class="btn-sm" onclick="window.RoutinesUI&&RoutinesUI.runCurrent()" style="padding:6px 14px;background:var(--green);border:none;color:#fff;border-radius:6px;cursor:pointer;font-size:12px">▶ Run now</button>'
|
|
15531
|
+
+ '<button class="btn-sm btn-primary" id="re-save-btn" onclick="window.RoutinesUI&&RoutinesUI.saveCurrent()" style="padding:6px 16px">Save</button>'
|
|
15532
|
+
+ '</div>';
|
|
15533
|
+
html += '<div id="re-status" style="font-size:11px;color:var(--text-muted);min-height:14px;padding:6px 0"></div>';
|
|
15534
|
+
html += '</div>';
|
|
15535
|
+
pane.innerHTML = html;
|
|
15536
|
+
if (window.hydrateLucideIcons) window.hydrateLucideIcons();
|
|
15537
|
+
},
|
|
15538
|
+
renderStepCard: function(step, idx) {
|
|
15539
|
+
var kind = step.kind || 'prompt';
|
|
15540
|
+
var kindColor = { prompt: '#5e72e4', mcp: '#2dce89', cli: '#fb6340', conditional: '#f5365c', channel: '#11cdef', transform: '#ffd600', loop: '#8965e0' }[kind] || '#888';
|
|
15541
|
+
var badge = '<span style="display:inline-block;background:' + kindColor + '22;color:' + kindColor + ';padding:2px 8px;border-radius:4px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.04em">' + kind + '</span>';
|
|
15542
|
+
// Model picker is only meaningful for prompt steps (other kinds don't call the LLM directly).
|
|
15543
|
+
var modelCtl = (kind === 'prompt')
|
|
15544
|
+
? R.modelSelect('', step.model || '', 'Use trick default', { idx: idx, small: true })
|
|
15545
|
+
: '';
|
|
15546
|
+
var head = '<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">'
|
|
15547
|
+
+ '<span style="font-size:11px;color:var(--text-muted);font-weight:600;min-width:24px">#' + (idx + 1) + '</span>'
|
|
15548
|
+
+ badge
|
|
15549
|
+
+ '<input type="text" value="' + R.esc(step.id) + '" onchange="window.RoutinesUI&&RoutinesUI.updateStep(' + idx + ',\\x27id\\x27,this.value)" style="font-family:\\x27JetBrains Mono\\x27,monospace;padding:3px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:11px;min-width:120px">'
|
|
15550
|
+
+ modelCtl
|
|
15551
|
+
+ '<span style="flex:1"></span>'
|
|
15552
|
+
+ (idx > 0 ? '<button title="Move up" onclick="window.RoutinesUI&&RoutinesUI.moveStep(' + idx + ',-1)" style="background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:14px">↑</button>' : '')
|
|
15553
|
+
+ (idx < (R.state.editing.routine.steps.length - 1) ? '<button title="Move down" onclick="window.RoutinesUI&&RoutinesUI.moveStep(' + idx + ',1)" style="background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:14px">↓</button>' : '')
|
|
15554
|
+
+ '<button title="Remove" onclick="window.RoutinesUI&&RoutinesUI.removeStep(' + idx + ')" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:14px">×</button>'
|
|
15555
|
+
+ '</div>';
|
|
15556
|
+
var body = R.renderStepBody(step, idx);
|
|
15557
|
+
var depsList = (step.dependsOn || []).join(', ');
|
|
15558
|
+
var depsRow = '<div style="display:flex;align-items:center;gap:8px;margin-top:8px"><label style="font-size:10px;color:var(--text-muted);font-weight:600;text-transform:uppercase;letter-spacing:0.04em;min-width:60px">After</label>'
|
|
15559
|
+
+ '<input type="text" value="' + R.esc(depsList) + '" placeholder="step ids, comma-separated (blank = independent)" onchange="window.RoutinesUI&&RoutinesUI.updateStepDeps(' + idx + ',this.value)" style="flex:1;padding:4px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:11px;font-family:\\x27JetBrains Mono\\x27,monospace"></div>';
|
|
15560
|
+
return '<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:12px 14px;margin-bottom:8px;border-left:3px solid ' + kindColor + '">' + head + body + depsRow + '</div>';
|
|
15561
|
+
},
|
|
15562
|
+
renderStepBody: function(step, idx) {
|
|
15563
|
+
var kind = step.kind || 'prompt';
|
|
15564
|
+
switch (kind) {
|
|
15565
|
+
case 'prompt':
|
|
15566
|
+
return '<textarea rows="3" placeholder="Prompt to send to the agent" oninput="window.RoutinesUI&&RoutinesUI.updateStep(' + idx + ',\\x27prompt\\x27,this.value)" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px;font-family:inherit;resize:vertical;box-sizing:border-box">' + R.esc(step.prompt || '') + '</textarea>';
|
|
15567
|
+
case 'mcp':
|
|
15568
|
+
var mcp = step.mcp || { server: '', tool: '', inputs: {} };
|
|
15569
|
+
var serverOptions = '<option value="">— pick server —</option>' + (R.state.mcpTools && R.state.mcpTools.servers || []).map(function(s){ return '<option value="' + R.esc(s.name) + '" ' + (mcp.server === s.name ? 'selected' : '') + '>' + R.esc(s.name) + ' (' + s.tools.length + ')</option>'; }).join('');
|
|
15570
|
+
var server = (R.state.mcpTools && R.state.mcpTools.servers || []).find(function(s){ return s.name === mcp.server; });
|
|
15571
|
+
var toolOptions = '<option value="">— pick tool —</option>' + (server ? server.tools.map(function(t){ return '<option value="' + R.esc(t) + '" ' + (mcp.tool === t ? 'selected' : '') + '>' + R.esc(t) + '</option>'; }).join('') : '');
|
|
15572
|
+
var inputsJson = JSON.stringify(mcp.inputs || {}, null, 2);
|
|
15573
|
+
return '<div style="display:flex;gap:8px;margin-bottom:6px;flex-wrap:wrap">'
|
|
15574
|
+
+ '<select onchange="window.RoutinesUI&&RoutinesUI.updateMcp(' + idx + ',\\x27server\\x27,this.value)" style="flex:1;min-width:160px;padding:6px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px">' + serverOptions + '</select>'
|
|
15575
|
+
+ '<select onchange="window.RoutinesUI&&RoutinesUI.updateMcp(' + idx + ',\\x27tool\\x27,this.value)" style="flex:1;min-width:160px;padding:6px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px">' + toolOptions + '</select>'
|
|
15576
|
+
+ '</div>'
|
|
15577
|
+
+ '<label style="display:block;font-size:10px;color:var(--text-muted);margin-bottom:4px">Inputs (JSON; values may use {{steps.x}} or {{input.name}})</label>'
|
|
15578
|
+
+ '<textarea rows="3" oninput="window.RoutinesUI&&RoutinesUI.updateMcpInputs(' + idx + ',this.value)" style="width:100%;padding:6px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:11px;font-family:\\x27JetBrains Mono\\x27,monospace;resize:vertical;box-sizing:border-box">' + R.esc(inputsJson) + '</textarea>';
|
|
15579
|
+
case 'cli':
|
|
15580
|
+
var cli = step.cli || { cmd: '', args: [] };
|
|
15581
|
+
var cliOptions = '<option value="">— pick CLI —</option>' + (R.state.cliTools || []).map(function(c){ return '<option value="' + R.esc(c.cmd) + '" ' + (cli.cmd === c.cmd ? 'selected' : '') + '>' + R.esc(c.cmd) + ' — ' + R.esc(c.description) + '</option>'; }).join('');
|
|
15582
|
+
if (cli.cmd && !(R.state.cliTools || []).some(function(c){ return c.cmd === cli.cmd; })) {
|
|
15583
|
+
cliOptions += '<option value="' + R.esc(cli.cmd) + '" selected>' + R.esc(cli.cmd) + ' (custom)</option>';
|
|
15584
|
+
}
|
|
15585
|
+
return '<div style="display:flex;gap:8px;margin-bottom:6px;flex-wrap:wrap">'
|
|
15586
|
+
+ '<select onchange="window.RoutinesUI&&RoutinesUI.updateCli(' + idx + ',\\x27cmd\\x27,this.value)" style="flex:1;min-width:200px;padding:6px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px">' + cliOptions + '</select>'
|
|
15587
|
+
+ '<input type="number" value="' + (cli.timeoutMs || 60000) + '" onchange="window.RoutinesUI&&RoutinesUI.updateCli(' + idx + ',\\x27timeoutMs\\x27,parseInt(this.value,10))" placeholder="timeout ms" style="width:120px;padding:6px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px" title="Timeout in milliseconds">'
|
|
15588
|
+
+ '</div>'
|
|
15589
|
+
+ '<label style="display:block;font-size:10px;color:var(--text-muted);margin-bottom:4px">Args (one per line; supports {{steps.x}} templates)</label>'
|
|
15590
|
+
+ '<textarea rows="3" oninput="window.RoutinesUI&&RoutinesUI.updateCliArgs(' + idx + ',this.value)" placeholder="--json\\norg list" style="width:100%;padding:6px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:11px;font-family:\\x27JetBrains Mono\\x27,monospace;resize:vertical;box-sizing:border-box">' + R.esc((cli.args || []).join('\\n')) + '</textarea>'
|
|
15591
|
+
+ '<label style="display:flex;align-items:center;gap:6px;margin-top:6px;font-size:11px;color:var(--text-secondary)"><input type="checkbox" ' + (cli.captureStderr ? 'checked' : '') + ' onchange="window.RoutinesUI&&RoutinesUI.updateCli(' + idx + ',\\x27captureStderr\\x27,this.checked)"> Include stderr in output</label>';
|
|
15592
|
+
case 'conditional':
|
|
15593
|
+
var cond = step.conditional || { condition: '', trueNext: [], falseNext: [] };
|
|
15594
|
+
return '<label style="display:block;font-size:10px;color:var(--text-muted);margin-bottom:4px">Condition (JS expression — has access to <code>steps.<id></code>)</label>'
|
|
15595
|
+
+ '<input type="text" value="' + R.esc(cond.condition || '') + '" placeholder="e.g. steps.s1.messages.length > 0" oninput="window.RoutinesUI&&RoutinesUI.updateConditional(' + idx + ',\\x27condition\\x27,this.value)" style="width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px;font-family:\\x27JetBrains Mono\\x27,monospace;margin-bottom:6px;box-sizing:border-box">'
|
|
15596
|
+
+ '<div style="display:flex;gap:8px"><div style="flex:1">'
|
|
15597
|
+
+ '<label style="display:block;font-size:10px;color:var(--text-muted);margin-bottom:4px">If true → run these step ids (comma)</label>'
|
|
15598
|
+
+ '<input type="text" value="' + R.esc((cond.trueNext || []).join(', ')) + '" oninput="window.RoutinesUI&&RoutinesUI.updateConditional(' + idx + ',\\x27trueNext\\x27,this.value)" style="width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:11px;font-family:\\x27JetBrains Mono\\x27,monospace;box-sizing:border-box">'
|
|
15599
|
+
+ '</div><div style="flex:1">'
|
|
15600
|
+
+ '<label style="display:block;font-size:10px;color:var(--text-muted);margin-bottom:4px">If false → run these step ids</label>'
|
|
15601
|
+
+ '<input type="text" value="' + R.esc((cond.falseNext || []).join(', ')) + '" oninput="window.RoutinesUI&&RoutinesUI.updateConditional(' + idx + ',\\x27falseNext\\x27,this.value)" style="width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:11px;font-family:\\x27JetBrains Mono\\x27,monospace;box-sizing:border-box">'
|
|
15602
|
+
+ '</div></div>';
|
|
15603
|
+
case 'channel':
|
|
15604
|
+
var ch = step.channel || { channel: 'discord', target: '', content: '' };
|
|
15605
|
+
return '<div style="display:flex;gap:8px;margin-bottom:6px;flex-wrap:wrap">'
|
|
15606
|
+
+ '<select onchange="window.RoutinesUI&&RoutinesUI.updateChannel(' + idx + ',\\x27channel\\x27,this.value)" style="padding:6px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px">'
|
|
15607
|
+
+ ['discord','slack','telegram','whatsapp','email','webhook'].map(function(c){ return '<option value="' + c + '"' + (ch.channel === c ? ' selected' : '') + '>' + c + '</option>'; }).join('')
|
|
15608
|
+
+ '</select>'
|
|
15609
|
+
+ '<input type="text" value="' + R.esc(ch.target || '') + '" placeholder="channel id, #channel, email, URL…" oninput="window.RoutinesUI&&RoutinesUI.updateChannel(' + idx + ',\\x27target\\x27,this.value)" style="flex:1;min-width:200px;padding:6px 10px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px">'
|
|
15610
|
+
+ '</div>'
|
|
15611
|
+
+ '<textarea rows="3" placeholder="Message content (supports {{steps.x}})" oninput="window.RoutinesUI&&RoutinesUI.updateChannel(' + idx + ',\\x27content\\x27,this.value)" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px;font-family:inherit;resize:vertical;box-sizing:border-box">' + R.esc(ch.content || '') + '</textarea>';
|
|
15612
|
+
case 'transform':
|
|
15613
|
+
var tr = step.transform || { expression: '' };
|
|
15614
|
+
return '<label style="display:block;font-size:10px;color:var(--text-muted);margin-bottom:4px">Expression (sandboxed JS; returns the step output)</label>'
|
|
15615
|
+
+ '<textarea rows="3" oninput="window.RoutinesUI&&RoutinesUI.updateTransform(' + idx + ',this.value)" placeholder="e.g. steps.s1.items.filter(x => x.urgent)" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px;font-family:\\x27JetBrains Mono\\x27,monospace;resize:vertical;box-sizing:border-box">' + R.esc(tr.expression || '') + '</textarea>';
|
|
15616
|
+
case 'loop':
|
|
15617
|
+
var lp = step.loop || { items: '', bodyStepIds: [] };
|
|
15618
|
+
return '<label style="display:block;font-size:10px;color:var(--text-muted);margin-bottom:4px">Items expression (yields an array)</label>'
|
|
15619
|
+
+ '<input type="text" value="' + R.esc(lp.items || '') + '" placeholder="e.g. steps.s1.results" oninput="window.RoutinesUI&&RoutinesUI.updateLoop(' + idx + ',\\x27items\\x27,this.value)" style="width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px;font-family:\\x27JetBrains Mono\\x27,monospace;margin-bottom:6px;box-sizing:border-box">'
|
|
15620
|
+
+ '<label style="display:block;font-size:10px;color:var(--text-muted);margin-bottom:4px">Body step ids (comma-separated)</label>'
|
|
15621
|
+
+ '<input type="text" value="' + R.esc((lp.bodyStepIds || []).join(', ')) + '" oninput="window.RoutinesUI&&RoutinesUI.updateLoop(' + idx + ',\\x27bodyStepIds\\x27,this.value)" style="width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:11px;font-family:\\x27JetBrains Mono\\x27,monospace;box-sizing:border-box">';
|
|
15622
|
+
default:
|
|
15623
|
+
return '<em style="font-size:11px;color:var(--text-muted)">Unknown step kind: ' + R.esc(kind) + '</em>';
|
|
15624
|
+
}
|
|
15625
|
+
},
|
|
15626
|
+
markDirty: function() {
|
|
15627
|
+
if (!R.state.editing) return;
|
|
15628
|
+
R.state.editing.dirty = true;
|
|
15629
|
+
// Sync header inputs back to the routine.
|
|
15630
|
+
var nm = document.getElementById('re-name'); if (nm) R.state.editing.routine.name = nm.value;
|
|
15631
|
+
var de = document.getElementById('re-description'); if (de) R.state.editing.routine.description = de.value;
|
|
15632
|
+
var sc = document.getElementById('re-schedule'); if (sc) {
|
|
15633
|
+
var v = sc.value.trim();
|
|
15634
|
+
R.state.editing.routine.trigger = v ? { schedule: v, manual: false } : { manual: true };
|
|
15635
|
+
}
|
|
15636
|
+
var en = document.getElementById('re-enabled'); if (en) R.state.editing.routine.enabled = en.checked;
|
|
15637
|
+
var md = document.getElementById('re-model'); if (md) R.state.editing.routine.model = md.value || undefined;
|
|
15638
|
+
R.setStatus('Unsaved changes');
|
|
15639
|
+
},
|
|
15640
|
+
updateStep: function(idx, field, value) {
|
|
15641
|
+
if (!R.state.editing) return;
|
|
15642
|
+
R.state.editing.routine.steps[idx][field] = value;
|
|
15643
|
+
R.markDirty();
|
|
15644
|
+
},
|
|
15645
|
+
updateStepDeps: function(idx, csv) {
|
|
15646
|
+
if (!R.state.editing) return;
|
|
15647
|
+
R.state.editing.routine.steps[idx].dependsOn = csv.split(',').map(function(s){ return s.trim(); }).filter(function(s){ return s; });
|
|
15648
|
+
R.markDirty();
|
|
15649
|
+
},
|
|
15650
|
+
updateMcp: function(idx, field, value) {
|
|
15651
|
+
if (!R.state.editing) return;
|
|
15652
|
+
var s = R.state.editing.routine.steps[idx];
|
|
15653
|
+
s.mcp = s.mcp || { server: '', tool: '', inputs: {} };
|
|
15654
|
+
s.mcp[field] = value;
|
|
15655
|
+
if (field === 'server') s.mcp.tool = ''; // reset tool when server changes
|
|
15656
|
+
R.markDirty();
|
|
15657
|
+
R.renderEditor(); // re-render so tool dropdown updates
|
|
15658
|
+
},
|
|
15659
|
+
updateMcpInputs: function(idx, json) {
|
|
15660
|
+
if (!R.state.editing) return;
|
|
15661
|
+
var s = R.state.editing.routine.steps[idx];
|
|
15662
|
+
s.mcp = s.mcp || { server: '', tool: '', inputs: {} };
|
|
15663
|
+
try { s.mcp.inputs = JSON.parse(json); R.setStatus(''); }
|
|
15664
|
+
catch (e) { R.setStatus('Invalid JSON in step ' + s.id + ' inputs (will not save until fixed)'); return; }
|
|
15665
|
+
R.markDirty();
|
|
15666
|
+
},
|
|
15667
|
+
updateCli: function(idx, field, value) {
|
|
15668
|
+
if (!R.state.editing) return;
|
|
15669
|
+
var s = R.state.editing.routine.steps[idx];
|
|
15670
|
+
s.cli = s.cli || { cmd: '', args: [] };
|
|
15671
|
+
s.cli[field] = value;
|
|
15672
|
+
R.markDirty();
|
|
15673
|
+
},
|
|
15674
|
+
updateCliArgs: function(idx, txt) {
|
|
15675
|
+
if (!R.state.editing) return;
|
|
15676
|
+
var s = R.state.editing.routine.steps[idx];
|
|
15677
|
+
s.cli = s.cli || { cmd: '', args: [] };
|
|
15678
|
+
s.cli.args = txt.split('\\n').map(function(l){ return l.trim(); }).filter(function(l){ return l; });
|
|
15679
|
+
R.markDirty();
|
|
15680
|
+
},
|
|
15681
|
+
updateConditional: function(idx, field, value) {
|
|
15682
|
+
if (!R.state.editing) return;
|
|
15683
|
+
var s = R.state.editing.routine.steps[idx];
|
|
15684
|
+
s.conditional = s.conditional || { condition: '', trueNext: [], falseNext: [] };
|
|
15685
|
+
if (field === 'trueNext' || field === 'falseNext') {
|
|
15686
|
+
s.conditional[field] = String(value).split(',').map(function(x){ return x.trim(); }).filter(function(x){ return x; });
|
|
15687
|
+
} else {
|
|
15688
|
+
s.conditional[field] = value;
|
|
15689
|
+
}
|
|
15690
|
+
R.markDirty();
|
|
15691
|
+
},
|
|
15692
|
+
updateChannel: function(idx, field, value) {
|
|
15693
|
+
if (!R.state.editing) return;
|
|
15694
|
+
var s = R.state.editing.routine.steps[idx];
|
|
15695
|
+
s.channel = s.channel || { channel: 'discord', target: '', content: '' };
|
|
15696
|
+
s.channel[field] = value;
|
|
15697
|
+
R.markDirty();
|
|
15698
|
+
},
|
|
15699
|
+
updateTransform: function(idx, value) {
|
|
15700
|
+
if (!R.state.editing) return;
|
|
15701
|
+
var s = R.state.editing.routine.steps[idx];
|
|
15702
|
+
s.transform = { expression: value };
|
|
15703
|
+
R.markDirty();
|
|
15704
|
+
},
|
|
15705
|
+
updateLoop: function(idx, field, value) {
|
|
15706
|
+
if (!R.state.editing) return;
|
|
15707
|
+
var s = R.state.editing.routine.steps[idx];
|
|
15708
|
+
s.loop = s.loop || { items: '', bodyStepIds: [] };
|
|
15709
|
+
if (field === 'bodyStepIds') {
|
|
15710
|
+
s.loop.bodyStepIds = String(value).split(',').map(function(x){ return x.trim(); }).filter(function(x){ return x; });
|
|
15711
|
+
} else {
|
|
15712
|
+
s.loop.items = value;
|
|
15713
|
+
}
|
|
15714
|
+
R.markDirty();
|
|
15715
|
+
},
|
|
15716
|
+
moveStep: function(idx, dir) {
|
|
15717
|
+
if (!R.state.editing) return;
|
|
15718
|
+
var steps = R.state.editing.routine.steps;
|
|
15719
|
+
var j = idx + dir;
|
|
15720
|
+
if (j < 0 || j >= steps.length) return;
|
|
15721
|
+
var t = steps[idx]; steps[idx] = steps[j]; steps[j] = t;
|
|
15722
|
+
R.markDirty();
|
|
15723
|
+
R.renderEditor();
|
|
15724
|
+
},
|
|
15725
|
+
removeStep: function(idx) {
|
|
15726
|
+
if (!R.state.editing) return;
|
|
15727
|
+
if (R.state.editing.routine.steps.length <= 1) { alert('A trick must have at least one step.'); return; }
|
|
15728
|
+
if (!confirm('Remove this step?')) return;
|
|
15729
|
+
var removed = R.state.editing.routine.steps.splice(idx, 1)[0];
|
|
15730
|
+
// Strip lingering dependsOn references.
|
|
15731
|
+
for (var i = 0; i < R.state.editing.routine.steps.length; i++) {
|
|
15732
|
+
R.state.editing.routine.steps[i].dependsOn = (R.state.editing.routine.steps[i].dependsOn || []).filter(function(d){ return d !== removed.id; });
|
|
15733
|
+
}
|
|
15734
|
+
R.markDirty();
|
|
15735
|
+
R.renderEditor();
|
|
15736
|
+
},
|
|
15737
|
+
// ── step picker ─────────────────────────────────────────────
|
|
15738
|
+
openStepPicker: function() {
|
|
15739
|
+
var modal = document.getElementById('routines-step-picker');
|
|
15740
|
+
var body = document.getElementById('routines-step-picker-body');
|
|
15741
|
+
if (!modal || !body) return;
|
|
15742
|
+
var kinds = [
|
|
15743
|
+
{ kind: 'prompt', label: 'Prompt', desc: 'Send a prompt to the agent. Use this when the work needs reasoning or freeform tools.' },
|
|
15744
|
+
{ kind: 'mcp', label: 'MCP tool', desc: 'Call a specific MCP server tool (Composio, Claude integrations, local MCP).' },
|
|
15745
|
+
{ kind: 'cli', label: 'Local CLI', desc: 'Run an installed CLI (sf, gh, gcloud, …) and capture stdout.' },
|
|
15746
|
+
{ kind: 'conditional', label: 'If / Else', desc: 'Branch on a JS expression evaluated against prior step outputs.' },
|
|
15747
|
+
{ kind: 'channel', label: 'Channel send', desc: 'Send a message to Discord, Slack, Telegram, email, or webhook.' },
|
|
15748
|
+
{ kind: 'transform', label: 'Transform', desc: 'Sandboxed JS expression that reshapes prior step output.' },
|
|
15749
|
+
{ kind: 'loop', label: 'Loop', desc: 'Iterate over an array; runs the listed body steps for each item.' },
|
|
15750
|
+
];
|
|
15751
|
+
body.innerHTML = kinds.map(function(k){
|
|
15752
|
+
return '<div onclick="window.RoutinesUI&&RoutinesUI.addStep(\\x27' + k.kind + '\\x27)" style="padding:12px 14px;border:1px solid var(--border);border-radius:6px;margin-bottom:8px;cursor:pointer;background:var(--bg-tertiary)"><div style="font-weight:600;font-size:13px;color:var(--text-primary);margin-bottom:3px">' + k.label + '</div><div style="font-size:11px;color:var(--text-muted)">' + k.desc + '</div></div>';
|
|
15753
|
+
}).join('');
|
|
15754
|
+
modal.style.display = 'flex';
|
|
15755
|
+
},
|
|
15756
|
+
closeStepPicker: function() {
|
|
15757
|
+
var m = document.getElementById('routines-step-picker'); if (m) m.style.display = 'none';
|
|
15758
|
+
},
|
|
15759
|
+
addStep: function(kind) {
|
|
15760
|
+
if (!R.state.editing) return;
|
|
15761
|
+
var steps = R.state.editing.routine.steps;
|
|
15762
|
+
var n = steps.length + 1;
|
|
15763
|
+
var nextId = 's' + n;
|
|
15764
|
+
while (steps.some(function(s){ return s.id === nextId; })) { n++; nextId = 's' + n; }
|
|
15765
|
+
var step = { id: nextId, prompt: '', dependsOn: steps.length ? [steps[steps.length - 1].id] : [], tier: 1, maxTurns: 15 };
|
|
15766
|
+
if (kind !== 'prompt') step.kind = kind;
|
|
15767
|
+
if (kind === 'mcp') step.mcp = { server: '', tool: '', inputs: {} };
|
|
15768
|
+
if (kind === 'cli') step.cli = { cmd: '', args: [], timeoutMs: 60000 };
|
|
15769
|
+
if (kind === 'conditional') step.conditional = { condition: '', trueNext: [], falseNext: [] };
|
|
15770
|
+
if (kind === 'channel') step.channel = { channel: 'discord', target: '', content: '' };
|
|
15771
|
+
if (kind === 'transform') step.transform = { expression: '' };
|
|
15772
|
+
if (kind === 'loop') step.loop = { items: '', bodyStepIds: [] };
|
|
15773
|
+
steps.push(step);
|
|
15774
|
+
R.markDirty();
|
|
15775
|
+
R.closeStepPicker();
|
|
15776
|
+
R.renderEditor();
|
|
15777
|
+
},
|
|
15778
|
+
// ── editor actions ──────────────────────────────────────────
|
|
15779
|
+
saveCurrent: function() {
|
|
15780
|
+
if (!R.state.editing) return;
|
|
15781
|
+
R.markDirty(); // capture latest header values
|
|
15782
|
+
var btn = document.getElementById('re-save-btn');
|
|
15783
|
+
if (btn) btn.textContent = 'Saving…';
|
|
15784
|
+
apiFetch('/api/routines/' + encodeURIComponent(R.state.editing.id), {
|
|
15785
|
+
method: 'PUT',
|
|
15786
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15787
|
+
body: JSON.stringify({ routine: R.state.editing.routine })
|
|
15788
|
+
}).then(function(r){ return r.json().then(function(j){ return { ok: r.ok, status: r.status, body: j }; }); })
|
|
15789
|
+
.then(function(res){
|
|
15790
|
+
if (btn) btn.textContent = 'Save';
|
|
15791
|
+
if (!res.ok) {
|
|
15792
|
+
if (res.body.error === 'validation') {
|
|
15793
|
+
var msg = (res.body.validation.issues || []).map(function(i){ return '• ' + i.severity + ': ' + i.message; }).join('\\n');
|
|
15794
|
+
if (confirm('Validation issues:\\n\\n' + msg + '\\n\\nSave anyway?')) {
|
|
15795
|
+
apiFetch('/api/routines/' + encodeURIComponent(R.state.editing.id), {
|
|
15796
|
+
method: 'PUT',
|
|
15797
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15798
|
+
body: JSON.stringify({ routine: R.state.editing.routine, force: true })
|
|
15799
|
+
}).then(function(){ R.state.editing.dirty = false; R.setStatus('Saved (with warnings)'); R.refreshList(); });
|
|
15800
|
+
}
|
|
15801
|
+
return;
|
|
15802
|
+
}
|
|
15803
|
+
R.setStatus('Save failed: ' + (res.body.error || res.body.detail || 'unknown'));
|
|
15804
|
+
return;
|
|
15805
|
+
}
|
|
15806
|
+
R.state.editing.dirty = false;
|
|
15807
|
+
R.setStatus('Saved.');
|
|
15808
|
+
R.refreshList();
|
|
15809
|
+
}).catch(function(err){
|
|
15810
|
+
if (btn) btn.textContent = 'Save';
|
|
15811
|
+
R.setStatus('Save error: ' + err);
|
|
15812
|
+
});
|
|
15813
|
+
},
|
|
15814
|
+
runCurrent: function() {
|
|
15815
|
+
if (!R.state.editing) return;
|
|
15816
|
+
if (R.state.editing.dirty && !confirm('You have unsaved changes. Run anyway (using last saved version)?')) return;
|
|
15817
|
+
R.run(R.state.editing.id);
|
|
15818
|
+
},
|
|
15819
|
+
dryRunCurrent: function() {
|
|
15820
|
+
if (!R.state.editing) return;
|
|
15821
|
+
apiFetch('/api/routines/' + encodeURIComponent(R.state.editing.id) + '/dry-run', { method: 'POST' })
|
|
15822
|
+
.then(function(r){ return r.json(); })
|
|
15823
|
+
.then(function(d){
|
|
15824
|
+
var lines = ['Dry-run for ' + R.state.editing.routine.name + ':\\n'];
|
|
15825
|
+
(d.steps || []).forEach(function(s){ lines.push('• ' + s.description + (s.warnings.length ? '\\n ⚠ ' + s.warnings.join('; ') : '')); });
|
|
15826
|
+
if (d.notes && d.notes.length) lines.push('\\n' + d.notes.join('\\n'));
|
|
15827
|
+
alert(lines.join('\\n'));
|
|
15828
|
+
}).catch(function(err){ alert('Dry-run failed: ' + err); });
|
|
15829
|
+
},
|
|
15830
|
+
testCurrent: function() {
|
|
15831
|
+
if (!R.state.editing) return;
|
|
15832
|
+
apiFetch('/api/routines/' + encodeURIComponent(R.state.editing.id) + '/test', {
|
|
15833
|
+
method: 'POST',
|
|
15834
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15835
|
+
body: JSON.stringify({ mode: 'mock' })
|
|
15836
|
+
}).then(function(r){ return r.json(); }).then(function(d){
|
|
15837
|
+
if (d.ok) R.setStatus('Mock test started (runId: ' + d.runId + '). See run history for output.');
|
|
15838
|
+
else R.setStatus('Test failed to start: ' + (d.error || 'unknown'));
|
|
15839
|
+
}).catch(function(err){ R.setStatus('Test error: ' + err); });
|
|
15840
|
+
},
|
|
15841
|
+
deleteCurrent: function() {
|
|
15842
|
+
if (!R.state.editing) return;
|
|
15843
|
+
if (!confirm('Delete routine "' + R.state.editing.routine.name + '"? This is permanent.')) return;
|
|
15844
|
+
apiFetch('/api/routines/' + encodeURIComponent(R.state.editing.id), { method: 'DELETE' })
|
|
15845
|
+
.then(function(r){ return r.json(); })
|
|
15846
|
+
.then(function(j){
|
|
15847
|
+
if (j.ok) { R.state.editing = null; R.closeEditor(); R.refreshList(); }
|
|
15848
|
+
else alert('Delete failed: ' + (j.error || 'unknown'));
|
|
15849
|
+
}).catch(function(err){ alert('Delete error: ' + err); });
|
|
15850
|
+
},
|
|
15851
|
+
setStatus: function(msg) {
|
|
15852
|
+
var el = document.getElementById('re-status');
|
|
15853
|
+
if (el) el.textContent = msg || '';
|
|
15854
|
+
},
|
|
15855
|
+
flash: function(msg) {
|
|
15856
|
+
// Lightweight toast — reuse existing flash if available, else log.
|
|
15857
|
+
if (window.flashMessage) window.flashMessage(msg);
|
|
15858
|
+
else if (window.console) console.log('[routines]', msg);
|
|
15859
|
+
},
|
|
15860
|
+
// ── runs drawer ─────────────────────────────────────────────
|
|
15861
|
+
openRuns: function(id) {
|
|
15862
|
+
if (!id) return;
|
|
15863
|
+
var drawer = document.getElementById('routines-runs-drawer');
|
|
15864
|
+
if (!drawer) return;
|
|
15865
|
+
drawer.innerHTML = '<div style="padding:18px"><div style="display:flex;align-items:center;gap:8px;margin-bottom:10px"><h3 style="margin:0;font-size:15px;font-weight:600;color:var(--text-primary)">Run history</h3><span style="flex:1"></span><button onclick="window.RoutinesUI&&RoutinesUI.closeRuns()" style="background:none;border:none;color:var(--text-muted);font-size:18px;cursor:pointer">×</button></div><div style="font-size:12px;color:var(--text-muted)">Loading…</div></div>';
|
|
15866
|
+
drawer.style.display = 'block';
|
|
15867
|
+
apiFetch('/api/routines/' + encodeURIComponent(id) + '/runs').then(function(r){ return r.json(); }).then(function(d){
|
|
15868
|
+
var runs = d.runs || [];
|
|
15869
|
+
var html = '<div style="padding:18px"><div style="display:flex;align-items:center;gap:8px;margin-bottom:14px"><h3 style="margin:0;font-size:15px;font-weight:600;color:var(--text-primary)">Run history</h3><span style="flex:1"></span><button onclick="window.RoutinesUI&&RoutinesUI.closeRuns()" style="background:none;border:none;color:var(--text-muted);font-size:18px;cursor:pointer">×</button></div>';
|
|
15870
|
+
if (runs.length === 0) {
|
|
15871
|
+
html += '<div style="font-size:12px;color:var(--text-muted);padding:24px 0;text-align:center">No runs yet.</div>';
|
|
15872
|
+
} else {
|
|
15873
|
+
html += runs.map(function(run){
|
|
15874
|
+
var when = run.startedAt || run.timestamp || '';
|
|
15875
|
+
var status = run.status || 'unknown';
|
|
15876
|
+
var color = { ok: 'var(--green)', error: 'var(--red)', partial: '#f5a623', skipped: 'var(--text-muted)', retried: '#f5a623' }[status] || 'var(--text-muted)';
|
|
15877
|
+
var dur = run.durationMs ? Math.round(run.durationMs / 100) / 10 + 's' : '';
|
|
15878
|
+
var preview = run.outputPreview || run.output_preview || run.outputPreview || '';
|
|
15879
|
+
return '<div style="background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;padding:10px 14px;margin-bottom:8px"><div style="display:flex;align-items:center;gap:10px;font-size:12px"><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' + color + '"></span><span style="color:var(--text-primary);font-weight:500">' + R.esc(status) + '</span><span style="color:var(--text-muted)">' + R.esc(when) + '</span><span style="flex:1"></span><span style="color:var(--text-muted);font-size:11px">' + dur + '</span></div>' + (preview ? '<div style="margin-top:8px;font-size:11px;color:var(--text-secondary);font-family:\\x27JetBrains Mono\\x27,monospace;white-space:pre-wrap;max-height:120px;overflow:auto">' + R.esc(String(preview).slice(0, 800)) + '</div>' : '') + '</div>';
|
|
15880
|
+
}).join('');
|
|
15881
|
+
}
|
|
15882
|
+
html += '</div>';
|
|
15883
|
+
drawer.innerHTML = html;
|
|
15884
|
+
}).catch(function(err){
|
|
15885
|
+
drawer.innerHTML = '<div style="padding:18px"><div style="display:flex;align-items:center;gap:8px"><h3 style="margin:0;font-size:15px;font-weight:600">Run history</h3><span style="flex:1"></span><button onclick="window.RoutinesUI&&RoutinesUI.closeRuns()" style="background:none;border:none;color:var(--text-muted);font-size:18px;cursor:pointer">×</button></div><div style="margin-top:14px;font-size:12px;color:var(--red)">Failed to load: ' + R.esc(String(err)) + '</div></div>';
|
|
15886
|
+
});
|
|
15887
|
+
},
|
|
15888
|
+
closeRuns: function() {
|
|
15889
|
+
var d = document.getElementById('routines-runs-drawer'); if (d) d.style.display = 'none';
|
|
15890
|
+
},
|
|
15891
|
+
// ── create modal ────────────────────────────────────────────
|
|
15892
|
+
openCreate: function() {
|
|
15893
|
+
var m = document.getElementById('routines-create-modal'); if (!m) return;
|
|
15894
|
+
document.getElementById('routines-create-name').value = '';
|
|
15895
|
+
document.getElementById('routines-create-description').value = '';
|
|
15896
|
+
document.getElementById('routines-create-schedule').value = '';
|
|
15897
|
+
m.style.display = 'flex';
|
|
15898
|
+
},
|
|
15899
|
+
closeCreate: function() {
|
|
15900
|
+
var m = document.getElementById('routines-create-modal'); if (m) m.style.display = 'none';
|
|
15901
|
+
},
|
|
15902
|
+
submitCreate: function() {
|
|
15903
|
+
var name = document.getElementById('routines-create-name').value.trim();
|
|
15904
|
+
if (!name) { alert('Name is required'); return; }
|
|
15905
|
+
var body = {
|
|
15906
|
+
name: name,
|
|
15907
|
+
description: document.getElementById('routines-create-description').value.trim(),
|
|
15908
|
+
schedule: document.getElementById('routines-create-schedule').value.trim() || undefined,
|
|
15909
|
+
agent: document.getElementById('routines-create-owner').value || undefined,
|
|
15910
|
+
};
|
|
15911
|
+
apiFetch('/api/routines', {
|
|
15912
|
+
method: 'POST',
|
|
15913
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15914
|
+
body: JSON.stringify(body)
|
|
15915
|
+
}).then(function(r){ return r.json().then(function(j){ return { ok: r.ok, body: j }; }); })
|
|
15916
|
+
.then(function(res){
|
|
15917
|
+
if (!res.ok) { alert('Create failed: ' + (res.body.error || 'unknown')); return; }
|
|
15918
|
+
R.closeCreate();
|
|
15919
|
+
R.refreshList();
|
|
15920
|
+
R.openEditor(res.body.id);
|
|
15921
|
+
}).catch(function(err){ alert('Create error: ' + err); });
|
|
15922
|
+
},
|
|
15923
|
+
// ── chat-first builder ──────────────────────────────────────
|
|
15924
|
+
// Multi-turn conversation that asks clarifying questions and
|
|
15925
|
+
// drafts a trick spec. The agent emits a fenced json-artifact
|
|
15926
|
+
// block; we parse it for the live preview + Save button.
|
|
15927
|
+
openChat: function() {
|
|
15928
|
+
var m = document.getElementById('routines-chat-modal'); if (!m) return;
|
|
15929
|
+
R.state.chatMessages = [];
|
|
15930
|
+
R.state.chatArtifact = null;
|
|
15931
|
+
R.state.chatBusy = false;
|
|
15932
|
+
document.getElementById('routines-chat-input').value = '';
|
|
15933
|
+
document.getElementById('routines-chat-status').textContent = '';
|
|
15934
|
+
document.getElementById('routines-chat-save').style.display = 'none';
|
|
15935
|
+
// Reset the builder session so the prior conversation doesn't leak in.
|
|
15936
|
+
apiFetch('/api/builder/reset', {
|
|
15937
|
+
method: 'POST',
|
|
15938
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15939
|
+
body: JSON.stringify({ artifactType: 'workflow' })
|
|
15940
|
+
}).catch(function(){ /* non-fatal */ });
|
|
15941
|
+
R.renderChatMessages();
|
|
15942
|
+
R.renderChatPreview();
|
|
15943
|
+
// Seed with a greeting from the assistant so the panel isn't empty.
|
|
15944
|
+
R.appendChatMessage('assistant', 'Hi! Tell me what you want Clementine to do — a sentence is fine. I\\x27ll ask a couple of follow-ups (when it should run, which tools she needs, which model) and draft a trick you can save.');
|
|
15945
|
+
m.style.display = 'flex';
|
|
15946
|
+
setTimeout(function(){ document.getElementById('routines-chat-input').focus(); }, 50);
|
|
15947
|
+
},
|
|
15948
|
+
closeChat: function(silent) {
|
|
15949
|
+
var m = document.getElementById('routines-chat-modal'); if (m) m.style.display = 'none';
|
|
15950
|
+
if (!silent) R.refreshList();
|
|
15951
|
+
},
|
|
15952
|
+
appendChatMessage: function(role, text) {
|
|
15953
|
+
R.state.chatMessages.push({ role: role, text: text });
|
|
15954
|
+
R.renderChatMessages();
|
|
15955
|
+
},
|
|
15956
|
+
renderChatMessages: function() {
|
|
15957
|
+
var box = document.getElementById('routines-chat-messages'); if (!box) return;
|
|
15958
|
+
box.innerHTML = (R.state.chatMessages || []).map(function(m){
|
|
15959
|
+
var isUser = m.role === 'user';
|
|
15960
|
+
var bg = isUser ? 'var(--clementine,#ff8c21)' : 'var(--bg-tertiary)';
|
|
15961
|
+
var color = isUser ? '#fff' : 'var(--text-primary)';
|
|
15962
|
+
var align = isUser ? 'flex-end' : 'flex-start';
|
|
15963
|
+
return '<div style="display:flex;justify-content:' + align + '"><div style="max-width:78%;padding:8px 12px;border-radius:10px;background:' + bg + ';color:' + color + ';font-size:13px;line-height:1.5;white-space:pre-wrap">' + R.esc(m.text) + '</div></div>';
|
|
15964
|
+
}).join('');
|
|
15965
|
+
box.scrollTop = box.scrollHeight;
|
|
15966
|
+
},
|
|
15967
|
+
renderChatPreview: function() {
|
|
15968
|
+
var pv = document.getElementById('routines-chat-preview'); if (!pv) return;
|
|
15969
|
+
var a = R.state.chatArtifact;
|
|
15970
|
+
if (!a || !a.name) { pv.style.display = 'none'; return; }
|
|
15971
|
+
pv.style.display = 'block';
|
|
15972
|
+
var schedule = a.schedule ? '<code style="font-family:\\x27JetBrains Mono\\x27,monospace;font-size:11px">' + R.esc(a.schedule) + '</code>' : '<em style="color:var(--text-muted)">manual</em>';
|
|
15973
|
+
var stepCount = (a.steps && typeof a.steps === 'string')
|
|
15974
|
+
? (a.steps.match(/^[a-zA-Z0-9_-]+:/gm) || []).length
|
|
15975
|
+
: (Array.isArray(a.steps) ? a.steps.length : 0);
|
|
15976
|
+
pv.innerHTML = '<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap"><strong style="color:var(--text-primary)">' + R.esc(a.name) + '</strong>'
|
|
15977
|
+
+ '<span style="color:var(--text-muted)">·</span><span>schedule ' + schedule + '</span>'
|
|
15978
|
+
+ '<span style="color:var(--text-muted)">·</span><span>' + stepCount + ' step' + (stepCount === 1 ? '' : 's') + '</span>'
|
|
15979
|
+
+ (a.model ? '<span style="color:var(--text-muted)">·</span><span>model <code>' + R.esc(a.model) + '</code></span>' : '')
|
|
15980
|
+
+ '</div>'
|
|
15981
|
+
+ (a.description ? '<div style="margin-top:4px;color:var(--text-muted);font-size:11px">' + R.esc(a.description) + '</div>' : '');
|
|
15982
|
+
// Save button shows once we have a name + at least one step.
|
|
15983
|
+
var saveBtn = document.getElementById('routines-chat-save');
|
|
15984
|
+
if (saveBtn) saveBtn.style.display = (a.name && stepCount > 0) ? '' : 'none';
|
|
15985
|
+
},
|
|
15986
|
+
sendChat: function() {
|
|
15987
|
+
if (R.state.chatBusy) return;
|
|
15988
|
+
var input = document.getElementById('routines-chat-input');
|
|
15989
|
+
var text = input.value.trim();
|
|
15990
|
+
if (!text) return;
|
|
15991
|
+
input.value = '';
|
|
15992
|
+
R.appendChatMessage('user', text);
|
|
15993
|
+
R.state.chatBusy = true;
|
|
15994
|
+
var sendBtn = document.getElementById('routines-chat-send');
|
|
15995
|
+
var status = document.getElementById('routines-chat-status');
|
|
15996
|
+
if (sendBtn) { sendBtn.textContent = 'Thinking…'; sendBtn.disabled = true; }
|
|
15997
|
+
if (status) status.textContent = 'Clementine is drafting…';
|
|
15998
|
+
apiFetch('/api/builder/chat', {
|
|
15999
|
+
method: 'POST',
|
|
16000
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16001
|
+
body: JSON.stringify({
|
|
16002
|
+
message: text,
|
|
16003
|
+
artifactType: 'workflow',
|
|
16004
|
+
currentArtifact: R.state.chatArtifact || undefined,
|
|
16005
|
+
})
|
|
16006
|
+
}).then(function(r){ return r.json().then(function(j){ return { ok: r.ok, body: j }; }); })
|
|
16007
|
+
.then(function(res){
|
|
16008
|
+
R.state.chatBusy = false;
|
|
16009
|
+
if (sendBtn) { sendBtn.textContent = 'Send'; sendBtn.disabled = false; }
|
|
16010
|
+
if (!res.ok) {
|
|
16011
|
+
if (status) status.textContent = 'Error: ' + (res.body && res.body.error || 'unknown');
|
|
16012
|
+
return;
|
|
16013
|
+
}
|
|
16014
|
+
// Endpoint returns { ok, response, artifact } — server already
|
|
16015
|
+
// strips the json-artifact fence and parses the JSON for us.
|
|
16016
|
+
var reply = (res.body && res.body.response) || '(no reply)';
|
|
16017
|
+
if (res.body && res.body.artifact) R.state.chatArtifact = res.body.artifact;
|
|
16018
|
+
R.appendChatMessage('assistant', reply);
|
|
16019
|
+
R.renderChatPreview();
|
|
16020
|
+
if (status) status.textContent = (res.body && res.body.artifact) ? 'Draft updated.' : '';
|
|
16021
|
+
})
|
|
16022
|
+
.catch(function(err){
|
|
16023
|
+
R.state.chatBusy = false;
|
|
16024
|
+
if (sendBtn) { sendBtn.textContent = 'Send'; sendBtn.disabled = false; }
|
|
16025
|
+
if (status) status.textContent = 'Chat error: ' + err;
|
|
16026
|
+
});
|
|
16027
|
+
},
|
|
16028
|
+
// Persist the current draft as a real trick. The artifact's steps
|
|
16029
|
+
// field can come back as a YAML-ish string (per the agent's prompt
|
|
16030
|
+
// template); we hand it off to /api/routines which parses it.
|
|
16031
|
+
saveChatDraft: function() {
|
|
16032
|
+
var a = R.state.chatArtifact;
|
|
16033
|
+
if (!a || !a.name) return;
|
|
16034
|
+
var btn = document.getElementById('routines-chat-save');
|
|
16035
|
+
if (btn) { btn.textContent = 'Saving…'; btn.disabled = true; }
|
|
16036
|
+
apiFetch('/api/routines', {
|
|
16037
|
+
method: 'POST',
|
|
16038
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16039
|
+
body: JSON.stringify({
|
|
16040
|
+
name: a.name,
|
|
16041
|
+
description: a.description || '',
|
|
16042
|
+
schedule: a.schedule || '',
|
|
16043
|
+
model: a.model || undefined,
|
|
16044
|
+
draftYaml: a.steps,
|
|
16045
|
+
})
|
|
16046
|
+
}).then(function(r){ return r.json().then(function(j){ return { ok: r.ok, body: j }; }); })
|
|
16047
|
+
.then(function(res){
|
|
16048
|
+
if (btn) { btn.textContent = 'Save trick'; btn.disabled = false; }
|
|
16049
|
+
if (!res.ok) {
|
|
16050
|
+
alert('Save failed: ' + (res.body && res.body.error || 'unknown'));
|
|
16051
|
+
return;
|
|
16052
|
+
}
|
|
16053
|
+
R.closeChat();
|
|
16054
|
+
if (res.body && res.body.id) R.openEditor(res.body.id);
|
|
16055
|
+
}).catch(function(err){
|
|
16056
|
+
if (btn) { btn.textContent = 'Save trick'; btn.disabled = false; }
|
|
16057
|
+
alert('Save failed: ' + err);
|
|
16058
|
+
});
|
|
16059
|
+
},
|
|
16060
|
+
// ── helpers ─────────────────────────────────────────────────
|
|
16061
|
+
esc: function(s) {
|
|
16062
|
+
if (s == null) return '';
|
|
16063
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
16064
|
+
},
|
|
16065
|
+
// Available Claude models for trick + step model pickers.
|
|
16066
|
+
MODEL_OPTS: [
|
|
16067
|
+
{ id: 'claude-opus-4-7', label: 'Opus 4.7 — most capable' },
|
|
16068
|
+
{ id: 'claude-sonnet-4-6', label: 'Sonnet 4.6 — balanced' },
|
|
16069
|
+
{ id: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5 — fastest' },
|
|
16070
|
+
],
|
|
16071
|
+
// Render a model select. If opts.idx is set, this is a per-step
|
|
16072
|
+
// picker and changes route through updateStep(idx, 'model', value);
|
|
16073
|
+
// otherwise it's the trick-level picker (id=re-model) that
|
|
16074
|
+
// markDirty reads.
|
|
16075
|
+
modelSelect: function(id, current, defaultLabel, opts) {
|
|
16076
|
+
var size = (opts && opts.small) ? 'padding:3px 6px;font-size:11px;min-width:140px' : 'padding:6px 10px;font-size:12px;min-width:200px';
|
|
16077
|
+
var idAttr = id ? ' id="' + id + '"' : '';
|
|
16078
|
+
var onchange = (opts && typeof opts.idx === 'number')
|
|
16079
|
+
? ' onchange="window.RoutinesUI&&RoutinesUI.updateStep(' + opts.idx + ',\\x27model\\x27,this.value)"'
|
|
16080
|
+
: ' onchange="window.RoutinesUI&&RoutinesUI.markDirty()"';
|
|
16081
|
+
var html = '<select' + idAttr + onchange + ' style="' + size + ';border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary)" title="' + R.esc(defaultLabel || '') + '">';
|
|
16082
|
+
html += '<option value=""' + (current ? '' : ' selected') + '>' + R.esc(defaultLabel || 'inherit') + '</option>';
|
|
16083
|
+
R.MODEL_OPTS.forEach(function(m){
|
|
16084
|
+
html += '<option value="' + R.esc(m.id) + '"' + (current === m.id ? ' selected' : '') + '>' + R.esc(m.label) + '</option>';
|
|
16085
|
+
});
|
|
16086
|
+
html += '</select>';
|
|
16087
|
+
return html;
|
|
16088
|
+
},
|
|
16089
|
+
};
|
|
16090
|
+
window.RoutinesUI = R;
|
|
16091
|
+
// Compatibility shims for legacy callers in other parts of the dashboard.
|
|
16092
|
+
// The old switchBuildTab(tab) is referenced from KPI tiles, getting-started cards,
|
|
16093
|
+
// and the navigateTo dispatcher. Map them all to the unified Routines view.
|
|
16094
|
+
if (typeof window.switchBuildTab !== 'function' || true) {
|
|
16095
|
+
window.switchBuildTab = function() { try { R.init(); } catch (e) { /* */ } };
|
|
16096
|
+
}
|
|
16097
|
+
// Auto-init when the user lands on the build page.
|
|
16098
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
16099
|
+
var nav = document.querySelector('[data-page="build"]');
|
|
16100
|
+
if (nav) nav.addEventListener('click', function() { setTimeout(function() { R.init(); }, 50); });
|
|
16101
|
+
// If page-build is already active on load (deep-link), init now.
|
|
16102
|
+
var page = document.getElementById('page-build');
|
|
16103
|
+
if (page && page.classList.contains('active')) R.init();
|
|
16104
|
+
});
|
|
16105
|
+
})();
|
|
16106
|
+
</script>
|
|
14865
16107
|
|
|
14866
16108
|
<!-- page-agent-detail merged into Team page; click an agent in Roster to drill down. -->
|
|
14867
16109
|
|
|
@@ -14963,12 +16205,12 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
14963
16205
|
</div>
|
|
14964
16206
|
<div class="tab-bar" id="intelligence-tabs" style="margin:0 0 0 18px">
|
|
14965
16207
|
<button class="active" data-icon="layoutDashboard" onclick="switchTab('intelligence','overview')"><span class="icon-slot"></span> Overview</button>
|
|
14966
|
-
<button data-icon="database" onclick="switchTab('intelligence','search')"><span class="icon-slot"></span>
|
|
16208
|
+
<button data-icon="database" onclick="switchTab('intelligence','search')"><span class="icon-slot"></span> Chunks</button>
|
|
14967
16209
|
<button data-icon="upload" onclick="switchTab('intelligence','seed')"><span class="icon-slot"></span> Seed</button>
|
|
14968
16210
|
<button data-icon="repeat" onclick="switchTab('intelligence','sources')"><span class="icon-slot"></span> Automate</button>
|
|
14969
16211
|
<button data-icon="listChecks" onclick="switchTab('intelligence','runs')"><span class="icon-slot"></span> Runs</button>
|
|
14970
16212
|
<button data-icon="sparkles" onclick="switchTab('intelligence','graph')"><span class="icon-slot"></span> Knowledge</button>
|
|
14971
|
-
<button data-icon="fileText" onclick="switchTab('intelligence','files')"><span class="icon-slot"></span>
|
|
16213
|
+
<button data-icon="fileText" onclick="switchTab('intelligence','files')"><span class="icon-slot"></span> Memory</button>
|
|
14972
16214
|
<button data-icon="zap" onclick="switchTab('intelligence','health')"><span class="icon-slot"></span> Health <span class="tab-badge" id="brain-health-badge" style="display:none;background:#ef4444;color:#fff">0</span></button>
|
|
14973
16215
|
<button data-icon="users" onclick="switchTab('intelligence','user-model')"><span class="icon-slot"></span> User Model</button>
|
|
14974
16216
|
<button data-icon="brain" onclick="switchTab('intelligence','learning')"><span class="icon-slot"></span> Learning <span class="tab-badge" id="brain-learning-badge" style="display:none;background:#f59e0b;color:#000">0</span></button>
|
|
@@ -15085,6 +16327,21 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
15085
16327
|
<div class="skel-block" style="padding:14px"><div class="skel-row med"></div><div class="skel-row short"></div></div>
|
|
15086
16328
|
</div>
|
|
15087
16329
|
</div>
|
|
16330
|
+
<div class="card" style="margin-bottom:14px">
|
|
16331
|
+
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap">
|
|
16332
|
+
<span>Persistent learnings</span>
|
|
16333
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
16334
|
+
<select id="learnings-filter-scope" onchange="refreshLearnings()" style="font-size:12px;padding:4px 6px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text)">
|
|
16335
|
+
<option value="active" selected>Active</option>
|
|
16336
|
+
<option value="all">Active + superseded + cancelled</option>
|
|
16337
|
+
</select>
|
|
16338
|
+
<span style="font-size:11px;color:var(--text-muted)">Distilled durable beliefs from past sessions</span>
|
|
16339
|
+
</div>
|
|
16340
|
+
</div>
|
|
16341
|
+
<div class="card-body" id="panel-learnings" style="padding:0">
|
|
16342
|
+
<div class="skel-block" style="padding:14px"><div class="skel-row med"></div><div class="skel-row short"></div></div>
|
|
16343
|
+
</div>
|
|
16344
|
+
</div>
|
|
15088
16345
|
<div class="card" style="margin-bottom:14px">
|
|
15089
16346
|
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap">
|
|
15090
16347
|
<span>Open commitments</span>
|
|
@@ -15331,24 +16588,55 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
15331
16588
|
<div id="brain-runs-list"></div>
|
|
15332
16589
|
</div>
|
|
15333
16590
|
<div class="tab-pane" id="tab-intelligence-files">
|
|
15334
|
-
<div style="display:flex;align-items:center;gap:
|
|
15335
|
-
<input type="text" id="vault-files-search" placeholder="Search title or
|
|
16591
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap">
|
|
16592
|
+
<input type="text" id="vault-files-search" placeholder="Search title, frontmatter, or content..." style="flex:1;min-width:220px;padding:7px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-input);color:var(--text-primary);font-size:13px" oninput="refreshVaultFiles()">
|
|
15336
16593
|
<select id="vault-files-agent-filter" onchange="refreshVaultFiles()" style="padding:7px 10px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-secondary);color:var(--text-primary);font-size:12px">
|
|
15337
16594
|
<option value="">All authors</option>
|
|
15338
16595
|
<option value="__shared__">Shared (vault root)</option>
|
|
15339
16596
|
</select>
|
|
15340
16597
|
<select id="vault-files-since" onchange="refreshVaultFiles()" style="padding:7px 10px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-secondary);color:var(--text-primary);font-size:12px">
|
|
15341
16598
|
<option value="7">Past 7 days</option>
|
|
15342
|
-
<option value="30"
|
|
16599
|
+
<option value="30">Past 30 days</option>
|
|
15343
16600
|
<option value="90">Past 90 days</option>
|
|
15344
16601
|
<option value="365">Past year</option>
|
|
16602
|
+
<option value="9999" selected>All time</option>
|
|
15345
16603
|
</select>
|
|
15346
|
-
<button class="btn-sm" onclick="refreshVaultFiles()"
|
|
16604
|
+
<button class="btn-sm" onclick="refreshVaultFiles()" title="Refresh"><span class="icon-slot" data-icon="refreshCw"></span></button>
|
|
15347
16605
|
</div>
|
|
15348
|
-
<div
|
|
15349
|
-
|
|
15350
|
-
|
|
16606
|
+
<div class="vault-mem-grid" style="display:grid;grid-template-columns:240px minmax(280px,1fr) minmax(360px,1.4fr);gap:12px;align-items:stretch;height:calc(100vh - 240px);min-height:520px">
|
|
16607
|
+
|
|
16608
|
+
<!-- Left rail: facet chips (folder / type / tag) -->
|
|
16609
|
+
<div class="vault-mem-rail" style="overflow-y:auto;padding:10px 8px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);font-size:12px">
|
|
16610
|
+
<div style="font-size:10px;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-muted);margin:2px 4px 6px">Folders</div>
|
|
16611
|
+
<div id="vault-files-folder-chips" class="vault-facet-list"></div>
|
|
16612
|
+
<div style="font-size:10px;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-muted);margin:14px 4px 6px">Type</div>
|
|
16613
|
+
<div id="vault-files-type-chips" class="vault-facet-list"></div>
|
|
16614
|
+
<div style="font-size:10px;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-muted);margin:14px 4px 6px">Tags</div>
|
|
16615
|
+
<div id="vault-files-tag-chips" class="vault-facet-list"></div>
|
|
16616
|
+
</div>
|
|
16617
|
+
|
|
16618
|
+
<!-- Middle: file list -->
|
|
16619
|
+
<div class="vault-mem-list" style="overflow-y:auto;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-card);display:flex;flex-direction:column;min-height:0">
|
|
16620
|
+
<div id="vault-files-list-meta" style="font-size:11px;color:var(--text-muted);padding:10px 14px;border-bottom:1px solid var(--border-light);flex-shrink:0">Loading…</div>
|
|
16621
|
+
<div id="vault-files-list" style="flex:1;overflow-y:auto;min-height:0">
|
|
16622
|
+
<div class="skel-block" style="padding:14px"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>
|
|
16623
|
+
</div>
|
|
16624
|
+
</div>
|
|
16625
|
+
|
|
16626
|
+
<!-- Right: inline reader -->
|
|
16627
|
+
<div class="vault-mem-reader" style="overflow-y:auto;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-card);display:flex;flex-direction:column;min-height:0">
|
|
16628
|
+
<div id="vault-reader-header" style="padding:14px 18px;border-bottom:1px solid var(--border-light);flex-shrink:0">
|
|
16629
|
+
<div style="font-weight:600;font-size:15px;color:var(--text-primary)">No file selected</div>
|
|
16630
|
+
<div style="font-size:11px;color:var(--text-muted);margin-top:2px">Pick a file from the list to read it here.</div>
|
|
16631
|
+
</div>
|
|
16632
|
+
<div id="vault-reader-body" style="flex:1;overflow-y:auto;padding:18px 22px;font-size:14px;line-height:1.6;min-height:0">
|
|
16633
|
+
<div style="color:var(--text-muted);font-size:13px">Tip: hover a row for a peek, click to open.</div>
|
|
16634
|
+
</div>
|
|
16635
|
+
</div>
|
|
15351
16636
|
</div>
|
|
16637
|
+
|
|
16638
|
+
<!-- Hover preview popover (shared, repositioned per row) -->
|
|
16639
|
+
<div id="vault-hover-popover" style="display:none;position:fixed;z-index:300;width:340px;max-width:90vw;background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:0 10px 30px rgba(0,0,0,0.25);padding:12px 14px;font-size:12px;line-height:1.5;pointer-events:none"></div>
|
|
15352
16640
|
</div>
|
|
15353
16641
|
<div class="tab-pane" id="tab-intelligence-health">
|
|
15354
16642
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:14px;flex-wrap:wrap">
|
|
@@ -18630,10 +19918,14 @@ function switchTab(group, tab) {
|
|
|
18630
19918
|
if (typeof refreshRecentWrites === 'function') refreshRecentWrites();
|
|
18631
19919
|
if (typeof refreshRecentEpisodes === 'function') refreshRecentEpisodes();
|
|
18632
19920
|
if (typeof refreshCommitments === 'function') refreshCommitments();
|
|
19921
|
+
if (typeof refreshLearnings === 'function') refreshLearnings();
|
|
18633
19922
|
if (typeof refreshSupersedes === 'function') refreshSupersedes();
|
|
18634
19923
|
if (typeof refreshCoverageStrip === 'function') refreshCoverageStrip();
|
|
18635
19924
|
}
|
|
18636
|
-
if (tab === 'files' && typeof refreshVaultFiles === 'function')
|
|
19925
|
+
if (tab === 'files' && typeof refreshVaultFiles === 'function') {
|
|
19926
|
+
if (typeof vaultRestoreFromHash === 'function') vaultRestoreFromHash();
|
|
19927
|
+
refreshVaultFiles();
|
|
19928
|
+
}
|
|
18637
19929
|
if (tab === 'sources') {
|
|
18638
19930
|
if (typeof brainLoadSources === 'function') brainLoadSources();
|
|
18639
19931
|
if (typeof brainLoadFeedConnectors === 'function') brainLoadFeedConnectors();
|
|
@@ -20061,7 +21353,7 @@ let scheduledWorkflowData = [];
|
|
|
20061
21353
|
let buildUsageByTask = {};
|
|
20062
21354
|
|
|
20063
21355
|
function jsStr(s) {
|
|
20064
|
-
return String(s ?? '').replace(
|
|
21356
|
+
return String(s ?? '').replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'").replace(/\\r/g, '\\\\r').replace(/\\n/g, '\\\\n');
|
|
20065
21357
|
}
|
|
20066
21358
|
|
|
20067
21359
|
function durationLabel(ms) {
|
|
@@ -24989,6 +26281,7 @@ async function submitQuickAddMemory() {
|
|
|
24989
26281
|
if (typeof refreshRecentWrites === 'function') refreshRecentWrites();
|
|
24990
26282
|
if (typeof refreshRecentEpisodes === 'function') refreshRecentEpisodes();
|
|
24991
26283
|
if (typeof refreshCommitments === 'function') refreshCommitments();
|
|
26284
|
+
if (typeof refreshLearnings === 'function') refreshLearnings();
|
|
24992
26285
|
if (typeof refreshMemory === 'function') refreshMemory();
|
|
24993
26286
|
}, 600);
|
|
24994
26287
|
} catch (err) {
|
|
@@ -25142,6 +26435,71 @@ async function refreshRecentWrites() {
|
|
|
25142
26435
|
}
|
|
25143
26436
|
}
|
|
25144
26437
|
|
|
26438
|
+
async function refreshLearnings() {
|
|
26439
|
+
var el = document.getElementById('panel-learnings');
|
|
26440
|
+
if (!el) return;
|
|
26441
|
+
try {
|
|
26442
|
+
var sel = document.getElementById('learnings-filter-scope');
|
|
26443
|
+
var scope = sel ? sel.value : 'active';
|
|
26444
|
+
var url = '/api/memory/learnings?limit=100' + (scope === 'all' ? '&all=1' : '');
|
|
26445
|
+
var r = await apiFetch(url);
|
|
26446
|
+
var d = await r.json();
|
|
26447
|
+
if (!d.ok || !Array.isArray(d.facts)) {
|
|
26448
|
+
el.innerHTML = '<div class="empty-state" style="padding:14px">' + esc(d.error || 'No data') + '</div>';
|
|
26449
|
+
return;
|
|
26450
|
+
}
|
|
26451
|
+
if (d.facts.length === 0) {
|
|
26452
|
+
el.innerHTML = '<div class="empty-state" style="padding:14px">No persistent learnings yet. They land automatically when episode consolidation extracts a durable user preference, fact, goal, or workflow pattern.</div>';
|
|
26453
|
+
return;
|
|
26454
|
+
}
|
|
26455
|
+
var html = '<table class="data-table" style="width:100%">';
|
|
26456
|
+
html += '<thead><tr>'
|
|
26457
|
+
+ '<th style="width:90px">Kind</th>'
|
|
26458
|
+
+ '<th>Belief</th>'
|
|
26459
|
+
+ '<th style="width:120px">Status</th>'
|
|
26460
|
+
+ '<th style="width:140px">Captured</th>'
|
|
26461
|
+
+ '<th style="width:140px">Actions</th>'
|
|
26462
|
+
+ '</tr></thead><tbody>';
|
|
26463
|
+
var kindColors = { preference: '#a78bfa', fact: '#10b981', goal: '#f59e0b', workflow: '#06b6d4' };
|
|
26464
|
+
for (var i = 0; i < d.facts.length; i++) {
|
|
26465
|
+
var f = d.facts[i];
|
|
26466
|
+
var color = kindColors[f.kind] || 'var(--text-muted)';
|
|
26467
|
+
var statusBadge = '';
|
|
26468
|
+
if (f.status === 'active') statusBadge = '<span style="color:#10b981">active</span>';
|
|
26469
|
+
else if (f.status === 'superseded') statusBadge = '<span style="color:var(--text-muted)">superseded → #' + (f.supersededById || '?') + '</span>';
|
|
26470
|
+
else if (f.status === 'cancelled') statusBadge = '<span style="color:#ef4444">cancelled</span>';
|
|
26471
|
+
else statusBadge = esc(f.status || '');
|
|
26472
|
+
var when = '';
|
|
26473
|
+
try { when = new Date(f.createdAt + 'Z').toLocaleString(); } catch { when = f.createdAt; }
|
|
26474
|
+
var actions = '';
|
|
26475
|
+
if (f.status === 'active') actions = '<button class="btn-sm" onclick="learningAction(' + f.id + ', \\'cancel\\')">Cancel</button>';
|
|
26476
|
+
else if (f.status === 'cancelled') actions = '<button class="btn-sm" onclick="learningAction(' + f.id + ', \\'reinstate\\')">Reinstate</button>';
|
|
26477
|
+
html += '<tr>'
|
|
26478
|
+
+ '<td style="font-size:11px;color:' + color + ';font-weight:600">' + esc(f.kind) + '</td>'
|
|
26479
|
+
+ '<td style="font-size:12px">' + esc(f.text) + '</td>'
|
|
26480
|
+
+ '<td style="font-size:11px">' + statusBadge + '</td>'
|
|
26481
|
+
+ '<td style="font-size:11px;color:var(--text-muted)">' + esc(when) + '</td>'
|
|
26482
|
+
+ '<td>' + actions + '</td>'
|
|
26483
|
+
+ '</tr>';
|
|
26484
|
+
}
|
|
26485
|
+
html += '</tbody></table>';
|
|
26486
|
+
el.innerHTML = html;
|
|
26487
|
+
} catch (err) {
|
|
26488
|
+
el.innerHTML = '<div class="empty-state" style="padding:14px">Failed to load: ' + esc(String(err)) + '</div>';
|
|
26489
|
+
}
|
|
26490
|
+
}
|
|
26491
|
+
|
|
26492
|
+
async function learningAction(id, action) {
|
|
26493
|
+
try {
|
|
26494
|
+
var r = await apiJson('POST', '/api/memory/learnings/action', { id: id, action: action });
|
|
26495
|
+
if (r.error) { toast('Action failed: ' + r.error, 'error'); return; }
|
|
26496
|
+
toast('Learning ' + action, 'success');
|
|
26497
|
+
refreshLearnings();
|
|
26498
|
+
} catch (err) {
|
|
26499
|
+
toast('Failed: ' + String(err), 'error');
|
|
26500
|
+
}
|
|
26501
|
+
}
|
|
26502
|
+
|
|
25145
26503
|
async function refreshCommitments() {
|
|
25146
26504
|
var el = document.getElementById('panel-commitments');
|
|
25147
26505
|
if (!el) return;
|
|
@@ -25303,28 +26661,118 @@ async function memoryHealthAction(action, extra) {
|
|
|
25303
26661
|
}
|
|
25304
26662
|
}
|
|
25305
26663
|
|
|
25306
|
-
// ──
|
|
25307
|
-
|
|
25308
|
-
|
|
26664
|
+
// ── Memory tab (Brain → Memory): unified file browser + reader ────
|
|
26665
|
+
// 3-pane layout (rail / list / reader). Search merges title+frontmatter
|
|
26666
|
+
// matches from /api/vault-files with content matches from /api/memory/search,
|
|
26667
|
+
// keyed by source_file. Filters and selected file persist in URL hash.
|
|
26668
|
+
var _vaultFilesCache = [];
|
|
26669
|
+
var _vaultFilesFolder = '';
|
|
26670
|
+
var _vaultFilesType = '';
|
|
26671
|
+
var _vaultFilesTag = '';
|
|
26672
|
+
var _vaultFilesActive = ''; // currently open relPath
|
|
26673
|
+
var _vaultContentMatches = {}; // relPath -> [chunkSnippets]
|
|
26674
|
+
var _vaultHoverTimer = null;
|
|
26675
|
+
var _vaultHoverCache = {};
|
|
26676
|
+
var _vaultSnippetCache = {}; // relPath -> snippet (from head=1)
|
|
26677
|
+
|
|
26678
|
+
function vaultParseHash() {
|
|
26679
|
+
var h = (location.hash || '').replace(/^#/, '');
|
|
26680
|
+
var p = h.startsWith('mem/') ? h.slice(4) : '';
|
|
26681
|
+
if (!p) return {};
|
|
26682
|
+
var out = {};
|
|
26683
|
+
p.split('&').forEach(function(kv) {
|
|
26684
|
+
var i = kv.indexOf('=');
|
|
26685
|
+
if (i < 0) return;
|
|
26686
|
+
out[decodeURIComponent(kv.slice(0, i))] = decodeURIComponent(kv.slice(i + 1));
|
|
26687
|
+
});
|
|
26688
|
+
return out;
|
|
26689
|
+
}
|
|
26690
|
+
|
|
26691
|
+
function vaultWriteHash() {
|
|
26692
|
+
var parts = [];
|
|
26693
|
+
if (_vaultFilesFolder) parts.push('folder=' + encodeURIComponent(_vaultFilesFolder));
|
|
26694
|
+
if (_vaultFilesType) parts.push('type=' + encodeURIComponent(_vaultFilesType));
|
|
26695
|
+
if (_vaultFilesTag) parts.push('tag=' + encodeURIComponent(_vaultFilesTag));
|
|
26696
|
+
if (_vaultFilesActive) parts.push('file=' + encodeURIComponent(_vaultFilesActive));
|
|
26697
|
+
var newHash = parts.length ? '#mem/' + parts.join('&') : '';
|
|
26698
|
+
if (location.hash !== newHash) {
|
|
26699
|
+
history.replaceState(null, '', location.pathname + location.search + newHash);
|
|
26700
|
+
}
|
|
26701
|
+
}
|
|
26702
|
+
|
|
26703
|
+
function vaultRestoreFromHash() {
|
|
26704
|
+
var p = vaultParseHash();
|
|
26705
|
+
if (!p || (!p.folder && !p.type && !p.tag && !p.file)) return false;
|
|
26706
|
+
_vaultFilesFolder = p.folder || '';
|
|
26707
|
+
_vaultFilesType = p.type || '';
|
|
26708
|
+
_vaultFilesTag = p.tag || '';
|
|
26709
|
+
_vaultFilesActive = p.file || '';
|
|
26710
|
+
return true;
|
|
26711
|
+
}
|
|
25309
26712
|
|
|
25310
26713
|
async function refreshVaultFiles() {
|
|
25311
26714
|
var listEl = document.getElementById('vault-files-list');
|
|
25312
26715
|
if (!listEl) return;
|
|
25313
|
-
var q = document.getElementById('vault-files-search')
|
|
25314
|
-
var agent = document.getElementById('vault-files-agent-filter')
|
|
25315
|
-
var since = document.getElementById('vault-files-since')
|
|
25316
|
-
|
|
25317
|
-
listEl.innerHTML = '<div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>';
|
|
26716
|
+
var q = (document.getElementById('vault-files-search') || {}).value || '';
|
|
26717
|
+
var agent = (document.getElementById('vault-files-agent-filter') || {}).value || '';
|
|
26718
|
+
var since = (document.getElementById('vault-files-since') || {}).value || '9999';
|
|
26719
|
+
listEl.innerHTML = '<div class="skel-block" style="padding:14px"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>';
|
|
25318
26720
|
try {
|
|
25319
26721
|
var url = '/api/vault-files?sinceDays=' + encodeURIComponent(since)
|
|
25320
26722
|
+ (q ? '&q=' + encodeURIComponent(q) : '')
|
|
25321
26723
|
+ (agent ? '&agent=' + encodeURIComponent(agent) : '')
|
|
25322
|
-
+ (_vaultFilesFolder ? '&folder=' + encodeURIComponent(_vaultFilesFolder) : '')
|
|
25323
|
-
|
|
25324
|
-
|
|
26724
|
+
+ (_vaultFilesFolder ? '&folder=' + encodeURIComponent(_vaultFilesFolder) : '')
|
|
26725
|
+
+ (_vaultFilesType ? '&type=' + encodeURIComponent(_vaultFilesType) : '')
|
|
26726
|
+
+ (_vaultFilesTag ? '&tag=' + encodeURIComponent(_vaultFilesTag) : '')
|
|
26727
|
+
+ '&limit=300';
|
|
26728
|
+
|
|
26729
|
+
// Run vault-files and content search in parallel when there is a query.
|
|
26730
|
+
var promises = [apiFetch(url).then(function(r) { return r.json(); })];
|
|
26731
|
+
if (q && q.length >= 2) {
|
|
26732
|
+
promises.push(apiFetch('/api/memory/search?q=' + encodeURIComponent(q) + '&limit=40')
|
|
26733
|
+
.then(function(r) { return r.json(); })
|
|
26734
|
+
.catch(function() { return null; }));
|
|
26735
|
+
}
|
|
26736
|
+
var results = await Promise.all(promises);
|
|
26737
|
+
var d = results[0];
|
|
26738
|
+
var contentHits = results[1];
|
|
26739
|
+
|
|
25325
26740
|
var files = d.files || [];
|
|
25326
26741
|
_vaultFilesCache = files;
|
|
25327
|
-
|
|
26742
|
+
_vaultContentMatches = {};
|
|
26743
|
+
|
|
26744
|
+
// Bucket content-search hits by source_file. Keep up to 2 snippets per file.
|
|
26745
|
+
if (contentHits && Array.isArray(contentHits.results)) {
|
|
26746
|
+
contentHits.results.forEach(function(row) {
|
|
26747
|
+
var sf = row.source_file || row.path || '';
|
|
26748
|
+
if (!sf) return;
|
|
26749
|
+
// source_file values often look like 'vault/02-People/Jordan.md' OR
|
|
26750
|
+
// a bare relPath. Normalize to relPath.
|
|
26751
|
+
var rel = sf.indexOf('vault/') === 0 ? sf.slice('vault/'.length) : sf;
|
|
26752
|
+
if (!_vaultContentMatches[rel]) _vaultContentMatches[rel] = [];
|
|
26753
|
+
if (_vaultContentMatches[rel].length < 2) {
|
|
26754
|
+
var txt = (row.content || '').replace(/\\s+/g, ' ').slice(0, 200);
|
|
26755
|
+
_vaultContentMatches[rel].push({ snippet: txt, section: row.section || '' });
|
|
26756
|
+
}
|
|
26757
|
+
});
|
|
26758
|
+
// Inject any content-only matches (file did not surface via title/path)
|
|
26759
|
+
// into the file list so the user can still open them.
|
|
26760
|
+
var existing = {};
|
|
26761
|
+
files.forEach(function(f) { existing[f.relPath] = true; });
|
|
26762
|
+
Object.keys(_vaultContentMatches).forEach(function(rel) {
|
|
26763
|
+
if (existing[rel]) return;
|
|
26764
|
+
files.push({
|
|
26765
|
+
path: '', relPath: rel,
|
|
26766
|
+
title: rel.split('/').pop().replace(/\\.md$/, ''),
|
|
26767
|
+
folder: rel.split('/')[0] || '',
|
|
26768
|
+
agentSlug: null, mtime: '', sizeBytes: 0,
|
|
26769
|
+
type: null, category: null, tags: [],
|
|
26770
|
+
_contentOnly: true,
|
|
26771
|
+
});
|
|
26772
|
+
});
|
|
26773
|
+
}
|
|
26774
|
+
|
|
26775
|
+
// Populate agent dropdown from cache (only first time)
|
|
25328
26776
|
var agentSel = document.getElementById('vault-files-agent-filter');
|
|
25329
26777
|
if (agentSel && agentSel.options.length <= 2) {
|
|
25330
26778
|
var slugs = [...new Set(files.map(function(f) { return f.agentSlug; }).filter(Boolean))].sort();
|
|
@@ -25335,100 +26783,226 @@ async function refreshVaultFiles() {
|
|
|
25335
26783
|
agentSel.appendChild(opt);
|
|
25336
26784
|
});
|
|
25337
26785
|
}
|
|
25338
|
-
|
|
25339
|
-
|
|
25340
|
-
|
|
25341
|
-
|
|
25342
|
-
|
|
25343
|
-
|
|
25344
|
-
|
|
25345
|
-
|
|
25346
|
-
|
|
25347
|
-
|
|
25348
|
-
|
|
25349
|
-
chipsEl.innerHTML = chipHtml;
|
|
26786
|
+
|
|
26787
|
+
// Render facet rails
|
|
26788
|
+
renderFacetChips('vault-files-folder-chips', d.folderCounts || {}, _vaultFilesFolder, vaultSetFolder, 30);
|
|
26789
|
+
renderFacetChips('vault-files-type-chips', d.typeCounts || {}, _vaultFilesType, vaultSetType, 20);
|
|
26790
|
+
renderFacetChips('vault-files-tag-chips', d.tagCounts || {}, _vaultFilesTag, vaultSetTag, 20);
|
|
26791
|
+
|
|
26792
|
+
// Meta line
|
|
26793
|
+
var metaEl = document.getElementById('vault-files-list-meta');
|
|
26794
|
+
if (metaEl) {
|
|
26795
|
+
var totalLabel = files.length === d.total ? files.length + ' files' : files.length + ' of ' + d.total + ' files';
|
|
26796
|
+
metaEl.textContent = totalLabel + (q ? ' matching "' + q + '"' : '') + (since !== '9999' ? ' (last ' + since + 'd)' : '');
|
|
25350
26797
|
}
|
|
26798
|
+
|
|
25351
26799
|
if (files.length === 0) {
|
|
25352
|
-
listEl.innerHTML = '<div class="empty-cta"><div class="label">No
|
|
26800
|
+
listEl.innerHTML = '<div class="empty-cta" style="padding:30px 14px"><div class="label">No matches</div><div class="hint">Try a wider time window, fewer filters, or a different search.</div></div>';
|
|
25353
26801
|
return;
|
|
25354
26802
|
}
|
|
25355
|
-
|
|
25356
|
-
html
|
|
26803
|
+
|
|
26804
|
+
var html = '';
|
|
25357
26805
|
for (var i = 0; i < files.length; i++) {
|
|
25358
26806
|
var f = files[i];
|
|
25359
|
-
var
|
|
25360
|
-
|
|
25361
|
-
|
|
25362
|
-
|
|
25363
|
-
|
|
25364
|
-
|
|
25365
|
-
|
|
25366
|
-
|
|
25367
|
-
|
|
25368
|
-
|
|
25369
|
-
|
|
25370
|
-
|
|
25371
|
-
|
|
26807
|
+
var matches = _vaultContentMatches[f.relPath];
|
|
26808
|
+
var pills = '';
|
|
26809
|
+
if (f.type) pills += '<span class="vault-pill type">' + esc(f.type) + '</span>';
|
|
26810
|
+
(f.tags || []).slice(0, 3).forEach(function(t) {
|
|
26811
|
+
pills += '<span class="vault-pill tag">' + esc(t) + '</span>';
|
|
26812
|
+
});
|
|
26813
|
+
if (matches && matches.length) pills += '<span class="vault-pill match">' + matches.length + ' match' + (matches.length > 1 ? 'es' : '') + '</span>';
|
|
26814
|
+
var when = f.mtime ? esc(timeAgo(f.mtime)) : '';
|
|
26815
|
+
var snippetHtml = '';
|
|
26816
|
+
if (matches && matches.length) {
|
|
26817
|
+
snippetHtml = '<div style="font-size:11px;color:var(--text-secondary);margin-top:4px;border-left:2px solid var(--clementine);padding-left:8px;line-height:1.5">' + esc(matches[0].snippet) + '…</div>';
|
|
26818
|
+
}
|
|
26819
|
+
html += '<div class="vault-mem-row' + (_vaultFilesActive === f.relPath ? ' active' : '') + '" data-path="' + esc(f.relPath) + '">'
|
|
26820
|
+
+ '<div class="vault-mem-row-title">' + esc(f.title) + '</div>'
|
|
26821
|
+
+ '<div class="vault-mem-row-meta">' + pills + (pills && when ? '<span style="opacity:0.5">·</span>' : '') + (when ? '<span>' + when + '</span>' : '') + '</div>'
|
|
26822
|
+
+ '<div class="vault-mem-row-path">' + esc(f.relPath) + '</div>'
|
|
26823
|
+
+ snippetHtml
|
|
25372
26824
|
+ '</div>';
|
|
25373
26825
|
}
|
|
25374
|
-
html += '</div>';
|
|
25375
26826
|
listEl.innerHTML = html;
|
|
25376
|
-
|
|
25377
|
-
|
|
25378
|
-
row.
|
|
26827
|
+
listEl.querySelectorAll('.vault-mem-row').forEach(function(row) {
|
|
26828
|
+
row.addEventListener('click', function() { openVaultFile(row.getAttribute('data-path')); });
|
|
26829
|
+
row.addEventListener('mouseenter', function(ev) { vaultStartHover(row.getAttribute('data-path'), ev); });
|
|
26830
|
+
row.addEventListener('mouseleave', vaultEndHover);
|
|
25379
26831
|
});
|
|
26832
|
+
|
|
26833
|
+
// Re-open the previously active file (or any file from hash on first load)
|
|
26834
|
+
if (_vaultFilesActive && files.some(function(f) { return f.relPath === _vaultFilesActive; })) {
|
|
26835
|
+
openVaultFile(_vaultFilesActive, true);
|
|
26836
|
+
}
|
|
25380
26837
|
} catch (err) {
|
|
25381
26838
|
listEl.innerHTML = '<div style="padding:24px;color:var(--red);font-size:13px">Failed to load: ' + esc(String(err)) + '</div>';
|
|
25382
26839
|
}
|
|
25383
26840
|
}
|
|
25384
26841
|
|
|
25385
|
-
|
|
26842
|
+
function renderFacetChips(elId, counts, current, onPick, maxItems) {
|
|
26843
|
+
var el = document.getElementById(elId);
|
|
26844
|
+
if (!el) return;
|
|
26845
|
+
var entries = Object.entries(counts).sort(function(a, b) { return b[1] - a[1]; });
|
|
26846
|
+
if (entries.length === 0) { el.innerHTML = '<div style="font-size:11px;color:var(--text-muted);padding:4px 8px">(none)</div>'; return; }
|
|
26847
|
+
var totalCount = entries.reduce(function(s, p) { return s + p[1]; }, 0);
|
|
26848
|
+
var html = '<div class="vault-facet-row' + (current === '' ? ' active' : '') + '" data-val="">All <span class="vault-facet-count">' + totalCount + '</span></div>';
|
|
26849
|
+
entries.slice(0, maxItems).forEach(function(p) {
|
|
26850
|
+
var k = p[0]; var c = p[1];
|
|
26851
|
+
if (!k) return;
|
|
26852
|
+
html += '<div class="vault-facet-row' + (current === k ? ' active' : '') + '" data-val="' + esc(k) + '">'
|
|
26853
|
+
+ '<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(k) + '</span>'
|
|
26854
|
+
+ '<span class="vault-facet-count">' + c + '</span>'
|
|
26855
|
+
+ '</div>';
|
|
26856
|
+
});
|
|
26857
|
+
el.innerHTML = html;
|
|
26858
|
+
el.querySelectorAll('.vault-facet-row').forEach(function(row) {
|
|
26859
|
+
row.addEventListener('click', function() { onPick(row.getAttribute('data-val')); });
|
|
26860
|
+
});
|
|
26861
|
+
}
|
|
26862
|
+
|
|
26863
|
+
function vaultSetFolder(v) { _vaultFilesFolder = v || ''; vaultWriteHash(); refreshVaultFiles(); }
|
|
26864
|
+
function vaultSetType(v) { _vaultFilesType = v || ''; vaultWriteHash(); refreshVaultFiles(); }
|
|
26865
|
+
function vaultSetTag(v) { _vaultFilesTag = v || ''; vaultWriteHash(); refreshVaultFiles(); }
|
|
26866
|
+
// Backwards-compat name used elsewhere.
|
|
26867
|
+
function setVaultFolderFilter(folder) { vaultSetFolder(folder); }
|
|
26868
|
+
|
|
26869
|
+
async function openVaultFile(relPath, skipHashUpdate) {
|
|
25386
26870
|
if (!relPath) return;
|
|
25387
|
-
|
|
25388
|
-
|
|
25389
|
-
|
|
25390
|
-
|
|
25391
|
-
|
|
25392
|
-
|
|
25393
|
-
|
|
25394
|
-
|
|
25395
|
-
|
|
25396
|
-
|
|
25397
|
-
|
|
25398
|
-
|
|
25399
|
-
+ '<button class="btn-icon btn-sm" onclick="closeVaultFileDrawer()" title="Close">' + lucide('x', 'icn-sm') + '</button>'
|
|
25400
|
-
+ '</div>'
|
|
25401
|
-
+ '<div id="vault-file-drawer-body" style="flex:1;overflow-y:auto;padding:18px 22px;font-size:13px;line-height:1.55"></div>';
|
|
25402
|
-
document.body.appendChild(drawer);
|
|
25403
|
-
}
|
|
25404
|
-
var titleEl = document.getElementById('vault-file-drawer-title');
|
|
25405
|
-
var pathEl = document.getElementById('vault-file-drawer-path');
|
|
25406
|
-
var body = document.getElementById('vault-file-drawer-body');
|
|
25407
|
-
if (titleEl) titleEl.textContent = relPath.split('/').pop().replace(/\\.md$/, '');
|
|
25408
|
-
if (pathEl) pathEl.textContent = relPath;
|
|
25409
|
-
if (body) body.innerHTML = '<div class="skel-block"><div class="skel-row"></div><div class="skel-row med"></div><div class="skel-row short"></div></div>';
|
|
25410
|
-
drawer.style.transform = 'translateX(0)';
|
|
26871
|
+
_vaultFilesActive = relPath;
|
|
26872
|
+
if (!skipHashUpdate) vaultWriteHash();
|
|
26873
|
+
// Visually mark active row
|
|
26874
|
+
document.querySelectorAll('#vault-files-list .vault-mem-row').forEach(function(r) {
|
|
26875
|
+
r.classList.toggle('active', r.getAttribute('data-path') === relPath);
|
|
26876
|
+
});
|
|
26877
|
+
var headerEl = document.getElementById('vault-reader-header');
|
|
26878
|
+
var bodyEl = document.getElementById('vault-reader-body');
|
|
26879
|
+
if (!headerEl || !bodyEl) return;
|
|
26880
|
+
headerEl.innerHTML = '<div style="font-weight:600;font-size:15px">' + esc(relPath.split('/').pop().replace(/\\.md$/, '')) + '</div>'
|
|
26881
|
+
+ '<div style="font-size:11px;color:var(--text-muted);font-family:\\x27JetBrains Mono\\x27,monospace;margin-top:2px">' + esc(relPath) + '</div>';
|
|
26882
|
+
bodyEl.innerHTML = '<div class="skel-block"><div class="skel-row"></div><div class="skel-row med"></div><div class="skel-row short"></div></div>';
|
|
25411
26883
|
try {
|
|
25412
26884
|
var r = await apiFetch('/api/vault-file?path=' + encodeURIComponent(relPath));
|
|
25413
26885
|
var d = await r.json();
|
|
25414
|
-
if (d.error) {
|
|
25415
|
-
|
|
25416
|
-
|
|
25417
|
-
|
|
25418
|
-
|
|
26886
|
+
if (d.error) { bodyEl.innerHTML = '<div style="color:var(--red)">' + esc(d.error) + '</div>'; return; }
|
|
26887
|
+
var raw = d.content || '';
|
|
26888
|
+
var fm = vaultExtractFrontmatter(raw);
|
|
26889
|
+
var bodyMd = fm.body;
|
|
26890
|
+
var fmCard = vaultRenderFmCard(fm.data);
|
|
26891
|
+
var rendered = renderMd(bodyMd);
|
|
26892
|
+
var toc = vaultBuildToc(bodyMd);
|
|
26893
|
+
bodyEl.innerHTML = fmCard + '<div class="vault-reader-body">' + rendered + '</div>' + toc;
|
|
25419
26894
|
} catch (err) {
|
|
25420
|
-
|
|
26895
|
+
bodyEl.innerHTML = '<div style="color:var(--red)">Failed: ' + esc(String(err)) + '</div>';
|
|
25421
26896
|
}
|
|
25422
26897
|
}
|
|
25423
26898
|
|
|
26899
|
+
// Back-compat (legacy drawer-style call sites). The drawer is gone, but the
|
|
26900
|
+
// inline reader covers the same job.
|
|
25424
26901
|
function closeVaultFileDrawer() {
|
|
25425
|
-
|
|
25426
|
-
|
|
25427
|
-
|
|
25428
|
-
|
|
25429
|
-
|
|
25430
|
-
|
|
25431
|
-
|
|
26902
|
+
_vaultFilesActive = '';
|
|
26903
|
+
vaultWriteHash();
|
|
26904
|
+
var headerEl = document.getElementById('vault-reader-header');
|
|
26905
|
+
var bodyEl = document.getElementById('vault-reader-body');
|
|
26906
|
+
if (headerEl) headerEl.innerHTML = '<div style="font-weight:600;font-size:15px">No file selected</div><div style="font-size:11px;color:var(--text-muted);margin-top:2px">Pick a file from the list to read it here.</div>';
|
|
26907
|
+
if (bodyEl) bodyEl.innerHTML = '<div style="color:var(--text-muted);font-size:13px">Tip: hover a row for a peek, click to open.</div>';
|
|
26908
|
+
document.querySelectorAll('#vault-files-list .vault-mem-row.active').forEach(function(r) { r.classList.remove('active'); });
|
|
26909
|
+
}
|
|
26910
|
+
|
|
26911
|
+
function vaultExtractFrontmatter(raw) {
|
|
26912
|
+
// Lightweight client-side YAML frontmatter parse — enough for display.
|
|
26913
|
+
if (!raw || raw.indexOf('---') !== 0) return { data: {}, body: raw || '' };
|
|
26914
|
+
var end = raw.indexOf('\\n---', 3);
|
|
26915
|
+
if (end < 0) return { data: {}, body: raw };
|
|
26916
|
+
var fmText = raw.slice(3, end).replace(/^\\n/, '');
|
|
26917
|
+
var body = raw.slice(end + 4).replace(/^\\n/, '');
|
|
26918
|
+
var data = {};
|
|
26919
|
+
fmText.split(/\\n/).forEach(function(line) {
|
|
26920
|
+
var m = line.match(/^([A-Za-z0-9_\\-]+)\\s*:\\s*(.*)$/);
|
|
26921
|
+
if (!m) return;
|
|
26922
|
+
var k = m[1]; var v = m[2].trim();
|
|
26923
|
+
if (v.startsWith('[') && v.endsWith(']')) {
|
|
26924
|
+
v = v.slice(1, -1).split(',').map(function(s) { return s.trim().replace(/^["\\x27]|["\\x27]$/g, ''); }).filter(Boolean);
|
|
26925
|
+
} else {
|
|
26926
|
+
v = v.replace(/^["\\x27]|["\\x27]$/g, '');
|
|
26927
|
+
}
|
|
26928
|
+
data[k] = v;
|
|
26929
|
+
});
|
|
26930
|
+
return { data: data, body: body };
|
|
26931
|
+
}
|
|
26932
|
+
|
|
26933
|
+
function vaultRenderFmCard(data) {
|
|
26934
|
+
var keys = Object.keys(data || {});
|
|
26935
|
+
if (keys.length === 0) return '';
|
|
26936
|
+
var html = '<div class="vault-reader-fm">';
|
|
26937
|
+
keys.forEach(function(k) {
|
|
26938
|
+
var v = data[k];
|
|
26939
|
+
var display = Array.isArray(v)
|
|
26940
|
+
? v.map(function(x) { return '<span class="vault-pill tag" style="margin-right:4px">' + esc(String(x)) + '</span>'; }).join('')
|
|
26941
|
+
: esc(String(v));
|
|
26942
|
+
html += '<div class="k">' + esc(k) + '</div><div class="v">' + display + '</div>';
|
|
26943
|
+
});
|
|
26944
|
+
html += '</div>';
|
|
26945
|
+
return html;
|
|
26946
|
+
}
|
|
26947
|
+
|
|
26948
|
+
function vaultBuildToc(md) {
|
|
26949
|
+
var lines = (md || '').split('\\n');
|
|
26950
|
+
var headings = [];
|
|
26951
|
+
for (var i = 0; i < lines.length; i++) {
|
|
26952
|
+
var m = lines[i].match(/^(#{2,3})\\s+(.+?)\\s*$/);
|
|
26953
|
+
if (!m) continue;
|
|
26954
|
+
var lvl = m[1].length;
|
|
26955
|
+
var txt = m[2];
|
|
26956
|
+
var slug = txt.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
26957
|
+
headings.push({ lvl: lvl, txt: txt, slug: slug });
|
|
26958
|
+
}
|
|
26959
|
+
if (headings.length < 2) return '';
|
|
26960
|
+
var html = '<div class="vault-reader-toc"><div class="vault-reader-toc-title">On this page</div>';
|
|
26961
|
+
headings.forEach(function(h) {
|
|
26962
|
+
html += '<a class="lvl-' + h.lvl + '" href="#' + esc(h.slug) + '">' + esc(h.txt) + '</a>';
|
|
26963
|
+
});
|
|
26964
|
+
html += '</div>';
|
|
26965
|
+
return html;
|
|
26966
|
+
}
|
|
26967
|
+
|
|
26968
|
+
function vaultStartHover(relPath, ev) {
|
|
26969
|
+
if (!relPath || relPath === _vaultFilesActive) return;
|
|
26970
|
+
clearTimeout(_vaultHoverTimer);
|
|
26971
|
+
var x = ev.clientX, y = ev.clientY;
|
|
26972
|
+
_vaultHoverTimer = setTimeout(async function() {
|
|
26973
|
+
var pop = document.getElementById('vault-hover-popover');
|
|
26974
|
+
if (!pop) return;
|
|
26975
|
+
var data = _vaultHoverCache[relPath];
|
|
26976
|
+
if (!data) {
|
|
26977
|
+
try {
|
|
26978
|
+
var r = await apiFetch('/api/vault-file?head=1&path=' + encodeURIComponent(relPath));
|
|
26979
|
+
data = await r.json();
|
|
26980
|
+
_vaultHoverCache[relPath] = data;
|
|
26981
|
+
} catch { return; }
|
|
26982
|
+
}
|
|
26983
|
+
if (!data || data.error) return;
|
|
26984
|
+
var fm = data.frontmatter || {};
|
|
26985
|
+
var fmHtml = '';
|
|
26986
|
+
Object.keys(fm).slice(0, 5).forEach(function(k) {
|
|
26987
|
+
var v = fm[k];
|
|
26988
|
+
var dv = Array.isArray(v) ? v.join(', ') : String(v);
|
|
26989
|
+
fmHtml += '<div style="display:flex;gap:6px"><span style="color:var(--text-muted);min-width:60px">' + esc(k) + '</span><span>' + esc(dv) + '</span></div>';
|
|
26990
|
+
});
|
|
26991
|
+
pop.innerHTML = '<div style="font-weight:600;font-size:13px;margin-bottom:4px">' + esc(relPath.split('/').pop().replace(/\\.md$/, '')) + '</div>'
|
|
26992
|
+
+ (fmHtml ? '<div style="margin-bottom:6px">' + fmHtml + '</div>' : '')
|
|
26993
|
+
+ '<div style="color:var(--text-secondary)">' + esc(data.snippet || '(empty)') + '</div>';
|
|
26994
|
+
var px = Math.min(window.innerWidth - 360, x + 16);
|
|
26995
|
+
var py = Math.min(window.innerHeight - 200, y + 16);
|
|
26996
|
+
pop.style.left = px + 'px';
|
|
26997
|
+
pop.style.top = py + 'px';
|
|
26998
|
+
pop.style.display = 'block';
|
|
26999
|
+
}, 250);
|
|
27000
|
+
}
|
|
27001
|
+
|
|
27002
|
+
function vaultEndHover() {
|
|
27003
|
+
clearTimeout(_vaultHoverTimer);
|
|
27004
|
+
var pop = document.getElementById('vault-hover-popover');
|
|
27005
|
+
if (pop) pop.style.display = 'none';
|
|
25432
27006
|
}
|
|
25433
27007
|
|
|
25434
27008
|
// ── Goals: inline create form ────────────────────────────────────
|