@yemi33/minions 0.1.2100 → 0.1.2102
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/slim/body.html +251 -0
- package/dashboard/slim/js/chat.js +329 -0
- package/dashboard/slim/js/command-send.js +190 -0
- package/dashboard/slim/js/helpers.js +26 -0
- package/dashboard/slim/js/history.js +442 -0
- package/dashboard/slim/js/link-pr.js +83 -0
- package/dashboard/slim/js/members.js +138 -0
- package/dashboard/slim/js/modals-tiles.js +161 -0
- package/dashboard/slim/js/projects.js +209 -0
- package/dashboard/slim/js/settings.js +130 -0
- package/dashboard/slim/js/status.js +122 -0
- package/dashboard/slim/layout.html +21 -0
- package/dashboard/slim/styles.css +1128 -0
- package/dashboard-build.js +33 -1
- package/dashboard.js +11 -10
- package/docs/pr-review-fix-loop.md +2 -2
- package/engine/ado.js +21 -0
- package/engine/github.js +21 -0
- package/engine/lifecycle.js +35 -0
- package/engine/shared.js +9 -11
- package/package.json +1 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
|
|
2
|
+
<div class="topbar">
|
|
3
|
+
<div class="topbar-title">Minions <span class="badge">SLIM UX</span></div>
|
|
4
|
+
<div class="topbar-actions">
|
|
5
|
+
<!-- Buttons get their click handlers wired via addEventListener inside the
|
|
6
|
+
IIFE below. Inline `onclick=` was brittle: it depended on the IIFE
|
|
7
|
+
exporting handlers to window, which a single mid-IIFE error could
|
|
8
|
+
silently break (handler attaches before crash, button still renders,
|
|
9
|
+
click fires but no global handler). addEventListener attaches inside
|
|
10
|
+
the same scope and is observable in DevTools when wiring fails. -->
|
|
11
|
+
<button id="slim-new-chat-btn" class="icon-btn" title="New chat (clears session)">✎</button>
|
|
12
|
+
<button id="slim-settings-btn" class="icon-btn" title="Settings">⚙</button>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="layout">
|
|
17
|
+
<!-- ACTIONS — chat + 4 concept-collapse buttons.
|
|
18
|
+
Visually dominant; this is what the human said was buried in the full
|
|
19
|
+
dashboard ("Command Center is not prominent enough"). -->
|
|
20
|
+
<div class="panel panel-actions">
|
|
21
|
+
<div class="panel-header">
|
|
22
|
+
Actions
|
|
23
|
+
<span class="panel-sub">Command Center & quick triggers</span>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="actions-chat">
|
|
26
|
+
<div class="chat-messages" id="chat-messages">
|
|
27
|
+
<div class="chat-empty">No messages yet — say hi to Command Center.</div>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="chat-context-strip" id="chat-context-strip" style="display:none">
|
|
30
|
+
<span class="context-label">Working in</span>
|
|
31
|
+
<span id="chat-context-controls"></span>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="chat-input-wrap">
|
|
34
|
+
<textarea
|
|
35
|
+
id="chat-input"
|
|
36
|
+
class="chat-input"
|
|
37
|
+
rows="2"
|
|
38
|
+
placeholder="Ask anything or give a command... (Enter to send, Shift+Enter for newline)"
|
|
39
|
+
></textarea>
|
|
40
|
+
<button id="chat-stop" class="chat-stop" type="button" title="Stop the in-flight response">Stop</button>
|
|
41
|
+
<button id="chat-send" class="chat-send" type="button">Send</button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="actions-buttons">
|
|
45
|
+
<button id="slim-linkpr-btn" class="link-pr-btn" type="button">
|
|
46
|
+
<span class="link-pr-icon">🔗</span>
|
|
47
|
+
<span class="link-pr-label">Link PR</span>
|
|
48
|
+
<span class="link-pr-sub">track a pull request</span>
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- STATUS — cockpit tiles. Lit when their indicator is non-zero. -->
|
|
54
|
+
<div class="panel panel-status">
|
|
55
|
+
<div class="panel-header">
|
|
56
|
+
Status
|
|
57
|
+
<span class="panel-sub" id="cockpit-stamp">refreshing…</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="panel-body">
|
|
60
|
+
<!-- Team sub-section: square member cards, one per agent. Populated by
|
|
61
|
+
renderMembers() from the /api/status `agents` array. This replaces
|
|
62
|
+
the old "Minions working" cockpit tile with a richer per-member view;
|
|
63
|
+
cards are clickable and open the agent-detail modal. -->
|
|
64
|
+
<div class="team-section">
|
|
65
|
+
<div class="member-grid" id="member-grid">
|
|
66
|
+
<div class="member-empty">Loading team…</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<!-- System sub-section: cockpit indicator tiles. The per-minion "working"
|
|
70
|
+
count now lives in the Team cards above, so that tile is removed. -->
|
|
71
|
+
<div class="cockpit-section">
|
|
72
|
+
<div class="cockpit-grid" id="cockpit-grid">
|
|
73
|
+
<div class="cockpit-tile" data-tile="engine">
|
|
74
|
+
<div class="cockpit-label"><span class="cockpit-dot"></span> Engine</div>
|
|
75
|
+
<div class="cockpit-value dim">—</div>
|
|
76
|
+
<div class="cockpit-detail">checking…</div>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="cockpit-tile" data-tile="dispatches">
|
|
79
|
+
<div class="cockpit-label"><span class="cockpit-dot"></span> Active dispatches</div>
|
|
80
|
+
<div class="cockpit-value dim">0</div>
|
|
81
|
+
<div class="cockpit-detail">none in flight</div>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="cockpit-tile" data-tile="queued">
|
|
84
|
+
<div class="cockpit-label"><span class="cockpit-dot"></span> Queued work</div>
|
|
85
|
+
<div class="cockpit-value dim">0</div>
|
|
86
|
+
<div class="cockpit-detail">queue empty</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="cockpit-tile" data-tile="prs">
|
|
89
|
+
<button id="slim-tile-linkpr-chip" class="linkpr-chip on-tile" type="button" title="Link a pull request">+ Link PR</button>
|
|
90
|
+
<div class="cockpit-label"><span class="cockpit-dot"></span> Active PRs</div>
|
|
91
|
+
<div class="cockpit-value dim">0</div>
|
|
92
|
+
<div class="cockpit-detail">no open PRs</div>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="cockpit-tile" data-tile="watches">
|
|
95
|
+
<div class="cockpit-label"><span class="cockpit-dot"></span> Watches</div>
|
|
96
|
+
<div class="cockpit-value dim">0</div>
|
|
97
|
+
<div class="cockpit-detail">no watches set</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<!-- HISTORY — chronological event log. Newest at top.
|
|
105
|
+
Round 1 sources: dispatch.completed (last 10), dispatch.active
|
|
106
|
+
(currently working), engineLog. -->
|
|
107
|
+
<div class="panel panel-history">
|
|
108
|
+
<div class="panel-header">
|
|
109
|
+
History
|
|
110
|
+
<span class="panel-sub" id="history-stamp">…</span>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="panel-body">
|
|
113
|
+
<div class="history-list" id="history-list">
|
|
114
|
+
<div class="history-empty">Loading recent activity…</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<!-- Settings dialog (preserved from r2): lists experimental flags so the
|
|
121
|
+
user can flip 'slim-ux' off and revert to the original dashboard. -->
|
|
122
|
+
<div class="modal-bg" id="slim-settings-modal">
|
|
123
|
+
<div class="modal">
|
|
124
|
+
<div class="modal-header">
|
|
125
|
+
<h3>Settings · Experimental flags</h3>
|
|
126
|
+
<button id="slim-settings-close" class="icon-btn" title="Close">×</button>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="modal-body" id="slim-settings-body">
|
|
129
|
+
<p>Loading flags…</p>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<!-- Generic action prompt modal — used by the four Action buttons.
|
|
135
|
+
TEMP: routes through the chat input rather than calling /api/work-items
|
|
136
|
+
etc. directly. CC parses "create a work item: …" into an actions block
|
|
137
|
+
and dispatches it. Round-2 will replace this with direct API calls. -->
|
|
138
|
+
<!-- Link Pull Request dialog — mirrors the old dashboard's PR-tab dialog.
|
|
139
|
+
Submits to POST /api/pull-requests/link. Opened from the action footer
|
|
140
|
+
button, the Active PRs tile chip, and the PR detail modal header. -->
|
|
141
|
+
<div class="modal-bg" id="slim-linkpr-modal">
|
|
142
|
+
<div class="modal">
|
|
143
|
+
<div class="modal-header">
|
|
144
|
+
<h3>Link Pull Request</h3>
|
|
145
|
+
<button id="slim-linkpr-close" class="icon-btn" title="Close">×</button>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="modal-body">
|
|
148
|
+
<div id="slim-linkpr-noproj" class="linkpr-warning" style="display:none">
|
|
149
|
+
<div class="linkpr-warning-title">⚠ No project configured.</div>
|
|
150
|
+
<div>This PR will be tracked, but auto-fix and auto-review can’t dispatch against it — agents need a project worktree. Add a project first to enable automation.</div>
|
|
151
|
+
</div>
|
|
152
|
+
<label class="linkpr-label">PR URL
|
|
153
|
+
<input id="slim-linkpr-url" class="linkpr-input" type="text" placeholder="https://github.com/org/repo/pull/123">
|
|
154
|
+
</label>
|
|
155
|
+
<label class="linkpr-label">Title
|
|
156
|
+
<input id="slim-linkpr-title" class="linkpr-input" type="text" placeholder="Short description (optional — auto-detected from URL)">
|
|
157
|
+
</label>
|
|
158
|
+
<label class="linkpr-label">Project
|
|
159
|
+
<select id="slim-linkpr-project" class="linkpr-input"></select>
|
|
160
|
+
</label>
|
|
161
|
+
<label class="linkpr-label">Context
|
|
162
|
+
<textarea id="slim-linkpr-context" class="linkpr-input" rows="3" placeholder="Why are you linking this? What should agents know about it?"></textarea>
|
|
163
|
+
</label>
|
|
164
|
+
<label class="linkpr-check">
|
|
165
|
+
<input type="checkbox" id="slim-linkpr-observe">
|
|
166
|
+
<span>Auto-observe <span class="linkpr-hint-inline">(monitor builds, resolve comments, fix failures)</span></span>
|
|
167
|
+
</label>
|
|
168
|
+
<div class="linkpr-hint">Off = context only (e.g. a teammate’s PR). On = agents actively monitor and fix issues.</div>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="modal-footer">
|
|
171
|
+
<button id="slim-linkpr-cancel" class="btn-secondary" type="button">Cancel</button>
|
|
172
|
+
<button id="slim-linkpr-submit" class="btn-primary" type="button">Link PR</button>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<!-- Add Project modal — entrypoint sits next to the "Working in" picker. Hits
|
|
178
|
+
/api/projects/browse for the folder dialog and /api/projects/add for the
|
|
179
|
+
link. Same endpoints as the main dashboard's Scan Projects flow but with
|
|
180
|
+
a single-path single-add UX tuned for slim. -->
|
|
181
|
+
<div class="modal-bg" id="slim-add-project-modal">
|
|
182
|
+
<div class="modal">
|
|
183
|
+
<div class="modal-header">
|
|
184
|
+
<h3>Link a project</h3>
|
|
185
|
+
<button id="slim-add-project-close" class="icon-btn" title="Close">×</button>
|
|
186
|
+
</div>
|
|
187
|
+
<div class="modal-body">
|
|
188
|
+
<p>Point to a local git repository. Minions will read its remote, name, and main branch automatically.</p>
|
|
189
|
+
<div style="display:flex; gap:8px; align-items:stretch">
|
|
190
|
+
<input id="slim-add-project-path" type="text" placeholder="C:\path\to\repo" style="flex:1; padding:8px 10px; background:var(--bg); border:1px solid var(--border); border-radius:var(--radius); color:var(--text); font-size:13px; font-family:inherit">
|
|
191
|
+
<button id="slim-add-project-browse" class="btn-secondary" type="button">Browse…</button>
|
|
192
|
+
</div>
|
|
193
|
+
<div id="slim-add-project-msg" style="margin-top:10px; font-size:12px; min-height:16px"></div>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="modal-footer">
|
|
196
|
+
<button id="slim-add-project-cancel" class="btn-secondary" type="button">Cancel</button>
|
|
197
|
+
<button id="slim-add-project-submit" class="btn-primary" type="button">Link project</button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<!-- Completion details modal (W-betterHistory). Opens when a completion
|
|
203
|
+
card in the history feed is clicked. Populated dynamically by
|
|
204
|
+
showCompletionDetails(d) — see the script below. -->
|
|
205
|
+
<div class="modal-bg" id="slim-completions-modal">
|
|
206
|
+
<div class="modal">
|
|
207
|
+
<div class="modal-header">
|
|
208
|
+
<h3 id="slim-completions-title">Completion</h3>
|
|
209
|
+
<button id="slim-completions-close" class="icon-btn" title="Close">×</button>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="modal-body" id="slim-completions-body"></div>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<!-- Agent detail modal — opened by clicking a Team member card. Body is
|
|
216
|
+
populated by openAgentDetail(id) from the cached /api/status agents. -->
|
|
217
|
+
<div class="modal-bg" id="slim-agent-modal">
|
|
218
|
+
<div class="modal">
|
|
219
|
+
<div class="modal-header">
|
|
220
|
+
<h3 id="slim-agent-title">Agent</h3>
|
|
221
|
+
<button id="slim-agent-close" class="icon-btn" title="Close">×</button>
|
|
222
|
+
</div>
|
|
223
|
+
<div class="modal-body" id="slim-agent-body"></div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<!-- Tool-calls modal — opened by clicking a response's tool panel. Body is
|
|
228
|
+
populated by openToolsModal(list) with the full tool-call list. -->
|
|
229
|
+
<div class="modal-bg" id="slim-tools-modal">
|
|
230
|
+
<div class="modal">
|
|
231
|
+
<div class="modal-header">
|
|
232
|
+
<h3 id="slim-tools-title">Tool calls</h3>
|
|
233
|
+
<button id="slim-tools-close" class="icon-btn" title="Close">×</button>
|
|
234
|
+
</div>
|
|
235
|
+
<div class="modal-body" id="slim-tools-body"></div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<!-- Cockpit-tile detail modal — opened by clicking a Status tile. Body is
|
|
240
|
+
populated by openTileModal(key) from the latest /api/status snapshot. -->
|
|
241
|
+
<div class="modal-bg" id="slim-tile-modal">
|
|
242
|
+
<div class="modal">
|
|
243
|
+
<div class="modal-header">
|
|
244
|
+
<h3 id="slim-tile-title">Details</h3>
|
|
245
|
+
<button id="slim-tile-modal-linkpr" class="linkpr-chip" type="button" style="display:none;margin-left:auto;margin-right:8px">+ Link PR</button>
|
|
246
|
+
<button id="slim-tile-close" class="icon-btn" title="Close">×</button>
|
|
247
|
+
</div>
|
|
248
|
+
<div class="modal-body" id="slim-tile-body"></div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// ── Chatbox wired to /api/command-center/stream ──────────────
|
|
2
|
+
// Mirrors the full dashboard's command center: SSE streaming, tool-call
|
|
3
|
+
// progress, abort, and per-tab session continuity.
|
|
4
|
+
var SLIM_STORAGE_KEY = 'slim-cc-state-v1';
|
|
5
|
+
var SLIM_TAB_KEY = 'slim-cc-tabid-v1';
|
|
6
|
+
var SLIM_PROJECT_KEY = 'slim-current-project-v1';
|
|
7
|
+
var SLIM_MAX_MESSAGES = 30;
|
|
8
|
+
var STREAM_TIMEOUT_MS = (60 * 60 * 1000) + 60000;
|
|
9
|
+
|
|
10
|
+
var sessionId = null;
|
|
11
|
+
var messages = [];
|
|
12
|
+
var sending = false;
|
|
13
|
+
var abortController = null;
|
|
14
|
+
var currentProject = null;
|
|
15
|
+
try { currentProject = localStorage.getItem(SLIM_PROJECT_KEY) || null; } catch (_e) { /* private mode */ }
|
|
16
|
+
|
|
17
|
+
var tabId = (function() {
|
|
18
|
+
try {
|
|
19
|
+
var t = sessionStorage.getItem(SLIM_TAB_KEY);
|
|
20
|
+
if (t) return t;
|
|
21
|
+
t = 'slim-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
22
|
+
sessionStorage.setItem(SLIM_TAB_KEY, t);
|
|
23
|
+
return t;
|
|
24
|
+
} catch (_e) {
|
|
25
|
+
return 'slim-' + Date.now().toString(36);
|
|
26
|
+
}
|
|
27
|
+
})();
|
|
28
|
+
|
|
29
|
+
var msgsEl = document.getElementById('chat-messages');
|
|
30
|
+
var inputEl = document.getElementById('chat-input');
|
|
31
|
+
var sendBtn = document.getElementById('chat-send');
|
|
32
|
+
var stopBtn = document.getElementById('chat-stop');
|
|
33
|
+
|
|
34
|
+
function loadState() {
|
|
35
|
+
try {
|
|
36
|
+
var raw = localStorage.getItem(SLIM_STORAGE_KEY);
|
|
37
|
+
if (!raw) return;
|
|
38
|
+
var data = JSON.parse(raw) || {};
|
|
39
|
+
sessionId = data.sessionId || null;
|
|
40
|
+
messages = Array.isArray(data.messages) ? data.messages.slice(-SLIM_MAX_MESSAGES) : [];
|
|
41
|
+
} catch (_e) { /* ignore */ }
|
|
42
|
+
}
|
|
43
|
+
var saveDebounce = null;
|
|
44
|
+
function saveState() {
|
|
45
|
+
if (saveDebounce) return;
|
|
46
|
+
saveDebounce = setTimeout(function() {
|
|
47
|
+
saveDebounce = null;
|
|
48
|
+
try {
|
|
49
|
+
localStorage.setItem(SLIM_STORAGE_KEY, JSON.stringify({
|
|
50
|
+
sessionId: sessionId,
|
|
51
|
+
messages: messages.slice(-SLIM_MAX_MESSAGES),
|
|
52
|
+
}));
|
|
53
|
+
} catch (_e) { /* localStorage full */ }
|
|
54
|
+
}, 300);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function escHtmlChat(s) {
|
|
58
|
+
return String(s == null ? '' : s)
|
|
59
|
+
.replace(/&/g, '&')
|
|
60
|
+
.replace(/</g, '<')
|
|
61
|
+
.replace(/>/g, '>')
|
|
62
|
+
.replace(/"/g, '"')
|
|
63
|
+
.replace(/'/g, ''');
|
|
64
|
+
}
|
|
65
|
+
function renderMarkdown(text) {
|
|
66
|
+
if (!text) return '';
|
|
67
|
+
var html = escHtmlChat(text);
|
|
68
|
+
var codeSlots = [];
|
|
69
|
+
html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, function(_, _lang, code) {
|
|
70
|
+
codeSlots.push('<pre><code>' + code + '</code></pre>');
|
|
71
|
+
return '\u0001CB' + (codeSlots.length - 1) + '\u0001';
|
|
72
|
+
});
|
|
73
|
+
html = html.replace(/`([^`\n]+)`/g, function(_, code) {
|
|
74
|
+
codeSlots.push('<code>' + code + '</code>');
|
|
75
|
+
return '\u0001CB' + (codeSlots.length - 1) + '\u0001';
|
|
76
|
+
});
|
|
77
|
+
html = html.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>');
|
|
78
|
+
html = html.replace(/(^|[^*\w])\*([^*\n]+)\*(?!\w)/g, '$1<em>$2</em>');
|
|
79
|
+
html = html.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, function(_, label, href) {
|
|
80
|
+
if (!/^(https?:|mailto:|\/)/i.test(href)) return label;
|
|
81
|
+
return '<a href="' + href + '" target="_blank" rel="noopener">' + label + '</a>';
|
|
82
|
+
});
|
|
83
|
+
html = html.replace(/\u0001CB(\d+)\u0001/g, function(_, i) { return codeSlots[+i] || ''; });
|
|
84
|
+
return html;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Reconcile a streamed `chunk` frame against the text accumulated so far.
|
|
88
|
+
// The CC server emits the FULL accumulated text in each chunk (not a delta),
|
|
89
|
+
// but the pool path and re-attach snapshots can resend or partially overlap.
|
|
90
|
+
// Naive `+=` therefore duplicates ("Let me…Let me locate…"); this collapses
|
|
91
|
+
// every case: identical, cumulative extension, stale prefix, or a tail/head
|
|
92
|
+
// overlap, falling back to append only when there's no overlap.
|
|
93
|
+
function mergeStreamText(prev, incoming) {
|
|
94
|
+
var current = prev || '';
|
|
95
|
+
var next = incoming || '';
|
|
96
|
+
if (!current) return next;
|
|
97
|
+
if (!next) return current;
|
|
98
|
+
if (next === current) return current;
|
|
99
|
+
if (next.indexOf(current) === 0) return next; // incoming extends prev → replace
|
|
100
|
+
if (current.indexOf(next) === 0) return current; // stale/short frame → keep
|
|
101
|
+
for (var overlap = Math.min(current.length, next.length); overlap > 0; overlap--) {
|
|
102
|
+
if (current.slice(-overlap) === next.slice(0, overlap)) return current + next.slice(overlap);
|
|
103
|
+
}
|
|
104
|
+
return current + next;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function clearEmpty() {
|
|
108
|
+
var empty = msgsEl.querySelector('.chat-empty');
|
|
109
|
+
if (empty) empty.remove();
|
|
110
|
+
}
|
|
111
|
+
function scrollToBottom() {
|
|
112
|
+
msgsEl.scrollTop = msgsEl.scrollHeight;
|
|
113
|
+
}
|
|
114
|
+
function appendBubble(role, text, toolCalls) {
|
|
115
|
+
clearEmpty();
|
|
116
|
+
var div = document.createElement('div');
|
|
117
|
+
div.className = 'chat-msg ' + role;
|
|
118
|
+
if (role === 'assistant') {
|
|
119
|
+
var body = document.createElement('div');
|
|
120
|
+
body.className = 'chat-body';
|
|
121
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: renderMarkdown() escapes input via escHtmlChat() first; later substitutions only re-insert pre-escaped <pre><code>/<a>/<strong>/<em> spans with scheme-validated hrefs
|
|
122
|
+
body.innerHTML = renderMarkdown(text);
|
|
123
|
+
div.appendChild(body);
|
|
124
|
+
// Replay the collapsed tool panel for responses that ran tools.
|
|
125
|
+
if (toolCalls && toolCalls.length) {
|
|
126
|
+
var toolbar = document.createElement('div');
|
|
127
|
+
toolbar.className = 'chat-toolbar';
|
|
128
|
+
var tp = buildToolsPanel(toolCalls);
|
|
129
|
+
toolbar.appendChild(tp.panel);
|
|
130
|
+
div.appendChild(toolbar);
|
|
131
|
+
tp.scroll.scrollTop = tp.scroll.scrollHeight;
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
div.textContent = text;
|
|
135
|
+
}
|
|
136
|
+
msgsEl.appendChild(div);
|
|
137
|
+
scrollToBottom();
|
|
138
|
+
return div;
|
|
139
|
+
}
|
|
140
|
+
function appendActionStatus(severity, label) {
|
|
141
|
+
var div = document.createElement('div');
|
|
142
|
+
div.className = 'chat-action ' + (severity || '');
|
|
143
|
+
div.textContent = label;
|
|
144
|
+
msgsEl.appendChild(div);
|
|
145
|
+
scrollToBottom();
|
|
146
|
+
return div;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Render a tool invocation as a one-line string. `full` keeps the complete
|
|
150
|
+
// command/args (used by the tool-calls modal); otherwise long values are
|
|
151
|
+
// clipped for the collapsed 3-line panel.
|
|
152
|
+
function formatTool(name, input, full) {
|
|
153
|
+
var inp = input || {};
|
|
154
|
+
function clip(s, n) {
|
|
155
|
+
s = String(s == null ? '' : s);
|
|
156
|
+
return (!full && s.length > n) ? s.slice(0, n - 3) + '...' : s;
|
|
157
|
+
}
|
|
158
|
+
switch (name) {
|
|
159
|
+
case 'Bash': return '$ ' + clip(inp.command, 80);
|
|
160
|
+
case 'Read': return 'Reading ' + (inp.file_path || '');
|
|
161
|
+
case 'Edit': return 'Editing ' + (inp.file_path || '');
|
|
162
|
+
case 'Write': return 'Writing ' + (inp.file_path || '');
|
|
163
|
+
case 'Grep': return 'Searching `' + (inp.pattern || '') + '` in ' + (inp.path || '.');
|
|
164
|
+
case 'Glob': return 'Glob ' + (inp.pattern || '');
|
|
165
|
+
case 'WebFetch': return 'Fetch ' + (inp.url || '');
|
|
166
|
+
case 'WebSearch': return 'Search "' + (inp.query || '') + '"';
|
|
167
|
+
case 'TodoWrite': {
|
|
168
|
+
var items = Array.isArray(inp.todos) ? inp.todos : [];
|
|
169
|
+
return 'Update todos (' + items.length + ' items)';
|
|
170
|
+
}
|
|
171
|
+
default: {
|
|
172
|
+
var keys = Object.keys(inp);
|
|
173
|
+
if (keys.length === 0) return name + '()';
|
|
174
|
+
if (full) {
|
|
175
|
+
return name + '(' + keys.map(function(k) { return k + ': ' + String(inp[k]); }).join(', ') + ')';
|
|
176
|
+
}
|
|
177
|
+
return name + '(' + keys[0] + ': ' + clip(inp[keys[0]], 40) + ')';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// A stored tool call is { name, input }; tolerate legacy plain-string entries.
|
|
183
|
+
function toolLineText(item, full) {
|
|
184
|
+
if (typeof item === 'string') return item;
|
|
185
|
+
return formatTool(item && item.name, item && item.input, full);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function makeToolLine(text) {
|
|
189
|
+
var line = document.createElement('div');
|
|
190
|
+
line.className = 'chat-tool';
|
|
191
|
+
line.textContent = text;
|
|
192
|
+
return line;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Open the tool-calls modal showing the full list (long commands wrapped).
|
|
196
|
+
function openToolsModal(list) {
|
|
197
|
+
var modal = document.getElementById('slim-tools-modal');
|
|
198
|
+
var body = document.getElementById('slim-tools-body');
|
|
199
|
+
var titleEl = document.getElementById('slim-tools-title');
|
|
200
|
+
if (!modal || !body) return;
|
|
201
|
+
if (titleEl) titleEl.textContent = 'Tool calls (' + (list ? list.length : 0) + ')';
|
|
202
|
+
body.textContent = '';
|
|
203
|
+
if (!list || !list.length) {
|
|
204
|
+
var none = document.createElement('div');
|
|
205
|
+
none.className = 'tools-modal-empty';
|
|
206
|
+
none.textContent = 'No tool calls.';
|
|
207
|
+
body.appendChild(none);
|
|
208
|
+
} else {
|
|
209
|
+
list.forEach(function(t) {
|
|
210
|
+
var row = document.createElement('div');
|
|
211
|
+
row.className = 'tools-modal-line';
|
|
212
|
+
row.textContent = toolLineText(t, true);
|
|
213
|
+
body.appendChild(row);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
modal.classList.add('open');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Build the collapsed tool panel (max 3 visible lines, scrolls, click -> modal).
|
|
220
|
+
// `list` is the backing array of formatted summaries — shared by reference so
|
|
221
|
+
// a live stream can push into it and the modal always shows the latest set.
|
|
222
|
+
function buildToolsPanel(list) {
|
|
223
|
+
var panel = document.createElement('div');
|
|
224
|
+
panel.className = 'chat-tools-panel';
|
|
225
|
+
panel.setAttribute('role', 'button');
|
|
226
|
+
panel.setAttribute('tabindex', '0');
|
|
227
|
+
panel.title = 'View all tool calls';
|
|
228
|
+
var scroll = document.createElement('div');
|
|
229
|
+
scroll.className = 'chat-tools';
|
|
230
|
+
list.forEach(function(t) { scroll.appendChild(makeToolLine(toolLineText(t, false))); });
|
|
231
|
+
panel.appendChild(scroll);
|
|
232
|
+
function open() { openToolsModal(list); }
|
|
233
|
+
panel.addEventListener('click', open);
|
|
234
|
+
panel.addEventListener('keydown', function(ev) {
|
|
235
|
+
if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); open(); }
|
|
236
|
+
});
|
|
237
|
+
return { panel: panel, scroll: scroll };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function buildStreamBubble() {
|
|
241
|
+
clearEmpty();
|
|
242
|
+
var div = document.createElement('div');
|
|
243
|
+
div.className = 'chat-msg assistant streaming';
|
|
244
|
+
|
|
245
|
+
var body = document.createElement('div');
|
|
246
|
+
body.className = 'chat-body';
|
|
247
|
+
div.appendChild(body);
|
|
248
|
+
|
|
249
|
+
// Bottom toolbar: thinking indicator (left) + tool panel (right). The panel
|
|
250
|
+
// is hidden until the first tool call arrives.
|
|
251
|
+
var toolbar = document.createElement('div');
|
|
252
|
+
toolbar.className = 'chat-toolbar';
|
|
253
|
+
div.appendChild(toolbar);
|
|
254
|
+
|
|
255
|
+
var thinking = document.createElement('div');
|
|
256
|
+
thinking.className = 'chat-thinking';
|
|
257
|
+
thinking.innerHTML = 'Thinking<span class="chat-thinking-dots"><span></span><span></span><span></span></span>';
|
|
258
|
+
toolbar.appendChild(thinking);
|
|
259
|
+
|
|
260
|
+
var toolList = [];
|
|
261
|
+
var tp = buildToolsPanel(toolList);
|
|
262
|
+
tp.panel.style.display = 'none';
|
|
263
|
+
toolbar.appendChild(tp.panel);
|
|
264
|
+
|
|
265
|
+
msgsEl.appendChild(div);
|
|
266
|
+
scrollToBottom();
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
root: div,
|
|
270
|
+
body: body,
|
|
271
|
+
thinking: thinking,
|
|
272
|
+
getToolList: function() { return toolList; },
|
|
273
|
+
addTool: function(name, input) {
|
|
274
|
+
toolList.push({ name: name, input: input });
|
|
275
|
+
tp.scroll.appendChild(makeToolLine(formatTool(name, input, false)));
|
|
276
|
+
tp.panel.style.display = '';
|
|
277
|
+
tp.scroll.scrollTop = tp.scroll.scrollHeight; // newest at bottom; older scroll off the top
|
|
278
|
+
scrollToBottom();
|
|
279
|
+
},
|
|
280
|
+
setText: function(text) {
|
|
281
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: renderMarkdown() escapes input via escHtmlChat() first; later substitutions only re-insert pre-escaped <pre><code>/<a>/<strong>/<em> spans with scheme-validated hrefs
|
|
282
|
+
body.innerHTML = renderMarkdown(text || '');
|
|
283
|
+
scrollToBottom();
|
|
284
|
+
},
|
|
285
|
+
finalize: function(text) {
|
|
286
|
+
if (thinking && thinking.parentNode) thinking.parentNode.removeChild(thinking);
|
|
287
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: renderMarkdown() escapes input via escHtmlChat() first; later substitutions only re-insert pre-escaped <pre><code>/<a>/<strong>/<em> spans with scheme-validated hrefs
|
|
288
|
+
body.innerHTML = renderMarkdown(text || '');
|
|
289
|
+
div.classList.remove('streaming');
|
|
290
|
+
// No tools ran — drop the now-empty toolbar entirely.
|
|
291
|
+
if (!toolList.length && toolbar.parentNode) toolbar.parentNode.removeChild(toolbar);
|
|
292
|
+
scrollToBottom();
|
|
293
|
+
},
|
|
294
|
+
replaceWithError: function(message) {
|
|
295
|
+
if (thinking && thinking.parentNode) thinking.parentNode.removeChild(thinking);
|
|
296
|
+
div.className = 'chat-msg error';
|
|
297
|
+
div.innerHTML = '';
|
|
298
|
+
div.textContent = message;
|
|
299
|
+
scrollToBottom();
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function setSending(on) {
|
|
305
|
+
sending = on;
|
|
306
|
+
sendBtn.disabled = on;
|
|
307
|
+
sendBtn.textContent = on ? 'Sending…' : 'Send';
|
|
308
|
+
stopBtn.style.display = on ? 'block' : 'none';
|
|
309
|
+
inputEl.disabled = on;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function rerenderHistory() {
|
|
313
|
+
msgsEl.innerHTML = '';
|
|
314
|
+
if (!messages.length) {
|
|
315
|
+
var empty = document.createElement('div');
|
|
316
|
+
empty.className = 'chat-empty';
|
|
317
|
+
empty.textContent = 'No messages yet — say hi to Command Center.';
|
|
318
|
+
msgsEl.appendChild(empty);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
for (var i = 0; i < messages.length; i++) {
|
|
322
|
+
var m = messages[i];
|
|
323
|
+
if (m.role === 'action') appendActionStatus(m.severity || '', m.text || '');
|
|
324
|
+
else appendBubble(m.role, m.text || '', m.toolCalls);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
loadState();
|
|
328
|
+
rerenderHistory();
|
|
329
|
+
|