fraim-framework 2.0.167 → 2.0.168

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 (54) hide show
  1. package/dist/src/ai-hub/catalog.js +28 -14
  2. package/dist/src/ai-hub/server.js +10 -403
  3. package/dist/src/cli/commands/init-project.js +1 -98
  4. package/dist/src/cli/commands/manager.js +40 -0
  5. package/dist/src/cli/commands/sync.js +17 -21
  6. package/dist/src/cli/fraim.js +2 -0
  7. package/dist/src/cli/utils/github-workflow-sync.js +12 -146
  8. package/dist/src/cli/utils/manager-pack-sync.js +188 -0
  9. package/dist/src/cli/utils/manager-publish.js +76 -0
  10. package/dist/src/cli/utils/user-config.js +20 -0
  11. package/dist/src/core/fraim-config-schema.generated.js +85 -10
  12. package/dist/src/core/manager-pack.js +26 -0
  13. package/dist/src/first-run/install-state.js +1 -0
  14. package/dist/src/first-run/server.js +9 -0
  15. package/dist/src/first-run/session-service.js +117 -23
  16. package/dist/src/first-run/types.js +2 -5
  17. package/dist/src/local-mcp-server/learning-context-builder.js +45 -8
  18. package/dist/src/local-mcp-server/stdio-server.js +28 -0
  19. package/index.js +1 -1
  20. package/package.json +1 -2
  21. package/public/ai-hub/index.html +0 -81
  22. package/public/ai-hub/powerpoint-taskpane/index.html +236 -236
  23. package/public/ai-hub/powerpoint-taskpane/manifest.xml +29 -29
  24. package/public/ai-hub/script.js +3 -219
  25. package/public/ai-hub/styles.css +8 -36
  26. package/public/first-run/index.html +1 -1
  27. package/public/first-run/script.js +459 -530
  28. package/public/first-run/styles.css +288 -73
  29. package/public/portfolio/ashley.html +1 -1
  30. package/public/portfolio/casey.html +1 -1
  31. package/public/portfolio/celia.html +1 -1
  32. package/public/portfolio/gautam.html +1 -1
  33. package/public/portfolio/hari.html +1 -1
  34. package/public/portfolio/maestro.html +1 -1
  35. package/public/portfolio/mandy.html +1 -1
  36. package/public/portfolio/pam.html +6 -6
  37. package/public/portfolio/qasm.html +1 -1
  38. package/public/portfolio/sade.html +1 -1
  39. package/public/portfolio/sam.html +1 -1
  40. package/public/portfolio/swen.html +6 -6
  41. package/dist/src/ai-hub/word-sideload.js +0 -95
  42. package/dist/src/cli/commands/test-mcp.js +0 -171
  43. package/dist/src/cli/setup/first-run.js +0 -242
  44. package/dist/src/config/ai-manager-hiring.js +0 -121
  45. package/dist/src/config/compat.js +0 -16
  46. package/dist/src/config/feature-flags.js +0 -25
  47. package/dist/src/config/persona-capability-bundles.js +0 -273
  48. package/dist/src/config/persona-hiring.js +0 -270
  49. package/dist/src/config/portfolio-slug-overrides.js +0 -17
  50. package/dist/src/config/pricing.js +0 -37
  51. package/dist/src/config/stripe.js +0 -43
  52. package/dist/src/core/config-writer.js +0 -75
  53. package/dist/src/core/utils/job-aliases.js +0 -47
  54. package/dist/src/core/utils/workflow-parser.js +0 -174
@@ -8,9 +8,20 @@
8
8
 
9
9
  const state = {
10
10
  session: null,
11
- runningRowId: null,
11
+ activeStep: 'prereqs',
12
+ configureInFlight: false,
13
+ configureError: null,
12
14
  };
13
15
 
16
+ const STEP_LABELS = {
17
+ prereqs: 'Prerequisites',
18
+ agents: 'Local Agents',
19
+ configure: 'Configure FRAIM',
20
+ use: 'Use FRAIM',
21
+ };
22
+
23
+ const STEP_ORDER = ['prereqs', 'agents', 'configure', 'use'];
24
+
14
25
  function setStatus(text, tone) {
15
26
  STATUS_EL.textContent = text || '';
16
27
  if (tone) STATUS_EL.setAttribute('data-tone', tone);
@@ -23,643 +34,561 @@
23
34
  if (LEDE_EL) LEDE_EL.textContent = lede;
24
35
  }
25
36
 
26
- function applyHeading(rows) {
27
- if (!LEDE_EL) return;
28
- const complete = rows.every((r) => r.status === 'ok' || r.status === 'manual-required') && rows.some((r) => r.status === 'ok');
29
- if (complete) {
30
- const setupResult = state.session && state.session.state ? state.session.state.setupResult : null;
31
- if (setupResult && setupResult.detectedSurfaceCount > 0) {
32
- LEDE_EL.textContent = 'Your FRAIM employees are ready to work. Here are the surfaces available to them.';
33
- } else {
34
- LEDE_EL.textContent = "No execution surfaces found. Your FRAIM employees need at least one agent (Claude Code, Codex, etc.) to work in.";
35
- }
36
- return;
37
- }
38
- const errored = rows.find((r) => r.status === 'error');
39
- if (errored) {
40
- LEDE_EL.textContent = "Something did not go through. Pick the right next step below.";
41
- return;
42
- }
43
- if (rows.some((r) => r.status === 'ok')) {
44
- LEDE_EL.textContent = 'FRAIM is finishing setup on this machine.';
45
- return;
46
- }
47
- LEDE_EL.textContent = 'We are getting your machine ready for FRAIM.';
48
- }
49
-
50
37
  async function api(path, method, body) {
51
38
  const headers = {};
52
39
  if (body !== undefined) headers['Content-Type'] = 'application/json';
53
- if (state.session && state.session.requestToken) {
54
- headers['x-fraim-first-run-token'] = state.session.requestToken;
55
- }
40
+ if (state.session && state.session.requestToken) headers['x-fraim-first-run-token'] = state.session.requestToken;
56
41
  const response = await fetch(path, {
57
42
  method: method || 'GET',
58
43
  headers: Object.keys(headers).length > 0 ? headers : undefined,
59
44
  body: body !== undefined ? JSON.stringify(body) : undefined,
60
45
  });
61
46
  if (response.status === 204) return null;
62
- let json = null;
63
- try {
64
- json = await response.json();
65
- } catch (e) {
66
- throw new Error(`Server returned invalid JSON (status ${response.status}).`);
67
- }
68
- if (!response.ok) {
69
- throw new Error(json && json.error ? json.error : `Request failed (status ${response.status}).`);
70
- }
47
+ const json = await response.json();
48
+ if (!response.ok) throw new Error(json && json.error ? json.error : `Request failed (status ${response.status}).`);
71
49
  return json;
72
50
  }
73
51
 
