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);
@@ -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:520px">
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()">&times;</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
- <div style="margin-bottom:12px">
19788
- <label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Name *</label>
19789
- <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">
19790
- </div>
19791
- <div style="margin-bottom:12px">
19792
- <label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Role Description *</label>
19793
- <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">
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
- <div style="margin-bottom:12px">
19796
- <label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Profile Photo URL</label>
19797
- <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">
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 &amp; 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
- <div style="margin-bottom:12px">
19800
- <label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Onboarding Brief</label>
19801
- <textarea id="agent-personality" rows="4" style="width:100%;padding:8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);resize:vertical" placeholder="You are a Research Agent specializing in..."></textarea>
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 &nbsp;
20169
+ <span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#f59e0b;vertical-align:middle;margin-right:4px"></span>Needs setup &nbsp;
20170
+ <span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#888;vertical-align:middle;margin-right:4px"></span>Not installed &nbsp;
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
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px">
19804
- <div>
19805
- <label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Channel</label>
19806
- <div id="agent-channel-list" style="max-height:140px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:6px 8px;background:var(--bg-input)">
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">Model</label>
19820
- <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)">
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
- </details>
19871
- <div style="margin-bottom:12px">
19872
- <label style="display:block;color:var(--text-muted);font-size:12px;margin-bottom:4px">Team Connections (comma-separated slugs)</label>
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
- </div>
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&apos;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
- <div style="display:flex;gap:8px;justify-content:flex-end">
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 the
38
- * primary persona without forcing users into Settings or env files.
39
- * Every field is optional; absent fields fall through to the existing
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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.92",
3
+ "version": "1.18.95",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",