clementine-agent 1.18.29 → 1.18.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/dashboard.js +1198 -168
- package/dist/dashboard/builder/dry-run.js +10 -0
- package/dist/dashboard/builder/runner.js +80 -0
- package/dist/dashboard/builder/serializer.js +4 -0
- package/dist/dashboard/builder/validation.js +12 -0
- package/dist/gateway/context-policy.d.ts +10 -0
- package/dist/gateway/context-policy.js +25 -1
- package/dist/gateway/entity-registry.d.ts +47 -0
- package/dist/gateway/entity-registry.js +92 -0
- package/dist/gateway/episodic-consolidation.d.ts +11 -16
- package/dist/gateway/episodic-consolidation.js +86 -3
- package/dist/gateway/router.js +41 -1
- package/dist/memory/store.d.ts +82 -0
- package/dist/memory/store.js +247 -0
- package/dist/tools/builder-tools.js +3 -1
- package/dist/types.d.ts +9 -1
- package/package.json +1 -1
package/dist/cli/dashboard.js
CHANGED
|
@@ -3989,6 +3989,362 @@ export async function cmdDashboard(opts) {
|
|
|
3989
3989
|
res.status(500).json({ error: 'Create failed', detail: String(err) });
|
|
3990
3990
|
}
|
|
3991
3991
|
});
|
|
3992
|
+
// ── Routines API (canonical surface for the Build tab) ─────────
|
|
3993
|
+
// The "Routines" UI uses this surface exclusively. Workflows + cron
|
|
3994
|
+
// jobs both flow through here as a single Routine concept; legacy
|
|
3995
|
+
// /api/builder/* and /api/cron/* endpoints remain for one minor
|
|
3996
|
+
// version, then are removed.
|
|
3997
|
+
app.get('/api/routines', async (_req, res) => {
|
|
3998
|
+
try {
|
|
3999
|
+
const { listAllForBuilder } = await import('../dashboard/builder/serializer.js');
|
|
4000
|
+
res.json({ routines: listAllForBuilder() });
|
|
4001
|
+
}
|
|
4002
|
+
catch (err) {
|
|
4003
|
+
res.status(500).json({ error: 'list failed', detail: String(err) });
|
|
4004
|
+
}
|
|
4005
|
+
});
|
|
4006
|
+
app.get('/api/routines/mcp-tools', async (_req, res) => {
|
|
4007
|
+
try {
|
|
4008
|
+
const { discoverMcpServers, loadToolInventory } = await import('../agent/mcp-bridge.js');
|
|
4009
|
+
const servers = discoverMcpServers();
|
|
4010
|
+
const inv = loadToolInventory();
|
|
4011
|
+
const allTools = inv?.tools ?? [];
|
|
4012
|
+
// Group flat tool names of shape `mcp__<server>__<tool>` (server may
|
|
4013
|
+
// contain underscores — split on the first `__` after the prefix).
|
|
4014
|
+
const grouped = {};
|
|
4015
|
+
for (const s of servers) {
|
|
4016
|
+
grouped[s.name] = { name: s.name, enabled: s.enabled !== false, tools: [] };
|
|
4017
|
+
}
|
|
4018
|
+
for (const t of allTools) {
|
|
4019
|
+
if (!t.startsWith('mcp__'))
|
|
4020
|
+
continue;
|
|
4021
|
+
const rest = t.slice(5);
|
|
4022
|
+
const idx = rest.indexOf('__');
|
|
4023
|
+
if (idx < 0)
|
|
4024
|
+
continue;
|
|
4025
|
+
const server = rest.slice(0, idx);
|
|
4026
|
+
const tool = rest.slice(idx + 2);
|
|
4027
|
+
if (!grouped[server])
|
|
4028
|
+
grouped[server] = { name: server, enabled: true, tools: [] };
|
|
4029
|
+
if (!grouped[server].tools.includes(tool))
|
|
4030
|
+
grouped[server].tools.push(tool);
|
|
4031
|
+
}
|
|
4032
|
+
const out = Object.values(grouped)
|
|
4033
|
+
.filter(s => s.tools.length > 0 || s.enabled)
|
|
4034
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
4035
|
+
res.json({ servers: out });
|
|
4036
|
+
}
|
|
4037
|
+
catch (err) {
|
|
4038
|
+
res.status(500).json({ error: 'mcp-tools failed', detail: String(err) });
|
|
4039
|
+
}
|
|
4040
|
+
});
|
|
4041
|
+
app.get('/api/routines/cli-tools', async (_req, res) => {
|
|
4042
|
+
try {
|
|
4043
|
+
// Reuse discoverCliTools() defined elsewhere in this file.
|
|
4044
|
+
const tools = discoverCliTools().filter(t => t.installed && !t.blocked);
|
|
4045
|
+
res.json({ tools: tools.map(t => ({ cmd: t.name, description: t.description, userDefined: !!t.userDefined })) });
|
|
4046
|
+
}
|
|
4047
|
+
catch (err) {
|
|
4048
|
+
res.status(500).json({ error: 'cli-tools failed', detail: String(err) });
|
|
4049
|
+
}
|
|
4050
|
+
});
|
|
4051
|
+
app.get('/api/routines/:id', async (req, res) => {
|
|
4052
|
+
try {
|
|
4053
|
+
const id = decodeURIComponent(req.params.id);
|
|
4054
|
+
const { readWorkflow } = await import('../dashboard/builder/serializer.js');
|
|
4055
|
+
const { validateWorkflow } = await import('../dashboard/builder/validation.js');
|
|
4056
|
+
const wf = readWorkflow(id);
|
|
4057
|
+
if (!wf) {
|
|
4058
|
+
res.status(404).json({ error: 'Not found' });
|
|
4059
|
+
return;
|
|
4060
|
+
}
|
|
4061
|
+
res.json({ id, routine: wf, validation: validateWorkflow(wf) });
|
|
4062
|
+
}
|
|
4063
|
+
catch (err) {
|
|
4064
|
+
res.status(500).json({ error: 'read failed', detail: String(err) });
|
|
4065
|
+
}
|
|
4066
|
+
});
|
|
4067
|
+
app.post('/api/routines', async (req, res) => {
|
|
4068
|
+
try {
|
|
4069
|
+
const body = req.body;
|
|
4070
|
+
if (!body || !body.name) {
|
|
4071
|
+
res.status(400).json({ error: 'name required' });
|
|
4072
|
+
return;
|
|
4073
|
+
}
|
|
4074
|
+
const [{ saveWorkflow, workflowId: makeId }, { emitBuilderEvent }] = await Promise.all([
|
|
4075
|
+
import('../dashboard/builder/serializer.js'),
|
|
4076
|
+
import('../dashboard/builder/events.js'),
|
|
4077
|
+
]);
|
|
4078
|
+
const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64) || 'routine';
|
|
4079
|
+
const agentSlug = body.agent ? (String(body.agent).trim() || undefined) : undefined;
|
|
4080
|
+
const wf = {
|
|
4081
|
+
name: body.name,
|
|
4082
|
+
description: body.description ?? '',
|
|
4083
|
+
enabled: true,
|
|
4084
|
+
trigger: body.schedule ? { schedule: body.schedule, manual: false } : { manual: true },
|
|
4085
|
+
inputs: {},
|
|
4086
|
+
steps: [{
|
|
4087
|
+
id: 's1',
|
|
4088
|
+
prompt: body.initialPrompt ?? 'Describe what this routine should do.',
|
|
4089
|
+
dependsOn: [],
|
|
4090
|
+
tier: 1,
|
|
4091
|
+
maxTurns: 15,
|
|
4092
|
+
}],
|
|
4093
|
+
sourceFile: '',
|
|
4094
|
+
agentSlug,
|
|
4095
|
+
};
|
|
4096
|
+
const id = makeId(slug, agentSlug);
|
|
4097
|
+
const result = saveWorkflow(id, wf);
|
|
4098
|
+
if (!result.ok) {
|
|
4099
|
+
res.status(400).json({ error: result.error });
|
|
4100
|
+
return;
|
|
4101
|
+
}
|
|
4102
|
+
emitBuilderEvent({ type: 'workflow:created', workflowId: id, payload: { workflow: wf } });
|
|
4103
|
+
res.json({ ok: true, id });
|
|
4104
|
+
}
|
|
4105
|
+
catch (err) {
|
|
4106
|
+
res.status(500).json({ error: 'create failed', detail: String(err) });
|
|
4107
|
+
}
|
|
4108
|
+
});
|
|
4109
|
+
app.put('/api/routines/:id', async (req, res) => {
|
|
4110
|
+
try {
|
|
4111
|
+
const id = decodeURIComponent(req.params.id);
|
|
4112
|
+
const body = req.body;
|
|
4113
|
+
if (!body || typeof body.routine !== 'object') {
|
|
4114
|
+
res.status(400).json({ error: 'Missing routine body' });
|
|
4115
|
+
return;
|
|
4116
|
+
}
|
|
4117
|
+
const [{ readWorkflow, saveWorkflow }, { validateWorkflow }, { emitBuilderEvent }] = await Promise.all([
|
|
4118
|
+
import('../dashboard/builder/serializer.js'),
|
|
4119
|
+
import('../dashboard/builder/validation.js'),
|
|
4120
|
+
import('../dashboard/builder/events.js'),
|
|
4121
|
+
]);
|
|
4122
|
+
const existing = readWorkflow(id);
|
|
4123
|
+
if (!existing) {
|
|
4124
|
+
res.status(404).json({ error: 'Not found' });
|
|
4125
|
+
return;
|
|
4126
|
+
}
|
|
4127
|
+
const incoming = body.routine;
|
|
4128
|
+
const next = { ...incoming, sourceFile: existing.sourceFile };
|
|
4129
|
+
const v = validateWorkflow(next);
|
|
4130
|
+
if (!v.ok && !body.force) {
|
|
4131
|
+
res.status(400).json({ error: 'validation', validation: v });
|
|
4132
|
+
return;
|
|
4133
|
+
}
|
|
4134
|
+
const result = saveWorkflow(id, next);
|
|
4135
|
+
if (!result.ok) {
|
|
4136
|
+
res.status(400).json({ error: result.error });
|
|
4137
|
+
return;
|
|
4138
|
+
}
|
|
4139
|
+
emitBuilderEvent({ type: 'workflow:patched', workflowId: id, payload: { workflow: next } });
|
|
4140
|
+
res.json({ ok: true, validation: v });
|
|
4141
|
+
}
|
|
4142
|
+
catch (err) {
|
|
4143
|
+
res.status(500).json({ error: 'save failed', detail: String(err) });
|
|
4144
|
+
}
|
|
4145
|
+
});
|
|
4146
|
+
app.delete('/api/routines/:id', async (req, res) => {
|
|
4147
|
+
try {
|
|
4148
|
+
const id = decodeURIComponent(req.params.id);
|
|
4149
|
+
const [{ readWorkflow, parseBuilderId }, { emitBuilderEvent }] = await Promise.all([
|
|
4150
|
+
import('../dashboard/builder/serializer.js'),
|
|
4151
|
+
import('../dashboard/builder/events.js'),
|
|
4152
|
+
]);
|
|
4153
|
+
const parsed = parseBuilderId(id);
|
|
4154
|
+
if (!parsed) {
|
|
4155
|
+
res.status(400).json({ error: 'Bad id' });
|
|
4156
|
+
return;
|
|
4157
|
+
}
|
|
4158
|
+
if (parsed.origin === 'cron') {
|
|
4159
|
+
res.status(400).json({ error: 'This routine came from a legacy cron entry — disable it instead, or edit CRON.md directly.' });
|
|
4160
|
+
return;
|
|
4161
|
+
}
|
|
4162
|
+
const wf = readWorkflow(id);
|
|
4163
|
+
if (!wf) {
|
|
4164
|
+
res.status(404).json({ error: 'Not found' });
|
|
4165
|
+
return;
|
|
4166
|
+
}
|
|
4167
|
+
if (wf.sourceFile && existsSync(wf.sourceFile))
|
|
4168
|
+
unlinkSync(wf.sourceFile);
|
|
4169
|
+
emitBuilderEvent({ type: 'workflow:deleted', workflowId: id });
|
|
4170
|
+
res.json({ ok: true });
|
|
4171
|
+
}
|
|
4172
|
+
catch (err) {
|
|
4173
|
+
res.status(500).json({ error: String(err) });
|
|
4174
|
+
}
|
|
4175
|
+
});
|
|
4176
|
+
app.post('/api/routines/:id/toggle', async (req, res) => {
|
|
4177
|
+
try {
|
|
4178
|
+
const id = decodeURIComponent(req.params.id);
|
|
4179
|
+
const { readWorkflow, saveWorkflow } = await import('../dashboard/builder/serializer.js');
|
|
4180
|
+
const wf = readWorkflow(id);
|
|
4181
|
+
if (!wf) {
|
|
4182
|
+
res.status(404).json({ error: 'Not found' });
|
|
4183
|
+
return;
|
|
4184
|
+
}
|
|
4185
|
+
wf.enabled = !wf.enabled;
|
|
4186
|
+
const result = saveWorkflow(id, wf);
|
|
4187
|
+
if (!result.ok) {
|
|
4188
|
+
res.status(400).json({ error: result.error });
|
|
4189
|
+
return;
|
|
4190
|
+
}
|
|
4191
|
+
res.json({ ok: true, enabled: wf.enabled });
|
|
4192
|
+
}
|
|
4193
|
+
catch (err) {
|
|
4194
|
+
res.status(500).json({ error: 'toggle failed', detail: String(err) });
|
|
4195
|
+
}
|
|
4196
|
+
});
|
|
4197
|
+
app.post('/api/routines/:id/run', async (req, res) => {
|
|
4198
|
+
try {
|
|
4199
|
+
const id = decodeURIComponent(req.params.id);
|
|
4200
|
+
const { readWorkflow, parseBuilderId } = await import('../dashboard/builder/serializer.js');
|
|
4201
|
+
const wf = readWorkflow(id);
|
|
4202
|
+
if (!wf) {
|
|
4203
|
+
res.status(404).json({ error: 'Not found' });
|
|
4204
|
+
return;
|
|
4205
|
+
}
|
|
4206
|
+
const parsed = parseBuilderId(id);
|
|
4207
|
+
const body = (req.body ?? {});
|
|
4208
|
+
// Cron-origin routines: spawn the cli `cron run <name>` (single-step prompt path).
|
|
4209
|
+
if (parsed?.origin === 'cron') {
|
|
4210
|
+
const child = spawn('node', [DIST_ENTRY, 'cron', 'run', wf.name], {
|
|
4211
|
+
detached: true,
|
|
4212
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4213
|
+
cwd: BASE_DIR,
|
|
4214
|
+
env: { ...process.env, CLEMENTINE_HOME: BASE_DIR },
|
|
4215
|
+
});
|
|
4216
|
+
child.on('exit', (code) => {
|
|
4217
|
+
broadcastEvent({ type: 'cron_complete', data: { job: wf.name, code } });
|
|
4218
|
+
responseCache.delete('activity:');
|
|
4219
|
+
});
|
|
4220
|
+
child.unref();
|
|
4221
|
+
broadcastEvent({ type: 'cron_triggered', data: { job: wf.name } });
|
|
4222
|
+
res.json({ ok: true, message: `Triggered routine: ${wf.name}` });
|
|
4223
|
+
return;
|
|
4224
|
+
}
|
|
4225
|
+
// Workflow-origin routines: side-effect approval gate, then route through gateway.handleWorkflow.
|
|
4226
|
+
const sideEffects = wf.steps
|
|
4227
|
+
.filter(step => {
|
|
4228
|
+
const kind = step.kind ?? 'prompt';
|
|
4229
|
+
if (kind === 'channel' || kind === 'mcp' || kind === 'cli')
|
|
4230
|
+
return true;
|
|
4231
|
+
return /\b(send|post|publish|email|webhook|delete|write|update|create)\b/i.test(step.prompt || '');
|
|
4232
|
+
})
|
|
4233
|
+
.map(step => ({
|
|
4234
|
+
id: step.id,
|
|
4235
|
+
kind: step.kind ?? 'prompt',
|
|
4236
|
+
label: step.channel
|
|
4237
|
+
? `${step.channel.channel}:${step.channel.target}`
|
|
4238
|
+
: step.mcp
|
|
4239
|
+
? `${step.mcp.server}.${step.mcp.tool}`
|
|
4240
|
+
: step.cli
|
|
4241
|
+
? `${step.cli.cmd}${step.cli.args?.length ? ' ' + step.cli.args.join(' ') : ''}`
|
|
4242
|
+
: step.prompt.slice(0, 80),
|
|
4243
|
+
}));
|
|
4244
|
+
if (sideEffects.length > 0 && body.approvedSideEffects !== true) {
|
|
4245
|
+
res.status(409).json({
|
|
4246
|
+
ok: false,
|
|
4247
|
+
error: 'approval_required',
|
|
4248
|
+
message: 'This routine may send, write, post, or call external tools. Approve side effects before running it.',
|
|
4249
|
+
sideEffects,
|
|
4250
|
+
});
|
|
4251
|
+
return;
|
|
4252
|
+
}
|
|
4253
|
+
res.json({ ok: true, message: `Routine "${wf.name}" triggered` });
|
|
4254
|
+
broadcastEvent({ type: 'workflow_triggered', data: { id, name: wf.name } });
|
|
4255
|
+
getGateway().then(gw => gw.handleWorkflow(wf, body.inputs || {})).then(result => {
|
|
4256
|
+
broadcastEvent({ type: 'workflow_complete', data: { id, name: wf.name, status: 'ok', preview: (result || '').slice(0, 300) } });
|
|
4257
|
+
}).catch(err => {
|
|
4258
|
+
broadcastEvent({ type: 'workflow_complete', data: { id, name: wf.name, status: 'error', error: String(err) } });
|
|
4259
|
+
});
|
|
4260
|
+
}
|
|
4261
|
+
catch (err) {
|
|
4262
|
+
res.status(500).json({ error: 'run failed', detail: String(err) });
|
|
4263
|
+
}
|
|
4264
|
+
});
|
|
4265
|
+
app.post('/api/routines/:id/dry-run', async (req, res) => {
|
|
4266
|
+
try {
|
|
4267
|
+
const id = decodeURIComponent(req.params.id);
|
|
4268
|
+
const [{ readWorkflow }, { dryRunWorkflow }] = await Promise.all([
|
|
4269
|
+
import('../dashboard/builder/serializer.js'),
|
|
4270
|
+
import('../dashboard/builder/dry-run.js'),
|
|
4271
|
+
]);
|
|
4272
|
+
const wf = readWorkflow(id);
|
|
4273
|
+
if (!wf) {
|
|
4274
|
+
res.status(404).json({ error: 'Not found' });
|
|
4275
|
+
return;
|
|
4276
|
+
}
|
|
4277
|
+
res.json(dryRunWorkflow(wf));
|
|
4278
|
+
}
|
|
4279
|
+
catch (err) {
|
|
4280
|
+
res.status(500).json({ error: 'dry-run failed', detail: String(err) });
|
|
4281
|
+
}
|
|
4282
|
+
});
|
|
4283
|
+
app.post('/api/routines/:id/test', async (req, res) => {
|
|
4284
|
+
try {
|
|
4285
|
+
const id = decodeURIComponent(req.params.id);
|
|
4286
|
+
const body = (req.body ?? {});
|
|
4287
|
+
const [{ readWorkflow }, { runWorkflowTest }] = await Promise.all([
|
|
4288
|
+
import('../dashboard/builder/serializer.js'),
|
|
4289
|
+
import('../dashboard/builder/runner.js'),
|
|
4290
|
+
]);
|
|
4291
|
+
const wf = readWorkflow(id);
|
|
4292
|
+
if (!wf) {
|
|
4293
|
+
res.status(404).json({ error: 'Not found' });
|
|
4294
|
+
return;
|
|
4295
|
+
}
|
|
4296
|
+
const runId = (await import('node:crypto')).randomUUID();
|
|
4297
|
+
res.json({ ok: true, runId });
|
|
4298
|
+
runWorkflowTest(wf, {
|
|
4299
|
+
workflowId: id,
|
|
4300
|
+
runId,
|
|
4301
|
+
mode: body.mode ?? 'mock',
|
|
4302
|
+
perStepTimeoutMs: body.perStepTimeoutMs,
|
|
4303
|
+
totalBudgetMs: body.totalBudgetMs,
|
|
4304
|
+
}).catch(() => { });
|
|
4305
|
+
}
|
|
4306
|
+
catch (err) {
|
|
4307
|
+
res.status(500).json({ error: 'test failed to start', detail: String(err) });
|
|
4308
|
+
}
|
|
4309
|
+
});
|
|
4310
|
+
app.get('/api/routines/:id/runs', async (req, res) => {
|
|
4311
|
+
try {
|
|
4312
|
+
const id = decodeURIComponent(req.params.id);
|
|
4313
|
+
const { readWorkflow } = await import('../dashboard/builder/serializer.js');
|
|
4314
|
+
const wf = readWorkflow(id);
|
|
4315
|
+
if (!wf) {
|
|
4316
|
+
res.status(404).json({ error: 'Not found' });
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
const safe = wf.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
4320
|
+
const cronLogPath = path.join(BASE_DIR, 'cron-logs', `${safe}.jsonl`);
|
|
4321
|
+
const workflowLogPath = path.join(BASE_DIR, 'workflows', 'runs', `${safe}.jsonl`);
|
|
4322
|
+
const runs = [];
|
|
4323
|
+
for (const file of [cronLogPath, workflowLogPath]) {
|
|
4324
|
+
if (!existsSync(file))
|
|
4325
|
+
continue;
|
|
4326
|
+
try {
|
|
4327
|
+
const lines = readFileSync(file, 'utf-8').split('\n').filter(l => l.trim());
|
|
4328
|
+
for (const line of lines.slice(-50)) {
|
|
4329
|
+
try {
|
|
4330
|
+
runs.push(JSON.parse(line));
|
|
4331
|
+
}
|
|
4332
|
+
catch { /* skip malformed */ }
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
catch { /* skip unreadable */ }
|
|
4336
|
+
}
|
|
4337
|
+
runs.sort((a, b) => {
|
|
4338
|
+
const at = String(a.startedAt || a.timestamp || '');
|
|
4339
|
+
const bt = String(b.startedAt || b.timestamp || '');
|
|
4340
|
+
return bt.localeCompare(at);
|
|
4341
|
+
});
|
|
4342
|
+
res.json({ runs: runs.slice(0, 50) });
|
|
4343
|
+
}
|
|
4344
|
+
catch (err) {
|
|
4345
|
+
res.status(500).json({ error: 'runs read failed', detail: String(err) });
|
|
4346
|
+
}
|
|
4347
|
+
});
|
|
3992
4348
|
// SSE events handler moved before auth middleware (see above)
|
|
3993
4349
|
// ── POST routes (actions) ──────────────────────────────────────
|
|
3994
4350
|
app.post('/api/cron/run/:job', (req, res) => {
|
|
@@ -7044,6 +7400,55 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
7044
7400
|
res.status(500).json({ error: String(err) });
|
|
7045
7401
|
}
|
|
7046
7402
|
});
|
|
7403
|
+
// Learned facts — durable cross-session beliefs with supersession lineage.
|
|
7404
|
+
app.get('/api/memory/learnings', async (req, res) => {
|
|
7405
|
+
try {
|
|
7406
|
+
const gateway = await getGateway();
|
|
7407
|
+
const store = gateway.assistant?.memoryStore;
|
|
7408
|
+
if (!store || typeof store.listAllLearnedFacts !== 'function') {
|
|
7409
|
+
res.status(503).json({ error: 'Learnings store not available' });
|
|
7410
|
+
return;
|
|
7411
|
+
}
|
|
7412
|
+
const limit = Math.min(parseInt(String(req.query.limit ?? '100'), 10) || 100, 1000);
|
|
7413
|
+
const showAll = String(req.query.all ?? '') === '1';
|
|
7414
|
+
const facts = showAll
|
|
7415
|
+
? store.listAllLearnedFacts({ limit })
|
|
7416
|
+
: store.listActiveLearnedFacts({ limit });
|
|
7417
|
+
res.json({ ok: true, facts });
|
|
7418
|
+
}
|
|
7419
|
+
catch (err) {
|
|
7420
|
+
res.status(500).json({ error: String(err) });
|
|
7421
|
+
}
|
|
7422
|
+
});
|
|
7423
|
+
app.post('/api/memory/learnings/action', async (req, res) => {
|
|
7424
|
+
try {
|
|
7425
|
+
const gateway = await getGateway();
|
|
7426
|
+
const store = gateway.assistant?.memoryStore;
|
|
7427
|
+
if (!store || typeof store.setLearnedFactStatus !== 'function') {
|
|
7428
|
+
res.status(503).json({ error: 'Learnings store not available' });
|
|
7429
|
+
return;
|
|
7430
|
+
}
|
|
7431
|
+
const id = Number(req.body?.id);
|
|
7432
|
+
const action = String(req.body?.action ?? '');
|
|
7433
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
7434
|
+
res.status(400).json({ error: 'id required' });
|
|
7435
|
+
return;
|
|
7436
|
+
}
|
|
7437
|
+
let updated = false;
|
|
7438
|
+
if (action === 'cancel')
|
|
7439
|
+
updated = store.setLearnedFactStatus(id, 'cancelled');
|
|
7440
|
+
else if (action === 'reinstate')
|
|
7441
|
+
updated = store.setLearnedFactStatus(id, 'active');
|
|
7442
|
+
else {
|
|
7443
|
+
res.status(400).json({ error: 'invalid action' });
|
|
7444
|
+
return;
|
|
7445
|
+
}
|
|
7446
|
+
res.json({ ok: updated });
|
|
7447
|
+
}
|
|
7448
|
+
catch (err) {
|
|
7449
|
+
res.status(500).json({ error: String(err) });
|
|
7450
|
+
}
|
|
7451
|
+
});
|
|
7047
7452
|
// Commitments — durable promises tracked across sessions.
|
|
7048
7453
|
app.get('/api/memory/commitments', async (req, res) => {
|
|
7049
7454
|
try {
|
|
@@ -14679,189 +15084,732 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
14679
15084
|
</div>
|
|
14680
15085
|
</div>
|
|
14681
15086
|
|
|
14682
|
-
<!-- ═══
|
|
15087
|
+
<!-- ═══ Build Page — Routines (single unified surface) ═══ -->
|
|
14683
15088
|
<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="">
|
|
15089
|
+
<!-- Toolbar -->
|
|
15090
|
+
<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">
|
|
15091
|
+
<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> Routines</h2>
|
|
15092
|
+
<span id="routines-count" style="font-size:11px;color:var(--text-muted)"></span>
|
|
15093
|
+
<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
15094
|
<span style="flex:1"></span>
|
|
14706
|
-
<
|
|
14707
|
-
<
|
|
14708
|
-
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
<
|
|
15095
|
+
<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>
|
|
15096
|
+
<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">
|
|
15097
|
+
<option value="__all__">All</option>
|
|
15098
|
+
<option value="__global__">Clementine (global)</option>
|
|
15099
|
+
</select>
|
|
15100
|
+
<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>
|
|
15101
|
+
<button id="routines-assist-btn" class="btn-sm" onclick="window.RoutinesUI && RoutinesUI.openAssist()" title="Describe a routine in natural language" 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)">Generate from prompt</button>
|
|
15102
|
+
<button id="routines-create-btn" class="btn-sm btn-primary" onclick="window.RoutinesUI && RoutinesUI.openCreate()" style="padding:6px 14px;border-radius:6px;cursor:pointer;font-size:12px">+ New Routine</button>
|
|
14712
15103
|
</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>
|
|
14738
|
-
</div>
|
|
14739
|
-
<div style="display:flex;gap:8px;padding:12px 16px;border-top:1px solid var(--border);align-items:center">
|
|
14740
|
-
<label style="cursor:pointer;display:flex;align-items:center;color:var(--text-muted);font-size:18px" title="Attach file">
|
|
14741
|
-
<input type="file" multiple accept=".csv,.md,.txt,.json,.docx,.xlsx,.yaml,.yml,.xml,.html,.py,.js,.ts" style="display:none" onchange="handleBuilderFileUpload(event)">📎
|
|
14742
|
-
</label>
|
|
14743
|
-
<input type="text" id="builder-input" placeholder="Describe what you want to build..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendBuilderChat()}" style="flex:1;padding:10px 14px;border:1px solid var(--border);border-radius:8px;background:var(--bg-input);color:var(--text-primary);font-size:13px">
|
|
14744
|
-
<button class="btn-primary" onclick="sendBuilderChat()" style="padding:10px 18px;border-radius:8px">Send</button>
|
|
15104
|
+
<!-- List view (default) -->
|
|
15105
|
+
<div id="routines-list-pane" style="flex:1;min-height:0;overflow-y:auto;padding:18px;background:var(--bg-primary)">
|
|
15106
|
+
<div id="routines-list-empty" class="empty-state" style="display:none;padding:64px 18px;text-align:center;color:var(--text-muted)">
|
|
15107
|
+
<div style="font-size:38px;opacity:0.4;margin-bottom:14px">⚙</div>
|
|
15108
|
+
<div style="font-size:15px;font-weight:500;color:var(--text-secondary);margin-bottom:6px">No routines yet</div>
|
|
15109
|
+
<div style="font-size:12px;line-height:1.5;max-width:380px;margin:0 auto 16px">A Routine is a sequence of steps — 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>
|
|
15110
|
+
<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap">
|
|
15111
|
+
<button class="btn-sm btn-primary" onclick="window.RoutinesUI && RoutinesUI.openCreate()" style="padding:6px 14px">+ New Routine</button>
|
|
15112
|
+
<button class="btn-sm" onclick="window.RoutinesUI && RoutinesUI.openAssist()" style="padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary)">Generate from prompt</button>
|
|
14745
15113
|
</div>
|
|
14746
15114
|
</div>
|
|
14747
|
-
|
|
14748
|
-
|
|
14749
|
-
|
|
14750
|
-
|
|
14751
|
-
|
|
14752
|
-
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14756
|
-
|
|
14757
|
-
|
|
14758
|
-
|
|
14759
|
-
|
|
14760
|
-
<
|
|
14761
|
-
</
|
|
14762
|
-
<div id="builder-preview" style="flex:1;overflow-y:auto;padding:16px">
|
|
14763
|
-
<div class="empty-state" style="font-size:13px;color:var(--text-muted)">The artifact will appear here as you build it</div>
|
|
14764
|
-
</div>
|
|
14765
|
-
<div id="builder-canvas-host" style="display:none;flex:1;flex-direction:column;min-height:0;position:relative">
|
|
14766
|
-
<div id="builder-canvas-banner" style="padding:8px 14px;background:var(--bg-tertiary);border-bottom:1px solid var(--border);font-size:11px;color:var(--text-muted);display:none"></div>
|
|
14767
|
-
<div id="builder-canvas" style="flex:1;background:var(--bg-tertiary);position:relative;overflow:hidden"></div>
|
|
14768
|
-
<!-- Floating add-node FAB + palette popover -->
|
|
14769
|
-
<button id="builder-palette-btn" onclick="toggleBuilderPalette()" title="Add a step" style="position:absolute;left:14px;bottom:48px;width:40px;height:40px;border-radius:50%;background:var(--clementine);color:#fff;border:none;font-size:20px;font-weight:600;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,0.25);z-index:10">+</button>
|
|
14770
|
-
<div id="builder-palette-pop" style="display:none;position:absolute;left:60px;bottom:48px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.2);z-index:11;min-width:160px">
|
|
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>
|
|
14791
|
-
</div>
|
|
14792
|
-
</div>
|
|
14793
|
-
<!-- Existing skills drawer (visible in skill mode) -->
|
|
14794
|
-
<div id="builder-skills-drawer" style="display:none;border-top:2px solid var(--border);max-height:260px;overflow-y:auto">
|
|
14795
|
-
<div style="padding:10px 16px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;background:var(--bg-secondary);z-index:1">
|
|
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>
|
|
14798
|
-
</div>
|
|
14799
|
-
<div id="builder-skills-list" style="padding:0 12px 12px"></div>
|
|
14800
|
-
</div>
|
|
15115
|
+
<div id="routines-list-wrap" style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden">
|
|
15116
|
+
<table style="width:100%;border-collapse:collapse;font-size:13px">
|
|
15117
|
+
<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)">
|
|
15118
|
+
<tr>
|
|
15119
|
+
<th style="padding:11px 14px">Name</th>
|
|
15120
|
+
<th style="padding:11px 14px">Owner</th>
|
|
15121
|
+
<th style="padding:11px 14px">Schedule</th>
|
|
15122
|
+
<th style="padding:11px 14px">Steps</th>
|
|
15123
|
+
<th style="padding:11px 14px">Last run</th>
|
|
15124
|
+
<th style="padding:11px 14px;text-align:center">Enabled</th>
|
|
15125
|
+
<th style="padding:11px 14px;text-align:right">Actions</th>
|
|
15126
|
+
</tr>
|
|
15127
|
+
</thead>
|
|
15128
|
+
<tbody id="routines-list-body"></tbody>
|
|
15129
|
+
</table>
|
|
14801
15130
|
</div>
|
|
14802
15131
|
</div>
|
|
14803
|
-
|
|
14804
|
-
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
|
|
14809
|
-
|
|
15132
|
+
<!-- Editor pane (hidden by default; replaces list when a routine is opened) -->
|
|
15133
|
+
<div id="routines-editor-pane" style="display:none;flex:1;min-height:0;overflow-y:auto;background:var(--bg-primary);padding:18px"></div>
|
|
15134
|
+
<!-- Run history drawer (slide-out from right) -->
|
|
15135
|
+
<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>
|
|
15136
|
+
<!-- Create modal -->
|
|
15137
|
+
<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">
|
|
15138
|
+
<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">
|
|
15139
|
+
<h3 style="margin:0;font-size:16px;font-weight:600;color:var(--text-primary)">New routine</h3>
|
|
15140
|
+
<label style="font-size:11px;color:var(--text-muted);font-weight:500;text-transform:uppercase;letter-spacing:0.04em">Name</label>
|
|
15141
|
+
<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">
|
|
15142
|
+
<label style="font-size:11px;color:var(--text-muted);font-weight:500;text-transform:uppercase;letter-spacing:0.04em">Description (optional)</label>
|
|
15143
|
+
<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">
|
|
15144
|
+
<label style="font-size:11px;color:var(--text-muted);font-weight:500;text-transform:uppercase;letter-spacing:0.04em">Schedule (optional cron expression)</label>
|
|
15145
|
+
<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">
|
|
15146
|
+
<label style="font-size:11px;color:var(--text-muted);font-weight:500;text-transform:uppercase;letter-spacing:0.04em">Owner</label>
|
|
15147
|
+
<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">
|
|
15148
|
+
<option value="">Clementine (global)</option>
|
|
15149
|
+
</select>
|
|
15150
|
+
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:6px">
|
|
15151
|
+
<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>
|
|
15152
|
+
<button class="btn-sm btn-primary" onclick="window.RoutinesUI && RoutinesUI.submitCreate()" style="padding:6px 14px">Create</button>
|
|
14810
15153
|
</div>
|
|
14811
|
-
|
|
14812
|
-
|
|
14813
|
-
|
|
15154
|
+
</div>
|
|
15155
|
+
</div>
|
|
15156
|
+
<!-- Assist modal (Generate from prompt) -->
|
|
15157
|
+
<div id="routines-assist-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:200;align-items:center;justify-content:center">
|
|
15158
|
+
<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:22px;width:560px;max-width:92vw;max-height:80vh;display:flex;flex-direction:column;gap:14px">
|
|
15159
|
+
<h3 style="margin:0;font-size:16px;font-weight:600;color:var(--text-primary)">Generate routine from prompt</h3>
|
|
15160
|
+
<p style="margin:0;font-size:12px;color:var(--text-muted);line-height:1.5">Describe what the routine should do. The assistant will draft a starter sequence you can edit. Example: “Every morning at 8am, check unread Gmail; if anything looks urgent, summarize and send to Slack #me.”</p>
|
|
15161
|
+
<textarea id="routines-assist-input" rows="6" placeholder="Describe the routine…" style="width:100%;padding: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"></textarea>
|
|
15162
|
+
<div id="routines-assist-status" style="font-size:11px;color:var(--text-muted);min-height:14px"></div>
|
|
15163
|
+
<div style="display:flex;gap:8px;justify-content:flex-end">
|
|
15164
|
+
<button class="btn-sm" onclick="window.RoutinesUI && RoutinesUI.closeAssist()" style="padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary)">Cancel</button>
|
|
15165
|
+
<button id="routines-assist-submit" class="btn-sm btn-primary" onclick="window.RoutinesUI && RoutinesUI.submitAssist()" style="padding:6px 14px">Generate</button>
|
|
14814
15166
|
</div>
|
|
14815
15167
|
</div>
|
|
14816
|
-
<div id="panel-cron"></div>
|
|
14817
15168
|
</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>
|
|
15169
|
+
<!-- Step picker modal -->
|
|
15170
|
+
<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">
|
|
15171
|
+
<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">
|
|
15172
|
+
<div style="padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px">
|
|
15173
|
+
<h3 style="margin:0;font-size:15px;font-weight:600;color:var(--text-primary)">Add step</h3>
|
|
15174
|
+
<span style="flex:1"></span>
|
|
15175
|
+
<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
15176
|
</div>
|
|
15177
|
+
<div id="routines-step-picker-body" style="padding:16px 20px;overflow-y:auto;flex:1"></div>
|
|
14862
15178
|
</div>
|
|
14863
15179
|
</div>
|
|
14864
15180
|
</div>
|
|
15181
|
+
<script>
|
|
15182
|
+
// ── Routines UI ─────────────────────────────────────────────────
|
|
15183
|
+
// Vanilla JS module that drives the new Routines surface. State-light,
|
|
15184
|
+
// re-fetches from /api/routines on most actions to stay correct without
|
|
15185
|
+
// a client-side data store. Renders linear step lists; no canvas.
|
|
15186
|
+
(function() {
|
|
15187
|
+
var R = {
|
|
15188
|
+
state: {
|
|
15189
|
+
list: [],
|
|
15190
|
+
owners: [],
|
|
15191
|
+
mcpTools: null, // { servers: [{name,enabled,tools:[]}, ...] }
|
|
15192
|
+
cliTools: null, // [{cmd,description,userDefined}, ...]
|
|
15193
|
+
editing: null, // { id, routine, dirty }
|
|
15194
|
+
assistBusy: false,
|
|
15195
|
+
},
|
|
15196
|
+
init: function() {
|
|
15197
|
+
// Load reference data lazily; trigger list render immediately.
|
|
15198
|
+
this.loadOwners();
|
|
15199
|
+
this.refreshList();
|
|
15200
|
+
this.loadMcpTools();
|
|
15201
|
+
this.loadCliTools();
|
|
15202
|
+
},
|
|
15203
|
+
// ── data ────────────────────────────────────────────────────
|
|
15204
|
+
loadOwners: function() {
|
|
15205
|
+
// Reuse the agent registry the rest of the dashboard uses.
|
|
15206
|
+
fetch('/api/agents').then(function(r){ return r.json(); }).then(function(data){
|
|
15207
|
+
R.state.owners = (data.agents || []).map(function(a){ return { slug: a.slug, name: a.name || a.slug }; });
|
|
15208
|
+
R.populateOwnerSelects();
|
|
15209
|
+
}).catch(function(){ /* non-fatal */ });
|
|
15210
|
+
},
|
|
15211
|
+
populateOwnerSelects: function() {
|
|
15212
|
+
var filter = document.getElementById('routines-owner-filter');
|
|
15213
|
+
var creator = document.getElementById('routines-create-owner');
|
|
15214
|
+
var keepFilter = filter && filter.value;
|
|
15215
|
+
if (filter) {
|
|
15216
|
+
filter.innerHTML = '<option value="__all__">All</option><option value="__global__">Clementine (global)</option>'
|
|
15217
|
+
+ R.state.owners.map(function(o){ return '<option value="' + R.esc(o.slug) + '">@' + R.esc(o.name) + '</option>'; }).join('');
|
|
15218
|
+
if (keepFilter) filter.value = keepFilter;
|
|
15219
|
+
}
|
|
15220
|
+
if (creator) {
|
|
15221
|
+
creator.innerHTML = '<option value="">Clementine (global)</option>'
|
|
15222
|
+
+ R.state.owners.map(function(o){ return '<option value="' + R.esc(o.slug) + '">@' + R.esc(o.name) + '</option>'; }).join('');
|
|
15223
|
+
}
|
|
15224
|
+
},
|
|
15225
|
+
loadMcpTools: function() {
|
|
15226
|
+
fetch('/api/routines/mcp-tools').then(function(r){ return r.json(); }).then(function(data){
|
|
15227
|
+
R.state.mcpTools = data && data.servers ? data : { servers: [] };
|
|
15228
|
+
}).catch(function(){ R.state.mcpTools = { servers: [] }; });
|
|
15229
|
+
},
|
|
15230
|
+
loadCliTools: function() {
|
|
15231
|
+
fetch('/api/routines/cli-tools').then(function(r){ return r.json(); }).then(function(data){
|
|
15232
|
+
R.state.cliTools = (data && data.tools) || [];
|
|
15233
|
+
}).catch(function(){ R.state.cliTools = []; });
|
|
15234
|
+
},
|
|
15235
|
+
refreshList: function() {
|
|
15236
|
+
fetch('/api/routines').then(function(r){ return r.json(); }).then(function(data){
|
|
15237
|
+
R.state.list = (data && data.routines) || [];
|
|
15238
|
+
R.renderList();
|
|
15239
|
+
}).catch(function(){ R.state.list = []; R.renderList(); });
|
|
15240
|
+
},
|
|
15241
|
+
// ── list view ───────────────────────────────────────────────
|
|
15242
|
+
renderList: function() {
|
|
15243
|
+
var body = document.getElementById('routines-list-body');
|
|
15244
|
+
var empty = document.getElementById('routines-list-empty');
|
|
15245
|
+
var wrap = document.getElementById('routines-list-wrap');
|
|
15246
|
+
var count = document.getElementById('routines-count');
|
|
15247
|
+
if (!body || !empty || !wrap) return;
|
|
15248
|
+
var filter = (document.getElementById('routines-owner-filter') || {}).value || '__all__';
|
|
15249
|
+
var rows = R.state.list.filter(function(r){
|
|
15250
|
+
if (filter === '__all__') return true;
|
|
15251
|
+
if (filter === '__global__') return r.scope === 'global';
|
|
15252
|
+
return r.scope === 'agent' && r.agentSlug === filter;
|
|
15253
|
+
});
|
|
15254
|
+
if (count) count.textContent = rows.length === 0 ? '' : rows.length + (rows.length === 1 ? ' routine' : ' routines');
|
|
15255
|
+
if (rows.length === 0) {
|
|
15256
|
+
empty.style.display = 'block';
|
|
15257
|
+
wrap.style.display = 'none';
|
|
15258
|
+
return;
|
|
15259
|
+
}
|
|
15260
|
+
empty.style.display = 'none';
|
|
15261
|
+
wrap.style.display = 'block';
|
|
15262
|
+
body.innerHTML = rows.map(function(r){
|
|
15263
|
+
var owner = r.scope === 'agent' ? '@' + R.esc(r.agentSlug || '?') : 'Clementine';
|
|
15264
|
+
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>';
|
|
15265
|
+
var enabledBadge = '<input type="checkbox" ' + (r.enabled ? 'checked' : '') + ' onchange="event.stopPropagation();window.RoutinesUI&&RoutinesUI.toggle(\\x27' + R.esc(r.id) + '\\x27)" style="cursor:pointer">';
|
|
15266
|
+
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>' : '';
|
|
15267
|
+
return '<tr style="border-top:1px solid var(--border);cursor:pointer" onclick="window.RoutinesUI&&RoutinesUI.openEditor(\\x27' + R.esc(r.id) + '\\x27)">'
|
|
15268
|
+
+ '<td style="padding:11px 14px;color:var(--text-primary);font-weight:500">' + R.esc(r.name) + origin + '</td>'
|
|
15269
|
+
+ '<td style="padding:11px 14px;color:var(--text-secondary);font-size:12px">' + R.esc(owner) + '</td>'
|
|
15270
|
+
+ '<td style="padding:11px 14px">' + schedule + '</td>'
|
|
15271
|
+
+ '<td style="padding:11px 14px;color:var(--text-secondary);font-size:12px">' + r.stepCount + '</td>'
|
|
15272
|
+
+ '<td style="padding:11px 14px;color:var(--text-muted);font-size:12px">—</td>'
|
|
15273
|
+
+ '<td style="padding:11px 14px;text-align:center" onclick="event.stopPropagation()">' + enabledBadge + '</td>'
|
|
15274
|
+
+ '<td style="padding:11px 14px;text-align:right;white-space:nowrap" onclick="event.stopPropagation()">'
|
|
15275
|
+
+ '<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>'
|
|
15276
|
+
+ '<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>'
|
|
15277
|
+
+ '</td></tr>';
|
|
15278
|
+
}).join('');
|
|
15279
|
+
},
|
|
15280
|
+
toggle: function(id) {
|
|
15281
|
+
fetch('/api/routines/' + encodeURIComponent(id) + '/toggle', { method: 'POST' })
|
|
15282
|
+
.then(function(r){ return r.json(); })
|
|
15283
|
+
.then(function(){ R.refreshList(); })
|
|
15284
|
+
.catch(function(err){ alert('Toggle failed: ' + err); });
|
|
15285
|
+
},
|
|
15286
|
+
run: function(id, approvedSideEffects) {
|
|
15287
|
+
fetch('/api/routines/' + encodeURIComponent(id) + '/run', {
|
|
15288
|
+
method: 'POST',
|
|
15289
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15290
|
+
body: JSON.stringify({ approvedSideEffects: approvedSideEffects === true })
|
|
15291
|
+
}).then(function(r){
|
|
15292
|
+
if (r.status === 409) {
|
|
15293
|
+
return r.json().then(function(j){
|
|
15294
|
+
var lines = (j.sideEffects || []).map(function(s){ return ' • ' + s.kind + ': ' + s.label; }).join('\\n');
|
|
15295
|
+
if (confirm('This routine has side effects:\\n\\n' + lines + '\\n\\nProceed?')) R.run(id, true);
|
|
15296
|
+
});
|
|
15297
|
+
}
|
|
15298
|
+
return r.json().then(function(j){
|
|
15299
|
+
if (j.ok) R.flash('Triggered.');
|
|
15300
|
+
else alert('Run failed: ' + (j.error || 'unknown'));
|
|
15301
|
+
});
|
|
15302
|
+
}).catch(function(err){ alert('Run failed: ' + err); });
|
|
15303
|
+
},
|
|
15304
|
+
// ── editor ──────────────────────────────────────────────────
|
|
15305
|
+
openEditor: function(id) {
|
|
15306
|
+
fetch('/api/routines/' + encodeURIComponent(id))
|
|
15307
|
+
.then(function(r){ return r.json(); })
|
|
15308
|
+
.then(function(data){
|
|
15309
|
+
if (!data || !data.routine) { alert('Failed to load routine'); return; }
|
|
15310
|
+
R.state.editing = { id: data.id, routine: data.routine, dirty: false, validation: data.validation };
|
|
15311
|
+
R.showEditor();
|
|
15312
|
+
}).catch(function(err){ alert('Open failed: ' + err); });
|
|
15313
|
+
},
|
|
15314
|
+
showEditor: function() {
|
|
15315
|
+
document.getElementById('routines-list-pane').style.display = 'none';
|
|
15316
|
+
document.getElementById('routines-editor-pane').style.display = 'block';
|
|
15317
|
+
document.getElementById('routines-back-btn').style.display = 'inline-block';
|
|
15318
|
+
document.getElementById('routines-create-btn').style.display = 'none';
|
|
15319
|
+
document.getElementById('routines-assist-btn').style.display = 'none';
|
|
15320
|
+
document.getElementById('routines-owner-filter').style.display = 'none';
|
|
15321
|
+
document.getElementById('routines-owner-label').style.display = 'none';
|
|
15322
|
+
document.getElementById('routines-editor-breadcrumb').style.display = 'inline';
|
|
15323
|
+
document.getElementById('routines-editor-name').textContent = R.state.editing.routine.name;
|
|
15324
|
+
R.renderEditor();
|
|
15325
|
+
},
|
|
15326
|
+
closeEditor: function() {
|
|
15327
|
+
if (R.state.editing && R.state.editing.dirty && !confirm('Discard unsaved changes?')) return;
|
|
15328
|
+
R.state.editing = null;
|
|
15329
|
+
document.getElementById('routines-list-pane').style.display = 'block';
|
|
15330
|
+
document.getElementById('routines-editor-pane').style.display = 'none';
|
|
15331
|
+
document.getElementById('routines-back-btn').style.display = 'none';
|
|
15332
|
+
document.getElementById('routines-create-btn').style.display = 'inline-block';
|
|
15333
|
+
document.getElementById('routines-assist-btn').style.display = 'inline-block';
|
|
15334
|
+
document.getElementById('routines-owner-filter').style.display = 'inline-block';
|
|
15335
|
+
document.getElementById('routines-owner-label').style.display = 'inline';
|
|
15336
|
+
document.getElementById('routines-editor-breadcrumb').style.display = 'none';
|
|
15337
|
+
R.refreshList();
|
|
15338
|
+
},
|
|
15339
|
+
renderEditor: function() {
|
|
15340
|
+
var pane = document.getElementById('routines-editor-pane');
|
|
15341
|
+
if (!pane || !R.state.editing) return;
|
|
15342
|
+
var wf = R.state.editing.routine;
|
|
15343
|
+
var html = '<div style="max-width:920px;margin:0 auto">';
|
|
15344
|
+
html += '<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px;margin-bottom:14px">'
|
|
15345
|
+
+ '<div style="display:flex;align-items:center;gap:12px;margin-bottom:10px">'
|
|
15346
|
+
+ '<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">'
|
|
15347
|
+
+ '<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>'
|
|
15348
|
+
+ '</div>'
|
|
15349
|
+
+ '<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">'
|
|
15350
|
+
+ '<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">Schedule</label>'
|
|
15351
|
+
+ '<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">'
|
|
15352
|
+
+ '</div></div>';
|
|
15353
|
+
// Steps
|
|
15354
|
+
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>';
|
|
15355
|
+
html += '<div id="re-steps-list">' + (wf.steps || []).map(function(s, i){ return R.renderStepCard(s, i); }).join('') + '</div>';
|
|
15356
|
+
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>';
|
|
15357
|
+
// Action bar
|
|
15358
|
+
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">'
|
|
15359
|
+
+ '<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>'
|
|
15360
|
+
+ '<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>'
|
|
15361
|
+
+ '<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>'
|
|
15362
|
+
+ '<span style="flex:1"></span>'
|
|
15363
|
+
+ (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>' : '')
|
|
15364
|
+
+ '<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>'
|
|
15365
|
+
+ '<button class="btn-sm btn-primary" id="re-save-btn" onclick="window.RoutinesUI&&RoutinesUI.saveCurrent()" style="padding:6px 16px">Save</button>'
|
|
15366
|
+
+ '</div>';
|
|
15367
|
+
html += '<div id="re-status" style="font-size:11px;color:var(--text-muted);min-height:14px;padding:6px 0"></div>';
|
|
15368
|
+
html += '</div>';
|
|
15369
|
+
pane.innerHTML = html;
|
|
15370
|
+
if (window.hydrateLucideIcons) window.hydrateLucideIcons();
|
|
15371
|
+
},
|
|
15372
|
+
renderStepCard: function(step, idx) {
|
|
15373
|
+
var kind = step.kind || 'prompt';
|
|
15374
|
+
var kindColor = { prompt: '#5e72e4', mcp: '#2dce89', cli: '#fb6340', conditional: '#f5365c', channel: '#11cdef', transform: '#ffd600', loop: '#8965e0' }[kind] || '#888';
|
|
15375
|
+
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>';
|
|
15376
|
+
var head = '<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">'
|
|
15377
|
+
+ '<span style="font-size:11px;color:var(--text-muted);font-weight:600;min-width:24px">#' + (idx + 1) + '</span>'
|
|
15378
|
+
+ badge
|
|
15379
|
+
+ '<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">'
|
|
15380
|
+
+ '<span style="flex:1"></span>'
|
|
15381
|
+
+ (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>' : '')
|
|
15382
|
+
+ (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>' : '')
|
|
15383
|
+
+ '<button title="Remove" onclick="window.RoutinesUI&&RoutinesUI.removeStep(' + idx + ')" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:14px">×</button>'
|
|
15384
|
+
+ '</div>';
|
|
15385
|
+
var body = R.renderStepBody(step, idx);
|
|
15386
|
+
var depsList = (step.dependsOn || []).join(', ');
|
|
15387
|
+
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>'
|
|
15388
|
+
+ '<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>';
|
|
15389
|
+
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>';
|
|
15390
|
+
},
|
|
15391
|
+
renderStepBody: function(step, idx) {
|
|
15392
|
+
var kind = step.kind || 'prompt';
|
|
15393
|
+
switch (kind) {
|
|
15394
|
+
case 'prompt':
|
|
15395
|
+
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>';
|
|
15396
|
+
case 'mcp':
|
|
15397
|
+
var mcp = step.mcp || { server: '', tool: '', inputs: {} };
|
|
15398
|
+
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('');
|
|
15399
|
+
var server = (R.state.mcpTools && R.state.mcpTools.servers || []).find(function(s){ return s.name === mcp.server; });
|
|
15400
|
+
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('') : '');
|
|
15401
|
+
var inputsJson = JSON.stringify(mcp.inputs || {}, null, 2);
|
|
15402
|
+
return '<div style="display:flex;gap:8px;margin-bottom:6px;flex-wrap:wrap">'
|
|
15403
|
+
+ '<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>'
|
|
15404
|
+
+ '<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>'
|
|
15405
|
+
+ '</div>'
|
|
15406
|
+
+ '<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>'
|
|
15407
|
+
+ '<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>';
|
|
15408
|
+
case 'cli':
|
|
15409
|
+
var cli = step.cli || { cmd: '', args: [] };
|
|
15410
|
+
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('');
|
|
15411
|
+
if (cli.cmd && !(R.state.cliTools || []).some(function(c){ return c.cmd === cli.cmd; })) {
|
|
15412
|
+
cliOptions += '<option value="' + R.esc(cli.cmd) + '" selected>' + R.esc(cli.cmd) + ' (custom)</option>';
|
|
15413
|
+
}
|
|
15414
|
+
return '<div style="display:flex;gap:8px;margin-bottom:6px;flex-wrap:wrap">'
|
|
15415
|
+
+ '<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>'
|
|
15416
|
+
+ '<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">'
|
|
15417
|
+
+ '</div>'
|
|
15418
|
+
+ '<label style="display:block;font-size:10px;color:var(--text-muted);margin-bottom:4px">Args (one per line; supports {{steps.x}} templates)</label>'
|
|
15419
|
+
+ '<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>'
|
|
15420
|
+
+ '<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>';
|
|
15421
|
+
case 'conditional':
|
|
15422
|
+
var cond = step.conditional || { condition: '', trueNext: [], falseNext: [] };
|
|
15423
|
+
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>'
|
|
15424
|
+
+ '<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">'
|
|
15425
|
+
+ '<div style="display:flex;gap:8px"><div style="flex:1">'
|
|
15426
|
+
+ '<label style="display:block;font-size:10px;color:var(--text-muted);margin-bottom:4px">If true → run these step ids (comma)</label>'
|
|
15427
|
+
+ '<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">'
|
|
15428
|
+
+ '</div><div style="flex:1">'
|
|
15429
|
+
+ '<label style="display:block;font-size:10px;color:var(--text-muted);margin-bottom:4px">If false → run these step ids</label>'
|
|
15430
|
+
+ '<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">'
|
|
15431
|
+
+ '</div></div>';
|
|
15432
|
+
case 'channel':
|
|
15433
|
+
var ch = step.channel || { channel: 'discord', target: '', content: '' };
|
|
15434
|
+
return '<div style="display:flex;gap:8px;margin-bottom:6px;flex-wrap:wrap">'
|
|
15435
|
+
+ '<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">'
|
|
15436
|
+
+ ['discord','slack','telegram','whatsapp','email','webhook'].map(function(c){ return '<option value="' + c + '"' + (ch.channel === c ? ' selected' : '') + '>' + c + '</option>'; }).join('')
|
|
15437
|
+
+ '</select>'
|
|
15438
|
+
+ '<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">'
|
|
15439
|
+
+ '</div>'
|
|
15440
|
+
+ '<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>';
|
|
15441
|
+
case 'transform':
|
|
15442
|
+
var tr = step.transform || { expression: '' };
|
|
15443
|
+
return '<label style="display:block;font-size:10px;color:var(--text-muted);margin-bottom:4px">Expression (sandboxed JS; returns the step output)</label>'
|
|
15444
|
+
+ '<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>';
|
|
15445
|
+
case 'loop':
|
|
15446
|
+
var lp = step.loop || { items: '', bodyStepIds: [] };
|
|
15447
|
+
return '<label style="display:block;font-size:10px;color:var(--text-muted);margin-bottom:4px">Items expression (yields an array)</label>'
|
|
15448
|
+
+ '<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">'
|
|
15449
|
+
+ '<label style="display:block;font-size:10px;color:var(--text-muted);margin-bottom:4px">Body step ids (comma-separated)</label>'
|
|
15450
|
+
+ '<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">';
|
|
15451
|
+
default:
|
|
15452
|
+
return '<em style="font-size:11px;color:var(--text-muted)">Unknown step kind: ' + R.esc(kind) + '</em>';
|
|
15453
|
+
}
|
|
15454
|
+
},
|
|
15455
|
+
markDirty: function() {
|
|
15456
|
+
if (!R.state.editing) return;
|
|
15457
|
+
R.state.editing.dirty = true;
|
|
15458
|
+
// Sync header inputs back to the routine.
|
|
15459
|
+
var nm = document.getElementById('re-name'); if (nm) R.state.editing.routine.name = nm.value;
|
|
15460
|
+
var de = document.getElementById('re-description'); if (de) R.state.editing.routine.description = de.value;
|
|
15461
|
+
var sc = document.getElementById('re-schedule'); if (sc) {
|
|
15462
|
+
var v = sc.value.trim();
|
|
15463
|
+
R.state.editing.routine.trigger = v ? { schedule: v, manual: false } : { manual: true };
|
|
15464
|
+
}
|
|
15465
|
+
var en = document.getElementById('re-enabled'); if (en) R.state.editing.routine.enabled = en.checked;
|
|
15466
|
+
R.setStatus('Unsaved changes');
|
|
15467
|
+
},
|
|
15468
|
+
updateStep: function(idx, field, value) {
|
|
15469
|
+
if (!R.state.editing) return;
|
|
15470
|
+
R.state.editing.routine.steps[idx][field] = value;
|
|
15471
|
+
R.markDirty();
|
|
15472
|
+
},
|
|
15473
|
+
updateStepDeps: function(idx, csv) {
|
|
15474
|
+
if (!R.state.editing) return;
|
|
15475
|
+
R.state.editing.routine.steps[idx].dependsOn = csv.split(',').map(function(s){ return s.trim(); }).filter(function(s){ return s; });
|
|
15476
|
+
R.markDirty();
|
|
15477
|
+
},
|
|
15478
|
+
updateMcp: function(idx, field, value) {
|
|
15479
|
+
if (!R.state.editing) return;
|
|
15480
|
+
var s = R.state.editing.routine.steps[idx];
|
|
15481
|
+
s.mcp = s.mcp || { server: '', tool: '', inputs: {} };
|
|
15482
|
+
s.mcp[field] = value;
|
|
15483
|
+
if (field === 'server') s.mcp.tool = ''; // reset tool when server changes
|
|
15484
|
+
R.markDirty();
|
|
15485
|
+
R.renderEditor(); // re-render so tool dropdown updates
|
|
15486
|
+
},
|
|
15487
|
+
updateMcpInputs: function(idx, json) {
|
|
15488
|
+
if (!R.state.editing) return;
|
|
15489
|
+
var s = R.state.editing.routine.steps[idx];
|
|
15490
|
+
s.mcp = s.mcp || { server: '', tool: '', inputs: {} };
|
|
15491
|
+
try { s.mcp.inputs = JSON.parse(json); R.setStatus(''); }
|
|
15492
|
+
catch (e) { R.setStatus('Invalid JSON in step ' + s.id + ' inputs (will not save until fixed)'); return; }
|
|
15493
|
+
R.markDirty();
|
|
15494
|
+
},
|
|
15495
|
+
updateCli: function(idx, field, value) {
|
|
15496
|
+
if (!R.state.editing) return;
|
|
15497
|
+
var s = R.state.editing.routine.steps[idx];
|
|
15498
|
+
s.cli = s.cli || { cmd: '', args: [] };
|
|
15499
|
+
s.cli[field] = value;
|
|
15500
|
+
R.markDirty();
|
|
15501
|
+
},
|
|
15502
|
+
updateCliArgs: function(idx, txt) {
|
|
15503
|
+
if (!R.state.editing) return;
|
|
15504
|
+
var s = R.state.editing.routine.steps[idx];
|
|
15505
|
+
s.cli = s.cli || { cmd: '', args: [] };
|
|
15506
|
+
s.cli.args = txt.split('\\n').map(function(l){ return l.trim(); }).filter(function(l){ return l; });
|
|
15507
|
+
R.markDirty();
|
|
15508
|
+
},
|
|
15509
|
+
updateConditional: function(idx, field, value) {
|
|
15510
|
+
if (!R.state.editing) return;
|
|
15511
|
+
var s = R.state.editing.routine.steps[idx];
|
|
15512
|
+
s.conditional = s.conditional || { condition: '', trueNext: [], falseNext: [] };
|
|
15513
|
+
if (field === 'trueNext' || field === 'falseNext') {
|
|
15514
|
+
s.conditional[field] = String(value).split(',').map(function(x){ return x.trim(); }).filter(function(x){ return x; });
|
|
15515
|
+
} else {
|
|
15516
|
+
s.conditional[field] = value;
|
|
15517
|
+
}
|
|
15518
|
+
R.markDirty();
|
|
15519
|
+
},
|
|
15520
|
+
updateChannel: function(idx, field, value) {
|
|
15521
|
+
if (!R.state.editing) return;
|
|
15522
|
+
var s = R.state.editing.routine.steps[idx];
|
|
15523
|
+
s.channel = s.channel || { channel: 'discord', target: '', content: '' };
|
|
15524
|
+
s.channel[field] = value;
|
|
15525
|
+
R.markDirty();
|
|
15526
|
+
},
|
|
15527
|
+
updateTransform: function(idx, value) {
|
|
15528
|
+
if (!R.state.editing) return;
|
|
15529
|
+
var s = R.state.editing.routine.steps[idx];
|
|
15530
|
+
s.transform = { expression: value };
|
|
15531
|
+
R.markDirty();
|
|
15532
|
+
},
|
|
15533
|
+
updateLoop: function(idx, field, value) {
|
|
15534
|
+
if (!R.state.editing) return;
|
|
15535
|
+
var s = R.state.editing.routine.steps[idx];
|
|
15536
|
+
s.loop = s.loop || { items: '', bodyStepIds: [] };
|
|
15537
|
+
if (field === 'bodyStepIds') {
|
|
15538
|
+
s.loop.bodyStepIds = String(value).split(',').map(function(x){ return x.trim(); }).filter(function(x){ return x; });
|
|
15539
|
+
} else {
|
|
15540
|
+
s.loop.items = value;
|
|
15541
|
+
}
|
|
15542
|
+
R.markDirty();
|
|
15543
|
+
},
|
|
15544
|
+
moveStep: function(idx, dir) {
|
|
15545
|
+
if (!R.state.editing) return;
|
|
15546
|
+
var steps = R.state.editing.routine.steps;
|
|
15547
|
+
var j = idx + dir;
|
|
15548
|
+
if (j < 0 || j >= steps.length) return;
|
|
15549
|
+
var t = steps[idx]; steps[idx] = steps[j]; steps[j] = t;
|
|
15550
|
+
R.markDirty();
|
|
15551
|
+
R.renderEditor();
|
|
15552
|
+
},
|
|
15553
|
+
removeStep: function(idx) {
|
|
15554
|
+
if (!R.state.editing) return;
|
|
15555
|
+
if (R.state.editing.routine.steps.length <= 1) { alert('A routine must have at least one step.'); return; }
|
|
15556
|
+
if (!confirm('Remove this step?')) return;
|
|
15557
|
+
var removed = R.state.editing.routine.steps.splice(idx, 1)[0];
|
|
15558
|
+
// Strip lingering dependsOn references.
|
|
15559
|
+
for (var i = 0; i < R.state.editing.routine.steps.length; i++) {
|
|
15560
|
+
R.state.editing.routine.steps[i].dependsOn = (R.state.editing.routine.steps[i].dependsOn || []).filter(function(d){ return d !== removed.id; });
|
|
15561
|
+
}
|
|
15562
|
+
R.markDirty();
|
|
15563
|
+
R.renderEditor();
|
|
15564
|
+
},
|
|
15565
|
+
// ── step picker ─────────────────────────────────────────────
|
|
15566
|
+
openStepPicker: function() {
|
|
15567
|
+
var modal = document.getElementById('routines-step-picker');
|
|
15568
|
+
var body = document.getElementById('routines-step-picker-body');
|
|
15569
|
+
if (!modal || !body) return;
|
|
15570
|
+
var kinds = [
|
|
15571
|
+
{ kind: 'prompt', label: 'Prompt', desc: 'Send a prompt to the agent. Use this when the work needs reasoning or freeform tools.' },
|
|
15572
|
+
{ kind: 'mcp', label: 'MCP tool', desc: 'Call a specific MCP server tool (Composio, Claude integrations, local MCP).' },
|
|
15573
|
+
{ kind: 'cli', label: 'Local CLI', desc: 'Run an installed CLI (sf, gh, gcloud, …) and capture stdout.' },
|
|
15574
|
+
{ kind: 'conditional', label: 'If / Else', desc: 'Branch on a JS expression evaluated against prior step outputs.' },
|
|
15575
|
+
{ kind: 'channel', label: 'Channel send', desc: 'Send a message to Discord, Slack, Telegram, email, or webhook.' },
|
|
15576
|
+
{ kind: 'transform', label: 'Transform', desc: 'Sandboxed JS expression that reshapes prior step output.' },
|
|
15577
|
+
{ kind: 'loop', label: 'Loop', desc: 'Iterate over an array; runs the listed body steps for each item.' },
|
|
15578
|
+
];
|
|
15579
|
+
body.innerHTML = kinds.map(function(k){
|
|
15580
|
+
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>';
|
|
15581
|
+
}).join('');
|
|
15582
|
+
modal.style.display = 'flex';
|
|
15583
|
+
},
|
|
15584
|
+
closeStepPicker: function() {
|
|
15585
|
+
var m = document.getElementById('routines-step-picker'); if (m) m.style.display = 'none';
|
|
15586
|
+
},
|
|
15587
|
+
addStep: function(kind) {
|
|
15588
|
+
if (!R.state.editing) return;
|
|
15589
|
+
var steps = R.state.editing.routine.steps;
|
|
15590
|
+
var n = steps.length + 1;
|
|
15591
|
+
var nextId = 's' + n;
|
|
15592
|
+
while (steps.some(function(s){ return s.id === nextId; })) { n++; nextId = 's' + n; }
|
|
15593
|
+
var step = { id: nextId, prompt: '', dependsOn: steps.length ? [steps[steps.length - 1].id] : [], tier: 1, maxTurns: 15 };
|
|
15594
|
+
if (kind !== 'prompt') step.kind = kind;
|
|
15595
|
+
if (kind === 'mcp') step.mcp = { server: '', tool: '', inputs: {} };
|
|
15596
|
+
if (kind === 'cli') step.cli = { cmd: '', args: [], timeoutMs: 60000 };
|
|
15597
|
+
if (kind === 'conditional') step.conditional = { condition: '', trueNext: [], falseNext: [] };
|
|
15598
|
+
if (kind === 'channel') step.channel = { channel: 'discord', target: '', content: '' };
|
|
15599
|
+
if (kind === 'transform') step.transform = { expression: '' };
|
|
15600
|
+
if (kind === 'loop') step.loop = { items: '', bodyStepIds: [] };
|
|
15601
|
+
steps.push(step);
|
|
15602
|
+
R.markDirty();
|
|
15603
|
+
R.closeStepPicker();
|
|
15604
|
+
R.renderEditor();
|
|
15605
|
+
},
|
|
15606
|
+
// ── editor actions ──────────────────────────────────────────
|
|
15607
|
+
saveCurrent: function() {
|
|
15608
|
+
if (!R.state.editing) return;
|
|
15609
|
+
R.markDirty(); // capture latest header values
|
|
15610
|
+
var btn = document.getElementById('re-save-btn');
|
|
15611
|
+
if (btn) btn.textContent = 'Saving…';
|
|
15612
|
+
fetch('/api/routines/' + encodeURIComponent(R.state.editing.id), {
|
|
15613
|
+
method: 'PUT',
|
|
15614
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15615
|
+
body: JSON.stringify({ routine: R.state.editing.routine })
|
|
15616
|
+
}).then(function(r){ return r.json().then(function(j){ return { ok: r.ok, status: r.status, body: j }; }); })
|
|
15617
|
+
.then(function(res){
|
|
15618
|
+
if (btn) btn.textContent = 'Save';
|
|
15619
|
+
if (!res.ok) {
|
|
15620
|
+
if (res.body.error === 'validation') {
|
|
15621
|
+
var msg = (res.body.validation.issues || []).map(function(i){ return '• ' + i.severity + ': ' + i.message; }).join('\\n');
|
|
15622
|
+
if (confirm('Validation issues:\\n\\n' + msg + '\\n\\nSave anyway?')) {
|
|
15623
|
+
fetch('/api/routines/' + encodeURIComponent(R.state.editing.id), {
|
|
15624
|
+
method: 'PUT',
|
|
15625
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15626
|
+
body: JSON.stringify({ routine: R.state.editing.routine, force: true })
|
|
15627
|
+
}).then(function(){ R.state.editing.dirty = false; R.setStatus('Saved (with warnings)'); R.refreshList(); });
|
|
15628
|
+
}
|
|
15629
|
+
return;
|
|
15630
|
+
}
|
|
15631
|
+
R.setStatus('Save failed: ' + (res.body.error || res.body.detail || 'unknown'));
|
|
15632
|
+
return;
|
|
15633
|
+
}
|
|
15634
|
+
R.state.editing.dirty = false;
|
|
15635
|
+
R.setStatus('Saved.');
|
|
15636
|
+
R.refreshList();
|
|
15637
|
+
}).catch(function(err){
|
|
15638
|
+
if (btn) btn.textContent = 'Save';
|
|
15639
|
+
R.setStatus('Save error: ' + err);
|
|
15640
|
+
});
|
|
15641
|
+
},
|
|
15642
|
+
runCurrent: function() {
|
|
15643
|
+
if (!R.state.editing) return;
|
|
15644
|
+
if (R.state.editing.dirty && !confirm('You have unsaved changes. Run anyway (using last saved version)?')) return;
|
|
15645
|
+
R.run(R.state.editing.id);
|
|
15646
|
+
},
|
|
15647
|
+
dryRunCurrent: function() {
|
|
15648
|
+
if (!R.state.editing) return;
|
|
15649
|
+
fetch('/api/routines/' + encodeURIComponent(R.state.editing.id) + '/dry-run', { method: 'POST' })
|
|
15650
|
+
.then(function(r){ return r.json(); })
|
|
15651
|
+
.then(function(d){
|
|
15652
|
+
var lines = ['Dry-run for ' + R.state.editing.routine.name + ':\\n'];
|
|
15653
|
+
(d.steps || []).forEach(function(s){ lines.push('• ' + s.description + (s.warnings.length ? '\\n ⚠ ' + s.warnings.join('; ') : '')); });
|
|
15654
|
+
if (d.notes && d.notes.length) lines.push('\\n' + d.notes.join('\\n'));
|
|
15655
|
+
alert(lines.join('\\n'));
|
|
15656
|
+
}).catch(function(err){ alert('Dry-run failed: ' + err); });
|
|
15657
|
+
},
|
|
15658
|
+
testCurrent: function() {
|
|
15659
|
+
if (!R.state.editing) return;
|
|
15660
|
+
fetch('/api/routines/' + encodeURIComponent(R.state.editing.id) + '/test', {
|
|
15661
|
+
method: 'POST',
|
|
15662
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15663
|
+
body: JSON.stringify({ mode: 'mock' })
|
|
15664
|
+
}).then(function(r){ return r.json(); }).then(function(d){
|
|
15665
|
+
if (d.ok) R.setStatus('Mock test started (runId: ' + d.runId + '). See run history for output.');
|
|
15666
|
+
else R.setStatus('Test failed to start: ' + (d.error || 'unknown'));
|
|
15667
|
+
}).catch(function(err){ R.setStatus('Test error: ' + err); });
|
|
15668
|
+
},
|
|
15669
|
+
deleteCurrent: function() {
|
|
15670
|
+
if (!R.state.editing) return;
|
|
15671
|
+
if (!confirm('Delete routine "' + R.state.editing.routine.name + '"? This is permanent.')) return;
|
|
15672
|
+
fetch('/api/routines/' + encodeURIComponent(R.state.editing.id), { method: 'DELETE' })
|
|
15673
|
+
.then(function(r){ return r.json(); })
|
|
15674
|
+
.then(function(j){
|
|
15675
|
+
if (j.ok) { R.state.editing = null; R.closeEditor(); R.refreshList(); }
|
|
15676
|
+
else alert('Delete failed: ' + (j.error || 'unknown'));
|
|
15677
|
+
}).catch(function(err){ alert('Delete error: ' + err); });
|
|
15678
|
+
},
|
|
15679
|
+
setStatus: function(msg) {
|
|
15680
|
+
var el = document.getElementById('re-status');
|
|
15681
|
+
if (el) el.textContent = msg || '';
|
|
15682
|
+
},
|
|
15683
|
+
flash: function(msg) {
|
|
15684
|
+
// Lightweight toast — reuse existing flash if available, else log.
|
|
15685
|
+
if (window.flashMessage) window.flashMessage(msg);
|
|
15686
|
+
else if (window.console) console.log('[routines]', msg);
|
|
15687
|
+
},
|
|
15688
|
+
// ── runs drawer ─────────────────────────────────────────────
|
|
15689
|
+
openRuns: function(id) {
|
|
15690
|
+
if (!id) return;
|
|
15691
|
+
var drawer = document.getElementById('routines-runs-drawer');
|
|
15692
|
+
if (!drawer) return;
|
|
15693
|
+
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>';
|
|
15694
|
+
drawer.style.display = 'block';
|
|
15695
|
+
fetch('/api/routines/' + encodeURIComponent(id) + '/runs').then(function(r){ return r.json(); }).then(function(d){
|
|
15696
|
+
var runs = d.runs || [];
|
|
15697
|
+
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>';
|
|
15698
|
+
if (runs.length === 0) {
|
|
15699
|
+
html += '<div style="font-size:12px;color:var(--text-muted);padding:24px 0;text-align:center">No runs yet.</div>';
|
|
15700
|
+
} else {
|
|
15701
|
+
html += runs.map(function(run){
|
|
15702
|
+
var when = run.startedAt || run.timestamp || '';
|
|
15703
|
+
var status = run.status || 'unknown';
|
|
15704
|
+
var color = { ok: 'var(--green)', error: 'var(--red)', partial: '#f5a623', skipped: 'var(--text-muted)', retried: '#f5a623' }[status] || 'var(--text-muted)';
|
|
15705
|
+
var dur = run.durationMs ? Math.round(run.durationMs / 100) / 10 + 's' : '';
|
|
15706
|
+
var preview = run.outputPreview || run.output_preview || run.outputPreview || '';
|
|
15707
|
+
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>';
|
|
15708
|
+
}).join('');
|
|
15709
|
+
}
|
|
15710
|
+
html += '</div>';
|
|
15711
|
+
drawer.innerHTML = html;
|
|
15712
|
+
}).catch(function(err){
|
|
15713
|
+
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>';
|
|
15714
|
+
});
|
|
15715
|
+
},
|
|
15716
|
+
closeRuns: function() {
|
|
15717
|
+
var d = document.getElementById('routines-runs-drawer'); if (d) d.style.display = 'none';
|
|
15718
|
+
},
|
|
15719
|
+
// ── create modal ────────────────────────────────────────────
|
|
15720
|
+
openCreate: function() {
|
|
15721
|
+
var m = document.getElementById('routines-create-modal'); if (!m) return;
|
|
15722
|
+
document.getElementById('routines-create-name').value = '';
|
|
15723
|
+
document.getElementById('routines-create-description').value = '';
|
|
15724
|
+
document.getElementById('routines-create-schedule').value = '';
|
|
15725
|
+
m.style.display = 'flex';
|
|
15726
|
+
},
|
|
15727
|
+
closeCreate: function() {
|
|
15728
|
+
var m = document.getElementById('routines-create-modal'); if (m) m.style.display = 'none';
|
|
15729
|
+
},
|
|
15730
|
+
submitCreate: function() {
|
|
15731
|
+
var name = document.getElementById('routines-create-name').value.trim();
|
|
15732
|
+
if (!name) { alert('Name is required'); return; }
|
|
15733
|
+
var body = {
|
|
15734
|
+
name: name,
|
|
15735
|
+
description: document.getElementById('routines-create-description').value.trim(),
|
|
15736
|
+
schedule: document.getElementById('routines-create-schedule').value.trim() || undefined,
|
|
15737
|
+
agent: document.getElementById('routines-create-owner').value || undefined,
|
|
15738
|
+
};
|
|
15739
|
+
fetch('/api/routines', {
|
|
15740
|
+
method: 'POST',
|
|
15741
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15742
|
+
body: JSON.stringify(body)
|
|
15743
|
+
}).then(function(r){ return r.json().then(function(j){ return { ok: r.ok, body: j }; }); })
|
|
15744
|
+
.then(function(res){
|
|
15745
|
+
if (!res.ok) { alert('Create failed: ' + (res.body.error || 'unknown')); return; }
|
|
15746
|
+
R.closeCreate();
|
|
15747
|
+
R.refreshList();
|
|
15748
|
+
R.openEditor(res.body.id);
|
|
15749
|
+
}).catch(function(err){ alert('Create error: ' + err); });
|
|
15750
|
+
},
|
|
15751
|
+
// ── assist (generate from prompt) ───────────────────────────
|
|
15752
|
+
openAssist: function() {
|
|
15753
|
+
var m = document.getElementById('routines-assist-modal'); if (!m) return;
|
|
15754
|
+
document.getElementById('routines-assist-input').value = '';
|
|
15755
|
+
document.getElementById('routines-assist-status').textContent = '';
|
|
15756
|
+
m.style.display = 'flex';
|
|
15757
|
+
setTimeout(function(){ document.getElementById('routines-assist-input').focus(); }, 50);
|
|
15758
|
+
},
|
|
15759
|
+
closeAssist: function() {
|
|
15760
|
+
var m = document.getElementById('routines-assist-modal'); if (m) m.style.display = 'none';
|
|
15761
|
+
},
|
|
15762
|
+
submitAssist: function() {
|
|
15763
|
+
if (R.state.assistBusy) return;
|
|
15764
|
+
var prompt = document.getElementById('routines-assist-input').value.trim();
|
|
15765
|
+
if (!prompt) return;
|
|
15766
|
+
R.state.assistBusy = true;
|
|
15767
|
+
var btn = document.getElementById('routines-assist-submit');
|
|
15768
|
+
var status = document.getElementById('routines-assist-status');
|
|
15769
|
+
if (btn) { btn.textContent = 'Generating…'; btn.disabled = true; }
|
|
15770
|
+
if (status) status.textContent = 'Asking the assistant to draft a routine…';
|
|
15771
|
+
// Reuse the existing /api/builder/chat which is already wired and
|
|
15772
|
+
// produces workflow drafts. We also pass mode hint so the agent
|
|
15773
|
+
// knows to focus on building one routine.
|
|
15774
|
+
fetch('/api/builder/chat', {
|
|
15775
|
+
method: 'POST',
|
|
15776
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15777
|
+
body: JSON.stringify({ message: prompt, mode: 'workflow' })
|
|
15778
|
+
}).then(function(r){ return r.json(); }).then(function(d){
|
|
15779
|
+
R.state.assistBusy = false;
|
|
15780
|
+
if (btn) { btn.textContent = 'Generate'; btn.disabled = false; }
|
|
15781
|
+
if (status) status.textContent = d && d.message ? 'Draft created. Refreshing list…' : 'Draft response received.';
|
|
15782
|
+
R.refreshList();
|
|
15783
|
+
setTimeout(function(){ R.closeAssist(); }, 800);
|
|
15784
|
+
}).catch(function(err){
|
|
15785
|
+
R.state.assistBusy = false;
|
|
15786
|
+
if (btn) { btn.textContent = 'Generate'; btn.disabled = false; }
|
|
15787
|
+
if (status) status.textContent = 'Assist failed: ' + err;
|
|
15788
|
+
});
|
|
15789
|
+
},
|
|
15790
|
+
// ── helpers ─────────────────────────────────────────────────
|
|
15791
|
+
esc: function(s) {
|
|
15792
|
+
if (s == null) return '';
|
|
15793
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
15794
|
+
},
|
|
15795
|
+
};
|
|
15796
|
+
window.RoutinesUI = R;
|
|
15797
|
+
// Compatibility shims for legacy callers in other parts of the dashboard.
|
|
15798
|
+
// The old switchBuildTab(tab) is referenced from KPI tiles, getting-started cards,
|
|
15799
|
+
// and the navigateTo dispatcher. Map them all to the unified Routines view.
|
|
15800
|
+
if (typeof window.switchBuildTab !== 'function' || true) {
|
|
15801
|
+
window.switchBuildTab = function() { try { R.init(); } catch (e) { /* */ } };
|
|
15802
|
+
}
|
|
15803
|
+
// Auto-init when the user lands on the build page.
|
|
15804
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
15805
|
+
var nav = document.querySelector('[data-page="build"]');
|
|
15806
|
+
if (nav) nav.addEventListener('click', function() { setTimeout(function() { R.init(); }, 50); });
|
|
15807
|
+
// If page-build is already active on load (deep-link), init now.
|
|
15808
|
+
var page = document.getElementById('page-build');
|
|
15809
|
+
if (page && page.classList.contains('active')) R.init();
|
|
15810
|
+
});
|
|
15811
|
+
})();
|
|
15812
|
+
</script>
|
|
14865
15813
|
|
|
14866
15814
|
<!-- page-agent-detail merged into Team page; click an agent in Roster to drill down. -->
|
|
14867
15815
|
|
|
@@ -15085,6 +16033,21 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
15085
16033
|
<div class="skel-block" style="padding:14px"><div class="skel-row med"></div><div class="skel-row short"></div></div>
|
|
15086
16034
|
</div>
|
|
15087
16035
|
</div>
|
|
16036
|
+
<div class="card" style="margin-bottom:14px">
|
|
16037
|
+
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap">
|
|
16038
|
+
<span>Persistent learnings</span>
|
|
16039
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
16040
|
+
<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)">
|
|
16041
|
+
<option value="active" selected>Active</option>
|
|
16042
|
+
<option value="all">Active + superseded + cancelled</option>
|
|
16043
|
+
</select>
|
|
16044
|
+
<span style="font-size:11px;color:var(--text-muted)">Distilled durable beliefs from past sessions</span>
|
|
16045
|
+
</div>
|
|
16046
|
+
</div>
|
|
16047
|
+
<div class="card-body" id="panel-learnings" style="padding:0">
|
|
16048
|
+
<div class="skel-block" style="padding:14px"><div class="skel-row med"></div><div class="skel-row short"></div></div>
|
|
16049
|
+
</div>
|
|
16050
|
+
</div>
|
|
15088
16051
|
<div class="card" style="margin-bottom:14px">
|
|
15089
16052
|
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap">
|
|
15090
16053
|
<span>Open commitments</span>
|
|
@@ -18630,6 +19593,7 @@ function switchTab(group, tab) {
|
|
|
18630
19593
|
if (typeof refreshRecentWrites === 'function') refreshRecentWrites();
|
|
18631
19594
|
if (typeof refreshRecentEpisodes === 'function') refreshRecentEpisodes();
|
|
18632
19595
|
if (typeof refreshCommitments === 'function') refreshCommitments();
|
|
19596
|
+
if (typeof refreshLearnings === 'function') refreshLearnings();
|
|
18633
19597
|
if (typeof refreshSupersedes === 'function') refreshSupersedes();
|
|
18634
19598
|
if (typeof refreshCoverageStrip === 'function') refreshCoverageStrip();
|
|
18635
19599
|
}
|
|
@@ -24989,6 +25953,7 @@ async function submitQuickAddMemory() {
|
|
|
24989
25953
|
if (typeof refreshRecentWrites === 'function') refreshRecentWrites();
|
|
24990
25954
|
if (typeof refreshRecentEpisodes === 'function') refreshRecentEpisodes();
|
|
24991
25955
|
if (typeof refreshCommitments === 'function') refreshCommitments();
|
|
25956
|
+
if (typeof refreshLearnings === 'function') refreshLearnings();
|
|
24992
25957
|
if (typeof refreshMemory === 'function') refreshMemory();
|
|
24993
25958
|
}, 600);
|
|
24994
25959
|
} catch (err) {
|
|
@@ -25142,6 +26107,71 @@ async function refreshRecentWrites() {
|
|
|
25142
26107
|
}
|
|
25143
26108
|
}
|
|
25144
26109
|
|
|
26110
|
+
async function refreshLearnings() {
|
|
26111
|
+
var el = document.getElementById('panel-learnings');
|
|
26112
|
+
if (!el) return;
|
|
26113
|
+
try {
|
|
26114
|
+
var sel = document.getElementById('learnings-filter-scope');
|
|
26115
|
+
var scope = sel ? sel.value : 'active';
|
|
26116
|
+
var url = '/api/memory/learnings?limit=100' + (scope === 'all' ? '&all=1' : '');
|
|
26117
|
+
var r = await apiFetch(url);
|
|
26118
|
+
var d = await r.json();
|
|
26119
|
+
if (!d.ok || !Array.isArray(d.facts)) {
|
|
26120
|
+
el.innerHTML = '<div class="empty-state" style="padding:14px">' + esc(d.error || 'No data') + '</div>';
|
|
26121
|
+
return;
|
|
26122
|
+
}
|
|
26123
|
+
if (d.facts.length === 0) {
|
|
26124
|
+
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>';
|
|
26125
|
+
return;
|
|
26126
|
+
}
|
|
26127
|
+
var html = '<table class="data-table" style="width:100%">';
|
|
26128
|
+
html += '<thead><tr>'
|
|
26129
|
+
+ '<th style="width:90px">Kind</th>'
|
|
26130
|
+
+ '<th>Belief</th>'
|
|
26131
|
+
+ '<th style="width:120px">Status</th>'
|
|
26132
|
+
+ '<th style="width:140px">Captured</th>'
|
|
26133
|
+
+ '<th style="width:140px">Actions</th>'
|
|
26134
|
+
+ '</tr></thead><tbody>';
|
|
26135
|
+
var kindColors = { preference: '#a78bfa', fact: '#10b981', goal: '#f59e0b', workflow: '#06b6d4' };
|
|
26136
|
+
for (var i = 0; i < d.facts.length; i++) {
|
|
26137
|
+
var f = d.facts[i];
|
|
26138
|
+
var color = kindColors[f.kind] || 'var(--text-muted)';
|
|
26139
|
+
var statusBadge = '';
|
|
26140
|
+
if (f.status === 'active') statusBadge = '<span style="color:#10b981">active</span>';
|
|
26141
|
+
else if (f.status === 'superseded') statusBadge = '<span style="color:var(--text-muted)">superseded → #' + (f.supersededById || '?') + '</span>';
|
|
26142
|
+
else if (f.status === 'cancelled') statusBadge = '<span style="color:#ef4444">cancelled</span>';
|
|
26143
|
+
else statusBadge = esc(f.status || '');
|
|
26144
|
+
var when = '';
|
|
26145
|
+
try { when = new Date(f.createdAt + 'Z').toLocaleString(); } catch { when = f.createdAt; }
|
|
26146
|
+
var actions = '';
|
|
26147
|
+
if (f.status === 'active') actions = '<button class="btn-sm" onclick="learningAction(' + f.id + ', \\'cancel\\')">Cancel</button>';
|
|
26148
|
+
else if (f.status === 'cancelled') actions = '<button class="btn-sm" onclick="learningAction(' + f.id + ', \\'reinstate\\')">Reinstate</button>';
|
|
26149
|
+
html += '<tr>'
|
|
26150
|
+
+ '<td style="font-size:11px;color:' + color + ';font-weight:600">' + esc(f.kind) + '</td>'
|
|
26151
|
+
+ '<td style="font-size:12px">' + esc(f.text) + '</td>'
|
|
26152
|
+
+ '<td style="font-size:11px">' + statusBadge + '</td>'
|
|
26153
|
+
+ '<td style="font-size:11px;color:var(--text-muted)">' + esc(when) + '</td>'
|
|
26154
|
+
+ '<td>' + actions + '</td>'
|
|
26155
|
+
+ '</tr>';
|
|
26156
|
+
}
|
|
26157
|
+
html += '</tbody></table>';
|
|
26158
|
+
el.innerHTML = html;
|
|
26159
|
+
} catch (err) {
|
|
26160
|
+
el.innerHTML = '<div class="empty-state" style="padding:14px">Failed to load: ' + esc(String(err)) + '</div>';
|
|
26161
|
+
}
|
|
26162
|
+
}
|
|
26163
|
+
|
|
26164
|
+
async function learningAction(id, action) {
|
|
26165
|
+
try {
|
|
26166
|
+
var r = await apiJson('POST', '/api/memory/learnings/action', { id: id, action: action });
|
|
26167
|
+
if (r.error) { toast('Action failed: ' + r.error, 'error'); return; }
|
|
26168
|
+
toast('Learning ' + action, 'success');
|
|
26169
|
+
refreshLearnings();
|
|
26170
|
+
} catch (err) {
|
|
26171
|
+
toast('Failed: ' + String(err), 'error');
|
|
26172
|
+
}
|
|
26173
|
+
}
|
|
26174
|
+
|
|
25145
26175
|
async function refreshCommitments() {
|
|
25146
26176
|
var el = document.getElementById('panel-commitments');
|
|
25147
26177
|
if (!el) return;
|