74
- async function loadSession() {
75
- state.session = await api('/api/first-run/session');
76
- render();
77
- }
78
-
79
52
  function setSessionFromActionResponse(actionResp) {
80
- if (!state.session) return;
53
+ if (!state.session || !actionResp) return;
81
54
  state.session.state = actionResp.state;
82
55
  state.session.rows = actionResp.rows;
83
56
  state.session.primaryButtonLabel = actionResp.primaryButtonLabel;
84
57
  state.session.currentAgentId = actionResp.state.agentId;
85
58
  }
86
59
 
87
- function makeIcon(status) {
88
- const icon = document.createElement('span');
89
- icon.className = 'icon';
90
- icon.setAttribute('aria-hidden', 'true');
91
- icon.textContent =
92
- status === 'ok' ? '✓' :
93
- status === 'in-progress' ? '...' :
94
- status === 'manual-required' ? '!' :
95
- status === 'error' ? '!' :
96
- '.';
97
- return icon;
60
+ function row(id) {
61
+ return (state.session && state.session.rows || []).find((r) => r.id === id) || null;
98
62
  }
99
63
 
100
- function renderRow(row) {
101
- const li = document.createElement('li');
102
- li.className = 'row';
103
- li.setAttribute('data-row-id', row.id);
104
- li.setAttribute('data-row-status', row.status);
105
-
106
- li.appendChild(makeIcon(row.status));
107
-
108
- const label = document.createElement('span');
109
- label.className = 'label';
110
- label.textContent = row.label;
111
- li.appendChild(label);
112
-
113
- const verb = document.createElement('span');
114
- verb.className = 'verb';
115
- verb.setAttribute('data-testid', 'row-verb');
116
- verb.textContent = row.verb || '';
117
- li.appendChild(verb);
118
-
119
- if (row.detail) {
120
- const detail = document.createElement('span');
121
- detail.className = 'detail';
122
- detail.setAttribute('data-testid', 'row-detail');
123
- detail.textContent = row.detail;
124
- li.appendChild(detail);
125
- }
126
-
127
- if (row.streamOutput) {
128
- const stream = document.createElement('pre');
129
- stream.className = 'row-stream';
130
- stream.setAttribute('data-testid', 'row-stream');
131
- stream.textContent = row.streamOutput;
132
- li.appendChild(stream);
133
- }
64
+ function requiredPrereqsReady() {
65
+ const node = row('node');
66
+ return Boolean(node && node.status === 'ok');
67
+ }
134
68
 
135
- if (row.manualMessage) {
136
- const message = document.createElement('div');
137
- message.className = 'manual-message';
138
- message.textContent = row.manualMessage;
139
- li.appendChild(message);
140
- }
69
+ function readyAgents() {
70
+ const installs = state.session && state.session.state ? state.session.state.agentInstalls || {} : {};
71
+ return Object.entries(installs)
72
+ .filter(([, entry]) => entry && entry.status === 'ready')
73
+ .map(([id, entry]) => ({ id, label: entry.label }));
74
+ }
141
75
 
142
- if (row.status === 'error' && row.errorFrame && window.FraimErrorFrame) {
143
- const frame = window.FraimErrorFrame.render(row.errorFrame, (action) => onErrorAction(row.id, action));
144
- li.appendChild(frame);
145
- }
76
+ function configuredSurfaces() {
77
+ const setupResult = state.session && state.session.state ? state.session.state.setupResult : null;
78
+ return setupResult && Array.isArray(setupResult.configuredSurfaces) ? setupResult.configuredSurfaces : [];
79
+ }
146
80
 
147
- return li;
81
+ function isAgentReady(opt) {
82
+ const installState = state.session && state.session.state && state.session.state.agentInstalls
83
+ ? state.session.state.agentInstalls[opt.id]
84
+ : null;
85
+ if (installState && installState.status === 'ready') return true;
86
+ return configuredSurfaces().some((surface) => surface && (surface.id === opt.id || surface.name === opt.label));
148
87
  }
149
88
 
150
- function renderSetupSummary() {
151
- const setupResult = state.session && state.session.state ? state.session.state.setupResult : null;
152
- if (!setupResult) return null;
153
-
154
- const li = document.createElement('li');
155
- li.className = 'setup-result';
156
- li.setAttribute('data-testid', 'setup-result');
157
-
158
- const title = document.createElement('strong');
159
- title.textContent = setupResult.detectedSurfaceCount > 0
160
- ? 'Your FRAIM employees are ready to work in these surfaces'
161
- : 'No execution surfaces found';
162
- li.appendChild(title);
163
-
164
- if (setupResult.detectedSurfaceCount > 0) {
165
- const list = document.createElement('ul');
166
- list.className = 'surface-list';
167
- for (const surface of setupResult.configuredSurfaces || []) {
168
- const item = document.createElement('li');
169
- item.textContent = surface.name;
170
- list.appendChild(item);
171
- }
172
- li.appendChild(list);
173
- } else {
174
- const copy = document.createElement('p');
175
- copy.textContent = "No execution surfaces found. Install Claude Code, Codex, or another agent to work in.";
176
- li.appendChild(copy);
89
+ function readyAgentLabels() {
90
+ const labels = new Set();
91
+ for (const agent of readyAgents()) labels.add(agent.label);
92
+ for (const surface of configuredSurfaces()) {
93
+ if (surface && surface.name) labels.add(surface.name);
177
94
  }
95
+ return Array.from(labels);
96
+ }
178
97
 
179
- return li;
98
+ function detectedAgentCount() {
99
+ const setupResult = state.session && state.session.state ? state.session.state.setupResult : null;
100
+ return (setupResult && setupResult.detectedSurfaceCount) || readyAgents().length;
180
101
  }
181
102
 
182
- async function runRow(rowId, extraBody) {
183
- if (state.runningRowId) return;
184
- state.runningRowId = rowId;
185
- PRIMARY_BUTTON.disabled = true;
186
- setStatus(`Running ${rowId}...`);
187
- try {
188
- const resp = await api(`/api/first-run/rows/${rowId}/run`, 'POST', extraBody || {});
189
- setSessionFromActionResponse(resp);
190
- setStatus(resp.message, resp.ok ? null : 'error');
191
- } catch (err) {
192
- setStatus(err.message, 'error');
193
- } finally {
194
- state.runningRowId = null;
195
- PRIMARY_BUTTON.disabled = false;
196
- render();
197
- }
103
+ function configureReady() {
104
+ const fraim = row('fraim');
105
+ return Boolean(fraim && fraim.status === 'ok' && detectedAgentCount() > 0);
198
106
  }
199
107
 
200
- async function onErrorAction(rowId, action) {
201
- const body = { errorActionId: action.id };
202
- if (action.id === 'alternative' && action.alternativeAgentId) {
203
- body.alternativeAgentId = action.alternativeAgentId;
204
- }
205
- await runRow(rowId, body);
108
+ function chooseActiveStep() {
109
+ if (!requiredPrereqsReady()) return 'prereqs';
110
+ if (detectedAgentCount() < 1) return 'agents';
111
+ if (!configureReady()) return 'configure';
112
+ return 'use';
206
113
  }
207
114
 
208
- async function onPrimaryClick() {
209
- if (!state.session) return;
115
+ function iconFor(status) {
116
+ if (status === 'ok') return '✓';
117
+ if (status === 'in-progress') return '...';
118
+ if (status === 'manual-required' || status === 'error') return '!';
119
+ return '.';
120
+ }
210
121
 
211
- if (state.session.primaryButtonLabel === 'Get Started') {
212
- PRIMARY_BUTTON.disabled = true;
213
- const setupResult = state.session.state ? state.session.state.setupResult : null;
214
- if (setupResult && setupResult.detectedSurfaceCount === 0) {
215
- renderRecruitAgents();
216
- return;
217
- }
218
- try {
219
- const ideData = await api('/api/first-run/ide-commands');
220
- renderStartWorking(ideData ? ideData.commands : []);
221
- } catch (_err) {
222
- renderStartWorking([]);
223
- }
224
- return;
122
+ function renderShell(renderPane) {
123
+ CHECKLIST_EL.className = 'setup-shell';
124
+ CHECKLIST_EL.innerHTML = '';
125
+ PRIMARY_BUTTON.style.display = 'none';
126
+ setHeader('Set up FRAIM', 'Complete these steps in order. FRAIM configures itself after a local AI agent is ready.');
127
+
128
+ const steps = document.createElement('div');
129
+ steps.className = 'setup-steps';
130
+ steps.setAttribute('data-testid', 'setup-stepper');
131
+
132
+ for (const step of STEP_ORDER) {
133
+ const btn = document.createElement('button');
134
+ btn.type = 'button';
135
+ btn.className = 'setup-step';
136
+ btn.setAttribute('data-step', step);
137
+ btn.setAttribute('aria-current', state.activeStep === step ? 'step' : 'false');
138
+ btn.setAttribute('data-status', stepStatus(step));
139
+ btn.disabled = (step === 'configure' && detectedAgentCount() < 1) || (step === 'use' && !configureReady());
140
+ btn.addEventListener('click', () => {
141
+ if (btn.disabled) return;
142
+ state.activeStep = step;
143
+ render();
144
+ });
145
+ const dot = document.createElement('span');
146
+ dot.className = 'step-dot';
147
+ dot.textContent = stepStatus(step) === 'done' ? '✓' : String(STEP_ORDER.indexOf(step) + 1);
148
+ const label = document.createElement('span');
149
+ label.textContent = STEP_LABELS[step];
150
+ btn.appendChild(dot);
151
+ btn.appendChild(label);
152
+ steps.appendChild(btn);
225
153
  }
226
154
 
227
- const next = state.session.rows.find((r) => r.status === 'pending');
228
- if (!next) return;
229
- await runRow(next.id);
155
+ const pane = document.createElement('div');
156
+ pane.className = 'setup-pane';
157
+ pane.setAttribute('data-testid', 'setup-pane');
158
+ renderPane(pane);
230
159
 
231
- while (state.session) {
232
- const blockingError = state.session.rows.find((r) => r.status === 'error');
233
- if (blockingError) break;
234
- const nextRow = state.session.rows.find((r) => r.status === 'pending');
235
- if (!nextRow) break;
236
- await runRow(nextRow.id);
237
- }
160
+ CHECKLIST_EL.appendChild(steps);
161
+ CHECKLIST_EL.appendChild(pane);
238
162
  }
239
163
 
240
- function renderRecruitAgents() {
241
- CHECKLIST_EL.className = 'selection-container recruit-container';
242
- CHECKLIST_EL.innerHTML = '';
243
- PRIMARY_BUTTON.style.display = 'none';
244
- setHeader('Recruit AI Employees', 'Choose how you want to add AI Employees to this machine.');
164
+ function stepStatus(step) {
165
+ if (step === 'prereqs') return requiredPrereqsReady() ? 'done' : 'active';
166
+ if (step === 'agents') return detectedAgentCount() > 0 ? 'done' : 'active';
167
+ if (step === 'configure') return configureReady() ? 'done' : (detectedAgentCount() > 0 ? 'active' : 'locked');
168
+ if (step === 'use') return configureReady() ? 'active' : 'locked';
169
+ return 'active';
170
+ }
245
171
 
246
- const agentOptions = (state.session && state.session.agentOptions) ? state.session.agentOptions : [
247
- { id: 'claude-code', label: 'Claude Code' },
248
- { id: 'codex', label: 'Codex' },
249
- { id: 'gemini-cli', label: 'Gemini CLI' },
250
- ];
172
+ function renderPrereqs(pane) {
173
+ const h = document.createElement('h2');
174
+ h.textContent = 'Prerequisites';
175
+ pane.appendChild(h);
251
176
 
252
- const AGENT_DESCS = {
253
- 'claude-code': 'Install Claude Code and connect it to FRAIM.',
254
- 'codex': 'Install Codex and connect it to FRAIM.',
255
- 'gemini-cli': 'Install Gemini CLI and connect it to FRAIM.',
256
- };
177
+ const list = document.createElement('ul');
178
+ list.className = 'row-list';
179
+ for (const id of ['node', 'git']) {
180
+ const r = row(id);
181
+ if (!r) continue;
182
+ const li = document.createElement('li');
183
+ li.className = 'row';
184
+ li.setAttribute('data-row-id', r.id);
185
+ li.setAttribute('data-row-status', r.status);
186
+ li.innerHTML = `<span class="icon" aria-hidden="true">${iconFor(r.status)}</span><span class="label"></span><span class="verb" data-testid="row-verb"></span>`;
187
+ li.querySelector('.label').textContent = r.label;
188
+ li.querySelector('.verb').textContent = r.verb || '';
189
+ list.appendChild(li);
190
+ }
191
+ pane.appendChild(list);
192
+
193
+ const btn = button(requiredPrereqsReady() ? 'Next' : 'Check prerequisites', 'primary');
194
+ btn.setAttribute('data-testid', 'check-prereqs');
195
+ btn.addEventListener('click', async () => {
196
+ btn.disabled = true;
197
+ try {
198
+ for (const id of ['node', 'git']) {
199
+ const r = row(id);
200
+ if (r && r.status !== 'ok') setSessionFromActionResponse(await api(`/api/first-run/rows/${id}/run`, 'POST', {}));
201
+ }
202
+ state.activeStep = 'agents';
203
+ setStatus('Prerequisites are ready.');
204
+ } catch (err) {
205
+ setStatus(err.message, 'error');
206
+ } finally {
207
+ btn.disabled = false;
208
+ render();
209
+ }
210
+ });
211
+ pane.appendChild(btn);
212
+ }
213
+
214
+ function renderAgents(pane) {
215
+ const h = document.createElement('h2');
216
+ h.textContent = 'Locally installed AI agents';
217
+ pane.appendChild(h);
218
+
219
+ const p = document.createElement('p');
220
+ p.className = 'pane-copy';
221
+ p.textContent = detectedAgentCount() > 0
222
+ ? 'At least one local AI agent is ready. You can add more agents or continue to configure FRAIM.'
223
+ : 'No problem, we will install AI agents next.';
224
+ pane.appendChild(p);
225
+
226
+ const readyLabels = readyAgentLabels();
227
+ if (readyLabels.length > 0) {
228
+ const readyList = document.createElement('div');
229
+ readyList.className = 'ready-strip';
230
+ readyList.setAttribute('data-testid', 'ready-agents');
231
+ readyList.textContent = `Already installed: ${readyLabels.join(', ')}`;
232
+ pane.appendChild(readyList);
233
+ } else {
234
+ const readyList = document.createElement('div');
235
+ readyList.className = 'ready-strip ready-strip--muted';
236
+ readyList.setAttribute('data-testid', 'ready-agents');
237
+ readyList.textContent = 'Already installed: none detected yet';
238
+ pane.appendChild(readyList);
239
+ }
257
240
 
241
+ const grid = document.createElement('div');
242
+ grid.className = 'agent-grid';
243
+ const agentOptions = state.session.agentOptions || [];
258
244
  for (const opt of agentOptions) {
259
- const li = document.createElement('li');
260
245
  const card = document.createElement('div');
261
246
  card.className = 'user-type-card recruit-card';
262
247
  card.setAttribute('data-agent-id', opt.id);
263
-
248
+ card.setAttribute('data-testid', `agent-card-${opt.id}`);
249
+ const agentReady = isAgentReady(opt);
264
250
  const title = document.createElement('strong');
265
251
  title.className = 'card-title';
266
252
  title.textContent = opt.label;
267
253
  const desc = document.createElement('p');
268
254
  desc.className = 'card-desc';
269
- desc.textContent = AGENT_DESCS[opt.id] || ('Install ' + opt.label + ' and connect it to FRAIM.');
255
+ desc.textContent = agentReady
256
+ ? 'Already installed and ready for FRAIM.'
257
+ : `Available to set up: install, sign in, and verify ${opt.label}.`;
258
+ const action = button(agentReady ? 'Ready' : 'Set up', 'secondary');
259
+ action.disabled = agentReady;
260
+ action.setAttribute('data-testid', `install-${opt.id}`);
261
+ action.addEventListener('click', () => openAgentModal(opt));
270
262
  card.appendChild(title);
271
263
  card.appendChild(desc);
272
-
273
- const installBtn = document.createElement('button');
274
- installBtn.type = 'button';
275
- installBtn.className = 'btn btn-secondary btn-block';
276
- installBtn.textContent = 'Install';
277
- installBtn.setAttribute('data-testid', 'install-' + opt.id);
278
- installBtn.addEventListener('click', () => renderAgentInstallFlow(opt));
279
- card.appendChild(installBtn);
280
-
281
- li.appendChild(card);
282
- CHECKLIST_EL.appendChild(li);
264
+ card.appendChild(action);
265
+ grid.appendChild(card);
283
266
  }
284
267
 
285
- const byoaLi = document.createElement('li');
286
- const byoaCard = document.createElement('div');
287
- byoaCard.className = 'user-type-card recruit-card';
288
- const byoaTitle = document.createElement('strong');
289
- byoaTitle.className = 'card-title';
290
- byoaTitle.textContent = 'Bring Your Own Agent';
291
- const byoaDesc = document.createElement('p');
292
- byoaDesc.className = 'card-desc';
293
- byoaDesc.textContent = 'Install Cursor, Windsurf, Kiro, VS Code, or another supported AI tool.';
294
- const byoaNote = document.createElement('p');
295
- byoaNote.className = 'card-desc';
296
- byoaNote.textContent = 'When you are done installing your agent, run:';
297
- const byoaCmd = document.createElement('div');
298
- byoaCmd.className = 'cmd-block';
299
- byoaCmd.textContent = 'npx fraim add-ide';
300
- byoaCard.appendChild(byoaTitle);
301
- byoaCard.appendChild(byoaDesc);
302
- byoaCard.appendChild(byoaNote);
303
- byoaCard.appendChild(byoaCmd);
304
- byoaLi.appendChild(byoaCard);
305
- CHECKLIST_EL.appendChild(byoaLi);
306
-
307
- const continueLi = document.createElement('li');
308
- const continueBtn = document.createElement('button');
309
- continueBtn.type = 'button';
310
- continueBtn.className = 'btn btn-primary btn-block';
311
- continueBtn.textContent = 'Continue without AI Agent';
312
- continueBtn.addEventListener('click', async () => {
313
- try {
314
- const ideData = await api('/api/first-run/ide-commands');
315
- renderStartWorking(ideData ? ideData.commands : []);
316
- } catch (_err) {
317
- renderStartWorking([]);
318
- }
319
- });
320
- continueLi.appendChild(continueBtn);
321
- CHECKLIST_EL.appendChild(continueLi);
322
- }
323
-
324
- function renderAgentInstallFlow(opt) {
325
- CHECKLIST_EL.className = 'selection-container recruit-container';
326
- CHECKLIST_EL.innerHTML = '';
327
- setHeader('Install ' + opt.label, 'Setting up ' + opt.label + ' on this machine...');
328
-
329
- const statusLi = document.createElement('li');
330
- const statusDiv = document.createElement('div');
331
- statusDiv.className = 'install-status';
332
- statusDiv.setAttribute('data-testid', 'agent-install-status');
333
- statusDiv.textContent = 'Installing ' + opt.label + '...';
334
- statusLi.appendChild(statusDiv);
335
- CHECKLIST_EL.appendChild(statusLi);
336
-
337
- (async () => {
338
- try {
339
- const result = await api('/api/first-run/install-agent', 'POST', { agentId: opt.id });
340
- if (!result || !result.ok) {
341
- statusDiv.textContent = (result && result.message) ? result.message : 'Install failed.';
342
- statusDiv.setAttribute('data-tone', 'error');
343
- const actions = document.createElement('div');
344
- actions.className = 'install-actions';
345
- const retryBtn = document.createElement('button');
346
- retryBtn.type = 'button';
347
- retryBtn.className = 'btn btn-primary';
348
- retryBtn.textContent = 'Retry';
349
- retryBtn.addEventListener('click', () => renderAgentInstallFlow(opt));
350
- const backBtn = document.createElement('button');
351
- backBtn.type = 'button';
352
- backBtn.className = 'btn btn-ghost';
353
- backBtn.textContent = 'Choose a different agent';
354
- backBtn.addEventListener('click', () => renderRecruitAgents());
355
- actions.appendChild(retryBtn);
356
- actions.appendChild(backBtn);
357
- statusLi.appendChild(actions);
358
- return;
359
- }
360
- statusDiv.textContent = opt.label + ' installed successfully!';
361
- renderAgentLoginStep(opt, result.loginCommand, result.loginHint, statusLi);
362
- } catch (err) {
363
- statusDiv.textContent = err.message || 'Install failed.';
364
- statusDiv.setAttribute('data-tone', 'error');
365
- const backBtn = document.createElement('button');
366
- backBtn.type = 'button';
367
- backBtn.className = 'btn btn-ghost btn-block';
368
- backBtn.textContent = 'Choose a different agent';
369
- backBtn.addEventListener('click', () => renderRecruitAgents());
370
- statusLi.appendChild(backBtn);
371
- }
372
- })();
268
+ const byoa = document.createElement('div');
269
+ byoa.className = 'user-type-card recruit-card byoa-card';
270
+ byoa.innerHTML = '<strong class="card-title">Bring Your Own Agent</strong><p class="card-desc">Use Cursor, Windsurf, Kiro, VS Code, or another supported local AI tool. If you install a new agent later and want FRAIM to use it, run this command from your project.</p><div class="cmd-block">npx fraim add-ide</div>';
271
+ grid.appendChild(byoa);
272
+ pane.appendChild(grid);
273
+
274
+ const done = button('Next', 'primary');
275
+ done.setAttribute('data-testid', 'done-installing-agents');
276
+ done.disabled = detectedAgentCount() < 1;
277
+ done.addEventListener('click', () => { state.activeStep = 'configure'; render(); });
278
+ pane.appendChild(done);
279
+
280
+ if (detectedAgentCount() < 1) {
281
+ const locked = document.createElement('p');
282
+ locked.className = 'locked-note';
283
+ locked.textContent = 'Configure FRAIM stays locked until one local AI agent is ready.';
284
+ pane.appendChild(locked);
285
+ }
373
286
  }
374
287
 
375
- function renderAgentLoginStep(opt, loginCommand, loginHint, parentLi) {
376
- const hintEl = document.createElement('p');
377
- hintEl.className = 'install-hint';
378
- hintEl.textContent = loginHint || ('Sign in to ' + opt.label + ' to activate it.');
379
- parentLi.appendChild(hintEl);
380
-
381
- const signInBtn = document.createElement('button');
382
- signInBtn.type = 'button';
383
- signInBtn.className = 'btn btn-primary btn-block';
384
- signInBtn.textContent = 'Sign In to ' + opt.label;
385
- signInBtn.addEventListener('click', async () => {
386
- signInBtn.disabled = true;
387
- signInBtn.textContent = 'Opening terminal...';
388
- try {
389
- const result = await api('/api/first-run/trigger-agent-login', 'POST', { agentId: opt.id });
390
- signInBtn.style.display = 'none';
391
- renderAgentReadyCheck(opt, result && result.message, parentLi);
392
- } catch (err) {
393
- signInBtn.disabled = false;
394
- signInBtn.textContent = 'Sign In to ' + opt.label;
395
- setStatus(err.message, 'error');
396
- }
397
- });
398
- parentLi.appendChild(signInBtn);
288
+ async function configureFraim() {
289
+ if (state.configureInFlight) return;
290
+ state.configureInFlight = true;
291
+ state.configureError = null;
292
+ render();
293
+ setStatus('Configuring FRAIM for your local AI agents...');
294
+ try {
295
+ const resp = await api('/api/first-run/done-recruiting', 'POST', {});
296
+ setSessionFromActionResponse(resp);
297
+ setStatus(resp.message, resp.ok ? null : 'error');
298
+ state.activeStep = 'configure';
299
+ state.configureError = resp && resp.ok ? null : (resp && resp.message ? resp.message : 'FRAIM setup did not complete.');
300
+ } catch (err) {
301
+ state.configureError = err.message;
302
+ setStatus(state.configureError, 'error');
303
+ } finally {
304
+ state.configureInFlight = false;
305
+ render();
306
+ }
399
307
  }
400
308
 
401
- function renderAgentReadyCheck(opt, loginMessage, parentLi) {
402
- const msgEl = document.createElement('p');
403
- msgEl.className = 'install-hint';
404
- msgEl.textContent = loginMessage || ('Complete sign-in in the terminal, then click Check if Ready.');
405
- parentLi.appendChild(msgEl);
406
-
407
- const checkBtn = document.createElement('button');
408
- checkBtn.type = 'button';
409
- checkBtn.className = 'btn btn-primary btn-block';
410
- checkBtn.textContent = 'Check if Ready';
411
-
412
- // Declare skipEl before the click handler so it can be hidden on success.
413
- const skipEl = document.createElement('p');
414
- const skipLink = document.createElement('button');
415
- skipLink.type = 'button';
416
- skipLink.className = 'text-button';
417
- skipLink.textContent = 'Skip for now — I will sign in later';
418
- skipLink.addEventListener('click', async () => {
419
- try {
420
- const ideData = await api('/api/first-run/ide-commands');
421
- renderStartWorking(ideData ? ideData.commands : []);
422
- } catch (_err) {
423
- renderStartWorking([]);
424
- }
425
- });
426
- skipEl.appendChild(skipLink);
309
+ function renderConfigure(pane) {
310
+ const h = document.createElement('h2');
311
+ h.textContent = 'Configure FRAIM';
312
+ pane.appendChild(h);
313
+
314
+ if (detectedAgentCount() < 1) {
315
+ const p = document.createElement('p');
316
+ p.className = 'pane-copy';
317
+ p.textContent = 'A local AI agent is required before FRAIM setup can finish.';
318
+ pane.appendChild(p);
319
+ const back = button('Choose a local agent', 'primary');
320
+ back.addEventListener('click', () => { state.activeStep = 'agents'; render(); });
321
+ pane.appendChild(back);
322
+ return;
323
+ }
427
324
 
428
- checkBtn.addEventListener('click', async () => {
429
- checkBtn.disabled = true;
430
- checkBtn.textContent = 'Checking...';
431
- try {
432
- const result = await api('/api/first-run/check-agent', 'POST', { agentId: opt.id });
433
- if (result && result.ready) {
434
- checkBtn.style.display = 'none';
435
- skipEl.style.display = 'none';
436
- msgEl.textContent = opt.label + ' is ready!';
437
- setHeader(opt.label + ' is ready!', 'You can now open the Hub.');
438
- renderOpenHubButton(parentLi);
439
- } else {
440
- checkBtn.disabled = false;
441
- checkBtn.textContent = 'Check if Ready';
442
- setStatus((result && result.message) || (opt.label + ' not detected yet. Complete sign-in and try again.'), 'error');
443
- }
444
- } catch (err) {
445
- checkBtn.disabled = false;
446
- checkBtn.textContent = 'Check if Ready';
447
- setStatus(err.message, 'error');
325
+ if (!configureReady()) {
326
+ const p = document.createElement('p');
327
+ p.className = 'pane-copy';
328
+ p.textContent = state.configureError
329
+ ? 'FRAIM setup needs attention before you continue.'
330
+ : 'FRAIM is configuring MCP and local setup files for every ready agent.';
331
+ pane.appendChild(p);
332
+ const status = document.createElement('div');
333
+ status.className = state.configureError ? 'ready-strip ready-strip--error' : 'ready-strip';
334
+ status.textContent = state.configureError || 'Configuring FRAIM...';
335
+ pane.appendChild(status);
336
+ if (state.configureError) {
337
+ const retry = button('Try again', 'primary');
338
+ retry.addEventListener('click', configureFraim);
339
+ pane.appendChild(retry);
340
+ } else if (!state.configureInFlight) {
341
+ window.setTimeout(configureFraim, 0);
448
342
  }
449
- });
343
+ return;
344
+ }
450
345
 
451
- parentLi.appendChild(checkBtn);
452
- parentLi.appendChild(skipEl);
346
+ const success = document.createElement('div');
347
+ success.className = 'ready-strip';
348
+ success.textContent = 'FRAIM setup succeeded for your ready local AI agents.';
349
+ pane.appendChild(success);
350
+ const next = button('Next', 'primary');
351
+ next.addEventListener('click', () => { state.activeStep = 'use'; render(); });
352
+ pane.appendChild(next);
453
353
  }
454
354
 
455
- function renderOpenHubButton(parentLi) {
456
- const openHubBtn = document.createElement('button');
457
- openHubBtn.type = 'button';
458
- openHubBtn.className = 'btn btn-primary btn-block';
459
- openHubBtn.textContent = 'Open Hub';
460
- openHubBtn.addEventListener('click', async () => {
461
- openHubBtn.disabled = true;
462
- setStatus('Opening Hub...');
463
- try {
464
- await api('/api/first-run/set-preference', 'POST', { choice: 'hub' });
465
- } catch (_err) { /* non-fatal */ }
466
- try {
467
- const openResp = await api('/api/first-run/open-hub', 'POST');
468
- if (openResp && openResp.message) setStatus(openResp.message);
469
- if (openResp && openResp.hubUrl) window.location.replace(openResp.hubUrl);
470
- else openHubBtn.disabled = false;
471
- } catch (err) {
472
- openHubBtn.disabled = false;
473
- setStatus(err.message, 'error');
474
- }
475
- });
476
- parentLi.appendChild(openHubBtn);
355
+ function renderUseFraim(pane) {
356
+ const h = document.createElement('h2');
357
+ h.textContent = 'Use FRAIM';
358
+ pane.appendChild(h);
359
+
360
+ const p = document.createElement('p');
361
+ p.className = 'pane-copy';
362
+ p.textContent = 'FRAIM is set up to use your ready local AI agents. Choose where you want to work.';
363
+ pane.appendChild(p);
364
+ renderStartWorkingChoices(pane);
477
365
  }
478
366
 
479
- function renderStartWorking(ideCommands) {
480
- CHECKLIST_EL.className = 'selection-container start-container';
481
- CHECKLIST_EL.innerHTML = '';
482
- PRIMARY_BUTTON.style.display = 'none';
483
- setHeader('Start in your IDE', 'Recommended: fully restart your favorite IDE, open your project there, and let FRAIM run in the surface you already use.');
367
+ function renderStartWorkingChoices(pane) {
368
+ const choices = document.createElement('div');
369
+ choices.className = 'choice-grid';
484
370
 
485
- const ideLi = document.createElement('li');
486
371
  const ideCard = document.createElement('div');
487
- ideCard.className = 'user-type-card user-type-card--featured user-type-card--ide-default';
488
- ideCard.appendChild(cardHeader('</>', 'In my IDE', 'Recommended. Restart Claude Code, Cursor, Codex, or your favorite AI IDE before you continue.'));
489
- ideCard.appendChild(cardEyebrow('Recommended default'));
490
- const ideNote = document.createElement('p');
491
- ideNote.className = 'route-note';
492
- ideNote.textContent = 'This keeps FRAIM inside the tool you already work in and avoids the extra layer of the Hub.';
493
- ideCard.appendChild(ideNote);
494
- const ideBtn = document.createElement('button');
495
- ideBtn.type = 'button';
496
- ideBtn.className = 'btn btn-primary btn-block';
497
- ideBtn.textContent = 'Continue in my IDE';
372
+ ideCard.className = 'user-type-card user-type-card--featured';
373
+ ideCard.innerHTML = '<strong class="card-title">In my IDE</strong><p class="card-desc">Use FRAIM inside Claude Code, Codex, Cursor, or the local agent tool you already use.</p>';
374
+ const ideBtn = button('Continue in my IDE', 'primary');
498
375
  ideBtn.addEventListener('click', async () => {
499
- try {
500
- await api('/api/first-run/set-preference', 'POST', { choice: 'ide' });
501
- } catch (_err) { /* non-fatal */ }
502
- renderIdeCommandDisplay(ideCommands);
376
+ try { await api('/api/first-run/set-preference', 'POST', { choice: 'ide' }); } catch (_) {}
377
+ const ideData = await api('/api/first-run/ide-commands');
378
+ renderIdeCommandDisplay(ideData ? ideData.commands : []);
503
379
  });
504
380
  ideCard.appendChild(ideBtn);
505
- ideLi.appendChild(ideCard);
506
- CHECKLIST_EL.appendChild(ideLi);
507
381
 
508
- const hubLi = document.createElement('li');
509
382
  const hubCard = document.createElement('div');
510
- hubCard.className = 'user-type-card user-type-card--compact user-type-card--alpha';
511
- hubCard.appendChild(cardHeader('α', 'AI Hub Alpha', 'Early testers only'));
512
- hubCard.appendChild(cardEyebrow('Optional fallback'));
513
- const hubNote = document.createElement('p');
514
- hubNote.className = 'route-note route-note--compact';
515
- hubNote.textContent = 'Use the Hub only if you specifically want the experimental browser shell.';
516
- hubCard.appendChild(hubNote);
517
- const hubBtn = document.createElement('button');
518
- hubBtn.type = 'button';
519
- hubBtn.className = 'btn btn-secondary';
520
- hubBtn.textContent = 'Open AI Hub Alpha';
383
+ hubCard.className = 'user-type-card';
384
+ hubCard.innerHTML = '<strong class="card-title">In FRAIM Hub</strong><p class="card-desc">Open the Company, Manager, and Projects shell.</p>';
385
+ const hubBtn = button('Open FRAIM Hub', 'secondary');
521
386
  hubBtn.addEventListener('click', async () => {
522
- try {
523
- await api('/api/first-run/set-preference', 'POST', { choice: 'hub' });
524
- } catch (_err) { /* non-fatal */ }
387
+ try { await api('/api/first-run/set-preference', 'POST', { choice: 'hub' }); } catch (_) {}
525
388
  try {
526
389
  hubBtn.disabled = true;
527
390
  setStatus('Opening Hub...');
528
391
  const openResp = await api('/api/first-run/open-hub', 'POST');
529
392
  if (openResp && openResp.needsAgentSetup) {
530
- renderRecruitAgents();
393
+ state.activeStep = 'agents';
394
+ render();
395
+ setStatus(openResp.message, 'error');
531
396
  return;
532
397
  }
533
- if (openResp && openResp.message) setStatus(openResp.message);
534
398
  if (openResp && openResp.hubUrl) window.location.replace(openResp.hubUrl);
535
- else hubBtn.disabled = false;
536
399
  } catch (err) {
537
400
  hubBtn.disabled = false;
538
401
  setStatus(err.message, 'error');
539
402
  }
540
403
  });
541
404
  hubCard.appendChild(hubBtn);
542
- hubLi.appendChild(hubCard);
543
- CHECKLIST_EL.appendChild(hubLi);
544
-
545
- setTimeout(() => ideBtn.focus(), 0);
546
- }
547
-
548
- function cardHeader(iconText, titleText, descText) {
549
- const header = document.createElement('div');
550
- header.className = 'card-header';
551
- const icon = document.createElement('div');
552
- icon.className = 'card-icon';
553
- icon.textContent = iconText;
554
- const text = document.createElement('div');
555
- const title = document.createElement('strong');
556
- title.className = 'card-title';
557
- title.textContent = titleText;
558
- const desc = document.createElement('p');
559
- desc.className = 'card-desc';
560
- desc.textContent = descText;
561
- text.appendChild(title);
562
- text.appendChild(desc);
563
- header.appendChild(icon);
564
- header.appendChild(text);
565
- return header;
566
- }
567
-
568
- function cardEyebrow(text) {
569
- const eyebrow = document.createElement('span');
570
- eyebrow.className = 'card-eyebrow';
571
- eyebrow.textContent = text;
572
- return eyebrow;
405
+ choices.appendChild(ideCard);
406
+ choices.appendChild(hubCard);
407
+ pane.appendChild(choices);
573
408
  }
574
409
 
575
410
  function renderIdeCommandDisplay(commands) {
576
- CHECKLIST_EL.className = 'selection-container';
577
- CHECKLIST_EL.innerHTML = '';
578
- setHeader('Restart your favorite IDE', 'Fully restart your IDE first so it picks up the new FRAIM setup. Then open your project there and type:');
579
-
580
- const cmdList = (commands && commands.length > 0) ? commands : ['/fraim onboard this project'];
581
- for (const cmd of cmdList) {
582
- const li = document.createElement('li');
583
- const row = document.createElement('div');
584
- row.className = 'command-row';
585
- const block = document.createElement('div');
586
- block.className = 'cmd-block';
587
- block.textContent = cmd;
588
- const copyBtn = document.createElement('button');
589
- copyBtn.type = 'button';
590
- copyBtn.className = 'btn btn-secondary';
591
- copyBtn.textContent = 'Copy';
592
- copyBtn.setAttribute('aria-label', `Copy command: ${cmd}`);
593
- copyBtn.addEventListener('click', async () => {
594
- try {
411
+ renderShell((pane) => {
412
+ const h = document.createElement('h2');
413
+ h.textContent = 'Continue in your IDE';
414
+ pane.appendChild(h);
415
+ const copy = document.createElement('p');
416
+ copy.className = 'pane-copy';
417
+ copy.textContent = 'Restart your IDE so it picks up FRAIM setup, then run:';
418
+ pane.appendChild(copy);
419
+ const cmdList = (commands && commands.length > 0) ? commands : ['/fraim onboard this project'];
420
+ for (const cmd of cmdList) {
421
+ const row = document.createElement('div');
422
+ row.className = 'command-row';
423
+ const block = document.createElement('div');
424
+ block.className = 'cmd-block';
425
+ block.textContent = cmd;
426
+ const copyBtn = button('Copy', 'secondary');
427
+ copyBtn.setAttribute('aria-label', `Copy command: ${cmd}`);
428
+ copyBtn.addEventListener('click', async () => {
595
429
  await navigator.clipboard.writeText(cmd);
596
430
  copyBtn.textContent = 'Copied';
597
431
  setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500);
598
- } catch (_err) {
599
- // Fallback for environments where clipboard API is unavailable.
600
- try {
601
- const ta = document.createElement('textarea');
602
- ta.value = cmd;
603
- ta.style.position = 'fixed';
604
- ta.style.opacity = '0';
605
- document.body.appendChild(ta);
606
- ta.select();
607
- document.execCommand('copy');
608
- document.body.removeChild(ta);
609
- copyBtn.textContent = 'Copied';
610
- setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500);
611
- } catch (_e) {
612
- copyBtn.textContent = 'Copy failed';
613
- setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500);
614
- }
615
- }
616
- });
617
- row.appendChild(block);
618
- row.appendChild(copyBtn);
619
- li.appendChild(row);
620
- CHECKLIST_EL.appendChild(li);
621
- }
432
+ });
433
+ row.appendChild(block);
434
+ row.appendChild(copyBtn);
435
+ pane.appendChild(row);
436
+ }
437
+ const back = button('Back to choices', 'secondary');
438
+ back.setAttribute('data-testid', 'back-to-use-fraim');
439
+ back.addEventListener('click', () => { state.activeStep = 'use'; render(); });
440
+ pane.appendChild(back);
441
+ });
442
+ }
622
443
 
623
- const switchLi = document.createElement('li');
624
- const switchLink = document.createElement('button');
625
- switchLink.type = 'button';
626
- switchLink.className = 'text-button';
627
- switchLink.textContent = 'Prefer AI Hub Alpha instead';
628
- switchLink.addEventListener('click', async () => {
444
+ function openAgentModal(opt) {
445
+ const overlay = document.createElement('div');
446
+ overlay.className = 'modal-backdrop';
447
+ overlay.setAttribute('data-testid', 'agent-install-modal');
448
+ const modal = document.createElement('div');
449
+ modal.className = 'modal';
450
+ modal.setAttribute('role', 'dialog');
451
+ modal.setAttribute('aria-modal', 'true');
452
+ modal.innerHTML = `<h2>Set up ${escapeHtml(opt.label)}</h2><p class="install-status" data-testid="agent-install-status">Ready to set up ${escapeHtml(opt.label)}.</p><p class="modal-help">Setup installs the CLI, opens sign-in, and verifies the agent is available before FRAIM uses it.</p>`;
453
+ const status = modal.querySelector('[data-testid="agent-install-status"]');
454
+ const actions = document.createElement('div');
455
+ actions.className = 'install-actions';
456
+
457
+ const install = button('Set up', 'primary');
458
+ const signIn = button(`Sign In to ${opt.label}`, 'secondary');
459
+ const check = button('Check readiness', 'secondary');
460
+ const close = button('Close', 'ghost');
461
+ signIn.disabled = true;
462
+ check.disabled = true;
463
+ close.addEventListener('click', () => { overlay.remove(); render(); });
464
+
465
+ install.addEventListener('click', async () => {
466
+ install.disabled = true;
467
+ status.textContent = `Setting up ${opt.label}...`;
629
468
  try {
630
- await api('/api/first-run/set-preference', 'POST', { choice: 'hub' });
631
- const openResp = await api('/api/first-run/open-hub', 'POST');
632
- if (openResp && openResp.needsAgentSetup) {
633
- renderRecruitAgents();
469
+ const result = await api('/api/first-run/install-agent', 'POST', { agentId: opt.id });
470
+ if (!result || !result.ok) throw new Error(result && result.message ? result.message : 'Setup failed.');
471
+ status.textContent = `${opt.label} setup started. Sign in next.`;
472
+ signIn.disabled = false;
473
+ await loadSession(false);
474
+ } catch (err) {
475
+ install.disabled = false;
476
+ install.textContent = 'Retry';
477
+ status.textContent = err.message;
478
+ status.setAttribute('data-tone', 'error');
479
+ showAgentInstallError(modal, opt, err.message, install, close);
480
+ }
481
+ });
482
+
483
+ signIn.addEventListener('click', async () => {
484
+ signIn.disabled = true;
485
+ status.textContent = 'Opening terminal for sign-in...';
486
+ try {
487
+ const result = await api('/api/first-run/trigger-agent-login', 'POST', { agentId: opt.id });
488
+ status.textContent = result && result.message ? result.message : 'Complete sign-in, then check readiness.';
489
+ check.disabled = false;
490
+ } catch (err) {
491
+ signIn.disabled = false;
492
+ status.textContent = err.message;
493
+ status.setAttribute('data-tone', 'error');
494
+ }
495
+ });
496
+
497
+ check.addEventListener('click', async () => {
498
+ check.disabled = true;
499
+ status.textContent = 'Checking that the CLI is installed, signed in, and available on PATH...';
500
+ try {
501
+ const result = await api('/api/first-run/check-agent', 'POST', { agentId: opt.id });
502
+ if (result && result.ready) {
503
+ status.textContent = `${opt.label} is ready!`;
504
+ await loadSession(false);
505
+ setTimeout(() => { overlay.remove(); state.activeStep = 'agents'; render(); }, 250);
634
506
  return;
635
507
  }
636
- if (openResp && openResp.hubUrl) window.location.replace(openResp.hubUrl);
508
+ status.textContent = result && result.message ? result.message : `${opt.label} is not ready yet.`;
509
+ status.setAttribute('data-tone', 'error');
510
+ check.disabled = false;
637
511
  } catch (err) {
638
- setStatus(err.message, 'error');
512
+ status.textContent = err.message;
513
+ status.setAttribute('data-tone', 'error');
514
+ check.disabled = false;
515
+ }
516
+ });
517
+
518
+ actions.appendChild(install);
519
+ actions.appendChild(signIn);
520
+ actions.appendChild(check);
521
+ actions.appendChild(close);
522
+ modal.appendChild(actions);
523
+ overlay.appendChild(modal);
524
+ document.body.appendChild(overlay);
525
+ install.focus();
526
+ }
527
+
528
+ function showAgentInstallError(modal, opt, message, retryButton, closeButton) {
529
+ const existing = modal.querySelector('[data-testid="error-frame"]');
530
+ if (existing) existing.remove();
531
+ if (!window.FraimErrorFrame || typeof window.FraimErrorFrame.render !== 'function') return;
532
+ const frame = window.FraimErrorFrame.render({
533
+ whatTried: `We tried to set up ${opt.label}.`,
534
+ whatHappened: message,
535
+ actions: [
536
+ { id: 'retry', label: 'Retry', variant: 'primary' },
537
+ { id: 'alternative', label: 'Choose another agent', variant: 'secondary' },
538
+ { id: 'manual', label: 'Manual setup help', variant: 'ghost' },
539
+ ],
540
+ }, (action) => {
541
+ if (action.id === 'retry') {
542
+ retryButton.click();
543
+ } else if (action.id === 'alternative') {
544
+ closeButton.click();
545
+ } else if (action.id === 'manual') {
546
+ const status = modal.querySelector('[data-testid="agent-install-status"]');
547
+ if (status) {
548
+ status.textContent = 'Run npx fraim add-ide for manual setup, then return here when your local agent is ready.';
549
+ status.removeAttribute('data-tone');
550
+ }
639
551
  }
640
552
  });
641
- switchLi.appendChild(switchLink);
642
- CHECKLIST_EL.appendChild(switchLi);
553
+ modal.appendChild(frame);
554
+ }
555
+
556
+ function button(text, variant) {
557
+ const btn = document.createElement('button');
558
+ btn.type = 'button';
559
+ btn.className = variant === 'primary' ? 'btn btn-primary' : variant === 'ghost' ? 'btn btn-ghost' : 'btn btn-secondary';
560
+ btn.textContent = text;
561
+ return btn;
562
+ }
563
+
564
+ function escapeHtml(value) {
565
+ return String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
566
+ }
567
+
568
+ async function loadSession(shouldRender) {
569
+ state.session = await api('/api/first-run/session');
570
+ if (!state.session.state.agentInstalls) state.session.state.agentInstalls = {};
571
+ if (shouldRender !== false) {
572
+ state.activeStep = chooseActiveStep();
573
+ render();
574
+ }
643
575
  }
644
576
 
645
577
  function render() {
646
578
  if (!state.session) return;
647
- CHECKLIST_EL.className = 'checklist';
648
- PRIMARY_BUTTON.style.display = '';
649
- const { rows, primaryButtonLabel } = state.session;
650
- CHECKLIST_EL.innerHTML = '';
651
- for (const row of rows) {
652
- CHECKLIST_EL.appendChild(renderRow(row));
653
- }
654
- if (primaryButtonLabel === 'Get Started') {
655
- const summary = renderSetupSummary();
656
- if (summary) CHECKLIST_EL.appendChild(summary);
657
- }
658
- PRIMARY_BUTTON.textContent = primaryButtonLabel || 'Set up FRAIM';
659
- applyHeading(rows);
579
+ if (!STEP_ORDER.includes(state.activeStep)) state.activeStep = chooseActiveStep();
580
+ renderShell((pane) => {
581
+ if (state.activeStep === 'prereqs') renderPrereqs(pane);
582
+ else if (state.activeStep === 'agents') renderAgents(pane);
583
+ else if (state.activeStep === 'configure') renderConfigure(pane);
584
+ else renderUseFraim(pane);
585
+ });
660
586
  }
661
587
 
662
- PRIMARY_BUTTON.addEventListener('click', onPrimaryClick);
588
+ PRIMARY_BUTTON.addEventListener('click', () => {
589
+ state.activeStep = chooseActiveStep();
590
+ render();
591
+ });
663
592
 
664
593
  loadSession().catch((err) => {
665
594
  setStatus(err.message || 'Could not load first-run.', 'error');