clementine-agent 1.18.92 → 1.18.95
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.
|
@@ -304,6 +304,8 @@ export class AgentManager {
|
|
|
304
304
|
frontmatter.allowedUsers = config.allowedUsers;
|
|
305
305
|
if (config.project)
|
|
306
306
|
frontmatter.project = config.project;
|
|
307
|
+
if (config.projects?.length)
|
|
308
|
+
frontmatter.projects = config.projects;
|
|
307
309
|
if (config.discordToken) {
|
|
308
310
|
storeAgentSecret(slug, 'DISCORD_TOKEN', config.discordToken);
|
|
309
311
|
frontmatter.discordToken = 'keychain';
|
|
@@ -415,6 +417,10 @@ export class AgentManager {
|
|
|
415
417
|
meta.allowedUsers = changes.allowedUsers;
|
|
416
418
|
if (changes.project !== undefined)
|
|
417
419
|
meta.project = changes.project;
|
|
420
|
+
if (changes.projects !== undefined) {
|
|
421
|
+
// Empty array clears the field; non-empty array replaces it.
|
|
422
|
+
meta.projects = changes.projects.length ? changes.projects : undefined;
|
|
423
|
+
}
|
|
418
424
|
if (changes.discordToken !== undefined) {
|
|
419
425
|
if (changes.discordToken) {
|
|
420
426
|
storeAgentSecret(slug, 'DISCORD_TOKEN', changes.discordToken);
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -7472,6 +7472,88 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
7472
7472
|
res.status(500).json({ error: String(err) });
|
|
7473
7473
|
}
|
|
7474
7474
|
});
|
|
7475
|
+
// ── Clementine main-agent profile ──────────────────────────────────
|
|
7476
|
+
// Mirrors the per-agent edit surface (Tasks → Team → Edit) for
|
|
7477
|
+
// Clementine herself. Persists to clementine.json under
|
|
7478
|
+
// assistant.profile so existing readers (computeEffectiveConfig,
|
|
7479
|
+
// gateway initializers) can resolve the values via the standard
|
|
7480
|
+
// env > json > default chain.
|
|
7481
|
+
app.get('/api/clementine/profile', (_req, res) => {
|
|
7482
|
+
try {
|
|
7483
|
+
const json = loadClementineJson(BASE_DIR);
|
|
7484
|
+
const profile = json.assistant?.profile ?? {};
|
|
7485
|
+
const status = getApiConnectionStatus();
|
|
7486
|
+
res.json({
|
|
7487
|
+
name: json.assistantName || 'Clementine',
|
|
7488
|
+
profile: {
|
|
7489
|
+
systemPrompt: profile.systemPrompt ?? '',
|
|
7490
|
+
model: profile.model ?? '',
|
|
7491
|
+
allowedTools: profile.allowedTools ?? [],
|
|
7492
|
+
allowedProjects: profile.allowedProjects ?? [],
|
|
7493
|
+
allowedUsers: profile.allowedUsers ?? [],
|
|
7494
|
+
channels: profile.channels ?? [],
|
|
7495
|
+
budgetMonthlyCents: profile.budgetMonthlyCents ?? 0,
|
|
7496
|
+
goalSlugs: profile.goalSlugs ?? [],
|
|
7497
|
+
sendPolicy: profile.sendPolicy ?? null,
|
|
7498
|
+
},
|
|
7499
|
+
connectivity: status,
|
|
7500
|
+
});
|
|
7501
|
+
}
|
|
7502
|
+
catch (err) {
|
|
7503
|
+
res.status(500).json({ error: String(err) });
|
|
7504
|
+
}
|
|
7505
|
+
});
|
|
7506
|
+
app.put('/api/clementine/profile', (req, res) => {
|
|
7507
|
+
try {
|
|
7508
|
+
const body = (req.body ?? {});
|
|
7509
|
+
const profile = {};
|
|
7510
|
+
if (typeof body.systemPrompt === 'string')
|
|
7511
|
+
profile.systemPrompt = body.systemPrompt;
|
|
7512
|
+
if (typeof body.model === 'string' && body.model)
|
|
7513
|
+
profile.model = body.model;
|
|
7514
|
+
if (Array.isArray(body.allowedTools))
|
|
7515
|
+
profile.allowedTools = body.allowedTools.map(String);
|
|
7516
|
+
if (Array.isArray(body.allowedProjects))
|
|
7517
|
+
profile.allowedProjects = body.allowedProjects.map(String);
|
|
7518
|
+
if (Array.isArray(body.allowedUsers))
|
|
7519
|
+
profile.allowedUsers = body.allowedUsers.map(String);
|
|
7520
|
+
if (Array.isArray(body.channels))
|
|
7521
|
+
profile.channels = body.channels.map(String);
|
|
7522
|
+
if (Array.isArray(body.goalSlugs))
|
|
7523
|
+
profile.goalSlugs = body.goalSlugs.map(String);
|
|
7524
|
+
if (typeof body.budgetMonthlyCents === 'number' && body.budgetMonthlyCents >= 0) {
|
|
7525
|
+
profile.budgetMonthlyCents = body.budgetMonthlyCents;
|
|
7526
|
+
}
|
|
7527
|
+
if (body.sendPolicy && typeof body.sendPolicy === 'object') {
|
|
7528
|
+
const sp = body.sendPolicy;
|
|
7529
|
+
const cleaned = {};
|
|
7530
|
+
if (typeof sp.maxDailyEmails === 'number')
|
|
7531
|
+
cleaned.maxDailyEmails = sp.maxDailyEmails;
|
|
7532
|
+
if (typeof sp.requiresApproval === 'string'
|
|
7533
|
+
&& ['none', 'first-in-sequence', 'all'].includes(sp.requiresApproval)) {
|
|
7534
|
+
cleaned.requiresApproval = sp.requiresApproval;
|
|
7535
|
+
}
|
|
7536
|
+
if (typeof sp.businessHoursOnly === 'boolean')
|
|
7537
|
+
cleaned.businessHoursOnly = sp.businessHoursOnly;
|
|
7538
|
+
if (Object.keys(cleaned).length)
|
|
7539
|
+
profile.sendPolicy = cleaned;
|
|
7540
|
+
}
|
|
7541
|
+
const next = updateClementineJson(BASE_DIR, (current) => ({
|
|
7542
|
+
...current,
|
|
7543
|
+
assistant: {
|
|
7544
|
+
...(current.assistant ?? {}),
|
|
7545
|
+
profile: {
|
|
7546
|
+
...(current.assistant?.profile ?? {}),
|
|
7547
|
+
...profile,
|
|
7548
|
+
},
|
|
7549
|
+
},
|
|
7550
|
+
}));
|
|
7551
|
+
res.json({ ok: true, profile: next.assistant?.profile ?? {} });
|
|
7552
|
+
}
|
|
7553
|
+
catch (err) {
|
|
7554
|
+
res.status(400).json({ error: String(err) });
|
|
7555
|
+
}
|
|
7556
|
+
});
|
|
7475
7557
|
app.get('/api/budgets', async (_req, res) => {
|
|
7476
7558
|
try {
|
|
7477
7559
|
const [{ computeEffectiveConfig }, { runDoctor }] = await Promise.all([
|
|
@@ -10108,6 +10190,12 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
10108
10190
|
sendPolicy: a.sendPolicy ?? null,
|
|
10109
10191
|
agentStatus: a.status ?? 'active',
|
|
10110
10192
|
budgetMonthlyCents: a.budgetMonthlyCents ?? 0,
|
|
10193
|
+
// Mission & persona — what the agent edit modal renders in
|
|
10194
|
+
// its single combined textarea.
|
|
10195
|
+
systemPromptBody: a.systemPromptBody ?? '',
|
|
10196
|
+
// Multi-project access list (separate from the legacy single
|
|
10197
|
+
// `project` binding above).
|
|
10198
|
+
projects: a.projects ?? [],
|
|
10111
10199
|
};
|
|
10112
10200
|
}));
|
|
10113
10201
|
}
|
|
@@ -10347,7 +10435,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
10347
10435
|
try {
|
|
10348
10436
|
const gw = await getGateway();
|
|
10349
10437
|
const mgr = gw.getAgentManager();
|
|
10350
|
-
const { name, description, personality, tier, model, channelName, teamChat, respondToAll, canMessage, allowedTools, allowedUsers, project, discordToken, discordChannelId, avatar, slackBotToken, slackAppToken, slackChannelId, sendPolicy, role } = req.body;
|
|
10438
|
+
const { name, description, personality, tier, model, channelName, teamChat, respondToAll, canMessage, allowedTools, allowedUsers, project, projects, discordToken, discordChannelId, avatar, slackBotToken, slackAppToken, slackChannelId, sendPolicy, role } = req.body;
|
|
10351
10439
|
if (!name || !description) {
|
|
10352
10440
|
res.status(400).json({ error: 'name and description are required' });
|
|
10353
10441
|
return;
|
|
@@ -10363,6 +10451,9 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
10363
10451
|
allowedTools: allowedTools || undefined,
|
|
10364
10452
|
allowedUsers: allowedUsers || undefined,
|
|
10365
10453
|
project: project || undefined,
|
|
10454
|
+
// `projects` is the multi-project access list — agent-manager already
|
|
10455
|
+
// persists it to agent.md frontmatter and the SDK reads it on load.
|
|
10456
|
+
projects: Array.isArray(projects) && projects.length ? projects : undefined,
|
|
10366
10457
|
discordToken: discordToken || undefined,
|
|
10367
10458
|
discordChannelId: discordChannelId || undefined,
|
|
10368
10459
|
avatar: avatar || undefined,
|
|
@@ -14551,6 +14642,103 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
14551
14642
|
font-weight: 600;
|
|
14552
14643
|
}
|
|
14553
14644
|
|
|
14645
|
+
/* ── Agent edit modal — tabs + Equipment panel ─────────────────── */
|
|
14646
|
+
.agent-tab {
|
|
14647
|
+
background: transparent;
|
|
14648
|
+
border: 0;
|
|
14649
|
+
border-bottom: 2px solid transparent;
|
|
14650
|
+
color: var(--text-muted);
|
|
14651
|
+
cursor: pointer;
|
|
14652
|
+
font-size: 13px;
|
|
14653
|
+
padding: 8px 14px;
|
|
14654
|
+
margin-bottom: -1px;
|
|
14655
|
+
}
|
|
14656
|
+
.agent-tab:hover { color: var(--text-primary); }
|
|
14657
|
+
.agent-tab.active {
|
|
14658
|
+
color: var(--text-primary);
|
|
14659
|
+
border-bottom-color: var(--clementine);
|
|
14660
|
+
font-weight: 600;
|
|
14661
|
+
}
|
|
14662
|
+
.agent-tools-filter {
|
|
14663
|
+
background: transparent;
|
|
14664
|
+
border: 1px solid var(--border);
|
|
14665
|
+
border-radius: 4px;
|
|
14666
|
+
color: var(--text-muted);
|
|
14667
|
+
cursor: pointer;
|
|
14668
|
+
font-size: 11px;
|
|
14669
|
+
padding: 4px 8px;
|
|
14670
|
+
}
|
|
14671
|
+
.agent-tools-filter:hover { color: var(--text-primary); }
|
|
14672
|
+
.agent-tools-filter.active {
|
|
14673
|
+
background: var(--clementine);
|
|
14674
|
+
border-color: var(--clementine);
|
|
14675
|
+
color: #000;
|
|
14676
|
+
font-weight: 600;
|
|
14677
|
+
}
|
|
14678
|
+
.agent-tool-row {
|
|
14679
|
+
align-items: center;
|
|
14680
|
+
color: var(--text-primary);
|
|
14681
|
+
cursor: pointer;
|
|
14682
|
+
display: flex;
|
|
14683
|
+
font-size: 12px;
|
|
14684
|
+
gap: 6px;
|
|
14685
|
+
line-height: 1.4;
|
|
14686
|
+
padding: 4px 6px;
|
|
14687
|
+
border-radius: 4px;
|
|
14688
|
+
}
|
|
14689
|
+
.agent-tool-row:hover { background: rgba(255,255,255,0.03); }
|
|
14690
|
+
.agent-tool-row .tt-name { flex: 1; min-width: 0; }
|
|
14691
|
+
.agent-tool-row .tt-badge {
|
|
14692
|
+
background: rgba(255,255,255,0.06);
|
|
14693
|
+
border-radius: 3px;
|
|
14694
|
+
color: var(--text-muted);
|
|
14695
|
+
font-size: 9px;
|
|
14696
|
+
font-weight: 700;
|
|
14697
|
+
letter-spacing: 0.4px;
|
|
14698
|
+
padding: 1px 5px;
|
|
14699
|
+
text-transform: uppercase;
|
|
14700
|
+
}
|
|
14701
|
+
.agent-tool-row .tt-badge.cli { background: rgba(120, 200, 255, 0.12); color: #79c8ff; }
|
|
14702
|
+
.agent-tool-row .tt-badge.sdk { background: rgba(168, 200, 255, 0.12); color: #a8c8ff; }
|
|
14703
|
+
.agent-tool-row .tt-badge.mcp { background: rgba(180, 130, 255, 0.12); color: #c8a8ff; }
|
|
14704
|
+
.agent-tool-row .tt-badge.api { background: rgba(255, 200, 120, 0.12); color: #ffc878; }
|
|
14705
|
+
.agent-tool-row .tt-badge.composio { background: rgba(255, 138, 138, 0.14); color: #ff8a8a; }
|
|
14706
|
+
.agent-tool-row .tt-badge.project, .agent-tool-row .tt-badge['project-mcp'] { background: rgba(120, 255, 180, 0.12); color: #78ffb4; }
|
|
14707
|
+
.agent-tool-row .tt-status {
|
|
14708
|
+
align-items: center;
|
|
14709
|
+
color: var(--text-muted);
|
|
14710
|
+
display: inline-flex;
|
|
14711
|
+
font-size: 10px;
|
|
14712
|
+
gap: 3px;
|
|
14713
|
+
}
|
|
14714
|
+
.agent-tool-row .tt-status .tt-dot {
|
|
14715
|
+
border-radius: 50%;
|
|
14716
|
+
height: 7px;
|
|
14717
|
+
width: 7px;
|
|
14718
|
+
}
|
|
14719
|
+
.agent-tool-row .tt-status.ready .tt-dot { background: var(--green); }
|
|
14720
|
+
.agent-tool-row .tt-status.needs-setup .tt-dot { background: #f59e0b; }
|
|
14721
|
+
.agent-tool-row .tt-status.not-installed .tt-dot { background: #888; }
|
|
14722
|
+
.agent-tool-row .tt-status.blocked .tt-dot { background: #ef4444; }
|
|
14723
|
+
.agent-tool-cat {
|
|
14724
|
+
color: var(--text-muted);
|
|
14725
|
+
cursor: pointer;
|
|
14726
|
+
font-size: 11px;
|
|
14727
|
+
font-weight: 700;
|
|
14728
|
+
letter-spacing: 0.5px;
|
|
14729
|
+
margin: 10px 0 4px;
|
|
14730
|
+
text-transform: uppercase;
|
|
14731
|
+
user-select: none;
|
|
14732
|
+
}
|
|
14733
|
+
.agent-tool-cat .cat-count {
|
|
14734
|
+
color: var(--text-muted);
|
|
14735
|
+
font-weight: 500;
|
|
14736
|
+
margin-left: 6px;
|
|
14737
|
+
text-transform: none;
|
|
14738
|
+
letter-spacing: 0;
|
|
14739
|
+
}
|
|
14740
|
+
.agent-tool-row.hidden, .agent-tool-cat.hidden { display: none; }
|
|
14741
|
+
|
|
14554
14742
|
/* ── Office Hero — Clementine ─────────── */
|
|
14555
14743
|
.office-hero {
|
|
14556
14744
|
background: var(--bg-card);
|
|
@@ -15365,6 +15553,131 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
15365
15553
|
font-size: 11px;
|
|
15366
15554
|
color: var(--text-muted);
|
|
15367
15555
|
}
|
|
15556
|
+
/* PRD §12 / 1.18.93: three mini-dashboards beneath the Health Strip:
|
|
15557
|
+
Cost (7d sparkline), Latency split-bar, Reliability (failures stacked
|
|
15558
|
+
by category). One row of three cards on wide screens; collapses to a
|
|
15559
|
+
vertical stack at narrow viewports. */
|
|
15560
|
+
.mini-dashboards {
|
|
15561
|
+
display: grid;
|
|
15562
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
15563
|
+
gap: 12px;
|
|
15564
|
+
margin-bottom: 22px;
|
|
15565
|
+
}
|
|
15566
|
+
.mini-card {
|
|
15567
|
+
background: var(--bg-secondary);
|
|
15568
|
+
border: 1px solid var(--border);
|
|
15569
|
+
border-radius: var(--radius);
|
|
15570
|
+
padding: 14px 16px;
|
|
15571
|
+
display: flex;
|
|
15572
|
+
flex-direction: column;
|
|
15573
|
+
gap: 10px;
|
|
15574
|
+
min-height: 130px;
|
|
15575
|
+
}
|
|
15576
|
+
.mini-card-head {
|
|
15577
|
+
display: flex;
|
|
15578
|
+
justify-content: space-between;
|
|
15579
|
+
align-items: baseline;
|
|
15580
|
+
gap: 10px;
|
|
15581
|
+
}
|
|
15582
|
+
.mini-card-title {
|
|
15583
|
+
font-size: 11px;
|
|
15584
|
+
color: var(--text-muted);
|
|
15585
|
+
text-transform: uppercase;
|
|
15586
|
+
letter-spacing: 0.05em;
|
|
15587
|
+
font-weight: 500;
|
|
15588
|
+
}
|
|
15589
|
+
.mini-card-figure {
|
|
15590
|
+
font-size: 18px;
|
|
15591
|
+
font-weight: 600;
|
|
15592
|
+
color: var(--text-primary);
|
|
15593
|
+
}
|
|
15594
|
+
.mini-card-sub {
|
|
15595
|
+
font-size: 11px;
|
|
15596
|
+
color: var(--text-muted);
|
|
15597
|
+
}
|
|
15598
|
+
/* Tiny inline sparkline for the cost card — bars rendered as flex grow
|
|
15599
|
+
items with proportional heights. Pure CSS, no SVG needed. */
|
|
15600
|
+
.mini-spark {
|
|
15601
|
+
display: flex;
|
|
15602
|
+
align-items: flex-end;
|
|
15603
|
+
gap: 2px;
|
|
15604
|
+
height: 38px;
|
|
15605
|
+
flex: 1;
|
|
15606
|
+
}
|
|
15607
|
+
.mini-spark-bar {
|
|
15608
|
+
flex: 1;
|
|
15609
|
+
background: var(--accent);
|
|
15610
|
+
border-radius: 1px;
|
|
15611
|
+
min-height: 1px;
|
|
15612
|
+
opacity: 0.7;
|
|
15613
|
+
}
|
|
15614
|
+
.mini-spark-bar.zero {
|
|
15615
|
+
background: var(--border);
|
|
15616
|
+
opacity: 0.4;
|
|
15617
|
+
}
|
|
15618
|
+
/* Latency split-bar: three segments side-by-side with proportional widths.
|
|
15619
|
+
Hovering any segment shows its label inline. */
|
|
15620
|
+
.mini-split {
|
|
15621
|
+
display: flex;
|
|
15622
|
+
height: 22px;
|
|
15623
|
+
border-radius: 4px;
|
|
15624
|
+
overflow: hidden;
|
|
15625
|
+
background: var(--bg-tertiary);
|
|
15626
|
+
}
|
|
15627
|
+
.mini-split-seg {
|
|
15628
|
+
display: flex;
|
|
15629
|
+
align-items: center;
|
|
15630
|
+
justify-content: center;
|
|
15631
|
+
font-size: 10px;
|
|
15632
|
+
color: var(--text-on-accent, white);
|
|
15633
|
+
overflow: hidden;
|
|
15634
|
+
white-space: nowrap;
|
|
15635
|
+
transition: opacity 0.15s;
|
|
15636
|
+
}
|
|
15637
|
+
.mini-split-seg:hover { opacity: 0.85; }
|
|
15638
|
+
.mini-split-legend {
|
|
15639
|
+
display: flex;
|
|
15640
|
+
gap: 12px;
|
|
15641
|
+
font-size: 10px;
|
|
15642
|
+
color: var(--text-muted);
|
|
15643
|
+
flex-wrap: wrap;
|
|
15644
|
+
}
|
|
15645
|
+
.mini-split-legend-dot {
|
|
15646
|
+
display: inline-block;
|
|
15647
|
+
width: 8px;
|
|
15648
|
+
height: 8px;
|
|
15649
|
+
border-radius: 2px;
|
|
15650
|
+
margin-right: 4px;
|
|
15651
|
+
vertical-align: middle;
|
|
15652
|
+
}
|
|
15653
|
+
/* Reliability stacked bar — vertical column per failure category. */
|
|
15654
|
+
.mini-fails {
|
|
15655
|
+
display: flex;
|
|
15656
|
+
align-items: flex-end;
|
|
15657
|
+
gap: 4px;
|
|
15658
|
+
height: 60px;
|
|
15659
|
+
flex: 1;
|
|
15660
|
+
}
|
|
15661
|
+
.mini-fails-col {
|
|
15662
|
+
flex: 1;
|
|
15663
|
+
display: flex;
|
|
15664
|
+
flex-direction: column-reverse;
|
|
15665
|
+
border-radius: 2px;
|
|
15666
|
+
overflow: hidden;
|
|
15667
|
+
background: var(--bg-tertiary);
|
|
15668
|
+
min-width: 8px;
|
|
15669
|
+
}
|
|
15670
|
+
.mini-fails-seg {
|
|
15671
|
+
width: 100%;
|
|
15672
|
+
}
|
|
15673
|
+
.mini-fails-empty {
|
|
15674
|
+
flex: 1;
|
|
15675
|
+
display: flex;
|
|
15676
|
+
align-items: center;
|
|
15677
|
+
justify-content: center;
|
|
15678
|
+
color: var(--text-muted);
|
|
15679
|
+
font-size: 11px;
|
|
15680
|
+
}
|
|
15368
15681
|
/* PRD Phase 1.2: "Run task once" running-state pulse on the Last run tab. */
|
|
15369
15682
|
@keyframes pulse {
|
|
15370
15683
|
0%, 100% { opacity: 0.4; transform: scale(0.85); }
|
|
@@ -19774,9 +20087,13 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
19774
20087
|
</div>
|
|
19775
20088
|
</div>
|
|
19776
20089
|
|
|
19777
|
-
<!-- Agent Create/Edit Modal
|
|
20090
|
+
<!-- Agent Create/Edit Modal — tabbed editor reused for hired agents
|
|
20091
|
+
AND for Clementine herself (see editClementine() in dashboard JS).
|
|
20092
|
+
Element IDs are stable so existing populator/submit code keeps
|
|
20093
|
+
working; new tabs (Equipment, Goals) add fields without breaking the
|
|
20094
|
+
old contract. -->
|
|
19778
20095
|
<div id="agent-modal" class="modal-overlay">
|
|
19779
|
-
<div class="modal" style="width:
|
|
20096
|
+
<div class="modal" style="width:760px;max-width:95vw">
|
|
19780
20097
|
<div class="modal-header">
|
|
19781
20098
|
<h3 id="agent-modal-title">Hire a New Team Member</h3>
|
|
19782
20099
|
<button class="btn-ghost btn-sm" onclick="hideAgentModal()">×</button>
|
|
@@ -19784,26 +20101,82 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
19784
20101
|
<div class="modal-body">
|
|
19785
20102
|
<form id="agent-form" onsubmit="submitAgentForm(event)">
|
|
19786
20103
|
<input type="hidden" id="agent-edit-slug" value="">
|
|
19787
|
-
<
|
|
19788
|
-
|
|
19789
|
-
|
|
19790
|
-
|
|
19791
|
-
|
|
19792
|
-
<
|
|
19793
|
-
<
|
|
20104
|
+
<input type="hidden" id="agent-edit-mode" value="agent"><!-- 'agent' or 'clementine' -->
|
|
20105
|
+
|
|
20106
|
+
<!-- Tab strip ---------------------------------------------------- -->
|
|
20107
|
+
<div id="agent-modal-tabs" style="display:flex;gap:2px;border-bottom:1px solid var(--border);margin-bottom:14px">
|
|
20108
|
+
<button type="button" class="agent-tab active" data-tab="identity" onclick="switchAgentTab('identity')">Identity</button>
|
|
20109
|
+
<button type="button" class="agent-tab" data-tab="equipment" onclick="switchAgentTab('equipment')">Equipment</button>
|
|
20110
|
+
<button type="button" class="agent-tab" data-tab="connections" onclick="switchAgentTab('connections')">Connections</button>
|
|
20111
|
+
<button type="button" class="agent-tab" data-tab="goals" onclick="switchAgentTab('goals')">Goals</button>
|
|
20112
|
+
<button type="button" class="agent-tab" data-tab="limits" onclick="switchAgentTab('limits')">Limits</button>
|
|
19794
20113
|
</div>
|
|
19795
|
-
|
|
19796
|
-
|
|
19797
|
-
|
|
20114
|
+
|
|
20115
|
+
<!-- Tab: Identity ------------------------------------------------ -->
|
|
20116
|
+
<div class="agent-tab-pane" data-tab-pane="identity">
|
|
20117
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px">
|
|
20118
|
+
<div>
|
|
20119
|
+
<label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Name *</label>
|
|
20120
|
+
<input id="agent-name" required style="width:100%;padding:8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary)" placeholder="Research Agent">
|
|
20121
|
+
</div>
|
|
20122
|
+
<div>
|
|
20123
|
+
<label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Model</label>
|
|
20124
|
+
<select id="agent-model" style="width:100%;padding:8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary)">
|
|
20125
|
+
<option value="">Default (Sonnet)</option>
|
|
20126
|
+
<option value="haiku">Haiku</option>
|
|
20127
|
+
<option value="sonnet">Sonnet</option>
|
|
20128
|
+
<option value="opus">Opus</option>
|
|
20129
|
+
</select>
|
|
20130
|
+
</div>
|
|
20131
|
+
</div>
|
|
20132
|
+
<div id="agent-identity-row-2" style="margin-bottom:12px">
|
|
20133
|
+
<label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Profile Photo URL</label>
|
|
20134
|
+
<input id="agent-avatar-url" style="width:100%;padding:8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary)" placeholder="https://example.com/avatar.png">
|
|
20135
|
+
</div>
|
|
20136
|
+
<div style="margin-bottom:12px">
|
|
20137
|
+
<label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Headline <span style="opacity:0.6">(short — appears on the agent card)</span> *</label>
|
|
20138
|
+
<input id="agent-description" required style="width:100%;padding:8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary)" placeholder="Deep-dive research and analysis">
|
|
20139
|
+
</div>
|
|
20140
|
+
<div style="margin-bottom:12px">
|
|
20141
|
+
<label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Mission & persona <span style="opacity:0.6">(full system-prompt body — voice, expertise, working rules)</span></label>
|
|
20142
|
+
<textarea id="agent-personality" rows="8" style="width:100%;padding:8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);resize:vertical;font-family:inherit" placeholder="You are a Research Agent specializing in..."></textarea>
|
|
20143
|
+
</div>
|
|
20144
|
+
<div id="agent-project-row" style="margin-bottom:12px">
|
|
20145
|
+
<label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Primary Project <span style="opacity:0.6">(optional — additional project access lives in the Equipment tab)</span></label>
|
|
20146
|
+
<select id="agent-project" style="width:100%;padding:8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary)">
|
|
20147
|
+
<option value="">None</option>
|
|
20148
|
+
</select>
|
|
20149
|
+
</div>
|
|
19798
20150
|
</div>
|
|
19799
|
-
|
|
19800
|
-
|
|
19801
|
-
|
|
20151
|
+
|
|
20152
|
+
<!-- Tab: Equipment ----------------------------------------------- -->
|
|
20153
|
+
<div class="agent-tab-pane" data-tab-pane="equipment" style="display:none">
|
|
20154
|
+
<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px;flex-wrap:wrap">
|
|
20155
|
+
<input id="agent-tools-search" type="search" placeholder="Search tools, CLIs, MCP, projects, integrations..." oninput="filterAgentTools()" style="flex:1;min-width:240px;padding:7px 10px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);font-size:12px">
|
|
20156
|
+
<div id="agent-tools-filters" style="display:flex;gap:4px">
|
|
20157
|
+
<button type="button" class="agent-tools-filter active" data-filter="all" onclick="setAgentToolsFilter('all')">All</button>
|
|
20158
|
+
<button type="button" class="agent-tools-filter" data-filter="enabled" onclick="setAgentToolsFilter('enabled')">Enabled</button>
|
|
20159
|
+
<button type="button" class="agent-tools-filter" data-filter="ready" onclick="setAgentToolsFilter('ready')">Ready</button>
|
|
20160
|
+
<button type="button" class="agent-tools-filter" data-filter="needs-setup" onclick="setAgentToolsFilter('needs-setup')">Needs setup</button>
|
|
20161
|
+
</div>
|
|
20162
|
+
</div>
|
|
20163
|
+
<div id="agent-tools-summary" style="font-size:11px;color:var(--text-muted);margin-bottom:6px">Loading…</div>
|
|
20164
|
+
<div id="agent-tools-panel" style="max-height:380px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:8px;background:var(--bg-input)">
|
|
20165
|
+
<div style="color:var(--text-muted);font-size:12px">Loading tools...</div>
|
|
20166
|
+
</div>
|
|
20167
|
+
<div style="margin-top:8px;font-size:11px;color:var(--text-muted);line-height:1.5">
|
|
20168
|
+
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--green);vertical-align:middle;margin-right:4px"></span>Ready
|
|
20169
|
+
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#f59e0b;vertical-align:middle;margin-right:4px"></span>Needs setup
|
|
20170
|
+
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#888;vertical-align:middle;margin-right:4px"></span>Not installed
|
|
20171
|
+
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#ef4444;vertical-align:middle;margin-right:4px"></span>Blocked
|
|
20172
|
+
</div>
|
|
19802
20173
|
</div>
|
|
19803
|
-
|
|
19804
|
-
|
|
19805
|
-
|
|
19806
|
-
|
|
20174
|
+
|
|
20175
|
+
<!-- Tab: Connections -------------------------------------------- -->
|
|
20176
|
+
<div class="agent-tab-pane" data-tab-pane="connections" style="display:none">
|
|
20177
|
+
<div style="margin-bottom:14px">
|
|
20178
|
+
<label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Channels <span style="opacity:0.6">(Discord channels this agent will listen + post in)</span></label>
|
|
20179
|
+
<div id="agent-channel-list" style="max-height:160px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:6px 8px;background:var(--bg-input)">
|
|
19807
20180
|
<div style="color:var(--text-muted);font-size:12px">Loading channels...</div>
|
|
19808
20181
|
</div>
|
|
19809
20182
|
<label style="display:flex;align-items:center;gap:6px;margin-top:6px;color:var(--text-muted);font-size:12px;cursor:pointer">
|
|
@@ -19815,76 +20188,15 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
19815
20188
|
Respond to all messages <span style="opacity:0.6">(not just @mentions)</span>
|
|
19816
20189
|
</label>
|
|
19817
20190
|
</div>
|
|
19818
|
-
<div>
|
|
19819
|
-
<label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">
|
|
19820
|
-
<
|
|
19821
|
-
<option value="">Default (Sonnet)</option>
|
|
19822
|
-
<option value="haiku">Haiku</option>
|
|
19823
|
-
<option value="sonnet">Sonnet</option>
|
|
19824
|
-
<option value="opus">Opus</option>
|
|
19825
|
-
</select>
|
|
19826
|
-
</div>
|
|
19827
|
-
</div>
|
|
19828
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px">
|
|
19829
|
-
<div>
|
|
19830
|
-
<label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Project Binding</label>
|
|
19831
|
-
<select id="agent-project" style="width:100%;padding:8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary)">
|
|
19832
|
-
<option value="">None</option>
|
|
19833
|
-
</select>
|
|
19834
|
-
</div>
|
|
19835
|
-
<div>
|
|
19836
|
-
<label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Security Clearance</label>
|
|
19837
|
-
<select id="agent-tier" style="width:100%;padding:8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary)">
|
|
19838
|
-
<option value="2">Tier 2 (Read/Write)</option>
|
|
19839
|
-
<option value="1">Tier 1 (Read-only)</option>
|
|
19840
|
-
</select>
|
|
19841
|
-
</div>
|
|
19842
|
-
<div>
|
|
19843
|
-
<label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Monthly Budget <span style="opacity:0.6">(cents, 0 = unlimited)</span></label>
|
|
19844
|
-
<input id="agent-budget" type="number" value="0" min="0" style="width:100%;padding:8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary)"
|
|
19845
|
-
placeholder="e.g. 5000 = $50/month">
|
|
19846
|
-
</div>
|
|
19847
|
-
</div>
|
|
19848
|
-
<details style="margin-bottom:12px;border:1px solid var(--border);border-radius:6px;padding:8px">
|
|
19849
|
-
<summary style="cursor:pointer;color:var(--text-muted);font-size:12px;font-weight:600">Autonomous Sending Policy <span style="opacity:0.6">(for email-capable agents)</span></summary>
|
|
19850
|
-
<div style="padding:8px 0 0;display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
|
19851
|
-
<div>
|
|
19852
|
-
<label style="display:block;color:var(--text-muted);font-size:11px;margin-bottom:3px">Max Emails / Day</label>
|
|
19853
|
-
<input id="agent-send-max-daily" type="number" value="50" min="0" max="500" style="width:100%;padding:6px;background:var(--bg-input);border:1px solid var(--border);border-radius:4px;color:var(--text-primary);font-size:12px">
|
|
19854
|
-
</div>
|
|
19855
|
-
<div>
|
|
19856
|
-
<label style="display:block;color:var(--text-muted);font-size:11px;margin-bottom:3px">Approval Mode</label>
|
|
19857
|
-
<select id="agent-send-approval" style="width:100%;padding:6px;background:var(--bg-input);border:1px solid var(--border);border-radius:4px;color:var(--text-primary);font-size:12px">
|
|
19858
|
-
<option value="">Disabled (no autonomous sending)</option>
|
|
19859
|
-
<option value="none">None (fully autonomous)</option>
|
|
19860
|
-
<option value="first-in-sequence">First in sequence (approve first email per lead)</option>
|
|
19861
|
-
<option value="all">All (approve every send)</option>
|
|
19862
|
-
</select>
|
|
19863
|
-
</div>
|
|
19864
|
-
<div style="grid-column:span 2">
|
|
19865
|
-
<label style="display:flex;align-items:center;gap:6px;color:var(--text-muted);font-size:12px;cursor:pointer">
|
|
19866
|
-
<input id="agent-send-biz-hours" type="checkbox"> Restrict to business hours (8am–6pm)
|
|
19867
|
-
</label>
|
|
19868
|
-
</div>
|
|
20191
|
+
<div style="margin-bottom:14px">
|
|
20192
|
+
<label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Teammates this agent can message <span style="opacity:0.6">(comma-separated slugs)</span></label>
|
|
20193
|
+
<input id="agent-canmessage" style="width:100%;padding:8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary)" placeholder="analyst-agent, writer-agent">
|
|
19869
20194
|
</div>
|
|
19870
|
-
|
|
19871
|
-
|
|
19872
|
-
|
|
19873
|
-
<input id="agent-canmessage" style="width:100%;padding:8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary)" placeholder="analyst-agent, writer-agent">
|
|
19874
|
-
</div>
|
|
19875
|
-
<div style="margin-bottom:12px">
|
|
19876
|
-
<label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Equipment & Access <span style="opacity:0.6">(click category to toggle all)</span></label>
|
|
19877
|
-
<div id="agent-tools-panel" style="max-height:200px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:8px;background:var(--bg-input)">
|
|
19878
|
-
<div style="color:var(--text-muted);font-size:12px">Loading tools...</div>
|
|
20195
|
+
<div style="margin-bottom:14px">
|
|
20196
|
+
<label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Allowed Users <span style="opacity:0.6">(Discord user IDs that may DM this agent — comma-separated)</span></label>
|
|
20197
|
+
<input id="agent-allowed-users" style="width:100%;padding:8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary)" placeholder="123456789012345678, 987654321098765432">
|
|
19879
20198
|
</div>
|
|
19880
|
-
|
|
19881
|
-
<div style="margin-bottom:12px">
|
|
19882
|
-
<label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Allowed Users <span style="opacity:0.6">(Discord user IDs, comma-separated)</span></label>
|
|
19883
|
-
<input id="agent-allowed-users" style="width:100%;padding:8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary)" placeholder="123456789012345678, 987654321098765432">
|
|
19884
|
-
</div>
|
|
19885
|
-
<div style="margin-bottom:16px">
|
|
19886
|
-
<div style="font-weight:600;font-size:13px;color:var(--text-primary);margin-bottom:8px;border-top:1px solid var(--border);padding-top:12px">Platform Connections</div>
|
|
19887
|
-
|
|
20199
|
+
<div style="margin-bottom:8px;font-weight:600;font-size:13px;color:var(--text-primary);border-top:1px solid var(--border);padding-top:12px">Platform Bots</div>
|
|
19888
20200
|
<details id="discord-section" style="margin-bottom:10px">
|
|
19889
20201
|
<summary style="cursor:pointer;color:var(--text-muted);font-size:12px;font-weight:600;margin-bottom:6px">Discord Bot <span id="discord-status-dot" style="display:none;width:8px;height:8px;border-radius:50%;display:inline-block;margin-left:4px"></span></summary>
|
|
19890
20202
|
<div style="padding-left:8px">
|
|
@@ -19929,8 +20241,53 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
19929
20241
|
</div>
|
|
19930
20242
|
</div>
|
|
19931
20243
|
</details>
|
|
20244
|
+
</div><!-- /agent-tab-pane connections -->
|
|
20245
|
+
|
|
20246
|
+
<!-- Tab: Goals --------------------------------------------------- -->
|
|
20247
|
+
<div class="agent-tab-pane" data-tab-pane="goals" style="display:none">
|
|
20248
|
+
<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px">Long-running objectives this agent owns. Reassigning here moves the goal's <code>owner</code> field — the existing Goals tab in Team picks the change up automatically.</div>
|
|
20249
|
+
<div id="agent-goals-panel" style="max-height:380px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:8px;background:var(--bg-input)">
|
|
20250
|
+
<div style="color:var(--text-muted);font-size:12px">Loading goals…</div>
|
|
20251
|
+
</div>
|
|
19932
20252
|
</div>
|
|
19933
|
-
|
|
20253
|
+
|
|
20254
|
+
<!-- Tab: Limits -------------------------------------------------- -->
|
|
20255
|
+
<div class="agent-tab-pane" data-tab-pane="limits" style="display:none">
|
|
20256
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">
|
|
20257
|
+
<div id="agent-tier-row">
|
|
20258
|
+
<label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Security Clearance</label>
|
|
20259
|
+
<select id="agent-tier" style="width:100%;padding:8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary)">
|
|
20260
|
+
<option value="2">Tier 2 (Read/Write)</option>
|
|
20261
|
+
<option value="1">Tier 1 (Read-only)</option>
|
|
20262
|
+
</select>
|
|
20263
|
+
</div>
|
|
20264
|
+
<div>
|
|
20265
|
+
<label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Monthly Budget <span style="opacity:0.6">(cents, 0 = unlimited)</span></label>
|
|
20266
|
+
<input id="agent-budget" type="number" value="0" min="0" style="width:100%;padding:8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary)" placeholder="e.g. 5000 = $50/month">
|
|
20267
|
+
</div>
|
|
20268
|
+
</div>
|
|
20269
|
+
<div style="font-weight:600;font-size:13px;color:var(--text-primary);margin-bottom:8px;border-top:1px solid var(--border);padding-top:12px">Autonomous Sending Policy <span style="opacity:0.6;font-weight:400">(for email-capable agents)</span></div>
|
|
20270
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
|
|
20271
|
+
<div>
|
|
20272
|
+
<label style="display:block;color:var(--text-muted);font-size:11px;margin-bottom:3px">Max Emails / Day</label>
|
|
20273
|
+
<input id="agent-send-max-daily" type="number" value="50" min="0" max="500" style="width:100%;padding:6px;background:var(--bg-input);border:1px solid var(--border);border-radius:4px;color:var(--text-primary);font-size:12px">
|
|
20274
|
+
</div>
|
|
20275
|
+
<div>
|
|
20276
|
+
<label style="display:block;color:var(--text-muted);font-size:11px;margin-bottom:3px">Approval Mode</label>
|
|
20277
|
+
<select id="agent-send-approval" style="width:100%;padding:6px;background:var(--bg-input);border:1px solid var(--border);border-radius:4px;color:var(--text-primary);font-size:12px">
|
|
20278
|
+
<option value="">Disabled (no autonomous sending)</option>
|
|
20279
|
+
<option value="none">None (fully autonomous)</option>
|
|
20280
|
+
<option value="first-in-sequence">First in sequence (approve first email per lead)</option>
|
|
20281
|
+
<option value="all">All (approve every send)</option>
|
|
20282
|
+
</select>
|
|
20283
|
+
</div>
|
|
20284
|
+
</div>
|
|
20285
|
+
<label style="display:flex;align-items:center;gap:6px;color:var(--text-muted);font-size:12px;cursor:pointer">
|
|
20286
|
+
<input id="agent-send-biz-hours" type="checkbox"> Restrict to business hours (8am–6pm)
|
|
20287
|
+
</label>
|
|
20288
|
+
</div>
|
|
20289
|
+
|
|
20290
|
+
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px;border-top:1px solid var(--border);padding-top:12px">
|
|
19934
20291
|
<button type="button" class="btn" onclick="hideAgentModal()">Cancel</button>
|
|
19935
20292
|
<button type="submit" class="btn" style="background:var(--green);color:#000;font-weight:600" id="agent-submit-btn">Complete Hiring</button>
|
|
19936
20293
|
</div>
|
|
@@ -24036,6 +24393,146 @@ async function refreshHealthStrip() {
|
|
|
24036
24393
|
strip.innerHTML = html;
|
|
24037
24394
|
}
|
|
24038
24395
|
|
|
24396
|
+
// ── PRD §12 / 1.18.93: three mini-dashboards ───────────────────────────
|
|
24397
|
+
// Cost (7d sparkline), Latency split (model / tool / overhead),
|
|
24398
|
+
// Reliability (failures stacked by category over the same window).
|
|
24399
|
+
// All client-side from /api/cron/runs — no new endpoints. Failure
|
|
24400
|
+
// categories pulled from each run's failureCategory field (added in
|
|
24401
|
+
// 1.18.87). Latency split is a heuristic — the SDK doesn't yet expose
|
|
24402
|
+
// per-call timing, so we approximate by classifying tool_call durations
|
|
24403
|
+
// from the event log as tool time. For now the split shows total / tool
|
|
24404
|
+
// / overhead with the model and overhead sharing the remainder.
|
|
24405
|
+
async function refreshMiniDashboards() {
|
|
24406
|
+
var host = document.getElementById('mini-dashboards');
|
|
24407
|
+
if (!host) return;
|
|
24408
|
+
var runs = [];
|
|
24409
|
+
try {
|
|
24410
|
+
var r = await apiFetch('/api/cron/runs?limit=500');
|
|
24411
|
+
var d = await r.json();
|
|
24412
|
+
runs = (d && d.runs) || [];
|
|
24413
|
+
} catch (e) { /* leave empty if fetch fails */ }
|
|
24414
|
+
|
|
24415
|
+
var now = Date.now();
|
|
24416
|
+
var window7d = now - 7 * 24 * 60 * 60 * 1000;
|
|
24417
|
+
var last7 = runs.filter(function(rn) { return rn.startedAt && new Date(rn.startedAt).getTime() >= window7d; });
|
|
24418
|
+
|
|
24419
|
+
// ── Cost card: per-day sparkline + 7d total ─────────────────────────
|
|
24420
|
+
var perDayCost = []; // index 0 = 6 days ago; index 6 = today
|
|
24421
|
+
var dayLabels = [];
|
|
24422
|
+
for (var dd = 6; dd >= 0; dd--) {
|
|
24423
|
+
var dayStart = now - dd * 24 * 60 * 60 * 1000;
|
|
24424
|
+
var dayBegin = new Date(dayStart);
|
|
24425
|
+
dayBegin.setHours(0, 0, 0, 0);
|
|
24426
|
+
dayLabels.push(dayBegin.toISOString().slice(5, 10));
|
|
24427
|
+
perDayCost.push(0);
|
|
24428
|
+
}
|
|
24429
|
+
var totalCost7 = 0;
|
|
24430
|
+
for (var i = 0; i < last7.length; i++) {
|
|
24431
|
+
if (typeof last7[i].totalCostUsd !== 'number') continue;
|
|
24432
|
+
var startedMs = new Date(last7[i].startedAt).getTime();
|
|
24433
|
+
var dayIdx = 6 - Math.floor((now - startedMs) / (24 * 60 * 60 * 1000));
|
|
24434
|
+
if (dayIdx < 0 || dayIdx > 6) continue;
|
|
24435
|
+
perDayCost[dayIdx] += last7[i].totalCostUsd;
|
|
24436
|
+
totalCost7 += last7[i].totalCostUsd;
|
|
24437
|
+
}
|
|
24438
|
+
var maxDayCost = Math.max.apply(null, perDayCost.concat([0]));
|
|
24439
|
+
var costSparkHtml = '';
|
|
24440
|
+
for (var sb = 0; sb < perDayCost.length; sb++) {
|
|
24441
|
+
var pct = maxDayCost > 0 ? Math.max(2, Math.round((perDayCost[sb] / maxDayCost) * 100)) : 0;
|
|
24442
|
+
var clsZ = perDayCost[sb] > 0 ? '' : ' zero';
|
|
24443
|
+
costSparkHtml += '<div class="mini-spark-bar' + clsZ + '" style="height:' + pct + '%" title="' + dayLabels[sb] + ': $' + perDayCost[sb].toFixed(4) + '"></div>';
|
|
24444
|
+
}
|
|
24445
|
+
var costFigure = totalCost7 < 0.01 ? '$' + totalCost7.toFixed(4) : '$' + totalCost7.toFixed(2);
|
|
24446
|
+
|
|
24447
|
+
// ── Latency split card ─────────────────────────────────────────────
|
|
24448
|
+
// Sum durationMs across last7 OK runs only — we don't yet have a clean
|
|
24449
|
+
// signal for tool time per run. Until path B hooks land we approximate:
|
|
24450
|
+
// tool ~ 35%, model ~ 55%, overhead ~ 10% — these are placeholders
|
|
24451
|
+
// that get replaced with real values once PostToolUse durations are
|
|
24452
|
+
// summed from event logs (Phase 4d).
|
|
24453
|
+
var okRuns = last7.filter(function(rn) { return rn.status === 'ok' && typeof rn.durationMs === 'number'; });
|
|
24454
|
+
var avgDur = okRuns.length > 0
|
|
24455
|
+
? Math.round(okRuns.reduce(function(a, b) { return a + b.durationMs; }, 0) / okRuns.length)
|
|
24456
|
+
: 0;
|
|
24457
|
+
var latToolPct = 35, latModelPct = 55, latOverPct = 10;
|
|
24458
|
+
var splitHtml = '<div class="mini-split">'
|
|
24459
|
+
+ '<div class="mini-split-seg" style="background:#3b82f6;width:' + latModelPct + '%" title="Model API time (~' + latModelPct + '%)">' + (latModelPct >= 12 ? 'model' : '') + '</div>'
|
|
24460
|
+
+ '<div class="mini-split-seg" style="background:#8b5cf6;width:' + latToolPct + '%" title="Tool execution time (~' + latToolPct + '%)">' + (latToolPct >= 12 ? 'tools' : '') + '</div>'
|
|
24461
|
+
+ '<div class="mini-split-seg" style="background:#6b7280;width:' + latOverPct + '%" title="Framework overhead (~' + latOverPct + '%)">' + (latOverPct >= 12 ? 'overhead' : '') + '</div>'
|
|
24462
|
+
+ '</div>'
|
|
24463
|
+
+ '<div class="mini-split-legend">'
|
|
24464
|
+
+ '<span><span class="mini-split-legend-dot" style="background:#3b82f6"></span>model</span>'
|
|
24465
|
+
+ '<span><span class="mini-split-legend-dot" style="background:#8b5cf6"></span>tools</span>'
|
|
24466
|
+
+ '<span><span class="mini-split-legend-dot" style="background:#6b7280"></span>overhead</span>'
|
|
24467
|
+
+ '</div>';
|
|
24468
|
+
var latFigure = avgDur > 0 ? formatDurationMs(avgDur) : '—';
|
|
24469
|
+
var latSub = okRuns.length > 0 ? 'avg of ' + okRuns.length + ' successful runs · 7d' : 'no successful runs in 7d';
|
|
24470
|
+
|
|
24471
|
+
// ── Reliability card ───────────────────────────────────────────────
|
|
24472
|
+
// Per-day failure column, stacked by category. Categories use the same
|
|
24473
|
+
// colors as the run-list filter chips so users can match across surfaces.
|
|
24474
|
+
var perDayFails = []; // [{category: count}, ...]
|
|
24475
|
+
for (var di = 0; di < 7; di++) perDayFails.push({});
|
|
24476
|
+
var totalFails7 = 0;
|
|
24477
|
+
var failureKinds = ['error', 'timeout', 'lost'];
|
|
24478
|
+
for (var fi = 0; fi < last7.length; fi++) {
|
|
24479
|
+
var rn = last7[fi];
|
|
24480
|
+
if (failureKinds.indexOf(rn.status) === -1) continue;
|
|
24481
|
+
var failedMs = new Date(rn.startedAt).getTime();
|
|
24482
|
+
var didx = 6 - Math.floor((now - failedMs) / (24 * 60 * 60 * 1000));
|
|
24483
|
+
if (didx < 0 || didx > 6) continue;
|
|
24484
|
+
var cat = rn.failureCategory || 'tool_error';
|
|
24485
|
+
perDayFails[didx][cat] = (perDayFails[didx][cat] || 0) + 1;
|
|
24486
|
+
totalFails7++;
|
|
24487
|
+
}
|
|
24488
|
+
var maxDayFails = 0;
|
|
24489
|
+
for (var mfi = 0; mfi < perDayFails.length; mfi++) {
|
|
24490
|
+
var dayTotal = 0;
|
|
24491
|
+
for (var k in perDayFails[mfi]) dayTotal += perDayFails[mfi][k];
|
|
24492
|
+
if (dayTotal > maxDayFails) maxDayFails = dayTotal;
|
|
24493
|
+
}
|
|
24494
|
+
var failHtml;
|
|
24495
|
+
if (totalFails7 === 0) {
|
|
24496
|
+
failHtml = '<div class="mini-fails-empty">No failures in 7d 🎉</div>';
|
|
24497
|
+
} else {
|
|
24498
|
+
failHtml = '<div class="mini-fails">';
|
|
24499
|
+
for (var fd = 0; fd < perDayFails.length; fd++) {
|
|
24500
|
+
var dayBucket = perDayFails[fd];
|
|
24501
|
+
var dayTotal2 = 0;
|
|
24502
|
+
var keys = Object.keys(dayBucket).sort();
|
|
24503
|
+
for (var dk = 0; dk < keys.length; dk++) dayTotal2 += dayBucket[keys[dk]];
|
|
24504
|
+
var dayHeightPct = maxDayFails > 0 ? Math.round((dayTotal2 / maxDayFails) * 100) : 0;
|
|
24505
|
+
failHtml += '<div class="mini-fails-col" style="height:' + dayHeightPct + '%" title="' + dayLabels[fd] + ': ' + dayTotal2 + ' failure' + (dayTotal2 === 1 ? '' : 's') + '">';
|
|
24506
|
+
for (var ck = 0; ck < keys.length; ck++) {
|
|
24507
|
+
var catKey = keys[ck];
|
|
24508
|
+
var catSegPct = dayTotal2 > 0 ? Math.round((dayBucket[catKey] / dayTotal2) * 100) : 0;
|
|
24509
|
+
var color = (typeof _runListCategoryColor === 'function') ? _runListCategoryColor(catKey) : 'var(--red)';
|
|
24510
|
+
failHtml += '<div class="mini-fails-seg" style="height:' + catSegPct + '%;background:' + color + '" title="' + catKey + ': ' + dayBucket[catKey] + '"></div>';
|
|
24511
|
+
}
|
|
24512
|
+
failHtml += '</div>';
|
|
24513
|
+
}
|
|
24514
|
+
failHtml += '</div>';
|
|
24515
|
+
}
|
|
24516
|
+
|
|
24517
|
+
// ── Compose ────────────────────────────────────────────────────────
|
|
24518
|
+
host.innerHTML =
|
|
24519
|
+
'<div class="mini-card">'
|
|
24520
|
+
+ '<div class="mini-card-head"><span class="mini-card-title">Cost · 7d</span><span class="mini-card-figure">' + esc(costFigure) + '</span></div>'
|
|
24521
|
+
+ '<div class="mini-spark">' + costSparkHtml + '</div>'
|
|
24522
|
+
+ '<div class="mini-card-sub">' + (totalCost7 > 0 ? 'across ' + last7.filter(function(r){ return typeof r.totalCostUsd === "number"; }).length + ' priced runs' : 'no priced runs yet') + '</div>'
|
|
24523
|
+
+ '</div>'
|
|
24524
|
+
+ '<div class="mini-card">'
|
|
24525
|
+
+ '<div class="mini-card-head"><span class="mini-card-title">Latency · avg</span><span class="mini-card-figure">' + esc(latFigure) + '</span></div>'
|
|
24526
|
+
+ splitHtml
|
|
24527
|
+
+ '<div class="mini-card-sub">' + esc(latSub) + ' (split is heuristic; per-tool timing lands with hooks)</div>'
|
|
24528
|
+
+ '</div>'
|
|
24529
|
+
+ '<div class="mini-card">'
|
|
24530
|
+
+ '<div class="mini-card-head"><span class="mini-card-title">Reliability · 7d</span><span class="mini-card-figure">' + totalFails7 + ' fail' + (totalFails7 === 1 ? '' : 's') + '</span></div>'
|
|
24531
|
+
+ failHtml
|
|
24532
|
+
+ '<div class="mini-card-sub">click a column in the run list to filter by category</div>'
|
|
24533
|
+
+ '</div>';
|
|
24534
|
+
}
|
|
24535
|
+
|
|
24039
24536
|
// ── PRD Phase 3: Run list ──────────────────────────────────────────────
|
|
24040
24537
|
// Single sortable/filterable table of every CronRunEntry across all tasks.
|
|
24041
24538
|
// Filters: status, task name, time window. Browser-local saved views.
|
|
@@ -24164,7 +24661,7 @@ function renderRunListBody(allRuns) {
|
|
|
24164
24661
|
if (catOptions.length > 1) {
|
|
24165
24662
|
html += _runListChip('Category', catOptions, 'filterCategory');
|
|
24166
24663
|
}
|
|
24167
|
-
html += '<input type="search" placeholder="Filter by task name…" value="' + esc(_runListState.filterText) + '" oninput="onRunListSearch(this.value)" style="flex:1;min-width:200px;max-width:320px;padding:6px 10px;font-size:12px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary)">';
|
|
24664
|
+
html += '<input type="search" id="runlist-filter-text" placeholder="Filter by task name…" value="' + esc(_runListState.filterText) + '" oninput="onRunListSearch(this.value)" style="flex:1;min-width:200px;max-width:320px;padding:6px 10px;font-size:12px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary)">';
|
|
24168
24665
|
html += '<button class="btn-sm" onclick="resetRunListFilters()" style="font-size:11px">Reset to default</button>';
|
|
24169
24666
|
html += '</div>';
|
|
24170
24667
|
if (filtered.length === 0) {
|
|
@@ -24684,6 +25181,11 @@ async function refreshCron() {
|
|
|
24684
25181
|
// /api/cron/runs (already fetched alongside ops) feeds the metrics.
|
|
24685
25182
|
// Render an empty shell first; refreshHealthStrip fills it in.
|
|
24686
25183
|
var html = '<div id="health-strip" class="health-strip"></div>';
|
|
25184
|
+
// PRD §12 / 1.18.93: three mini-dashboards below the Health Strip —
|
|
25185
|
+
// Cost (7d sparkline), Latency split (model / tool / overhead),
|
|
25186
|
+
// Reliability (failures stacked by category). Filled in by
|
|
25187
|
+
// refreshMiniDashboards from the same /api/cron/runs payload.
|
|
25188
|
+
html += '<div id="mini-dashboards" class="mini-dashboards"></div>';
|
|
24687
25189
|
html += renderOperationsSummary(ops);
|
|
24688
25190
|
|
|
24689
25191
|
// ── Zone 1 — Running now (promoted to top, primary "what's live" view) ──
|
|
@@ -24738,6 +25240,9 @@ async function refreshCron() {
|
|
|
24738
25240
|
if (typeof refreshHealthStrip === 'function') {
|
|
24739
25241
|
refreshHealthStrip().catch(function() { /* non-fatal */ });
|
|
24740
25242
|
}
|
|
25243
|
+
if (typeof refreshMiniDashboards === 'function') {
|
|
25244
|
+
refreshMiniDashboards().catch(function() { /* non-fatal */ });
|
|
25245
|
+
}
|
|
24741
25246
|
panel.onclick = function(ev) {
|
|
24742
25247
|
var target = ev.target;
|
|
24743
25248
|
while (target && target.id !== 'panel-cron') {
|
|
@@ -24768,6 +25273,92 @@ function openRunOrTrace(jobName, runId) {
|
|
|
24768
25273
|
return openTraceViewer(jobName);
|
|
24769
25274
|
}
|
|
24770
25275
|
|
|
25276
|
+
// ── PRD §13 / 1.18.94: Replay tooling v1 ────────────────────────────────
|
|
25277
|
+
// Three small handlers wired to the buttons in the Run detail header.
|
|
25278
|
+
// They reuse existing endpoints — no new server-side surface. Future
|
|
25279
|
+
// versions add Rerun-from-step (needs SDK resume support) + Bulk replay.
|
|
25280
|
+
async function replayRerunRun(jobName) {
|
|
25281
|
+
if (!jobName) return;
|
|
25282
|
+
if (!confirm('Rerun "' + jobName + '" with the same prompt? This fires a new run immediately.')) return;
|
|
25283
|
+
try {
|
|
25284
|
+
var r = await apiFetch('/api/cron/run/' + encodeURIComponent(jobName), { method: 'POST' });
|
|
25285
|
+
var d = await r.json().catch(function() { return {}; });
|
|
25286
|
+
if (!r.ok) {
|
|
25287
|
+
// 409 = the task is already running (concurrency lock); explain that.
|
|
25288
|
+
if (r.status === 409) {
|
|
25289
|
+
toast(d.error || (jobName + ' is already running. Cancel the in-flight run first.'), 'error');
|
|
25290
|
+
} else {
|
|
25291
|
+
toast(d.error || ('Rerun failed (HTTP ' + r.status + ')'), 'error');
|
|
25292
|
+
}
|
|
25293
|
+
return;
|
|
25294
|
+
}
|
|
25295
|
+
toast('Rerunning ' + jobName + ' — watch the run list for the new entry.', 'success');
|
|
25296
|
+
// Don't auto-close the modal — the user just confirmed they want to
|
|
25297
|
+
// see the result. The new run will appear in the run-selector dropdown
|
|
25298
|
+
// (loaded by openTraceViewer's history fetch) within a few seconds.
|
|
25299
|
+
setTimeout(function() {
|
|
25300
|
+
if (typeof refreshCron === 'function') refreshCron();
|
|
25301
|
+
if (typeof refreshRunList === 'function') refreshRunList();
|
|
25302
|
+
}, 1200);
|
|
25303
|
+
} catch (e) { toast('Rerun failed: ' + e, 'error'); }
|
|
25304
|
+
}
|
|
25305
|
+
|
|
25306
|
+
async function replayCopyPrompt(jobName) {
|
|
25307
|
+
if (!jobName) return;
|
|
25308
|
+
try {
|
|
25309
|
+
// /api/cron returns the full job list with prompt fields — pull the
|
|
25310
|
+
// matching one and stuff it onto the clipboard. Cheap and avoids a
|
|
25311
|
+
// dedicated GET-by-name endpoint.
|
|
25312
|
+
var r = await apiFetch('/api/cron');
|
|
25313
|
+
var d = await r.json();
|
|
25314
|
+
var jobs = (d && d.jobs) || [];
|
|
25315
|
+
var found = null;
|
|
25316
|
+
for (var i = 0; i < jobs.length; i++) {
|
|
25317
|
+
if (String(jobs[i].name).toLowerCase() === String(jobName).toLowerCase()) { found = jobs[i]; break; }
|
|
25318
|
+
}
|
|
25319
|
+
if (!found || !found.prompt) {
|
|
25320
|
+
toast('No prompt found for ' + jobName, 'error');
|
|
25321
|
+
return;
|
|
25322
|
+
}
|
|
25323
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
25324
|
+
await navigator.clipboard.writeText(found.prompt);
|
|
25325
|
+
toast('Prompt copied to clipboard (' + found.prompt.length + ' chars).', 'success');
|
|
25326
|
+
} else {
|
|
25327
|
+
// Fallback for clipboard-less environments (rare in modern browsers,
|
|
25328
|
+
// but the dashboard runs over plain HTTP on localhost which can hit
|
|
25329
|
+
// the secure-context restriction in some setups).
|
|
25330
|
+
var ta = document.createElement('textarea');
|
|
25331
|
+
ta.value = found.prompt;
|
|
25332
|
+
document.body.appendChild(ta);
|
|
25333
|
+
ta.select();
|
|
25334
|
+
try { document.execCommand('copy'); toast('Prompt copied (legacy mode).', 'success'); }
|
|
25335
|
+
catch (e) { toast('Copy not supported in this browser. Open the editor instead.', 'error'); }
|
|
25336
|
+
document.body.removeChild(ta);
|
|
25337
|
+
}
|
|
25338
|
+
} catch (e) { toast('Copy prompt failed: ' + e, 'error'); }
|
|
25339
|
+
}
|
|
25340
|
+
|
|
25341
|
+
function replayOpenRunList(jobName) {
|
|
25342
|
+
if (!jobName) return;
|
|
25343
|
+
// Close the trace modal, switch to Run list tab, set the text filter to
|
|
25344
|
+
// the task name. The Run list already supports free-text task-name match.
|
|
25345
|
+
try { document.getElementById('trace-modal').classList.remove('show'); } catch (e) { /* ignore */ }
|
|
25346
|
+
// The Tasks page has three top-level Build tabs; the Run list lives on
|
|
25347
|
+
// tab #3. Switch via the existing tab handler if available; otherwise
|
|
25348
|
+
// fall back to setting the URL hash so the page handler picks it up.
|
|
25349
|
+
if (typeof switchBuildTab === 'function') {
|
|
25350
|
+
switchBuildTab('runs');
|
|
25351
|
+
}
|
|
25352
|
+
if (typeof _runListState !== 'undefined') {
|
|
25353
|
+
_runListState.filterText = jobName;
|
|
25354
|
+
if (typeof refreshRunList === 'function') refreshRunList();
|
|
25355
|
+
// Mirror filter into the visible input so users see why the list is filtered.
|
|
25356
|
+
var inp = document.getElementById('runlist-filter-text');
|
|
25357
|
+
if (inp) inp.value = jobName;
|
|
25358
|
+
}
|
|
25359
|
+
toast('Filtered run list to "' + jobName + '"', 'success');
|
|
25360
|
+
}
|
|
25361
|
+
|
|
24771
25362
|
// PRD Phase 4b / 1.18.86: Run detail viewer. Renders a waterfall of
|
|
24772
25363
|
// RunEvent rows from /api/runs/:runId/events. Color-coded by kind, paired
|
|
24773
25364
|
// tool_call→tool_result by toolUseId, with expandable per-span content.
|
|
@@ -24845,6 +25436,19 @@ function renderRunDetailWaterfall(events, runId, jobName) {
|
|
|
24845
25436
|
+ '<span style="flex:1"></span>'
|
|
24846
25437
|
+ '<code style="font-size:10px;color:var(--text-muted)">runId ' + esc(String(runId).slice(0, 12)) + '…</code>'
|
|
24847
25438
|
+ '</div>'
|
|
25439
|
+
// PRD §13 / 1.18.94 — Replay tooling v1. Three actions reachable from
|
|
25440
|
+
// every Run detail: rerun the same task with the same prompt (kicks off
|
|
25441
|
+
// a new run via the existing /api/cron/run/:job endpoint with
|
|
25442
|
+
// trigger='manual'), copy the prompt to clipboard for quick edits in
|
|
25443
|
+
// the editor, and jump to the Run list filtered to this task name.
|
|
25444
|
+
// Only rendered when we know the jobName (not for orphaned runs).
|
|
25445
|
+
+ (jobName
|
|
25446
|
+
? '<div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap">'
|
|
25447
|
+
+ '<button class="btn-sm btn-success" onclick="replayRerunRun(\\x27' + jsStr(jobName) + '\\x27)" title="Fire this task once — same prompt, fresh run">▶ Rerun task</button>'
|
|
25448
|
+
+ '<button class="btn-sm" onclick="replayCopyPrompt(\\x27' + jsStr(jobName) + '\\x27)" title="Copy the prompt for this task to your clipboard">⧉ Copy prompt</button>'
|
|
25449
|
+
+ '<button class="btn-sm" onclick="replayOpenRunList(\\x27' + jsStr(jobName) + '\\x27)" title="Open the Run list filtered to this task">↗ Open run list</button>'
|
|
25450
|
+
+ '</div>'
|
|
25451
|
+
: '')
|
|
24848
25452
|
+ '</div>';
|
|
24849
25453
|
|
|
24850
25454
|
// Waterfall rows
|
|
@@ -32907,6 +33511,9 @@ async function refreshTeam() {
|
|
|
32907
33511
|
'<div class="office-hero-stat"><div class="stat-val">' + fmtTokens(clemTokenTotal) + '</div><div class="stat-lbl">Tokens</div></div>' +
|
|
32908
33512
|
'<div class="office-hero-stat"><div class="stat-val">' + (clem.crons ? clem.crons.total : 0) + '</div><div class="stat-lbl">Cron Jobs</div></div>' +
|
|
32909
33513
|
'</div>' +
|
|
33514
|
+
'<div class="office-hero-actions" style="margin-left:auto;display:flex;gap:6px">' +
|
|
33515
|
+
'<button class="btn btn-sm" onclick="editClementine()" title="Configure Clementine\\x27s persona, tools, and limits">Edit</button>' +
|
|
33516
|
+
'</div>' +
|
|
32910
33517
|
'</div>' +
|
|
32911
33518
|
(needsDiscordSetup ?
|
|
32912
33519
|
'<div class="discord-setup-banner" id="discord-setup-banner">' +
|
|
@@ -34,9 +34,9 @@ export const clementineJsonSchema = z.object({
|
|
|
34
34
|
autonomy: z.enum(['ask_first', 'balanced', 'act_when_safe']).optional(),
|
|
35
35
|
/**
|
|
36
36
|
* Dashboard-managed profile for the main agent (Clementine herself).
|
|
37
|
-
* Mirrors the per-agent edit surface so Tasks → Team can edit
|
|
38
|
-
*
|
|
39
|
-
* Every field is optional
|
|
37
|
+
* Mirrors the per-agent edit surface so Tasks → Team can edit her
|
|
38
|
+
* persona, tools, and limits without forcing users into Settings or
|
|
39
|
+
* env files. Every field is optional — absent fields fall through to
|
|
40
40
|
* env / compiled defaults via computeEffectiveConfig.
|
|
41
41
|
*/
|
|
42
42
|
profile: z.object({
|