clementine-agent 1.18.81 → 1.18.83

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.
@@ -100,6 +100,21 @@ export declare class PersonalAssistant {
100
100
  }>;
101
101
  updatedAt: string;
102
102
  };
103
+ /**
104
+ * PRD Phase 2.1: clear the cached status for one server so the next query
105
+ * repopulates it from a fresh handshake. The SDK manages connections
106
+ * internally; we don't have a direct "reconnect now" hook, but invalidating
107
+ * the cached entry tells the dashboard to render 'pending' and resets any
108
+ * stale error/auth state. Returns the post-clear cached snapshot.
109
+ */
110
+ invalidateMcpStatus(serverName: string): {
111
+ servers: Array<{
112
+ name: string;
113
+ status: string;
114
+ }>;
115
+ updatedAt: string;
116
+ cleared: boolean;
117
+ };
103
118
  /** Inject a background work result into the session as silent follow-up context. */
104
119
  injectPendingContext(sessionKey: string, userPrompt: string, result: string): void;
105
120
  private initMemoryStore;
@@ -810,6 +810,21 @@ export class PersonalAssistant {
810
810
  getMcpStatus() {
811
811
  return { servers: this._lastMcpStatus, updatedAt: this._lastMcpStatusTime };
812
812
  }
813
+ /**
814
+ * PRD Phase 2.1: clear the cached status for one server so the next query
815
+ * repopulates it from a fresh handshake. The SDK manages connections
816
+ * internally; we don't have a direct "reconnect now" hook, but invalidating
817
+ * the cached entry tells the dashboard to render 'pending' and resets any
818
+ * stale error/auth state. Returns the post-clear cached snapshot.
819
+ */
820
+ invalidateMcpStatus(serverName) {
821
+ const beforeLen = this._lastMcpStatus.length;
822
+ this._lastMcpStatus = this._lastMcpStatus.filter((s) => s.name !== serverName);
823
+ const cleared = this._lastMcpStatus.length < beforeLen;
824
+ if (cleared)
825
+ this._lastMcpStatusTime = new Date().toISOString();
826
+ return { servers: this._lastMcpStatus, updatedAt: this._lastMcpStatusTime, cleared };
827
+ }
813
828
  /** Inject a background work result into the session as silent follow-up context. */
814
829
  injectPendingContext(sessionKey, userPrompt, result) {
815
830
  const pending = this.pendingContext.get(sessionKey) ?? [];
@@ -9266,6 +9266,31 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
9266
9266
  app.get('/api/mcp-status', gwHandler(async (gw, _req, res) => {
9267
9267
  res.json(gw.getMcpStatus());
9268
9268
  }));
9269
+ // PRD Phase 2.1: Reconnect a single MCP server. Clears the cached status so
9270
+ // the next query handshake repopulates it. The SDK doesn't expose a direct
9271
+ // reconnect call, so this is the closest equivalent: kick the cache.
9272
+ app.post('/api/mcp-servers/:name/reconnect', gwHandler(async (gw, req, res) => {
9273
+ const rawName = req.params.name;
9274
+ const name = Array.isArray(rawName) ? rawName[0] : rawName;
9275
+ if (!name) {
9276
+ res.status(400).json({ ok: false, error: 'name required' });
9277
+ return;
9278
+ }
9279
+ try {
9280
+ const result = gw.invalidateMcpStatus(String(name));
9281
+ res.json({
9282
+ ok: true,
9283
+ cleared: result.cleared,
9284
+ message: result.cleared
9285
+ ? `Reconnect queued for "${name}" — status will refresh on the next query.`
9286
+ : `"${name}" had no cached status to clear; next query will populate it.`,
9287
+ status: result,
9288
+ });
9289
+ }
9290
+ catch (err) {
9291
+ res.status(500).json({ ok: false, error: String(err) });
9292
+ }
9293
+ }));
9269
9294
  // ── Self-Improvement API ─────────────────────────────────────────
9270
9295
  // ── MCP Server Management API ───────────────────────────────────────
9271
9296
  app.get('/api/mcp-servers', (_req, res) => {
@@ -16364,6 +16389,9 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
16364
16389
  <button class="build-tab-btn active" data-build-tab="crons" onclick="switchBuildTab('crons')" style="padding:8px 14px;border-radius:6px 6px 0 0;border:none;background:transparent;color:var(--text-primary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent">
16365
16390
  <span style="margin-right:6px">📅</span>Tasks <span id="build-tab-cron-count" style="display:none;margin-left:4px;font-size:10px;background:var(--bg-tertiary);padding:1px 6px;border-radius:999px;color:var(--text-muted)">0</span>
16366
16391
  </button>
16392
+ <button class="build-tab-btn" data-build-tab="runs" onclick="switchBuildTab('runs')" style="padding:8px 14px;border-radius:6px 6px 0 0;border:none;background:transparent;color:var(--text-secondary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent">
16393
+ <span style="margin-right:6px">🕒</span>Runs <span id="build-tab-runs-count" style="display:none;margin-left:4px;font-size:10px;background:var(--bg-tertiary);padding:1px 6px;border-radius:999px;color:var(--text-muted)">0</span>
16394
+ </button>
16367
16395
  <button class="build-tab-btn" data-build-tab="toolsmcp" onclick="switchBuildTab('toolsmcp')" style="padding:8px 14px;border-radius:6px 6px 0 0;border:none;background:transparent;color:var(--text-secondary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent">
16368
16396
  <span style="margin-right:6px">🧰</span>Tools &amp; MCP <span id="build-tab-toolsmcp-count" style="display:none;margin-left:4px;font-size:10px;background:var(--bg-tertiary);padding:1px 6px;border-radius:999px;color:var(--text-muted)">0</span>
16369
16397
  </button>
@@ -16379,6 +16407,12 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
16379
16407
  <div id="build-tab-crons" style="display:none;flex:1;min-height:0;overflow-y:auto;padding:18px;background:var(--bg-primary)">
16380
16408
  <div id="panel-cron"><div class="empty-state" style="padding:24px;color:var(--text-muted)">Loading scheduled tasks…</div></div>
16381
16409
  </div>
16410
+ <!-- ── PRD Phase 3: Run list ───────────────────────────────────────────
16411
+ Single table of every run across all tasks, with filters + saved
16412
+ views. Default view is "Failures (last 24h)". -->
16413
+ <div id="build-tab-runs" style="display:none;flex:1;min-height:0;overflow-y:auto;padding:18px;background:var(--bg-primary)">
16414
+ <div id="panel-runs"><div class="empty-state" style="padding:24px;color:var(--text-muted)">Loading run history…</div></div>
16415
+ </div>
16382
16416
  <!-- ── PRD Phase 2: Tools & MCP catalog ────────────────────────────────
16383
16417
  Read-only foundation in 1.18.81. Future slices: per-tool bindings,
16384
16418
  Reconnect/Toggle/Edit actions, Approval Mode + Max-auto-runs config. -->
@@ -20319,6 +20353,77 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
20319
20353
 
20320
20354
  <!-- (legacy standalone Preview modal removed in 1.18.70 — preview now lives as a tab inside the cron modal) -->
20321
20355
 
20356
+ <!-- ═══ MCP Server Edit Modal — PRD Phase 2.1 ═══ -->
20357
+ <div class="modal-overlay" id="mcp-edit-modal">
20358
+ <div class="modal" style="max-width:640px;width:96vw">
20359
+ <div class="modal-header">
20360
+ <h3 id="mcp-edit-title">Edit MCP Server</h3>
20361
+ <button class="btn-ghost btn-sm" onclick="closeMcpServerEditModal()">&times;</button>
20362
+ </div>
20363
+ <div class="modal-body" style="padding:18px">
20364
+ <div id="mcp-edit-readonly-note" style="display:none;margin-bottom:14px;padding:10px 12px;border-radius:6px;background:rgba(245,158,11,0.10);border:1px solid rgba(245,158,11,0.30);color:var(--yellow);font-size:12px">
20365
+ ⚠ Auto-detected server. Edits to this config aren't persisted by Clementine — change it in the source file (Claude Desktop config, Claude Code settings, or extensions). Use the toggle on the catalog card to enable/disable.
20366
+ </div>
20367
+ <div class="form-group">
20368
+ <label class="form-label">Name</label>
20369
+ <input type="text" id="mcp-edit-name">
20370
+ <div class="form-hint">Identifier — cannot be renamed via this dialog.</div>
20371
+ </div>
20372
+ <div class="form-row">
20373
+ <div class="form-group">
20374
+ <label class="form-label">Transport</label>
20375
+ <select id="mcp-edit-type" onchange="syncMcpEditTransportRows()">
20376
+ <option value="stdio">stdio (local process)</option>
20377
+ <option value="http">http</option>
20378
+ <option value="sse">sse</option>
20379
+ </select>
20380
+ </div>
20381
+ <div class="form-group">
20382
+ <label class="form-label" style="display:flex;align-items:center;gap:8px">
20383
+ <input type="checkbox" id="mcp-edit-enabled"> Enabled
20384
+ </label>
20385
+ <div class="form-hint">Disable to remove this server from every task without deleting it.</div>
20386
+ </div>
20387
+ </div>
20388
+ <div class="form-group">
20389
+ <label class="form-label">Description</label>
20390
+ <input type="text" id="mcp-edit-description" placeholder="What this server does">
20391
+ </div>
20392
+ <!-- stdio-only fields -->
20393
+ <div id="mcp-edit-stdio-rows">
20394
+ <div class="form-group">
20395
+ <label class="form-label">Command</label>
20396
+ <input type="text" id="mcp-edit-command" placeholder="e.g. npx, python, ./bin/server">
20397
+ </div>
20398
+ <div class="form-group">
20399
+ <label class="form-label">Args <span style="color:var(--text-muted);font-weight:normal">(one per line)</span></label>
20400
+ <textarea id="mcp-edit-args" rows="3" placeholder="--port&#10;3001" style="font-family:'JetBrains Mono',monospace;font-size:11px"></textarea>
20401
+ </div>
20402
+ <div class="form-group">
20403
+ <label class="form-label">Env <span style="color:var(--text-muted);font-weight:normal">(JSON object, optional)</span></label>
20404
+ <textarea id="mcp-edit-env" rows="3" placeholder='{ "API_KEY": "..." }' style="font-family:'JetBrains Mono',monospace;font-size:11px"></textarea>
20405
+ </div>
20406
+ </div>
20407
+ <!-- http/sse fields -->
20408
+ <div id="mcp-edit-http-rows" style="display:none">
20409
+ <div class="form-group">
20410
+ <label class="form-label">URL</label>
20411
+ <input type="text" id="mcp-edit-url" placeholder="https://example.com/mcp">
20412
+ </div>
20413
+ <div class="form-group">
20414
+ <label class="form-label">Headers <span style="color:var(--text-muted);font-weight:normal">(JSON object, optional)</span></label>
20415
+ <textarea id="mcp-edit-headers" rows="3" placeholder='{ "Authorization": "Bearer ..." }' style="font-family:'JetBrains Mono',monospace;font-size:11px"></textarea>
20416
+ </div>
20417
+ </div>
20418
+ </div>
20419
+ <div class="modal-footer">
20420
+ <span style="flex:1"></span>
20421
+ <button onclick="closeMcpServerEditModal()">Close</button>
20422
+ <button class="btn-primary" id="mcp-edit-save" onclick="saveMcpServerEdit()">Save</button>
20423
+ </div>
20424
+ </div>
20425
+ </div>
20426
+
20322
20427
  <!-- ═══ Goal Modal ═══ -->
20323
20428
  <div class="modal-overlay" id="goal-modal">
20324
20429
  <div class="modal" style="width:520px">
@@ -21165,8 +21270,10 @@ function switchBuildTab(tab) {
21165
21270
  // Always close any open workflow when changing tabs — switching context
21166
21271
  // is a clean slate, not a stale node hanging on the canvas.
21167
21272
  if (typeof closeBuilderCanvas === 'function') closeBuilderCanvas();
21168
- // Default: hide the Tools & MCP pane unless we're explicitly on it.
21273
+ var runsPane = document.getElementById('build-tab-runs');
21274
+ // Default: hide the Tools & MCP + Runs panes unless we're explicitly on them.
21169
21275
  if (toolsmcpPane && tab !== 'toolsmcp') toolsmcpPane.style.display = 'none';
21276
+ if (runsPane && tab !== 'runs') runsPane.style.display = 'none';
21170
21277
  if (tab === 'toolsmcp') {
21171
21278
  // PRD Phase 2: Tools & MCP catalog. Read-only foundation in 1.18.81.
21172
21279
  if (workPane) workPane.style.display = 'none';
@@ -21179,6 +21286,18 @@ function switchBuildTab(tab) {
21179
21286
  if (typeof refreshToolsMcpCatalog === 'function') refreshToolsMcpCatalog();
21180
21287
  return;
21181
21288
  }
21289
+ if (tab === 'runs') {
21290
+ // PRD Phase 3: Run list — every run across every task.
21291
+ if (workPane) workPane.style.display = 'none';
21292
+ if (cronPane) cronPane.style.display = 'none';
21293
+ if (tplPane) tplPane.style.display = 'none';
21294
+ if (runsPane) runsPane.style.display = 'block';
21295
+ if (headerStrip) headerStrip.style.display = 'none';
21296
+ if (usagePanel) usagePanel.style.display = 'none';
21297
+ if (newBtn) newBtn.style.display = 'none';
21298
+ if (typeof refreshRunList === 'function') refreshRunList();
21299
+ return;
21300
+ }
21182
21301
  if (tab === 'templates') {
21183
21302
  if (workPane) workPane.style.display = 'none';
21184
21303
  if (cronPane) cronPane.style.display = 'none';
@@ -23557,6 +23676,245 @@ function renderRunningCard(item) {
23557
23676
  + '</div></div>';
23558
23677
  }
23559
23678
 
23679
+ // ── PRD Phase 3: Run list ──────────────────────────────────────────────
23680
+ // Single sortable/filterable table of every CronRunEntry across all tasks.
23681
+ // Filters: status, task name, time window. Browser-local saved views.
23682
+ // Default view: "Failures (last 24h)". No new endpoints — reuses
23683
+ // /api/cron/runs (CronRunLog.readAllRecent).
23684
+
23685
+ var _runListState = {
23686
+ filterStatus: 'all', // 'all' | 'failed' | 'ok'
23687
+ filterWindow: '24h', // '24h' | '7d' | 'all'
23688
+ filterText: '', // free-text task name match
23689
+ data: [], // raw runs from /api/cron/runs
23690
+ };
23691
+
23692
+ function _runListLoadDefaultView() {
23693
+ // First-time visit: PRD §5.3 — default Saved View is "Failures (last 24h)".
23694
+ try {
23695
+ var raw = localStorage.getItem('runListView');
23696
+ if (raw) {
23697
+ var saved = JSON.parse(raw);
23698
+ _runListState.filterStatus = saved.filterStatus || 'all';
23699
+ _runListState.filterWindow = saved.filterWindow || '24h';
23700
+ _runListState.filterText = saved.filterText || '';
23701
+ return;
23702
+ }
23703
+ } catch (e) { /* ignore */ }
23704
+ // Default: failures, last 24h.
23705
+ _runListState.filterStatus = 'failed';
23706
+ _runListState.filterWindow = '24h';
23707
+ _runListState.filterText = '';
23708
+ }
23709
+
23710
+ function _runListSaveView() {
23711
+ try {
23712
+ localStorage.setItem('runListView', JSON.stringify({
23713
+ filterStatus: _runListState.filterStatus,
23714
+ filterWindow: _runListState.filterWindow,
23715
+ filterText: _runListState.filterText,
23716
+ }));
23717
+ } catch (e) { /* ignore */ }
23718
+ }
23719
+
23720
+ function _runListApplyFilters(runs) {
23721
+ var now = Date.now();
23722
+ var windowMs = _runListState.filterWindow === '24h' ? 24 * 60 * 60 * 1000
23723
+ : _runListState.filterWindow === '7d' ? 7 * 24 * 60 * 60 * 1000
23724
+ : Infinity;
23725
+ var query = (_runListState.filterText || '').trim().toLowerCase();
23726
+ return runs.filter(function(r) {
23727
+ if (_runListState.filterStatus === 'failed') {
23728
+ if (r.status !== 'error' && r.status !== 'timeout' && r.status !== 'lost') return false;
23729
+ } else if (_runListState.filterStatus === 'ok') {
23730
+ if (r.status !== 'ok') return false;
23731
+ }
23732
+ if (query && String(r.jobName || '').toLowerCase().indexOf(query) === -1) return false;
23733
+ if (windowMs !== Infinity && r.startedAt) {
23734
+ var age = now - new Date(r.startedAt).getTime();
23735
+ if (age > windowMs) return false;
23736
+ }
23737
+ return true;
23738
+ });
23739
+ }
23740
+
23741
+ async function refreshRunList() {
23742
+ var panel = document.getElementById('panel-runs');
23743
+ if (!panel) return;
23744
+ if (!_runListState.data.length) {
23745
+ _runListLoadDefaultView();
23746
+ }
23747
+ panel.innerHTML = '<div class="empty-state" style="padding:24px;color:var(--text-muted)">Loading run history…</div>';
23748
+ try {
23749
+ var r = await apiFetch('/api/cron/runs?limit=200');
23750
+ var d = await r.json();
23751
+ _runListState.data = (d && d.runs) || [];
23752
+ } catch (e) {
23753
+ panel.innerHTML = '<div class="empty-state" style="padding:24px;color:var(--red)">Failed to load runs: ' + esc(String(e)) + '</div>';
23754
+ return;
23755
+ }
23756
+ panel.innerHTML = renderRunListBody(_runListState.data);
23757
+ // Update tab count badge with total runs (not filtered count — that's
23758
+ // shown alongside the filter chips).
23759
+ var tabCount = document.getElementById('build-tab-runs-count');
23760
+ if (tabCount) {
23761
+ tabCount.textContent = _runListState.data.length;
23762
+ tabCount.style.display = _runListState.data.length > 0 ? '' : 'none';
23763
+ }
23764
+ }
23765
+
23766
+ function renderRunListBody(allRuns) {
23767
+ var filtered = _runListApplyFilters(allRuns);
23768
+ var html = '';
23769
+ // Header
23770
+ html += '<div style="margin-bottom:18px"><h2 style="margin:0 0 4px;font-size:18px;font-weight:600;color:var(--text-primary)">Runs</h2>'
23771
+ + '<div style="font-size:12px;color:var(--text-muted)">'+ filtered.length +' of '+ allRuns.length +' total runs · default view: <strong>Failures (last 24h)</strong></div></div>';
23772
+ // Filter row — saved automatically to localStorage on change.
23773
+ html += '<div style="display:flex;gap:10px;align-items:center;margin-bottom:14px;flex-wrap:wrap">';
23774
+ html += _runListChip('Status', [
23775
+ { value: 'all', label: 'All' },
23776
+ { value: 'ok', label: 'OK' },
23777
+ { value: 'failed', label: 'Failed' },
23778
+ ], 'filterStatus');
23779
+ html += _runListChip('Window', [
23780
+ { value: '24h', label: 'Last 24h' },
23781
+ { value: '7d', label: 'Last 7 days' },
23782
+ { value: 'all', label: 'All time' },
23783
+ ], 'filterWindow');
23784
+ 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)">';
23785
+ html += '<button class="btn-sm" onclick="resetRunListFilters()" style="font-size:11px">Reset to default</button>';
23786
+ html += '</div>';
23787
+ if (filtered.length === 0) {
23788
+ html += '<div class="empty-state" style="padding:36px 24px;text-align:center;color:var(--text-muted)"><div style="font-size:14px;margin-bottom:6px">No runs match the current filter.</div><div style="font-size:12px">Try widening the time window or clearing the task-name filter.</div></div>';
23789
+ return html;
23790
+ }
23791
+ // Table — same shape as the Recent History list on the Tasks page,
23792
+ // but sortable and with a Trigger column.
23793
+ html += '<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius)">';
23794
+ html += '<div style="display:grid;grid-template-columns:24px 24px minmax(180px,1.2fr) 90px minmax(180px,1fr) 90px auto;gap:10px;padding:8px 14px;border-bottom:1px solid var(--border);font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;font-weight:500">'
23795
+ + '<div></div><div title="Goal verdict">Goal</div><div>Task</div><div>Trigger</div><div>Started</div><div>Duration</div><div></div>'
23796
+ + '</div>';
23797
+ for (var i = 0; i < filtered.length; i++) {
23798
+ var entry = filtered[i] || {};
23799
+ var status = entry.status || 'unknown';
23800
+ var statusColor, statusIcon;
23801
+ if (status === 'ok') { statusColor = 'var(--green)'; statusIcon = '&#10003;'; }
23802
+ else if (status === 'error') { statusColor = 'var(--red)'; statusIcon = '&#10007;'; }
23803
+ else if (status === 'retried') { statusColor = 'var(--yellow)'; statusIcon = '&#8635;'; }
23804
+ else if (status === 'timeout') { statusColor = 'var(--yellow)'; statusIcon = '&#9203;'; }
23805
+ else if (status === 'lost') { statusColor = 'var(--red)'; statusIcon = '?'; }
23806
+ else if (status === 'running') { statusColor = 'var(--accent)'; statusIcon = '●'; }
23807
+ else if (status === 'skipped') { statusColor = 'var(--text-muted)'; statusIcon = '&minus;'; }
23808
+ else { statusColor = 'var(--text-muted)'; statusIcon = '&middot;'; }
23809
+ var jobName = entry.jobName || '(unknown)';
23810
+ var safeName = jsStr(jobName);
23811
+ var startedAt = entry.startedAt ? new Date(entry.startedAt) : null;
23812
+ var startedLabel = startedAt ? startedAt.toLocaleString() : '—';
23813
+ var durationLabel = entry.durationMs != null ? formatDurationMs(entry.durationMs) : '—';
23814
+ // Trigger heuristic until we persist it for real (Phase 3.1):
23815
+ // 'unleashed' mode + manual cron run = manual; otherwise scheduled.
23816
+ // The cron-running.json sidecar already carries pid which can hint at
23817
+ // manual but isn't on terminal entries. Best-effort label only.
23818
+ var triggerLabel = entry.attempt > 1 ? 'retry' : 'scheduled';
23819
+ var triggerColor = 'var(--text-muted)';
23820
+ // Goal cell
23821
+ var goalCell = '<div></div>';
23822
+ if (entry.goalCheck) {
23823
+ var gc = entry.goalCheck;
23824
+ var gIcon = gc.status === 'pass' ? '🎯' : gc.status === 'fail' ? '✗' : gc.status === 'error' ? '⚠' : '';
23825
+ var gColor = gc.status === 'pass' ? 'var(--green)' : gc.status === 'fail' ? 'var(--red)' : 'var(--yellow)';
23826
+ var gTip = gc.evaluatorReason || (Array.isArray(gc.schemaErrors) ? gc.schemaErrors.join('; ') : gc.status);
23827
+ goalCell = '<div style="color:' + gColor + ';font-size:13px;line-height:18px;text-align:center" title="' + esc(gTip) + '">' + gIcon + '</div>';
23828
+ }
23829
+ var preview = '';
23830
+ if (status === 'error' && entry.error) {
23831
+ preview = '<div style="font-size:11px;color:var(--red);margin-top:2px;word-break:break-word">' + esc(String(entry.error).slice(0, 140)) + '</div>';
23832
+ } else if (entry.outputPreview) {
23833
+ preview = '<div style="font-size:11px;color:var(--text-muted);margin-top:2px;word-break:break-word">' + esc(String(entry.outputPreview).slice(0, 120)) + '</div>';
23834
+ }
23835
+ html += '<div class="history-row" data-trace-job="' + esc(jobName) + '" style="display:grid;grid-template-columns:24px 24px minmax(180px,1.2fr) 90px minmax(180px,1fr) 90px auto;gap:10px;align-items:start;padding:8px 14px;border-bottom:1px solid var(--border);cursor:pointer">'
23836
+ + '<div style="color:' + statusColor + ';font-size:14px;line-height:18px;text-align:center" title="' + esc(status) + '">' + statusIcon + '</div>'
23837
+ + goalCell
23838
+ + '<div style="min-width:0">'
23839
+ + '<div style="font-weight:500;color:var(--text-primary);font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(jobName) + '">' + esc(jobName) + (entry.attempt > 1 ? ' · attempt ' + esc(entry.attempt) : '') + '</div>'
23840
+ + preview
23841
+ + '</div>'
23842
+ + '<div style="font-size:11px;color:' + triggerColor + ';line-height:18px">' + esc(triggerLabel) + '</div>'
23843
+ + '<div style="font-size:12px;color:var(--text-secondary);line-height:18px">' + esc(startedLabel) + '</div>'
23844
+ + '<div style="font-size:12px;color:var(--text-muted);line-height:18px">' + esc(durationLabel) + '</div>'
23845
+ + '<div style="display:flex;gap:6px;align-items:center"><button class="btn-sm" onclick="event.stopPropagation();openTraceViewer(\\x27' + safeName + '\\x27)" style="font-size:11px;padding:3px 8px">Trace</button></div>'
23846
+ + '</div>';
23847
+ }
23848
+ html += '</div>';
23849
+ return html;
23850
+ }
23851
+
23852
+ function _runListChip(label, options, stateKey) {
23853
+ var current = _runListState[stateKey];
23854
+ var html = '<span style="display:inline-flex;align-items:center;gap:4px">';
23855
+ html += '<span style="font-size:11px;color:var(--text-muted);font-weight:500;text-transform:uppercase;letter-spacing:0.04em;margin-right:2px">' + esc(label) + '</span>';
23856
+ for (var i = 0; i < options.length; i++) {
23857
+ var o = options[i];
23858
+ var active = o.value === current;
23859
+ var bg = active ? 'var(--accent)' : 'var(--bg-secondary)';
23860
+ var fg = active ? '#fff' : 'var(--text-primary)';
23861
+ html += '<button class="btn-sm" onclick="onRunListChipClick(\\x27' + jsStr(stateKey) + '\\x27,\\x27' + jsStr(o.value) + '\\x27)" style="font-size:11px;padding:4px 10px;background:' + bg + ';color:' + fg + ';border:1px solid var(--border);border-radius:999px">' + esc(o.label) + '</button>';
23862
+ }
23863
+ html += '</span>';
23864
+ return html;
23865
+ }
23866
+
23867
+ function onRunListChipClick(key, value) {
23868
+ _runListState[key] = value;
23869
+ _runListSaveView();
23870
+ var panel = document.getElementById('panel-runs');
23871
+ if (panel) panel.innerHTML = renderRunListBody(_runListState.data);
23872
+ }
23873
+
23874
+ function onRunListSearch(value) {
23875
+ _runListState.filterText = value;
23876
+ _runListSaveView();
23877
+ // Debounce-by-render: just re-render. Filtering is in-memory + cheap.
23878
+ var panel = document.getElementById('panel-runs');
23879
+ if (panel) panel.innerHTML = renderRunListBody(_runListState.data);
23880
+ }
23881
+
23882
+ function resetRunListFilters() {
23883
+ _runListState.filterStatus = 'failed';
23884
+ _runListState.filterWindow = '24h';
23885
+ _runListState.filterText = '';
23886
+ _runListSaveView();
23887
+ var panel = document.getElementById('panel-runs');
23888
+ if (panel) panel.innerHTML = renderRunListBody(_runListState.data);
23889
+ }
23890
+
23891
+ // Wire the panel's click handler so clicking anywhere on a row opens the
23892
+ // trace viewer (the row's data-trace-job attribute is what the existing
23893
+ // global panel-cron click handler reads).
23894
+ function _runListAttachClickHandler() {
23895
+ var pane = document.getElementById('build-tab-runs');
23896
+ if (!pane || pane._handlerAttached) return;
23897
+ pane.addEventListener('click', function(ev) {
23898
+ var t = ev.target;
23899
+ while (t && t !== pane) {
23900
+ if (t.dataset && t.dataset.traceJob) {
23901
+ openTraceViewer(t.dataset.traceJob);
23902
+ return;
23903
+ }
23904
+ t = t.parentElement;
23905
+ }
23906
+ });
23907
+ pane._handlerAttached = true;
23908
+ }
23909
+ // Attach once on first DOM ready — runs idempotent thanks to the flag.
23910
+ if (typeof document !== 'undefined') {
23911
+ if (document.readyState === 'complete' || document.readyState === 'interactive') {
23912
+ setTimeout(_runListAttachClickHandler, 0);
23913
+ } else {
23914
+ document.addEventListener('DOMContentLoaded', _runListAttachClickHandler);
23915
+ }
23916
+ }
23917
+
23560
23918
  // ── PRD Phase 2: Tools & MCP catalog ──────────────────────────────────
23561
23919
  // Read-only foundation in 1.18.81. Renders the four-card taxonomy:
23562
23920
  // • Built-in — Claude SDK native tools (Read/Write/Bash/etc.)
@@ -23677,14 +24035,159 @@ function renderMcpCatalogCard(server, statusMap) {
23677
24035
  + (server.description ? '<div style="font-size:12px;color:var(--text-secondary);line-height:1.45">' + esc(String(server.description).slice(0, 240)) + '</div>' : '')
23678
24036
  + (lastError ? '<div style="font-size:11px;color:var(--red);background:rgba(239,68,68,0.06);padding:6px 8px;border-radius:4px;word-break:break-word">' + esc(String(lastError).slice(0, 200)) + '</div>' : '')
23679
24037
  + (lastChecked ? '<div style="font-size:11px;color:var(--text-muted)">Checked ' + esc(timeAgo(lastChecked)) + '</div>' : '')
23680
- + '<div style="display:flex;gap:6px;margin-top:4px">'
24038
+ + '<div style="display:flex;gap:6px;margin-top:4px;flex-wrap:wrap">'
23681
24039
  + '<button class="btn-sm" onclick="toggleMcpServerEnabled(\\x27' + jsStr(name) + '\\x27,' + (enabled ? 'false' : 'true') + ')" title="' + (enabled ? 'Disable this MCP server for all tasks' : 'Enable this MCP server') + '">' + (enabled ? 'Disable' : 'Enable') + '</button>'
23682
- + '<button class="btn-sm" disabled title="Edit + Reconnect coming in the next slice" style="opacity:0.55;cursor:not-allowed">Edit</button>'
24040
+ + '<button class="btn-sm" onclick="reconnectMcpServer(\\x27' + jsStr(name) + '\\x27)" title="Clear cached status — next query will reconnect">Reconnect</button>'
24041
+ + '<button class="btn-sm" onclick="openMcpServerEditModal(\\x27' + jsStr(name) + '\\x27)" title="View or edit this server\\x27s config">Edit</button>'
23683
24042
  + '</div>'
23684
24043
  + '</div>';
23685
24044
  return html;
23686
24045
  }
23687
24046
 
24047
+ // PRD Phase 2.1: Reconnect — invalidate cached status server-side, refresh
24048
+ // the catalog so the user sees the pending pill until the next query
24049
+ // handshake repopulates it.
24050
+ async function reconnectMcpServer(name) {
24051
+ try {
24052
+ var r = await apiFetch('/api/mcp-servers/' + encodeURIComponent(name) + '/reconnect', { method: 'POST' });
24053
+ var d = await r.json();
24054
+ if (!r.ok || d.ok === false) {
24055
+ toast('Reconnect failed: ' + (d.error || 'unknown'), 'error');
24056
+ return;
24057
+ }
24058
+ toast(d.message || 'Reconnect queued.', 'info');
24059
+ refreshToolsMcpCatalog();
24060
+ } catch (e) {
24061
+ toast('Reconnect failed: ' + String(e), 'error');
24062
+ }
24063
+ }
24064
+
24065
+ // PRD Phase 2.1: Edit modal. User-managed servers get an editable config
24066
+ // form; auto-detected servers render the same fields read-only with a note
24067
+ // pointing the user at the underlying config file.
24068
+ async function openMcpServerEditModal(name) {
24069
+ // Pull the latest server config — don't trust whatever was on the rendered card.
24070
+ var server;
24071
+ try {
24072
+ var r = await apiFetch('/api/mcp-servers');
24073
+ var d = await r.json();
24074
+ server = (d && d.servers || []).find(function(s) { return s.name === name; });
24075
+ } catch (e) {
24076
+ toast('Failed to load server: ' + String(e), 'error');
24077
+ return;
24078
+ }
24079
+ if (!server) { toast('Server "' + name + '" not found', 'error'); return; }
24080
+ var modal = document.getElementById('mcp-edit-modal');
24081
+ if (!modal) { toast('Edit modal missing from DOM', 'error'); return; }
24082
+ document.getElementById('mcp-edit-title').textContent = 'Edit: ' + name;
24083
+ var roNote = document.getElementById('mcp-edit-readonly-note');
24084
+ var isReadOnly = server.source === 'auto-detected';
24085
+ if (roNote) roNote.style.display = isReadOnly ? '' : 'none';
24086
+ // Set fields
24087
+ document.getElementById('mcp-edit-name').value = server.name || '';
24088
+ document.getElementById('mcp-edit-name').disabled = true; // never rename via this path
24089
+ document.getElementById('mcp-edit-type').value = server.type || 'stdio';
24090
+ document.getElementById('mcp-edit-type').disabled = isReadOnly;
24091
+ document.getElementById('mcp-edit-description').value = server.description || '';
24092
+ document.getElementById('mcp-edit-description').disabled = isReadOnly;
24093
+ document.getElementById('mcp-edit-enabled').checked = server.enabled !== false;
24094
+ document.getElementById('mcp-edit-enabled').disabled = isReadOnly;
24095
+ document.getElementById('mcp-edit-command').value = server.command || '';
24096
+ document.getElementById('mcp-edit-command').disabled = isReadOnly;
24097
+ document.getElementById('mcp-edit-args').value = Array.isArray(server.args) ? server.args.join('\\n') : '';
24098
+ document.getElementById('mcp-edit-args').disabled = isReadOnly;
24099
+ document.getElementById('mcp-edit-url').value = server.url || '';
24100
+ document.getElementById('mcp-edit-url').disabled = isReadOnly;
24101
+ document.getElementById('mcp-edit-headers').value = server.headers && Object.keys(server.headers).length ? JSON.stringify(server.headers, null, 2) : '';
24102
+ document.getElementById('mcp-edit-headers').disabled = isReadOnly;
24103
+ document.getElementById('mcp-edit-env').value = server.env && Object.keys(server.env).length ? JSON.stringify(server.env, null, 2) : '';
24104
+ document.getElementById('mcp-edit-env').disabled = isReadOnly;
24105
+ // Show the right transport fields
24106
+ syncMcpEditTransportRows();
24107
+ // Save button hidden for read-only auto-detected servers.
24108
+ var saveBtn = document.getElementById('mcp-edit-save');
24109
+ if (saveBtn) saveBtn.style.display = isReadOnly ? 'none' : '';
24110
+ modal.classList.add('show');
24111
+ }
24112
+
24113
+ function closeMcpServerEditModal() {
24114
+ var modal = document.getElementById('mcp-edit-modal');
24115
+ if (modal) modal.classList.remove('show');
24116
+ }
24117
+
24118
+ // Show only the row matching the selected transport (stdio vs http/sse).
24119
+ function syncMcpEditTransportRows() {
24120
+ var t = (document.getElementById('mcp-edit-type') || {}).value || 'stdio';
24121
+ var stdioRow = document.getElementById('mcp-edit-stdio-rows');
24122
+ var httpRow = document.getElementById('mcp-edit-http-rows');
24123
+ if (stdioRow) stdioRow.style.display = (t === 'stdio') ? '' : 'none';
24124
+ if (httpRow) httpRow.style.display = (t === 'http' || t === 'sse') ? '' : 'none';
24125
+ }
24126
+
24127
+ async function saveMcpServerEdit() {
24128
+ var name = (document.getElementById('mcp-edit-name') || {}).value;
24129
+ if (!name) return;
24130
+ var type = document.getElementById('mcp-edit-type').value;
24131
+ var description = document.getElementById('mcp-edit-description').value.trim();
24132
+ var enabled = !!document.getElementById('mcp-edit-enabled').checked;
24133
+ var commandRaw = document.getElementById('mcp-edit-command').value.trim();
24134
+ var argsRaw = document.getElementById('mcp-edit-args').value;
24135
+ var url = document.getElementById('mcp-edit-url').value.trim();
24136
+ var headersRaw = document.getElementById('mcp-edit-headers').value.trim();
24137
+ var envRaw = document.getElementById('mcp-edit-env').value.trim();
24138
+
24139
+ var headers, env;
24140
+ if (headersRaw) {
24141
+ try {
24142
+ headers = JSON.parse(headersRaw);
24143
+ if (!headers || typeof headers !== 'object' || Array.isArray(headers)) throw new Error('Headers must be a JSON object');
24144
+ } catch (e) {
24145
+ toast('Headers JSON invalid: ' + (e.message || String(e)), 'error');
24146
+ document.getElementById('mcp-edit-headers').focus();
24147
+ return;
24148
+ }
24149
+ }
24150
+ if (envRaw) {
24151
+ try {
24152
+ env = JSON.parse(envRaw);
24153
+ if (!env || typeof env !== 'object' || Array.isArray(env)) throw new Error('Env must be a JSON object');
24154
+ } catch (e) {
24155
+ toast('Env JSON invalid: ' + (e.message || String(e)), 'error');
24156
+ document.getElementById('mcp-edit-env').focus();
24157
+ return;
24158
+ }
24159
+ }
24160
+ var args = argsRaw.split(/\\r?\\n/).map(function(s){ return s.trim(); }).filter(Boolean);
24161
+ var body = { type: type, description: description, enabled: enabled };
24162
+ if (type === 'stdio') {
24163
+ if (!commandRaw) { toast('Command is required for stdio transport', 'error'); document.getElementById('mcp-edit-command').focus(); return; }
24164
+ body.command = commandRaw;
24165
+ body.args = args;
24166
+ if (env) body.env = env;
24167
+ } else {
24168
+ if (!url) { toast('URL is required for ' + type + ' transport', 'error'); document.getElementById('mcp-edit-url').focus(); return; }
24169
+ body.url = url;
24170
+ if (headers) body.headers = headers;
24171
+ }
24172
+ try {
24173
+ var r = await apiFetch('/api/mcp-servers/' + encodeURIComponent(name), {
24174
+ method: 'PUT',
24175
+ headers: { 'Content-Type': 'application/json' },
24176
+ body: JSON.stringify(body),
24177
+ });
24178
+ var d = await r.json();
24179
+ if (!r.ok || d.error) {
24180
+ toast('Save failed: ' + (d.error || 'unknown'), 'error');
24181
+ return;
24182
+ }
24183
+ toast('Saved.', 'success');
24184
+ closeMcpServerEditModal();
24185
+ refreshToolsMcpCatalog();
24186
+ } catch (e) {
24187
+ toast('Save failed: ' + String(e), 'error');
24188
+ }
24189
+ }
24190
+
23688
24191
  // PUT helper for the Toggle button. Lazy: re-fetches the catalog after
23689
24192
  // the round-trip so the new state is reflected. Future slice will swap
23690
24193
  // to optimistic update + rollback on error.
@@ -35354,6 +35857,14 @@ try {
35354
35857
  if (evt.type === 'cron_complete' && evt.data && evt.data.job && typeof handleCronRunOnceComplete === 'function') {
35355
35858
  try { handleCronRunOnceComplete(evt.data.job); } catch (err) { /* non-fatal */ }
35356
35859
  }
35860
+ // PRD Phase 3: if the Runs tab is visible, refresh it too so a new
35861
+ // run appears at the top without a manual reload.
35862
+ if (currentPage === 'build' && typeof refreshRunList === 'function') {
35863
+ var runsPane = document.getElementById('build-tab-runs');
35864
+ if (runsPane && runsPane.style.display !== 'none') {
35865
+ try { refreshRunList(); } catch (err) { /* non-fatal */ }
35866
+ }
35867
+ }
35357
35868
  }
35358
35869
  // A delete on one tab should drop the card from every open dashboard
35359
35870
  // without waiting for the next poll. cron_toggled is similar but lighter.
@@ -255,6 +255,15 @@ export declare class Gateway {
255
255
  }>;
256
256
  updatedAt: string;
257
257
  };
258
+ /** PRD Phase 2.1: thin pass-through for the dashboard's Reconnect button. */
259
+ invalidateMcpStatus(serverName: string): {
260
+ servers: Array<{
261
+ name: string;
262
+ status: string;
263
+ }>;
264
+ updatedAt: string;
265
+ cleared: boolean;
266
+ };
258
267
  getPresenceInfo(sessionKey: string): {
259
268
  model: string;
260
269
  project: string | null;
@@ -2241,6 +2241,10 @@ export class Gateway {
2241
2241
  getMcpStatus() {
2242
2242
  return this.assistant.getMcpStatus();
2243
2243
  }
2244
+ /** PRD Phase 2.1: thin pass-through for the dashboard's Reconnect button. */
2245
+ invalidateMcpStatus(serverName) {
2246
+ return this.assistant.invalidateMcpStatus(serverName);
2247
+ }
2244
2248
  getPresenceInfo(sessionKey) {
2245
2249
  const sess = this.sessions.get(sessionKey);
2246
2250
  const modelName = sess?.model
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.81",
3
+ "version": "1.18.83",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",