clementine-agent 1.18.69 → 1.18.70

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.
Files changed (2) hide show
  1. package/dist/cli/dashboard.js +493 -219
  2. package/package.json +1 -1
@@ -6594,6 +6594,11 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
6594
6594
  tags: job.tags ?? [],
6595
6595
  category: job.category ?? null,
6596
6596
  predictable: typeof job.predictable === 'boolean' ? job.predictable : null,
6597
+ // Configured allowlists — included so the dashboard can chip-color
6598
+ // "pinned vs auto-injected" without re-fetching the YAML.
6599
+ allowedMcpServers: Array.isArray(job.allowedMcpServers) ? job.allowedMcpServers : null,
6600
+ allowedTools: Array.isArray(job.allowedTools) ? job.allowedTools : null,
6601
+ pinnedSkills: Array.isArray(job.skills) ? job.skills : null,
6597
6602
  },
6598
6603
  predictable: plan.predictable,
6599
6604
  profile: profile ? { slug: profile.slug, name: profile.name } : null,
@@ -15028,6 +15033,99 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
15028
15033
  color: var(--text-secondary); white-space: pre-wrap; word-break: break-word;
15029
15034
  max-height: 180px; overflow-y: auto;
15030
15035
  }
15036
+ /* ── Cron modal: tabs + section cards + legacy banner ─────────── */
15037
+ .cron-modal { width: 820px !important; max-width: 96vw; }
15038
+ .cron-tabs {
15039
+ display: flex; gap: 0; padding: 0 22px;
15040
+ border-bottom: 1px solid var(--border); background: var(--bg-secondary);
15041
+ flex-shrink: 0;
15042
+ }
15043
+ .cron-tab-btn {
15044
+ padding: 12px 18px; background: transparent; border: none;
15045
+ color: var(--text-secondary); font-size: 13px; font-weight: 500;
15046
+ cursor: pointer; border-bottom: 2px solid transparent;
15047
+ margin-bottom: -1px;
15048
+ }
15049
+ .cron-tab-btn.active {
15050
+ color: var(--accent); border-bottom-color: var(--accent);
15051
+ }
15052
+ .cron-tab-btn:hover:not(.active) { color: var(--text-primary); }
15053
+ .cron-tab-btn[disabled] { opacity: 0.4; cursor: not-allowed; }
15054
+ .cron-tab-pane { display: none; }
15055
+ .cron-tab-pane.active { display: block; }
15056
+ .cron-section-card {
15057
+ background: var(--bg-secondary);
15058
+ border: 1px solid var(--border);
15059
+ border-radius: 10px;
15060
+ padding: 14px 16px;
15061
+ margin-bottom: 14px;
15062
+ }
15063
+ .cron-section-card > h4 {
15064
+ font-size: 11px; font-weight: 700; color: var(--text-muted);
15065
+ text-transform: uppercase; letter-spacing: 0.6px;
15066
+ margin: 0 0 4px 0;
15067
+ }
15068
+ .cron-section-desc {
15069
+ font-size: 11px; color: var(--text-muted);
15070
+ margin: 0 0 10px 0; line-height: 1.5;
15071
+ }
15072
+ .cron-banner {
15073
+ border-radius: 8px; padding: 12px 14px;
15074
+ margin-bottom: 14px; font-size: 12px; line-height: 1.5;
15075
+ }
15076
+ .cron-banner.warn {
15077
+ background: rgba(245, 158, 11, 0.10);
15078
+ border: 1px solid rgba(245, 158, 11, 0.32);
15079
+ color: var(--text-primary);
15080
+ }
15081
+ .cron-banner.warn h5 {
15082
+ color: var(--yellow); font-size: 12px; font-weight: 700;
15083
+ margin: 0 0 4px 0; text-transform: uppercase; letter-spacing: 0.4px;
15084
+ }
15085
+ .cron-banner.ok {
15086
+ background: rgba(16, 185, 129, 0.08);
15087
+ border: 1px solid rgba(16, 185, 129, 0.28);
15088
+ color: var(--text-primary);
15089
+ }
15090
+ .cron-banner.ok h5 {
15091
+ color: var(--green); font-size: 12px; font-weight: 700;
15092
+ margin: 0 0 4px 0; text-transform: uppercase; letter-spacing: 0.4px;
15093
+ }
15094
+ .cron-banner .banner-actions { margin-top: 10px; display: flex; gap: 8px; }
15095
+ .cron-banner .banner-actions button {
15096
+ font-size: 12px; padding: 6px 12px; border-radius: 6px; cursor: pointer;
15097
+ }
15098
+ .cron-prompt-textarea {
15099
+ width: 100%; min-height: 180px;
15100
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
15101
+ font-size: 12px; line-height: 1.55;
15102
+ padding: 10px 12px; border: 1px solid var(--border); border-radius: 6px;
15103
+ background: var(--bg-input); color: var(--text-primary);
15104
+ resize: vertical;
15105
+ }
15106
+ .cron-predictable-card {
15107
+ display: flex; align-items: flex-start; gap: 12px;
15108
+ padding: 12px 14px; border-radius: 8px;
15109
+ background: var(--bg-secondary); border: 1px solid var(--border);
15110
+ }
15111
+ .cron-predictable-card.on { border-color: rgba(16, 185, 129, 0.32); background: rgba(16, 185, 129, 0.06); }
15112
+ .cron-predictable-card.off { border-color: rgba(245, 158, 11, 0.32); background: rgba(245, 158, 11, 0.06); }
15113
+ .cron-predictable-card .pred-icon { font-size: 18px; line-height: 1; padding-top: 2px; }
15114
+ .cron-predictable-card .pred-title { font-weight: 600; font-size: 13px; color: var(--text-primary); }
15115
+ .cron-predictable-card .pred-sub { font-size: 11px; color: var(--text-muted); margin-top: 3px; line-height: 1.5; }
15116
+ /* Effective-vs-configured chip coloring on Preview tab */
15117
+ .preview-chip {
15118
+ display: inline-flex; align-items: center; gap: 4px;
15119
+ padding: 3px 9px; margin: 2px 4px 2px 0; border-radius: 999px;
15120
+ font-size: 11px; line-height: 1.6;
15121
+ border: 1px solid var(--border);
15122
+ }
15123
+ .preview-chip.pinned { background: rgba(124, 58, 237, 0.10); color: var(--purple); border-color: rgba(124, 58, 237, 0.30); }
15124
+ .preview-chip.auto { background: rgba(245, 158, 11, 0.10); color: var(--yellow); border-color: rgba(245, 158, 11, 0.30); }
15125
+ .preview-chip-group-label {
15126
+ font-size: 10px; text-transform: uppercase; letter-spacing: 0.4px;
15127
+ color: var(--text-muted); margin-right: 4px;
15128
+ }
15031
15129
  .preview-warn {
15032
15130
  padding: 6px 10px; border-radius: 6px; background: rgba(245, 158, 11, 0.10);
15033
15131
  color: var(--yellow); font-size: 12px; margin-bottom: 6px;
@@ -19511,22 +19609,45 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19511
19609
  </div><!-- /content -->
19512
19610
  </div><!-- /layout -->
19513
19611
 
19514
- <!-- ═══ Create/Edit Cron Modal ═══ -->
19612
+ <!-- ═══ Create/Edit Cron Modal — tabbed (Configure | What will run) ═══ -->
19515
19613
  <div class="modal-overlay" id="cron-modal">
19516
- <div class="modal">
19614
+ <div class="modal cron-modal">
19517
19615
  <div class="modal-header">
19518
19616
  <h3 id="cron-modal-title">New Scheduled Task</h3>
19519
19617
  <button class="btn-ghost btn-sm" onclick="closeCronModal()">&times;</button>
19520
19618
  </div>
19619
+ <div class="cron-tabs" role="tablist">
19620
+ <button type="button" class="cron-tab-btn active" data-cron-tab="configure" onclick="switchCronTab('configure')">Configure</button>
19621
+ <button type="button" class="cron-tab-btn" id="cron-tab-btn-preview" data-cron-tab="preview" onclick="switchCronTab('preview')" title="See exactly what the agent will receive at fire-time">What will run</button>
19622
+ </div>
19521
19623
  <div class="modal-body">
19522
- <div class="form-group">
19523
- <label class="form-label">Task Name</label>
19524
- <input type="text" id="cron-name" placeholder="e.g. morning-briefing">
19525
- <div class="form-hint">Unique identifier. Use lowercase with dashes.</div>
19526
- </div>
19527
- <div class="form-group">
19528
- <label class="form-label">Schedule</label>
19529
- <div class="schedule-builder" id="schedule-builder">
19624
+ <!-- ── Tab: Configure ─────────────────────────────────────────── -->
19625
+ <div class="cron-tab-pane active" id="cron-tab-configure">
19626
+ <!-- Legacy / mismatch banner (populated by openEditCronModal) -->
19627
+ <div id="cron-legacy-banner-host"></div>
19628
+
19629
+ <!-- Identity: name + category + tags -->
19630
+ <div class="cron-section-card">
19631
+ <h4>Identity</h4>
19632
+ <p class="cron-section-desc">A unique name and optional grouping for the dashboard.</p>
19633
+ <div class="form-group">
19634
+ <label class="form-label">Task Name</label>
19635
+ <input type="text" id="cron-name" placeholder="e.g. morning-briefing">
19636
+ <div class="form-hint">Unique identifier. Use lowercase with dashes.</div>
19637
+ </div>
19638
+ <div class="form-group">
19639
+ <label class="form-label">Category <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
19640
+ <input type="text" id="cron-category" placeholder="e.g. ops, research, morning">
19641
+ <div class="form-hint">Used for grouping in the dashboard.</div>
19642
+ </div>
19643
+ </div>
19644
+
19645
+ <!-- Schedule -->
19646
+ <div class="cron-section-card">
19647
+ <h4>Schedule</h4>
19648
+ <p class="cron-section-desc">When this task fires. Pick a frequency or write a cron expression.</p>
19649
+ <div class="form-group" style="margin:0">
19650
+ <div class="schedule-builder" id="schedule-builder">
19530
19651
  <div class="form-row">
19531
19652
  <select id="sched-freq" onchange="updateScheduleBuilder()">
19532
19653
  <option value="daily">Every day</option>
@@ -19601,158 +19722,179 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19601
19722
  <input type="text" id="cron-schedule" placeholder="0 9 * * *" oninput="updateScheduleHint()">
19602
19723
  </div>
19603
19724
  <div class="form-hint" id="cron-schedule-hint" style="margin-top:6px"></div>
19725
+ </div>
19726
+ </div>
19604
19727
  </div>
19605
- </div>
19606
- <div class="form-row">
19607
- <div class="form-group">
19608
- <label class="form-label">Tier</label>
19609
- <select id="cron-tier">
19610
- <option value="1">Tier 1 — Read-only (vault, search, web)</option>
19611
- <option value="2">Tier 2 — Read + Write (Bash, files, sub-agents)</option>
19612
- <option value="3">Tier 3 Full access (use with caution)</option>
19613
- </select>
19614
- </div>
19615
- <div class="form-group">
19616
- <label class="form-label">Category <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
19617
- <input type="text" id="cron-category" placeholder="e.g. ops, research, morning">
19618
- <div class="form-hint">Used for grouping in the dashboard.</div>
19619
- </div>
19620
- </div>
19621
- <div class="form-group">
19622
- <label class="form-label">Project Context <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
19623
- <select id="cron-workdir">
19624
- <option value="">None — runs in default context</option>
19625
- </select>
19626
- <div class="form-hint">Run this task inside a project directory. The agent gets access to that project's tools, CLAUDE.md, and files.</div>
19627
- </div>
19628
- <div class="form-row">
19629
- <div class="form-group">
19630
- <label class="form-label">Mode</label>
19631
- <select id="cron-mode" onchange="toggleUnleashedOptions()">
19632
- <option value="standard">Standard</option>
19633
- <option value="unleashed">Unleashed (long-running)</option>
19634
- </select>
19635
- <div class="form-hint">Unleashed mode runs in phases with checkpointing for tasks that take hours.</div>
19636
- </div>
19637
- <div class="form-group" id="cron-maxhours-group" style="display:none">
19638
- <label class="form-label">Max Hours</label>
19639
- <select id="cron-maxhours">
19640
- <option value="1">1 hour</option>
19641
- <option value="2">2 hours</option>
19642
- <option value="4">4 hours</option>
19643
- <option value="6" selected>6 hours (default)</option>
19644
- <option value="8">8 hours</option>
19645
- <option value="12">12 hours</option>
19646
- <option value="24">24 hours</option>
19647
- </select>
19648
- </div>
19649
- </div>
19650
- <div class="form-row">
19651
- <div class="form-group">
19652
- <label class="form-label">Max Retries <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
19653
- <select id="cron-max-retries">
19654
- <option value="">Auto (based on error history)</option>
19655
- <option value="0">0 — No retries</option>
19656
- <option value="1">1</option>
19657
- <option value="2">2</option>
19658
- <option value="3">3</option>
19659
- <option value="5">5</option>
19660
- </select>
19661
- <div class="form-hint">Override automatic retry count for transient errors.</div>
19662
- </div>
19663
- <div class="form-group">
19664
- <label class="form-label">After Job <span style="color:var(--text-muted);font-weight:normal">(chain)</span></label>
19665
- <select id="cron-after">
19666
- <option value="">None — runs on schedule</option>
19667
- </select>
19668
- <div class="form-hint">Trigger this job after another succeeds (ignores schedule).</div>
19728
+
19729
+ <!-- What it does: prompt + context + reference files -->
19730
+ <div class="cron-section-card">
19731
+ <h4>What it does</h4>
19732
+ <p class="cron-section-desc">The instruction the agent receives. Long prompts are fine — drag the corner to resize.</p>
19733
+ <div class="form-group">
19734
+ <label class="form-label">Prompt</label>
19735
+ <textarea id="cron-prompt" class="cron-prompt-textarea" placeholder="What should the AI do when this task runs?"></textarea>
19736
+ <div class="form-hint">The instruction sent to the AI agent when this task fires.</div>
19737
+ </div>
19738
+ <div class="form-group">
19739
+ <label class="form-label">Context <span style="color:var(--text-muted);font-weight:normal">(optional — injected at runtime)</span></label>
19740
+ <textarea id="cron-context" rows="4" placeholder="Guidelines, examples, formatting rules, data sources, or any context the agent should know when running this task..."></textarea>
19741
+ <div class="form-hint">Freeform notes injected into the prompt at execution time. Use this for training data, style guides, or standing instructions.</div>
19742
+ </div>
19743
+ <div class="form-group" style="margin-bottom:0">
19744
+ <label class="form-label">Reference Files <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
19745
+ <div id="cron-attachments-list" style="margin-bottom:8px"></div>
19746
+ <label class="btn btn-sm" style="cursor:pointer;display:inline-flex;align-items:center;gap:4px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;padding:6px 12px;font-size:12px;color:var(--text-primary)">
19747
+ + Attach File
19748
+ <input type="file" multiple accept=".csv,.md,.txt,.json,.docx,.xlsx" style="display:none" onchange="handleCronFileUpload(event)">
19749
+ </label>
19750
+ <div class="form-hint">CSV, Markdown, or text files the agent can reference during execution. Max 10MB per file.</div>
19751
+ </div>
19669
19752
  </div>
19670
- </div>
19671
- <div class="form-group">
19672
- <label class="form-label">Prompt</label>
19673
- <textarea id="cron-prompt" rows="5" placeholder="What should the AI do when this task runs?"></textarea>
19674
- <div class="form-hint">The instruction sent to the AI agent when this task fires.</div>
19675
- </div>
19676
- <div class="form-group">
19677
- <label class="form-label">Context <span style="color:var(--text-muted);font-weight:normal">(optional — injected at runtime)</span></label>
19678
- <textarea id="cron-context" rows="4" placeholder="Guidelines, examples, formatting rules, data sources, or any context the agent should know when running this task..."></textarea>
19679
- <div class="form-hint">Freeform notes injected into the prompt at execution time. Use this for training data, style guides, or standing instructions.</div>
19680
- </div>
19681
- <!-- ── Capabilities ── -->
19682
- <div class="form-group">
19683
- <label class="form-label">Capabilities <span style="color:var(--text-muted);font-weight:normal">(optional — pin skills + scope tools/MCP)</span></label>
19684
- <div class="form-hint" style="margin-bottom:6px">Pin learned procedures and constrain which tools / MCP servers this trick can use. Empty = inherit defaults. Use Preview on the card to see exactly what gets sent.</div>
19685
- <div class="cap-section">
19686
- <label class="cap-section-label">Predictable Mode</label>
19687
- <label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer;font-size:12px;color:var(--text-primary)">
19688
- <input type="checkbox" id="cron-predictable" checked style="margin-top:3px">
19689
- <span>
19690
- <strong>Run with only what's attached</strong> (recommended)
19691
- <div style="font-size:11px;color:var(--text-muted);margin-top:3px;line-height:1.5">
19692
- ON: MEMORY.md, team comms, delegation queue, and auto-matched skills are SKIPPED at fire-time. The trick runs ONLY with the prompt + pinned skills + tools you see here. No drift, no surprise.<br>
19693
- OFF: legacy mode — runner injects MEMORY.md and other live context. Use only when the trick legitimately needs to re-read memory each fire (e.g. "summarize yesterday's daily note").
19694
- </div>
19753
+
19754
+ <!-- Capabilities: predictable mode + skills + MCP + tools + tags -->
19755
+ <div class="cron-section-card">
19756
+ <h4>Capabilities</h4>
19757
+ <p class="cron-section-desc">Predictable mode locks the run to exactly what's pinned here. Switch to the <strong>What will run</strong> tab to see the resolved final state.</p>
19758
+
19759
+ <!-- Predictable Mode (prominent at top) -->
19760
+ <label id="cron-predictable-card" class="cron-predictable-card on" style="cursor:pointer;margin-bottom:12px">
19761
+ <span class="pred-icon" id="cron-predictable-icon">🔒</span>
19762
+ <input type="checkbox" id="cron-predictable" checked style="margin-top:3px" onchange="onPredictableChange()">
19763
+ <span style="flex:1">
19764
+ <span class="pred-title" id="cron-predictable-title">Predictable mode — ON</span>
19765
+ <div class="pred-sub" id="cron-predictable-sub">Runs with ONLY the prompt + pinned skills/MCP below. MEMORY.md, team comms, and auto-matched skills are skipped at fire-time. Recommended.</div>
19695
19766
  </span>
19696
19767
  </label>
19697
- </div>
19698
- <div class="cap-section">
19699
- <label class="cap-section-label">Pinned Skills</label>
19700
- <div class="cap-picker-chips" id="cron-skills-chips"></div>
19701
- <button type="button" class="cap-toggle-link" id="cron-skills-add-btn" onclick="toggleSkillsPickerSearch()">+ Add skill</button>
19702
- <div id="cron-skills-search-panel" style="display:none;margin-top:6px">
19703
- <input type="text" class="cap-picker-search" id="cron-skills-search" placeholder="Search skills…" oninput="renderSkillsPickerList()">
19704
- <div class="cap-picker-list" id="cron-skills-list"><div class="cap-picker-empty-state">Loading skills…</div></div>
19768
+
19769
+ <div class="cap-section">
19770
+ <label class="cap-section-label">Pinned Skills</label>
19771
+ <div class="cap-picker-chips" id="cron-skills-chips"></div>
19772
+ <button type="button" class="cap-toggle-link" id="cron-skills-add-btn" onclick="toggleSkillsPickerSearch()">+ Add skill</button>
19773
+ <div id="cron-skills-search-panel" style="display:none;margin-top:6px">
19774
+ <input type="text" class="cap-picker-search" id="cron-skills-search" placeholder="Search skills…" oninput="renderSkillsPickerList()">
19775
+ <div class="cap-picker-list" id="cron-skills-list"><div class="cap-picker-empty-state">Loading skills…</div></div>
19776
+ </div>
19705
19777
  </div>
19706
- </div>
19707
- <div class="cap-section">
19708
- <label class="cap-section-label">Allowed MCP Servers</label>
19709
- <div class="cap-picker-chips" id="cron-mcp-chips"></div>
19710
- <button type="button" class="cap-toggle-link" id="cron-mcp-add-btn" onclick="toggleMcpPickerSearch()">+ Add MCP server</button>
19711
- <div id="cron-mcp-search-panel" style="display:none;margin-top:6px">
19712
- <input type="text" class="cap-picker-search" id="cron-mcp-search" placeholder="Search MCP servers…" oninput="renderMcpPickerList()">
19713
- <div class="cap-picker-list" id="cron-mcp-list"><div class="cap-picker-empty-state">Loading MCP servers…</div></div>
19778
+ <div class="cap-section">
19779
+ <label class="cap-section-label">Allowed MCP Servers</label>
19780
+ <div class="cap-picker-chips" id="cron-mcp-chips"></div>
19781
+ <button type="button" class="cap-toggle-link" id="cron-mcp-add-btn" onclick="toggleMcpPickerSearch()">+ Add MCP server</button>
19782
+ <div id="cron-mcp-search-panel" style="display:none;margin-top:6px">
19783
+ <input type="text" class="cap-picker-search" id="cron-mcp-search" placeholder="Search MCP servers…" oninput="renderMcpPickerList()">
19784
+ <div class="cap-picker-list" id="cron-mcp-list"><div class="cap-picker-empty-state">Loading MCP servers…</div></div>
19785
+ </div>
19714
19786
  </div>
19715
- </div>
19716
- <div class="cap-section">
19717
- <label class="cap-section-label">Allowed Tools (raw allowlist · power users)</label>
19718
- <button type="button" class="cap-toggle-link" id="cron-tools-toggle-btn" onclick="toggleAllowedToolsPanel()">▾ Show</button>
19719
- <div id="cron-tools-panel" style="display:none;margin-top:6px">
19720
- <textarea id="cron-allowed-tools" class="cap-tools-textarea" rows="2" placeholder="Read, Edit, Bash, mcp__slack__send_message, ..."></textarea>
19721
- <div class="form-hint">Comma-separated tool names. Empty = inherit from agent profile / default. 'Agent' is always force-included.</div>
19787
+ <div class="cap-section">
19788
+ <label class="cap-section-label">Allowed Tools (raw allowlist · power users)</label>
19789
+ <button type="button" class="cap-toggle-link" id="cron-tools-toggle-btn" onclick="toggleAllowedToolsPanel()">▾ Show</button>
19790
+ <div id="cron-tools-panel" style="display:none;margin-top:6px">
19791
+ <textarea id="cron-allowed-tools" class="cap-tools-textarea" rows="2" placeholder="Read, Edit, Bash, mcp__slack__send_message, ..."></textarea>
19792
+ <div class="form-hint">Comma-separated tool names. Empty = inherit from agent profile / default. 'Agent' is always force-included.</div>
19793
+ </div>
19794
+ </div>
19795
+ <div class="cap-section">
19796
+ <label class="cap-section-label">Tags</label>
19797
+ <div class="cap-picker-chips" id="cron-tags-chips"></div>
19798
+ <input type="text" class="cap-tag-input" id="cron-tags-input" placeholder="Type a tag and press Enter (e.g. morning, ops)" onkeydown="handleTagInputKeydown(event)">
19722
19799
  </div>
19723
19800
  </div>
19724
- <div class="cap-section">
19725
- <label class="cap-section-label">Tags</label>
19726
- <div class="cap-picker-chips" id="cron-tags-chips"></div>
19727
- <input type="text" class="cap-tag-input" id="cron-tags-input" placeholder="Type a tag and press Enter (e.g. morning, ops)" onkeydown="handleTagInputKeydown(event)">
19728
- </div>
19729
- </div>
19730
- <div class="form-group">
19731
- <label class="form-label">Reference Files <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
19732
- <div id="cron-attachments-list" style="margin-bottom:8px"></div>
19733
- <label class="btn btn-sm" style="cursor:pointer;display:inline-flex;align-items:center;gap:4px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;padding:6px 12px;font-size:12px;color:var(--text-primary)">
19734
- + Attach File
19735
- <input type="file" multiple accept=".csv,.md,.txt,.json,.docx,.xlsx" style="display:none" onchange="handleCronFileUpload(event)">
19736
- </label>
19737
- <div class="form-hint">CSV, Markdown, or text files the agent can reference during execution. Max 10MB per file.</div>
19738
- </div>
19739
19801
 
19740
- <!-- Training Chat -->
19741
- <div class="form-group" id="cron-training-section" style="display:none">
19742
- <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
19743
- <label class="form-label" style="margin:0">Training Chat</label>
19744
- <button class="btn btn-sm" onclick="toggleCronTraining()" style="font-size:11px" id="cron-training-toggle">Hide</button>
19745
- </div>
19746
- <div id="cron-training-chat" style="border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);overflow:hidden">
19747
- <div id="cron-training-messages" style="max-height:240px;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:8px">
19748
- <div class="empty-state" style="padding:16px;font-size:12px;color:var(--text-muted)">Chat with the agent to refine this task. Ask for help with the prompt, suggest improvements, or add context.</div>
19802
+ <!-- Advanced: tier + workdir + mode + max-hours + retries + after -->
19803
+ <details class="cron-section-card" style="padding:0">
19804
+ <summary style="padding:14px 16px;cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between">
19805
+ <span><strong style="font-size:11px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.6px">Advanced</strong> <span style="color:var(--text-muted);font-size:11px;margin-left:6px">tier, mode, retries, project context</span></span>
19806
+ <span style="color:var(--text-muted);font-size:11px">▾</span>
19807
+ </summary>
19808
+ <div style="padding:0 16px 14px">
19809
+ <div class="form-row">
19810
+ <div class="form-group">
19811
+ <label class="form-label">Tier</label>
19812
+ <select id="cron-tier">
19813
+ <option value="1">Tier 1 — Read-only (vault, search, web)</option>
19814
+ <option value="2">Tier 2 — Read + Write (Bash, files, sub-agents)</option>
19815
+ <option value="3">Tier 3 — Full access (use with caution)</option>
19816
+ </select>
19817
+ </div>
19818
+ <div class="form-group">
19819
+ <label class="form-label">Project Context <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
19820
+ <select id="cron-workdir">
19821
+ <option value="">None — runs in default context</option>
19822
+ </select>
19823
+ <div class="form-hint">Run inside a project directory. Agent gets that project's CLAUDE.md.</div>
19824
+ </div>
19825
+ </div>
19826
+ <div class="form-row">
19827
+ <div class="form-group">
19828
+ <label class="form-label">Mode</label>
19829
+ <select id="cron-mode" onchange="toggleUnleashedOptions()">
19830
+ <option value="standard">Standard</option>
19831
+ <option value="unleashed">Unleashed (long-running)</option>
19832
+ </select>
19833
+ <div class="form-hint">Unleashed runs in phases with checkpointing for hour-scale tasks.</div>
19834
+ </div>
19835
+ <div class="form-group" id="cron-maxhours-group" style="display:none">
19836
+ <label class="form-label">Max Hours</label>
19837
+ <select id="cron-maxhours">
19838
+ <option value="1">1 hour</option>
19839
+ <option value="2">2 hours</option>
19840
+ <option value="4">4 hours</option>
19841
+ <option value="6" selected>6 hours (default)</option>
19842
+ <option value="8">8 hours</option>
19843
+ <option value="12">12 hours</option>
19844
+ <option value="24">24 hours</option>
19845
+ </select>
19846
+ </div>
19847
+ </div>
19848
+ <div class="form-row">
19849
+ <div class="form-group">
19850
+ <label class="form-label">Max Retries <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
19851
+ <select id="cron-max-retries">
19852
+ <option value="">Auto (based on error history)</option>
19853
+ <option value="0">0 — No retries</option>
19854
+ <option value="1">1</option>
19855
+ <option value="2">2</option>
19856
+ <option value="3">3</option>
19857
+ <option value="5">5</option>
19858
+ </select>
19859
+ <div class="form-hint">Override automatic retry count for transient errors.</div>
19860
+ </div>
19861
+ <div class="form-group">
19862
+ <label class="form-label">After Job <span style="color:var(--text-muted);font-weight:normal">(chain)</span></label>
19863
+ <select id="cron-after">
19864
+ <option value="">None — runs on schedule</option>
19865
+ </select>
19866
+ <div class="form-hint">Trigger after another job succeeds (ignores schedule).</div>
19867
+ </div>
19868
+ </div>
19869
+ </div>
19870
+ </details>
19871
+
19872
+ <!-- Training Chat -->
19873
+ <div class="form-group" id="cron-training-section" style="display:none">
19874
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
19875
+ <label class="form-label" style="margin:0">Training Chat</label>
19876
+ <button class="btn btn-sm" onclick="toggleCronTraining()" style="font-size:11px" id="cron-training-toggle">Hide</button>
19877
+ </div>
19878
+ <div id="cron-training-chat" style="border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);overflow:hidden">
19879
+ <div id="cron-training-messages" style="max-height:240px;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:8px">
19880
+ <div class="empty-state" style="padding:16px;font-size:12px;color:var(--text-muted)">Chat with the agent to refine this task. Ask for help with the prompt, suggest improvements, or add context.</div>
19881
+ </div>
19882
+ <div style="display:flex;gap:6px;padding:8px;border-top:1px solid var(--border);background:var(--bg-primary)">
19883
+ <input type="text" id="cron-training-input" placeholder="Ask for help refining this task..." style="flex:1;font-size:12px;padding:6px 10px" onkeydown="if(event.key==='Enter')sendCronTraining()">
19884
+ <button class="btn btn-sm btn-primary" id="cron-training-send" onclick="sendCronTraining()" style="font-size:11px;padding:4px 12px">Send</button>
19885
+ </div>
19749
19886
  </div>
19750
- <div style="display:flex;gap:6px;padding:8px;border-top:1px solid var(--border);background:var(--bg-primary)">
19751
- <input type="text" id="cron-training-input" placeholder="Ask for help refining this task..." style="flex:1;font-size:12px;padding:6px 10px" onkeydown="if(event.key==='Enter')sendCronTraining()">
19752
- <button class="btn btn-sm btn-primary" id="cron-training-send" onclick="sendCronTraining()" style="font-size:11px;padding:4px 12px">Send</button>
19887
+ </div>
19888
+ </div><!-- /cron-tab-configure -->
19889
+
19890
+ <!-- ── Tab: What will run ─────────────────────────────────────── -->
19891
+ <div class="cron-tab-pane" id="cron-tab-preview">
19892
+ <div id="cron-preview-body" style="padding:0">
19893
+ <div style="padding:36px 24px;color:var(--text-muted);text-align:center;font-size:13px">
19894
+ Save the task first, then switch back here to see exactly what the agent will receive.
19753
19895
  </div>
19754
19896
  </div>
19755
- </div>
19897
+ </div><!-- /cron-tab-preview -->
19756
19898
  </div>
19757
19899
  <div class="modal-footer">
19758
19900
  <div style="display:flex;align-items:center;gap:8px;flex:1">
@@ -19764,21 +19906,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19764
19906
  </div>
19765
19907
  </div>
19766
19908
 
19767
- <!-- ═══ Preview Modalsee exactly what a trick will run with ═══ -->
19768
- <div class="modal-overlay" id="cron-preview-modal">
19769
- <div class="modal" style="width:760px;max-width:96vw">
19770
- <div class="modal-header">
19771
- <h3 id="cron-preview-title">Preview</h3>
19772
- <button class="btn-ghost btn-sm" onclick="closeCronPreviewModal()">&times;</button>
19773
- </div>
19774
- <div class="modal-body" id="cron-preview-body" style="padding:0;max-height:75vh;overflow-y:auto">
19775
- <div style="padding:24px;color:var(--text-muted);text-align:center">Loading…</div>
19776
- </div>
19777
- <div class="modal-footer">
19778
- <button onclick="closeCronPreviewModal()">Close</button>
19779
- </div>
19780
- </div>
19781
- </div>
19909
+ <!-- (legacy standalone Preview modal removed in 1.18.70 preview now lives as a tab inside the cron modal) -->
19782
19910
 
19783
19911
  <!-- ═══ Goal Modal ═══ -->
19784
19912
  <div class="modal-overlay" id="goal-modal">
@@ -24064,9 +24192,109 @@ function toggleUnleashedOptions() {
24064
24192
  document.getElementById('cron-maxhours-group').style.display = mode === 'unleashed' ? '' : 'none';
24065
24193
  }
24066
24194
 
24195
+ // ── Cron modal: tab switching ────────────────────────────────────
24196
+ // Tracks whether the current modal session has fetched preview data, so
24197
+ // switching tabs doesn't refetch on every flip but a save-then-flip will.
24198
+ var _cronPreviewLoadedFor = null;
24199
+ var _cronActiveTab = 'configure';
24200
+
24201
+ function switchCronTab(tab) {
24202
+ _cronActiveTab = tab;
24203
+ document.querySelectorAll('.cron-tab-btn').forEach(function(b) {
24204
+ b.classList.toggle('active', b.getAttribute('data-cron-tab') === tab);
24205
+ });
24206
+ var configurePane = document.getElementById('cron-tab-configure');
24207
+ var previewPane = document.getElementById('cron-tab-preview');
24208
+ if (configurePane) configurePane.classList.toggle('active', tab === 'configure');
24209
+ if (previewPane) previewPane.classList.toggle('active', tab === 'preview');
24210
+ if (tab === 'preview') {
24211
+ var name = editingCronJob;
24212
+ if (!name) {
24213
+ var body = document.getElementById('cron-preview-body');
24214
+ if (body) body.innerHTML = '<div style="padding:36px 24px;color:var(--text-muted);text-align:center;font-size:13px">Save the task first, then switch back here to see exactly what the agent will receive.</div>';
24215
+ return;
24216
+ }
24217
+ if (_cronPreviewLoadedFor !== name) loadCronPreviewIntoTab(name);
24218
+ }
24219
+ }
24220
+
24221
+ async function loadCronPreviewIntoTab(jobName) {
24222
+ var body = document.getElementById('cron-preview-body');
24223
+ if (!body) return;
24224
+ body.innerHTML = '<div style="padding:36px 24px;color:var(--text-muted);text-align:center;font-size:13px">Loading preview…</div>';
24225
+ try {
24226
+ var r = await apiFetch('/api/cron/' + encodeURIComponent(jobName) + '/preview');
24227
+ var d = await r.json();
24228
+ if (!r.ok || d.error) {
24229
+ body.innerHTML = '<div class="preview-section" style="color:var(--red)">Preview failed: ' + esc(d.error || 'unknown') + '</div>';
24230
+ return;
24231
+ }
24232
+ body.innerHTML = renderCronPreview(d);
24233
+ _cronPreviewLoadedFor = jobName;
24234
+ } catch(e) {
24235
+ body.innerHTML = '<div class="preview-section" style="color:var(--red)">Preview failed: ' + esc(String(e)) + '</div>';
24236
+ }
24237
+ }
24238
+
24239
+ // Mark the preview as stale (call after save so next tab visit refetches).
24240
+ function markCronPreviewDirty() { _cronPreviewLoadedFor = null; }
24241
+
24242
+ // ── Predictable mode: visual card sync + legacy banner ───────────
24243
+ function onPredictableChange() {
24244
+ var predEl = document.getElementById('cron-predictable');
24245
+ var card = document.getElementById('cron-predictable-card');
24246
+ var icon = document.getElementById('cron-predictable-icon');
24247
+ var title = document.getElementById('cron-predictable-title');
24248
+ var sub = document.getElementById('cron-predictable-sub');
24249
+ if (!predEl || !card) return;
24250
+ var on = !!predEl.checked;
24251
+ card.classList.toggle('on', on);
24252
+ card.classList.toggle('off', !on);
24253
+ if (icon) icon.textContent = on ? '🔒' : '🔄';
24254
+ if (title) title.textContent = on ? 'Predictable mode — ON' : 'Predictable mode — OFF (legacy)';
24255
+ if (sub) {
24256
+ sub.innerHTML = on
24257
+ ? "Runs with ONLY the prompt + pinned skills/MCP below. MEMORY.md, team comms, and auto-matched skills are skipped at fire-time. Recommended."
24258
+ : "Legacy mode — fire-time injects MEMORY.md, recent team activity, and auto-matched skills/MCP servers based on prompt text. Use only if the task legitimately needs to re-read memory each fire (e.g. \\"summarize yesterday's daily note\\").";
24259
+ }
24260
+ // Hide legacy banner if user just turned predictable on (won't persist
24261
+ // until they Save, but the visual feedback matters).
24262
+ var host = document.getElementById('cron-legacy-banner-host');
24263
+ if (on && host) host.innerHTML = '';
24264
+ }
24265
+
24266
+ function renderCronLegacyBanner(job) {
24267
+ var host = document.getElementById('cron-legacy-banner-host');
24268
+ if (!host) return;
24269
+ // Banner only when predictable is undefined or explicitly false. Jobs
24270
+ // saved with predictable: true are migrated and skip the banner.
24271
+ if (job && job.predictable === true) { host.innerHTML = ''; return; }
24272
+ var msg = (job && job.predictable === false)
24273
+ ? "This task is set to legacy mode. At fire-time the runner injects MEMORY.md, recent team activity, the delegation queue, and auto-matches MCP servers based on prompt text — even if your prompt forbids them. The <strong>What will run</strong> tab shows what actually gets attached."
24274
+ : "This task was created before Predictable Mode existed. At fire-time the runner still injects MEMORY.md, recent team activity, and auto-matches MCP servers based on prompt text. Open the <strong>What will run</strong> tab to see what actually gets attached.";
24275
+ host.innerHTML =
24276
+ '<div class="cron-banner warn">'
24277
+ + '<h5>⚠ Legacy mode — output may not match what you see here</h5>'
24278
+ + '<div>' + msg + '</div>'
24279
+ + '<div class="banner-actions">'
24280
+ + '<button class="btn-primary btn-sm" onclick="enablePredictableFromBanner()">Switch to Predictable Mode</button>'
24281
+ + '<button class="btn-sm" onclick="switchCronTab(\\x27preview\\x27)" style="background:transparent;border:1px solid var(--border);color:var(--text-primary)">See what will run</button>'
24282
+ + '</div>'
24283
+ + '</div>';
24284
+ }
24285
+
24286
+ function enablePredictableFromBanner() {
24287
+ var predEl = document.getElementById('cron-predictable');
24288
+ if (!predEl) return;
24289
+ predEl.checked = true;
24290
+ onPredictableChange();
24291
+ toast('Predictable Mode enabled — click Save Changes to lock it in.', 'success');
24292
+ }
24293
+
24067
24294
  function openCreateCronModal(agentSlug) {
24068
24295
  _cronAgentContext = agentSlug || '';
24069
24296
  editingCronJob = null;
24297
+ _cronPreviewLoadedFor = null;
24070
24298
  document.getElementById('cron-modal-title').textContent = 'New Scheduled Task';
24071
24299
  document.getElementById('cron-modal-save').textContent = 'Create Task';
24072
24300
  document.getElementById('cron-name').value = '';
@@ -24089,6 +24317,13 @@ function openCreateCronModal(agentSlug) {
24089
24317
  document.getElementById('cron-train-btn').style.display = '';
24090
24318
  resetCronTrainingChat();
24091
24319
  resetTrickCapabilityState();
24320
+ // No saved state to preview when creating — disable the Preview tab.
24321
+ var previewBtn = document.getElementById('cron-tab-btn-preview');
24322
+ if (previewBtn) previewBtn.setAttribute('disabled', 'disabled');
24323
+ var host = document.getElementById('cron-legacy-banner-host');
24324
+ if (host) host.innerHTML = '';
24325
+ switchCronTab('configure');
24326
+ onPredictableChange();
24092
24327
  document.getElementById('cron-modal').classList.add('show');
24093
24328
  }
24094
24329
 
@@ -24097,6 +24332,7 @@ function openEditCronModal(jobName) {
24097
24332
  if (!job) return;
24098
24333
  _cronAgentContext = job.agent || '';
24099
24334
  editingCronJob = jobName;
24335
+ _cronPreviewLoadedFor = null;
24100
24336
  document.getElementById('cron-modal-title').textContent = 'Edit: ' + jobName;
24101
24337
  document.getElementById('cron-modal-save').textContent = 'Save Changes';
24102
24338
  document.getElementById('cron-name').value = job.name;
@@ -24130,64 +24366,59 @@ function openEditCronModal(jobName) {
24130
24366
  var catEl = document.getElementById('cron-category');
24131
24367
  if (catEl) catEl.value = job.category || '';
24132
24368
  // Predictable: respect saved value; if undefined (legacy trick), keep
24133
- // unchecked so we don't silently change runner behavior.
24369
+ // unchecked so we don't silently change runner behavior. The legacy
24370
+ // banner surfaces the migration choice instead.
24134
24371
  var predEl = document.getElementById('cron-predictable');
24135
24372
  if (predEl) predEl.checked = (job.predictable === true);
24373
+ onPredictableChange();
24374
+ renderCronLegacyBanner(job);
24136
24375
  renderSkillsPickerChips();
24137
24376
  renderMcpPickerChips();
24138
24377
  renderTagsPickerChips();
24139
24378
  _pendingAttachments = [];
24140
24379
  loadCronAttachments(jobName);
24380
+ // Existing job has saved state, enable Preview tab.
24381
+ var previewBtn = document.getElementById('cron-tab-btn-preview');
24382
+ if (previewBtn) previewBtn.removeAttribute('disabled');
24383
+ switchCronTab('configure');
24141
24384
  document.getElementById('cron-modal').classList.add('show');
24142
24385
  }
24143
24386
 
24144
24387
  /**
24145
- * Open the trick preview modal shows the EXACT prompt + resolved skills
24146
- * + tool/MCP allowlists this trick will run with on the next cron tick,
24147
- * without dispatching to the agent. The visibility tool that catches
24148
- * "configured via chat → surprise at fire time" before the surprise.
24388
+ * Open the unified cron modal directly on the "What will run" tab
24389
+ * what used to be a separate Preview modal in 1.18.69 and earlier.
24390
+ * Loads both Configure data (so editing works) and the live preview.
24149
24391
  */
24150
24392
  async function openCronPreview(jobName) {
24151
- document.getElementById('cron-preview-title').textContent = 'Preview: ' + jobName;
24152
- var body = document.getElementById('cron-preview-body');
24153
- body.innerHTML = '<div style="padding:24px;color:var(--text-muted);text-align:center">Loading preview…</div>';
24154
- document.getElementById('cron-preview-modal').classList.add('show');
24155
- try {
24156
- var r = await apiFetch('/api/cron/' + encodeURIComponent(jobName) + '/preview');
24157
- var d = await r.json();
24158
- if (!r.ok || d.error) {
24159
- body.innerHTML = '<div class="preview-section" style="color:var(--red)">Preview failed: ' + esc(d.error || 'unknown') + '</div>';
24160
- return;
24161
- }
24162
- body.innerHTML = renderCronPreview(d);
24163
- } catch(e) {
24164
- body.innerHTML = '<div class="preview-section" style="color:var(--red)">Preview failed: ' + esc(String(e)) + '</div>';
24165
- }
24393
+ openEditCronModal(jobName);
24394
+ switchCronTab('preview');
24166
24395
  }
24167
24396
 
24168
- function closeCronPreviewModal() {
24169
- document.getElementById('cron-preview-modal').classList.remove('show');
24170
- }
24397
+ // Backwards-compat alias — kept in case external buttons still reference
24398
+ // it. The standalone preview modal was removed in 1.18.70.
24399
+ function closeCronPreviewModal() { closeCronModal(); }
24171
24400
 
24172
24401
  function renderCronPreview(d) {
24173
24402
  var html = '';
24174
- // Predictable verdict line the headline visibility win.
24403
+ // Headline: this is THE place to surface the predictable verdict, since
24404
+ // this whole tab exists to answer "what will the agent actually receive?"
24175
24405
  html += '<div class="preview-section">';
24176
24406
  if (d.predictable === true) {
24177
24407
  html += '<div style="padding:10px 12px;border-radius:6px;background:rgba(16,185,129,0.12);color:var(--green);font-size:13px;font-weight:500">'
24178
24408
  + '🔒 <strong>Predictable</strong> — what you see here is exactly what will run. No MEMORY.md, no team comms, no auto-matched skills injected at fire-time.'
24179
24409
  + '</div>';
24180
- } else if (d.predictable === false) {
24181
- html += '<div style="padding:10px 12px;border-radius:6px;background:rgba(245,158,11,0.12);color:var(--yellow);font-size:13px;font-weight:500">'
24182
- + '⚠ <strong>Reads memory at fire-time</strong> — fire-time will ALSO inject MEMORY.md, recent team comms, delegation queue, and auto-matched skills. Output may differ from this preview if those drift between now and fire.'
24183
- + '</div>';
24184
24410
  } else {
24185
- html += '<div style="padding:10px 12px;border-radius:6px;background:var(--bg-tertiary);color:var(--text-muted);font-size:12px">'
24186
- + 'Legacy trick — predictable mode not set. Runs in dynamic mode (injects MEMORY.md, etc). Edit and turn on Predictable Mode to lock down behavior.'
24411
+ var head = (d.predictable === false)
24412
+ ? '⚠ <strong>Legacy mode (explicit)</strong>'
24413
+ : '⚠ <strong>Legacy mode (default)</strong>';
24414
+ html += '<div style="padding:10px 12px;border-radius:6px;background:rgba(245,158,11,0.12);color:var(--yellow);font-size:13px;font-weight:500">'
24415
+ + head + ' — fire-time ALSO injects MEMORY.md, team activity, the delegation queue, and auto-matches MCP servers based on prompt text. '
24416
+ + '<a href="#" onclick="switchCronTab(\\x27configure\\x27);enablePredictableFromBanner();return false" style="color:var(--accent);text-decoration:underline">Switch to Predictable Mode →</a>'
24187
24417
  + '</div>';
24188
24418
  }
24189
24419
  html += '</div>';
24190
- // Warnings band
24420
+
24421
+ // Warnings band (e.g. pinned skill missing)
24191
24422
  if (Array.isArray(d.warnings) && d.warnings.length > 0) {
24192
24423
  html += '<div class="preview-section">';
24193
24424
  for (var w = 0; w < d.warnings.length; w++) {
@@ -24195,17 +24426,55 @@ function renderCronPreview(d) {
24195
24426
  }
24196
24427
  html += '</div>';
24197
24428
  }
24198
- // Summary header
24429
+
24430
+ // Summary
24199
24431
  html += '<div class="preview-section">';
24200
24432
  html += '<h4>Summary</h4>';
24201
- html += '<div style="font-size:12px;color:var(--text-secondary);line-height:1.6">';
24433
+ html += '<div style="font-size:12px;color:var(--text-secondary);line-height:1.7">';
24202
24434
  html += '<div><strong>Schedule:</strong> ' + esc(d.job.schedule) + '</div>';
24203
24435
  html += '<div><strong>Tier:</strong> ' + esc(d.tier) + ' (effort: ' + esc(d.effort) + (d.maxBudgetUsd ? ', budget: $' + d.maxBudgetUsd : '') + ')</div>';
24204
24436
  html += '<div><strong>Agent:</strong> ' + esc(d.profile ? (d.profile.name + ' (' + d.profile.slug + ')') : 'Clementine (global)') + '</div>';
24205
24437
  if (d.job.tags && d.job.tags.length) html += '<div><strong>Tags:</strong> ' + d.job.tags.map(function(t) { return '#' + esc(t); }).join(', ') + '</div>';
24206
24438
  if (d.job.category) html += '<div><strong>Category:</strong> ' + esc(d.job.category) + '</div>';
24207
24439
  html += '</div></div>';
24208
- // Skills (with full content) — the headline visibility win
24440
+
24441
+ // ── MCP servers — split into pinned (allowlist) vs auto-injected ─
24442
+ // The headline mismatch: a job's prompt may forbid kernel, but legacy
24443
+ // mode auto-injects it because the bundle router matched on prompt
24444
+ // text. Surface that explicitly with chip color so the user can act.
24445
+ var configuredMcp = (d.job && Array.isArray(d.job.allowedMcpServers)) ? d.job.allowedMcpServers : null;
24446
+ var pinnedSet = new Set(configuredMcp || []);
24447
+ var hasAllowlist = configuredMcp !== null && configuredMcp.length > 0;
24448
+ html += '<div class="preview-section">';
24449
+ html += '<h4>MCP servers attached at fire-time (' + d.mcpServers.length + ')</h4>';
24450
+ if (d.mcpServers.length === 0) {
24451
+ html += '<div style="color:var(--text-muted);font-size:12px;font-style:italic">No additional MCP servers (clementine-tools always available).</div>';
24452
+ } else {
24453
+ var anyAuto = false;
24454
+ var chips = '';
24455
+ for (var k = 0; k < d.mcpServers.length; k++) {
24456
+ var m = d.mcpServers[k];
24457
+ var isPinned = hasAllowlist && pinnedSet.has(m.name);
24458
+ var cls = (hasAllowlist && isPinned) ? 'preview-chip pinned' : (hasAllowlist ? 'preview-chip' : 'preview-chip auto');
24459
+ if (!hasAllowlist || !isPinned) anyAuto = true;
24460
+ chips += '<span class="' + cls + '" title="' + esc(m.description || '') + '">' + esc(m.name) + '</span>';
24461
+ }
24462
+ if (hasAllowlist) {
24463
+ html += '<div style="margin-bottom:6px"><span class="preview-chip-group-label">Pinned by your allowlist:</span></div>';
24464
+ } else if (anyAuto) {
24465
+ html += '<div style="margin-bottom:6px"><span class="preview-chip-group-label" style="color:var(--yellow)">Auto-injected by prompt match (no allowlist set):</span></div>';
24466
+ }
24467
+ html += '<div>' + chips + '</div>';
24468
+ if (!hasAllowlist && d.mcpServers.length > 0 && d.predictable !== true) {
24469
+ html += '<div style="margin-top:8px;padding:8px 10px;border-radius:6px;background:var(--bg-tertiary);font-size:11px;color:var(--text-muted);line-height:1.5">'
24470
+ + 'These servers were auto-attached because the prompt text matched bundle keywords. '
24471
+ + 'To restrict the surface, switch to Predictable Mode and add an explicit MCP allowlist on the Configure tab.'
24472
+ + '</div>';
24473
+ }
24474
+ }
24475
+ html += '</div>';
24476
+
24477
+ // ── Skills — pinned vs auto-matched, split visually ─
24209
24478
  html += '<div class="preview-section">';
24210
24479
  html += '<h4>Skills injected (' + d.skillsApplied.length + ')</h4>';
24211
24480
  if (d.skillsApplied.length === 0) {
@@ -24214,8 +24483,8 @@ function renderCronPreview(d) {
24214
24483
  for (var i = 0; i < d.skillsApplied.length; i++) {
24215
24484
  var s = d.skillsApplied[i];
24216
24485
  var sourceTag = s.source === 'pinned'
24217
- ? '<span style="color:var(--accent);font-size:10px;text-transform:uppercase;letter-spacing:0.4px">pinned</span>'
24218
- : '<span style="color:var(--text-muted);font-size:10px;text-transform:uppercase;letter-spacing:0.4px">auto-matched</span>';
24486
+ ? '<span style="color:var(--purple);font-size:10px;text-transform:uppercase;letter-spacing:0.4px;font-weight:600">PINNED</span>'
24487
+ : '<span style="color:var(--yellow);font-size:10px;text-transform:uppercase;letter-spacing:0.4px;font-weight:600">AUTO-MATCHED</span>';
24219
24488
  html += '<div class="preview-skill">';
24220
24489
  html += '<div class="preview-skill-title">' + esc(s.title) + ' <span style="color:var(--text-muted);font-size:10px;font-weight:normal">(' + esc(s.name) + ')</span> ' + sourceTag + '</div>';
24221
24490
  if (s.toolsUsed && s.toolsUsed.length) {
@@ -24226,21 +24495,7 @@ function renderCronPreview(d) {
24226
24495
  }
24227
24496
  }
24228
24497
  html += '</div>';
24229
- // MCP servers
24230
- html += '<div class="preview-section">';
24231
- html += '<h4>MCP servers (' + d.mcpServers.length + ')</h4>';
24232
- if (d.mcpServers.length === 0) {
24233
- html += '<div style="color:var(--text-muted);font-size:12px;font-style:italic">No additional MCP servers (clementine-tools always available).</div>';
24234
- } else {
24235
- for (var k = 0; k < d.mcpServers.length; k++) {
24236
- var m = d.mcpServers[k];
24237
- html += '<div style="padding:6px 0;border-bottom:1px dashed var(--border-light)">';
24238
- html += '<div style="font-weight:600;font-size:12px;color:var(--purple)">' + esc(m.name) + '</div>';
24239
- if (m.description) html += '<div style="font-size:11px;color:var(--text-muted)">' + esc(m.description) + '</div>';
24240
- html += '</div>';
24241
- }
24242
- }
24243
- html += '</div>';
24498
+
24244
24499
  // Tool allowlist
24245
24500
  html += '<div class="preview-section">';
24246
24501
  html += '<h4>Tool allowlist</h4>';
@@ -24252,9 +24507,10 @@ function renderCronPreview(d) {
24252
24507
  html += '</div>';
24253
24508
  }
24254
24509
  html += '</div>';
24255
- // Built prompt (collapsed by default)
24510
+
24511
+ // Built prompt (what the agent literally receives)
24256
24512
  html += '<div class="preview-section">';
24257
- html += '<h4>Built prompt <span style="font-weight:normal;color:var(--text-muted)">(' + d.builtPrompt.length + ' chars — what the agent receives)</span></h4>';
24513
+ html += '<h4>Built prompt <span style="font-weight:normal;color:var(--text-muted)">(' + d.builtPrompt.length + ' chars — what the agent receives verbatim)</span></h4>';
24258
24514
  html += '<div class="preview-prompt-box">' + esc(d.builtPrompt) + '</div>';
24259
24515
  html += '</div>';
24260
24516
  return html;
@@ -24263,8 +24519,15 @@ function renderCronPreview(d) {
24263
24519
  function closeCronModal() {
24264
24520
  document.getElementById('cron-modal').classList.remove('show');
24265
24521
  editingCronJob = null;
24522
+ _cronPreviewLoadedFor = null;
24266
24523
  var attachList = document.getElementById('cron-attachments-list');
24267
24524
  if (attachList) attachList.innerHTML = '';
24525
+ var bannerHost = document.getElementById('cron-legacy-banner-host');
24526
+ if (bannerHost) bannerHost.innerHTML = '';
24527
+ // Reset preview pane content so a stale view from a previous job doesn't
24528
+ // flash on the next open.
24529
+ var previewBody = document.getElementById('cron-preview-body');
24530
+ if (previewBody) previewBody.innerHTML = '<div style="padding:36px 24px;color:var(--text-muted);text-align:center;font-size:13px">Save the task first, then switch back here to see exactly what the agent will receive.</div>';
24268
24531
  resetCronTrainingChat();
24269
24532
  }
24270
24533
 
@@ -24414,6 +24677,7 @@ async function saveCronJob() {
24414
24677
  predictable,
24415
24678
  };
24416
24679
 
24680
+ var wasEditing = !!editingCronJob;
24417
24681
  if (editingCronJob) {
24418
24682
  await apiJson('PUT', '/api/cron/' + encodeURIComponent(editingCronJob), body);
24419
24683
  if (_pendingAttachments.length > 0) await uploadPendingAttachments(editingCronJob);
@@ -24423,8 +24687,18 @@ async function saveCronJob() {
24423
24687
  var attachJobName = _cronAgentContext ? (_cronAgentContext + ':' + name) : name;
24424
24688
  if (_pendingAttachments.length > 0) await uploadPendingAttachments(attachJobName);
24425
24689
  }
24426
- closeCronModal();
24690
+ // Refresh card list so the data is fresh for any subsequent edit.
24427
24691
  refreshCron();
24692
+ // After save: stay open and flip to "What will run" so the user can
24693
+ // confirm what they just saved actually runs the way they intended.
24694
+ // This is the close-the-loop UX move that makes Predictable Mode visible.
24695
+ markCronPreviewDirty();
24696
+ if (wasEditing) {
24697
+ toast('Saved. Showing what will run…', 'success');
24698
+ switchCronTab('preview');
24699
+ } else {
24700
+ closeCronModal();
24701
+ }
24428
24702
  }
24429
24703
 
24430
24704
  // ── Cron Training Chat ───────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.69",
3
+ "version": "1.18.70",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",