@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.
@@ -0,0 +1,83 @@
1
+ // ── Link Pull Request ──────────────────────────────────────────
2
+ // Mirrors the old dashboard's PR-tab dialog: POST /api/pull-requests/link
3
+ // with { url, title, project, context, autoObserve }. Opened from the action
4
+ // footer button, the Active PRs tile chip, and the PR detail modal header.
5
+ function openLinkPrModal() {
6
+ var modal = document.getElementById('slim-linkpr-modal');
7
+ if (!modal) return;
8
+ var projects = (lastStatusData && Array.isArray(lastStatusData.projects) ? lastStatusData.projects : [])
9
+ .map(function(p) { return p && p.name ? String(p.name) : null; })
10
+ .filter(Boolean);
11
+ var sel = document.getElementById('slim-linkpr-project');
12
+ sel.textContent = '';
13
+ var auto = document.createElement('option');
14
+ auto.value = '';
15
+ auto.textContent = 'Auto-detect from URL (central if no unique match)';
16
+ sel.appendChild(auto);
17
+ projects.forEach(function(name) {
18
+ var o = document.createElement('option');
19
+ o.value = name;
20
+ o.textContent = name;
21
+ sel.appendChild(o);
22
+ });
23
+ var warn = document.getElementById('slim-linkpr-noproj');
24
+ if (warn) warn.style.display = projects.length ? 'none' : '';
25
+ document.getElementById('slim-linkpr-url').value = '';
26
+ document.getElementById('slim-linkpr-title').value = '';
27
+ document.getElementById('slim-linkpr-context').value = '';
28
+ document.getElementById('slim-linkpr-observe').checked = false;
29
+ modal.classList.add('open');
30
+ setTimeout(function() { var u = document.getElementById('slim-linkpr-url'); if (u) u.focus(); }, 40);
31
+ }
32
+ function closeLinkPrModal() {
33
+ var modal = document.getElementById('slim-linkpr-modal');
34
+ if (modal) modal.classList.remove('open');
35
+ }
36
+ async function submitLinkPr() {
37
+ var url = (document.getElementById('slim-linkpr-url').value || '').trim();
38
+ if (!url) { alert('PR URL is required'); return; }
39
+ var title = (document.getElementById('slim-linkpr-title').value || '').trim();
40
+ var project = document.getElementById('slim-linkpr-project').value || '';
41
+ var context = document.getElementById('slim-linkpr-context').value || '';
42
+ var autoObserve = !!document.getElementById('slim-linkpr-observe').checked;
43
+ var btn = document.getElementById('slim-linkpr-submit');
44
+ if (btn) { btn.disabled = true; btn.textContent = 'Linking…'; }
45
+ try {
46
+ var res = await fetch('/api/pull-requests/link', {
47
+ method: 'POST',
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify({ url: url, title: title, project: project, context: context, autoObserve: autoObserve }),
50
+ });
51
+ var data = await res.json().catch(function() { return {}; });
52
+ if (res.ok) {
53
+ closeLinkPrModal();
54
+ scheduleStatusRefresh(400); // refresh the Active PRs tile
55
+ } else {
56
+ alert('Failed to link PR: ' + (data.error || ('HTTP ' + res.status)));
57
+ }
58
+ } catch (e) {
59
+ alert('Error linking PR: ' + (e && e.message ? e.message : e));
60
+ } finally {
61
+ if (btn) { btn.disabled = false; btn.textContent = 'Link PR'; }
62
+ }
63
+ }
64
+ bindModalClose('slim-linkpr-modal', 'slim-linkpr-close');
65
+ (function bindLinkPrUi() {
66
+ ['slim-linkpr-btn', 'slim-tile-linkpr-chip', 'slim-tile-modal-linkpr'].forEach(function(id) {
67
+ var el = document.getElementById(id);
68
+ if (!el) return;
69
+ el.addEventListener('click', function(ev) {
70
+ ev.stopPropagation(); // don't trigger the cockpit tile's own click
71
+ if (id === 'slim-tile-modal-linkpr') {
72
+ var tm = document.getElementById('slim-tile-modal');
73
+ if (tm) tm.classList.remove('open');
74
+ }
75
+ openLinkPrModal();
76
+ });
77
+ });
78
+ var cancel = document.getElementById('slim-linkpr-cancel');
79
+ if (cancel) cancel.addEventListener('click', closeLinkPrModal);
80
+ var submit = document.getElementById('slim-linkpr-submit');
81
+ if (submit) submit.addEventListener('click', submitLinkPr);
82
+ })();
83
+
@@ -0,0 +1,138 @@
1
+ // ── Team member cards (status panel, top sub-section) ──────────
2
+ // Maps an agent status to one of the four card classes.
3
+ function memberStatusClass(s) {
4
+ return (s === 'working' || s === 'done' || s === 'error') ? s : 'idle';
5
+ }
6
+
7
+ // Build the colored status pill shared by the member card and the detail
8
+ // modal. DOM-built (no innerHTML) to keep the SEC-03 baseline clean.
9
+ function createStatusChip(status) {
10
+ var chip = document.createElement('span');
11
+ chip.className = 'member-status ' + memberStatusClass(status);
12
+ chip.textContent = status || 'idle';
13
+ return chip;
14
+ }
15
+
16
+ // Last-rendered agents payload — skip the full grid rebuild when it's
17
+ // unchanged between polls (avoids 5s DOM churn and preserves card focus).
18
+ var _lastAgentsJson = null;
19
+
20
+ // Render the square member-card grid from the /api/status `agents` array.
21
+ // Name, role and emoji are config-sourced, so everything goes through
22
+ // textContent rather than innerHTML.
23
+ function renderMembers(agents) {
24
+ var grid = document.getElementById('member-grid');
25
+ if (!grid) return;
26
+ var json = JSON.stringify(agents);
27
+ if (json === _lastAgentsJson) return;
28
+ _lastAgentsJson = json;
29
+ grid.textContent = '';
30
+ if (!agents.length) {
31
+ var empty = document.createElement('div');
32
+ empty.className = 'member-empty';
33
+ empty.textContent = 'No team members';
34
+ grid.appendChild(empty);
35
+ return;
36
+ }
37
+ var frag = document.createDocumentFragment();
38
+ agents.forEach(function(a) {
39
+ if (!a || !a.id) return;
40
+ var card = document.createElement('div');
41
+ card.className = 'member-card ' + memberStatusClass(a.status);
42
+ card.setAttribute('role', 'button');
43
+ card.setAttribute('tabindex', '0');
44
+ card.title = (a.name || a.id) + (a.role ? ' — ' + a.role : '');
45
+ // The clicked agent object is captured directly — no id->lookup round-trip.
46
+ card.addEventListener('click', function() { openAgentDetail(a); });
47
+ card.addEventListener('keydown', function(ev) {
48
+ if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); openAgentDetail(a); }
49
+ });
50
+
51
+ var emoji = document.createElement('div');
52
+ emoji.className = 'member-emoji';
53
+ emoji.textContent = a.emoji || '🤖';
54
+ card.appendChild(emoji);
55
+
56
+ var name = document.createElement('div');
57
+ name.className = 'member-name';
58
+ name.textContent = a.name || a.id;
59
+ card.appendChild(name);
60
+
61
+ var role = document.createElement('div');
62
+ role.className = 'member-role';
63
+ role.textContent = a.role || '';
64
+ card.appendChild(role);
65
+
66
+ card.appendChild(createStatusChip(a.status));
67
+ frag.appendChild(card);
68
+ });
69
+ grid.appendChild(frag);
70
+ }
71
+
72
+ // Append a labelled key/value row to the agent detail modal body.
73
+ function appendAgentRow(body, key, value, muted) {
74
+ var row = document.createElement('div');
75
+ row.className = 'agent-detail-row';
76
+ var k = document.createElement('div');
77
+ k.className = 'agent-detail-key';
78
+ k.textContent = key;
79
+ var v = document.createElement('div');
80
+ v.className = 'agent-detail-val' + (muted ? ' muted' : '');
81
+ v.textContent = value;
82
+ row.appendChild(k);
83
+ row.appendChild(v);
84
+ body.appendChild(row);
85
+ }
86
+
87
+ // Populate + open the agent detail modal for the given agent object (passed
88
+ // straight from the clicked card).
89
+ function openAgentDetail(a) {
90
+ var modal = document.getElementById('slim-agent-modal');
91
+ var body = document.getElementById('slim-agent-body');
92
+ var titleEl = document.getElementById('slim-agent-title');
93
+ if (!a || !modal || !body) return;
94
+ if (titleEl) titleEl.textContent = a.name || a.id;
95
+ body.textContent = '';
96
+
97
+ // Header: emoji + name + role.
98
+ var head = document.createElement('div');
99
+ head.className = 'agent-detail-head';
100
+ var hEmoji = document.createElement('div');
101
+ hEmoji.className = 'agent-detail-emoji';
102
+ hEmoji.textContent = a.emoji || '🤖';
103
+ head.appendChild(hEmoji);
104
+ var hText = document.createElement('div');
105
+ var hName = document.createElement('div');
106
+ hName.className = 'agent-detail-name';
107
+ hName.textContent = a.name || a.id;
108
+ var hRole = document.createElement('div');
109
+ hRole.className = 'agent-detail-role';
110
+ hRole.textContent = a.role || '';
111
+ hText.appendChild(hName);
112
+ hText.appendChild(hRole);
113
+ head.appendChild(hText);
114
+ body.appendChild(head);
115
+
116
+ // Status row — reuse the colored member-status chip.
117
+ var statusRow = document.createElement('div');
118
+ statusRow.className = 'agent-detail-row';
119
+ var sk = document.createElement('div');
120
+ sk.className = 'agent-detail-key';
121
+ sk.textContent = 'Status';
122
+ statusRow.appendChild(sk);
123
+ statusRow.appendChild(createStatusChip(a.status));
124
+ body.appendChild(statusRow);
125
+
126
+ var runtime = [a.runtime, a.model].filter(Boolean).join(' · ');
127
+ appendAgentRow(body, 'Runtime', runtime || '—', !runtime);
128
+
129
+ var task = a.currentTask || a.lastAction || '';
130
+ appendAgentRow(body, 'Current', task || 'Idle — nothing in flight', !task);
131
+
132
+ appendAgentRow(body, 'Last result', a.resultSummary || 'No recent output', !a.resultSummary);
133
+
134
+ modal.classList.add('open');
135
+ }
136
+
137
+ // Shared modal close-binding: close button + backdrop click + Escape.
138
+ // Used by the agent modal and the completions modal below.
@@ -0,0 +1,161 @@
1
+ function bindModalClose(modalId, closeBtnId) {
2
+ var modal = document.getElementById(modalId);
3
+ if (!modal) return;
4
+ function close() { modal.classList.remove('open'); }
5
+ var closeBtn = document.getElementById(closeBtnId);
6
+ if (closeBtn) closeBtn.addEventListener('click', close);
7
+ modal.addEventListener('click', function(ev) { if (ev.target === modal) close(); });
8
+ document.addEventListener('keydown', function(ev) {
9
+ if (ev.key === 'Escape' && modal.classList.contains('open')) close();
10
+ });
11
+ }
12
+ bindModalClose('slim-agent-modal', 'slim-agent-close');
13
+ bindModalClose('slim-tools-modal', 'slim-tools-close');
14
+ bindModalClose('slim-tile-modal', 'slim-tile-close');
15
+
16
+ function tileEmpty(body, text) {
17
+ var d = document.createElement('div');
18
+ d.className = 'tile-empty';
19
+ d.textContent = text;
20
+ body.appendChild(d);
21
+ }
22
+
23
+ // One list row for the tile detail modal. opts: { title, meta, chip:{text,cls}, href }.
24
+ // Built as an <a> when href is present so PR rows open the PR in a new tab.
25
+ function tileCard(opts) {
26
+ var card = document.createElement(opts.href ? 'a' : 'div');
27
+ card.className = 'tile-item';
28
+ if (opts.href) { card.href = opts.href; card.target = '_blank'; card.rel = 'noopener'; }
29
+ var top = document.createElement('div');
30
+ top.className = 'tile-item-top';
31
+ var title = document.createElement('span');
32
+ title.className = 'tile-item-title';
33
+ title.textContent = opts.title;
34
+ title.title = opts.title;
35
+ top.appendChild(title);
36
+ if (opts.chip && opts.chip.text) {
37
+ var chip = document.createElement('span');
38
+ chip.className = 'tile-chip ' + (opts.chip.cls || '');
39
+ chip.textContent = opts.chip.text;
40
+ top.appendChild(chip);
41
+ }
42
+ card.appendChild(top);
43
+ if (opts.meta) {
44
+ var meta = document.createElement('div');
45
+ meta.className = 'tile-item-meta';
46
+ meta.textContent = opts.meta;
47
+ card.appendChild(meta);
48
+ }
49
+ return card;
50
+ }
51
+
52
+ // Per-tile body renderers for the detail modal. Each reads the latest status
53
+ // snapshot and fills the modal body; openTileModal looks them up by key.
54
+ function renderEngineTileBody(body, data) {
55
+ var e = data.engine || {};
56
+ appendAgentRow(body, 'State', e.state || '—', !e.state);
57
+ appendAgentRow(body, 'Version', e.codeVersion || '—', !e.codeVersion);
58
+ appendAgentRow(body, 'PID', e.pid != null ? String(e.pid) : '—', e.pid == null);
59
+ appendAgentRow(body, 'Started', e.started_at ? (relTime(e.started_at) + ' · ' + new Date(e.started_at).toLocaleString()) : '—', !e.started_at);
60
+ appendAgentRow(body, 'Last tick', e.lastTickAt ? relTime(e.lastTickAt) : '—', !e.lastTickAt);
61
+ appendAgentRow(body, 'Tick interval', e.tickInterval ? Math.round(e.tickInterval / 1000) + 's' : '—', !e.tickInterval);
62
+ appendAgentRow(body, 'Worktrees', e.worktreeCount != null ? String(e.worktreeCount) : '—', e.worktreeCount == null);
63
+ appendAgentRow(body, 'Heartbeat', e.heartbeatStale ? 'stale' : 'ok', false);
64
+ }
65
+
66
+ function renderDispatchTileBody(body, data, isActive) {
67
+ var dispatch = data.dispatch || {};
68
+ var dlist = Array.isArray(dispatch[isActive ? 'active' : 'pending']) ? dispatch[isActive ? 'active' : 'pending'] : [];
69
+ if (!dlist.length) { tileEmpty(body, isActive ? 'Nothing in flight.' : 'Queue is empty.'); return; }
70
+ dlist.forEach(function(d) {
71
+ var who = d.agentName || d.agent || d.dispatched_to || '—';
72
+ var task = (d.meta && d.meta.item && d.meta.item.title) || d.task || d.title || d.workItemTitle || d.id || '(untitled)';
73
+ var when = d.started_at || d.startedAt || d.created_at || d.createdAt;
74
+ body.appendChild(tileCard({
75
+ title: task,
76
+ meta: who + (when ? ' · ' + (isActive ? 'started ' : 'queued ') + relTime(when) : ''),
77
+ chip: { text: d.type || 'task', cls: isActive ? 'blue' : 'amber' },
78
+ }));
79
+ });
80
+ }
81
+
82
+ function renderPrTileBody(body, data) {
83
+ var prs = Array.isArray(data.pullRequests) ? data.pullRequests : [];
84
+ if (!prs.length) { tileEmpty(body, 'No tracked pull requests.'); return; }
85
+ prs.forEach(function(p) {
86
+ var num = p.prNumber || p.number || p.id;
87
+ var build = p.buildStatus || '';
88
+ var cls = p.status === 'merged' ? 'green'
89
+ : (build === 'failing' ? 'red'
90
+ : ((p.status === 'active' || p.status === 'linked') ? 'blue' : ''));
91
+ body.appendChild(tileCard({
92
+ title: (num ? '#' + num + ' ' : '') + (p.title || '(untitled PR)'),
93
+ meta: [p.status || 'unknown', build ? 'build ' + build : 'no build', p.reviewStatus ? 'review ' + p.reviewStatus : null].filter(Boolean).join(' · '),
94
+ chip: { text: p.status || '—', cls: cls },
95
+ href: p.url || null,
96
+ }));
97
+ });
98
+ }
99
+
100
+ function renderWatchTileBody(body, data) {
101
+ var watches = Array.isArray(data.watches) ? data.watches : [];
102
+ if (!watches.length) { tileEmpty(body, 'No watches configured.'); return; }
103
+ watches.forEach(function(w) {
104
+ var st = w.status || '';
105
+ var cls = st === 'active' ? 'blue' : (st === 'triggered' ? 'green' : (st === 'expired' ? 'red' : 'amber'));
106
+ body.appendChild(tileCard({
107
+ title: (w.targetType ? w.targetType + ': ' : '') + (w.target || w.id || '(target)'),
108
+ meta: [w.condition ? 'when ' + w.condition : null, w.interval ? 'every ' + w.interval + ' ticks' : null].filter(Boolean).join(' · '),
109
+ chip: { text: st || '—', cls: cls },
110
+ }));
111
+ });
112
+ }
113
+
114
+ // tile key -> { title, render }. Adding a tile is a one-line table edit.
115
+ var TILE_VIEWS = {
116
+ engine: { title: 'Engine', render: renderEngineTileBody },
117
+ dispatches: { title: 'Active dispatches', render: function(body, data) { renderDispatchTileBody(body, data, true); } },
118
+ queued: { title: 'Queued work', render: function(body, data) { renderDispatchTileBody(body, data, false); } },
119
+ prs: { title: 'Pull requests', render: renderPrTileBody },
120
+ watches: { title: 'Watches', render: renderWatchTileBody },
121
+ };
122
+
123
+ // Populate + open the cockpit-tile detail modal from the latest status
124
+ // snapshot. Mirrors the corresponding old-dashboard tab for each tile.
125
+ function openTileModal(key) {
126
+ var view = TILE_VIEWS[key];
127
+ if (!view) return;
128
+ var modal = document.getElementById('slim-tile-modal');
129
+ var titleEl = document.getElementById('slim-tile-title');
130
+ var body = document.getElementById('slim-tile-body');
131
+ if (!modal || !body) return;
132
+ body.textContent = '';
133
+ if (titleEl) titleEl.textContent = view.title;
134
+ // The "+ Link PR" header chip only applies to the PR list.
135
+ var headerChip = document.getElementById('slim-tile-modal-linkpr');
136
+ if (headerChip) headerChip.style.display = (key === 'prs') ? '' : 'none';
137
+ view.render(body, lastStatusData || {});
138
+ modal.classList.add('open');
139
+ }
140
+
141
+ // Make the cockpit tiles clickable / keyboard-activatable.
142
+ (function bindCockpitTiles() {
143
+ var grid = document.getElementById('cockpit-grid');
144
+ if (!grid) return;
145
+ var tiles = grid.querySelectorAll('.cockpit-tile');
146
+ for (var i = 0; i < tiles.length; i++) {
147
+ (function(tile) {
148
+ var key = tile.getAttribute('data-tile');
149
+ if (!key) return;
150
+ tile.setAttribute('role', 'button');
151
+ tile.setAttribute('tabindex', '0');
152
+ tile.title = 'View details';
153
+ tile.addEventListener('click', function() { openTileModal(key); });
154
+ tile.addEventListener('keydown', function(ev) {
155
+ if (ev.target !== tile) return; // ignore keys from inner controls (e.g. the Link-PR chip)
156
+ if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); openTileModal(key); }
157
+ });
158
+ })(tiles[i]);
159
+ }
160
+ })();
161
+
@@ -0,0 +1,209 @@
1
+ // Project context picker: populated from /api/status (which already returns
2
+ // the configured project list). Single-project: rendered as a static label
3
+ // so it's still visible but not clickable. Multi-project: <select> dropdown.
4
+ // Zero-project: a hint to add one. Selection persists per-browser in
5
+ // localStorage and is included in every /api/command-center/stream call.
6
+ // Build the "+ Add" button as a real DOM node (was an HTML string + innerHTML
7
+ // injection — SEC-03 ratchet prefers we avoid that pattern, see
8
+ // test/unit.test.js DYNAMIC_INNERHTML_BASELINE).
9
+ function makeContextAddBtn() {
10
+ var btn = document.createElement('button');
11
+ btn.type = 'button';
12
+ btn.className = 'context-add';
13
+ btn.id = 'chat-context-add';
14
+ btn.title = 'Link a new project';
15
+ btn.textContent = '+ Add';
16
+ return btn;
17
+ }
18
+ // Strip variant: empty-projects message.
19
+ function makeContextEmptyNode() {
20
+ var empty = document.createElement('span');
21
+ empty.className = 'context-empty';
22
+ empty.textContent = 'No projects linked yet.';
23
+ return empty;
24
+ }
25
+ // Strip variant: single-project static label (no picker, just the name).
26
+ function makeContextStaticNode(name) {
27
+ var staticSpan = document.createElement('span');
28
+ staticSpan.className = 'context-static';
29
+ staticSpan.textContent = name;
30
+ return staticSpan;
31
+ }
32
+ // Strip variant: multi-project <select>. Returns the select so the caller
33
+ // can attach a change listener.
34
+ function makeContextSelect(projects, selected) {
35
+ var sel = document.createElement('select');
36
+ sel.id = 'chat-context-select';
37
+ sel.setAttribute('aria-label', 'Working in project');
38
+ for (var i = 0; i < projects.length; i++) {
39
+ var p = projects[i];
40
+ var opt = document.createElement('option');
41
+ opt.value = p;
42
+ opt.textContent = p;
43
+ if (p === selected) opt.selected = true;
44
+ sel.appendChild(opt);
45
+ }
46
+ return sel;
47
+ }
48
+ async function loadProjectsAndRenderContext(opts) {
49
+ var stripEl = document.getElementById('chat-context-strip');
50
+ var controlsEl = document.getElementById('chat-context-controls');
51
+ if (!stripEl || !controlsEl) return;
52
+ var preferProject = (opts && opts.preferProject) || null;
53
+ try {
54
+ var res = await fetch('/api/status');
55
+ if (!res.ok) throw new Error('HTTP ' + res.status);
56
+ var data = await res.json();
57
+ var projects = (data && Array.isArray(data.projects) ? data.projects : [])
58
+ .map(function(p) { return p && p.name ? String(p.name) : null; })
59
+ .filter(Boolean);
60
+ if (projects.length === 0) {
61
+ stripEl.style.display = '';
62
+ controlsEl.replaceChildren(makeContextEmptyNode(), makeContextAddBtn());
63
+ currentProject = null;
64
+ } else {
65
+ if (preferProject && projects.indexOf(preferProject) !== -1) {
66
+ currentProject = preferProject;
67
+ try { localStorage.setItem(SLIM_PROJECT_KEY, currentProject); } catch (_e) {}
68
+ } else if (currentProject && projects.indexOf(currentProject) === -1) {
69
+ currentProject = null;
70
+ try { localStorage.removeItem(SLIM_PROJECT_KEY); } catch (_e) {}
71
+ }
72
+ if (!currentProject) {
73
+ currentProject = projects[0];
74
+ try { localStorage.setItem(SLIM_PROJECT_KEY, currentProject); } catch (_e) {}
75
+ }
76
+ if (projects.length === 1) {
77
+ stripEl.style.display = '';
78
+ controlsEl.replaceChildren(makeContextStaticNode(currentProject), makeContextAddBtn());
79
+ } else {
80
+ var sel = makeContextSelect(projects, currentProject);
81
+ stripEl.style.display = '';
82
+ controlsEl.replaceChildren(sel, makeContextAddBtn());
83
+ sel.addEventListener('change', function(ev) {
84
+ currentProject = ev.target.value || null;
85
+ try { localStorage.setItem(SLIM_PROJECT_KEY, currentProject); } catch (_e) {}
86
+ });
87
+ }
88
+ }
89
+ var addBtn = document.getElementById('chat-context-add');
90
+ if (addBtn) addBtn.addEventListener('click', openAddProjectModal);
91
+ } catch (_e) {
92
+ stripEl.style.display = 'none';
93
+ }
94
+ }
95
+
96
+ // ── Add-project modal ────────────────────────────────────────
97
+ function openAddProjectModal() {
98
+ var modal = document.getElementById('slim-add-project-modal');
99
+ var input = document.getElementById('slim-add-project-path');
100
+ var msg = document.getElementById('slim-add-project-msg');
101
+ if (!modal) return;
102
+ if (input) { input.value = ''; input.disabled = false; }
103
+ if (msg) { msg.textContent = ''; msg.style.color = ''; }
104
+ modal.classList.add('open');
105
+ if (input) setTimeout(function() { input.focus(); }, 0);
106
+ }
107
+ // Reset both modal fields here so cancel / close-after-success / Esc /
108
+ // backdrop-click all behave the same way: open the modal next time and
109
+ // start fresh, no stale path or status text leaking across openings.
110
+ function closeAddProjectModal() {
111
+ var modal = document.getElementById('slim-add-project-modal');
112
+ var input = document.getElementById('slim-add-project-path');
113
+ var msg = document.getElementById('slim-add-project-msg');
114
+ if (modal) modal.classList.remove('open');
115
+ if (input) { input.value = ''; input.disabled = false; }
116
+ if (msg) { msg.textContent = ''; msg.style.color = ''; }
117
+ }
118
+ async function browseProjectPath() {
119
+ var input = document.getElementById('slim-add-project-path');
120
+ var msg = document.getElementById('slim-add-project-msg');
121
+ if (!input) return;
122
+ if (msg) { msg.textContent = 'Opening folder picker…'; msg.style.color = 'var(--muted)'; }
123
+ // Pre-seed the picker with whatever is in the text box so users who've
124
+ // typed a partial path don't have to re-navigate to it from their home
125
+ // dir. Server validates this strictly (walks up to nearest existing dir).
126
+ // modernPicker: true opts into the slim-UX Explorer-style picker
127
+ // (Microsoft.Win32.OpenFolderDialog with native path bar + InitialDirectory
128
+ // support). The classic dashboard doesn't send this flag and gets the
129
+ // unchanged WinForms FolderBrowserDialog for its Scan Projects flow.
130
+ var initialPath = (input.value || '').trim();
131
+ try {
132
+ var res = await fetch('/api/projects/browse', {
133
+ method: 'POST',
134
+ headers: { 'Content-Type': 'application/json' },
135
+ body: JSON.stringify({ initialPath: initialPath || undefined, modernPicker: true }),
136
+ });
137
+ if (!res.ok) throw new Error('HTTP ' + res.status);
138
+ var data = await res.json();
139
+ if (data && data.path) {
140
+ input.value = data.path;
141
+ if (msg) msg.textContent = '';
142
+ } else if (msg) {
143
+ msg.textContent = 'No folder selected.';
144
+ }
145
+ } catch (e) {
146
+ if (msg) { msg.textContent = 'Browse failed: ' + (e && e.message ? e.message : 'unknown error'); msg.style.color = 'var(--red)'; }
147
+ }
148
+ }
149
+ async function submitAddProject() {
150
+ var input = document.getElementById('slim-add-project-path');
151
+ var msg = document.getElementById('slim-add-project-msg');
152
+ var submitBtn = document.getElementById('slim-add-project-submit');
153
+ if (!input || !msg) return;
154
+ var p = (input.value || '').trim();
155
+ if (!p) { msg.textContent = 'Enter a path or click Browse.'; msg.style.color = 'var(--red)'; return; }
156
+ msg.textContent = 'Linking…';
157
+ msg.style.color = 'var(--muted)';
158
+ if (submitBtn) submitBtn.disabled = true;
159
+ if (input) input.disabled = true;
160
+ try {
161
+ var res = await fetch('/api/projects/add', {
162
+ method: 'POST',
163
+ headers: { 'Content-Type': 'application/json' },
164
+ body: JSON.stringify({ path: p }),
165
+ });
166
+ var data = await res.json().catch(function() { return {}; });
167
+ if (!res.ok) {
168
+ var hint = data && data.needsConfirmation
169
+ ? ' (path is not a git repo — use Settings → Scan Projects to confirm a non-repo path)'
170
+ : '';
171
+ throw new Error((data && data.error) ? data.error + hint : ('HTTP ' + res.status + hint));
172
+ }
173
+ var newName = data && data.name ? data.name : null;
174
+ msg.textContent = 'Linked ' + (newName || p) + '. Refreshing…';
175
+ msg.style.color = 'var(--green)';
176
+ await loadProjectsAndRenderContext({ preferProject: newName });
177
+ setTimeout(closeAddProjectModal, 600);
178
+ } catch (e) {
179
+ msg.textContent = 'Failed: ' + (e && e.message ? e.message : 'unknown error');
180
+ msg.style.color = 'var(--red)';
181
+ } finally {
182
+ if (submitBtn) submitBtn.disabled = false;
183
+ if (input) input.disabled = false;
184
+ }
185
+ }
186
+
187
+ loadProjectsAndRenderContext();
188
+ // Wire add-project modal once at startup; the trigger button gets a click
189
+ // listener re-bound on every render of the context strip.
190
+ (function wireAddProjectModal() {
191
+ var modal = document.getElementById('slim-add-project-modal');
192
+ if (!modal) return;
193
+ var closeBtn = document.getElementById('slim-add-project-close');
194
+ var cancelBtn = document.getElementById('slim-add-project-cancel');
195
+ var browseBtn = document.getElementById('slim-add-project-browse');
196
+ var submitBtn = document.getElementById('slim-add-project-submit');
197
+ var input = document.getElementById('slim-add-project-path');
198
+ if (closeBtn) closeBtn.addEventListener('click', closeAddProjectModal);
199
+ if (cancelBtn) cancelBtn.addEventListener('click', closeAddProjectModal);
200
+ if (browseBtn) browseBtn.addEventListener('click', browseProjectPath);
201
+ if (submitBtn) submitBtn.addEventListener('click', submitAddProject);
202
+ if (input) input.addEventListener('keydown', function(ev) {
203
+ if (ev.key === 'Enter') { ev.preventDefault(); submitAddProject(); }
204
+ });
205
+ modal.addEventListener('click', function(ev) {
206
+ if (ev.target === modal) closeAddProjectModal();
207
+ });
208
+ })();
209
+