fraim-framework 2.0.196 → 2.0.198

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.
@@ -1891,6 +1891,22 @@ img.coach-employee-avatar { object-fit: cover; border-radius: 4px; }
1891
1891
 
1892
1892
  /* Agent install panel */
1893
1893
  #agent-install-panel { margin-top: 10px; }
1894
+ .hub-agent-setup-panel {
1895
+ width: min(560px, 100%);
1896
+ margin: 16px 0;
1897
+ padding: 14px;
1898
+ border: 1px solid var(--line);
1899
+ border-radius: 8px;
1900
+ background: var(--surface);
1901
+ box-shadow: var(--shadow);
1902
+ text-align: left;
1903
+ }
1904
+ .cp-agent-install-panel {
1905
+ margin: 0 14px 12px;
1906
+ padding: 12px;
1907
+ border-top: 1px solid var(--line);
1908
+ background: var(--surface);
1909
+ }
1894
1910
  .install-panel-heading {
1895
1911
  font-size: 12px;
1896
1912
  font-weight: 600;
@@ -1899,6 +1915,12 @@ img.coach-employee-avatar { object-fit: cover; border-radius: 4px; }
1899
1915
  letter-spacing: .04em;
1900
1916
  margin-bottom: 6px;
1901
1917
  }
1918
+ .install-panel-copy {
1919
+ margin: 0 0 8px;
1920
+ max-width: none;
1921
+ font-size: 13px;
1922
+ color: var(--muted);
1923
+ }
1902
1924
  .install-row {
1903
1925
  display: flex;
1904
1926
  align-items: center;
@@ -4409,6 +4431,51 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
4409
4431
  .nav-right { height: 44px; display: flex; align-items: center; }
4410
4432
  .avatar-btn { margin-left: 8px; }
4411
4433
 
4434
+ /* ════════════════ Issue #744 — Hub cobranding ════════════════ */
4435
+ /* Company brand lockup at the far left of the top nav (before the tabs).
4436
+ Sits in the Electron drag region; interactive children opt out. */
4437
+ .hub-brand { display: inline-flex; align-items: center; gap: 9px; height: 44px; padding: 0 4px 0 14px; flex-shrink: 0; -webkit-app-region: drag; }
4438
+ .hub-brand[hidden] { display: none; }
4439
+ .hub-brand-logo { width: 22px; height: 22px; border-radius: 6px; display: block; flex-shrink: 0; box-shadow: 0 0 0 1px var(--line); overflow: hidden; }
4440
+ .hub-brand-logo img, .hub-brand-logo svg { width: 100%; height: 100%; object-fit: contain; display: block; border-radius: 6px; }
4441
+ /* R1.6: a normal name shows in full; a very long one caps and ellipsizes so it
4442
+ never wraps or pushes the tabs off-screen. */
4443
+ .hub-brand-name { font-size: 14px; font-weight: 650; color: var(--text); letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 220px; }
4444
+ .hub-brand-divider { width: 1px; height: 20px; background: var(--line); margin: 0 6px 0 14px; flex-shrink: 0; }
4445
+ .hub-brand-divider[hidden] { display: none; }
4446
+ /* FRAIM co-mark on the right, before the avatar (co-branding, not white-label). */
4447
+ .hub-cobrand { display: inline-flex; align-items: center; gap: 6px; font-size: 11px; color: var(--muted); margin: 0 10px 0 0; white-space: nowrap; -webkit-app-region: no-drag; }
4448
+ .hub-cobrand[hidden] { display: none; }
4449
+ .hub-cobrand-mark { width: 17px; height: 17px; border-radius: 5px; display: block; }
4450
+ /* Very narrow windows (the Hub is desktop/Electron-first, but keep the dense nav
4451
+ from forcing horizontal page scroll): drop the co-mark and the company name,
4452
+ keeping the company logo mark + tabs. */
4453
+ @media (max-width: 560px) {
4454
+ .hub-cobrand { display: none; }
4455
+ .hub-brand-name { display: none; }
4456
+ .hub-brand { padding: 0 2px 0 10px; }
4457
+ .hub-brand-divider { margin: 0 4px 0 8px; }
4458
+ .hub-tab { padding: 10px 12px; }
4459
+ }
4460
+
4461
+ /* Brand editor (Company tab → Brand accordion). */
4462
+ .brand-editor { padding: 4px 2px 2px; }
4463
+ .brand-field { margin: 0 0 16px; }
4464
+ .brand-field > label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; color: var(--text); }
4465
+ .brand-field .brand-hint { font-size: 12px; color: var(--muted); margin-top: 5px; }
4466
+ .brand-field .brand-hint.warn { color: var(--warn); }
4467
+ .brand-logo-drop { display: flex; align-items: center; gap: 14px; border: 1px dashed color-mix(in srgb, var(--accent) 40%, var(--line)); border-radius: 12px; padding: 14px; background: var(--accent-soft); cursor: pointer; }
4468
+ .brand-logo-drop .blp { width: 44px; height: 44px; border-radius: 10px; box-shadow: 0 0 0 1px var(--line); flex-shrink: 0; overflow: hidden; background: var(--surface); display: flex; align-items: center; justify-content: center; }
4469
+ .brand-logo-drop .blp svg, .brand-logo-drop .blp img { width: 100%; height: 100%; object-fit: contain; }
4470
+ .brand-logo-drop .blp-txt { font-size: 12.5px; color: var(--text); }
4471
+ .brand-swatch-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
4472
+ .brand-swatch { width: 34px; height: 34px; border-radius: 9px; cursor: pointer; box-shadow: 0 0 0 1px var(--line); border: none; padding: 0; }
4473
+ .brand-swatch.sel { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent); }
4474
+ .brand-color-hex { width: 96px; }
4475
+ .brand-editor-actions { display: flex; gap: 10px; margin-top: 6px; align-items: center; }
4476
+ .brand-preview { border: 1px solid var(--line); border-radius: 12px; overflow: hidden; margin-top: 14px; }
4477
+ .brand-preview-cap { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); padding: 10px 14px; background: var(--bg); border-bottom: 1px solid var(--line); }
4478
+
4412
4479
  /* ── New-project: folder picker row ── */
4413
4480
  .np-folder-row { display:flex; gap:8px; }
4414
4481
  .np-folder-row input { flex:1; }
@@ -79,6 +79,19 @@
79
79
  return setupResult && Array.isArray(setupResult.configuredSurfaces) ? setupResult.configuredSurfaces : [];
80
80
  }
81
81
 
82
+ function readyAgentLabels() {
83
+ const labels = new Set();
84
+ for (const agent of readyAgents()) labels.add(agent.label);
85
+ for (const surface of configuredSurfaces()) {
86
+ if (surface && surface.name) labels.add(surface.name);
87
+ }
88
+ return Array.from(labels);
89
+ }
90
+
91
+ function slugifyAgentName(name) {
92
+ return String(name || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
93
+ }
94
+
82
95
  function isAgentReady(opt) {
83
96
  const installState = state.session && state.session.state && state.session.state.agentInstalls
84
97
  ? state.session.state.agentInstalls[opt.id]
@@ -208,38 +221,54 @@
208
221
  h.textContent = 'Locally installed AI agents';
209
222
  pane.appendChild(h);
210
223
 
211
- const anyDetected = detectedAgentCount() > 0;
212
224
  const p = document.createElement('p');
213
225
  p.className = 'pane-copy';
214
- p.textContent = anyDetected
215
- ? 'An AI tool is ready. You can add more or continue to configure FRAIM.'
216
- : 'Install any of the AI tools below, then click Rescan once it is running.';
226
+ p.textContent = detectedAgentCount() > 0
227
+ ? 'At least one local AI agent is ready. You can add more agents or continue to configure FRAIM.'
228
+ : 'No problem, we will install AI agents next.';
217
229
  pane.appendChild(p);
218
230
 
219
- // Server-driven list: session.supportedAgents contains every GUI-detectable IDE
220
- // with detection status and download URL already resolved.
221
- const agents = (state.session && state.session.supportedAgents) || [];
231
+ const readyLabels = readyAgentLabels();
232
+ const readyList = document.createElement('div');
233
+ readyList.className = readyLabels.length > 0 ? 'ready-strip' : 'ready-strip ready-strip--muted';
234
+ readyList.setAttribute('data-testid', 'ready-agents');
235
+ readyList.textContent = readyLabels.length > 0
236
+ ? `Already installed: ${readyLabels.join(', ')}`
237
+ : 'Already installed: none detected yet';
238
+ pane.appendChild(readyList);
239
+
240
+ const catalogHeading = document.createElement('h3');
241
+ catalogHeading.className = 'agent-section-heading';
242
+ catalogHeading.textContent = 'Supported AI tools and IDEs';
243
+ pane.appendChild(catalogHeading);
244
+
245
+ const catalogCopy = document.createElement('p');
246
+ catalogCopy.className = 'pane-copy';
247
+ catalogCopy.textContent = 'FRAIM can configure every supported local agent or IDE it detects. Download links stay available for tools not installed yet.';
248
+ pane.appendChild(catalogCopy);
222
249
 
223
250
  const list = document.createElement('ul');
224
251
  list.className = 'agent-list';
225
- list.setAttribute('data-testid', 'agent-list');
226
-
227
- for (const agent of agents) {
252
+ list.setAttribute('data-testid', 'supported-agent-list');
253
+ const supportedAgents = (state.session && state.session.supportedAgents) || [];
254
+ for (const agent of supportedAgents) {
255
+ const slug = slugifyAgentName(agent.name);
256
+ const detected = agent.detected || readyLabels.includes(agent.name);
228
257
  const li = document.createElement('li');
229
- li.className = 'agent-row' + (agent.detected ? ' agent-row--detected' : '');
230
- li.setAttribute('data-testid', `agent-row-${agent.name.toLowerCase().replace(/\s+/g, '-')}`);
258
+ li.className = 'agent-row' + (detected ? ' agent-row--detected' : '');
259
+ li.setAttribute('data-testid', `supported-agent-row-${slug}`);
231
260
 
232
261
  const nameSpan = document.createElement('span');
233
262
  nameSpan.className = 'agent-row__name';
234
263
  nameSpan.textContent = agent.name;
264
+ li.appendChild(nameSpan);
235
265
 
236
266
  const actionSpan = document.createElement('span');
237
267
  actionSpan.className = 'agent-row__action';
238
-
239
- if (agent.detected) {
268
+ if (detected) {
240
269
  const badge = document.createElement('span');
241
270
  badge.className = 'agent-badge agent-badge--installed';
242
- badge.setAttribute('data-testid', `agent-installed-${agent.name.toLowerCase().replace(/\s+/g, '-')}`);
271
+ badge.setAttribute('data-testid', `supported-agent-installed-${slug}`);
243
272
  badge.textContent = 'Installed';
244
273
  actionSpan.appendChild(badge);
245
274
  } else if (agent.downloadUrl) {
@@ -249,23 +278,20 @@
249
278
  link.target = '_blank';
250
279
  link.rel = 'noopener noreferrer';
251
280
  link.textContent = 'Download';
252
- link.setAttribute('data-testid', `agent-download-${agent.name.toLowerCase().replace(/\s+/g, '-')}`);
281
+ link.setAttribute('data-testid', `supported-agent-download-${slug}`);
253
282
  actionSpan.appendChild(link);
283
+ } else {
284
+ const unavailable = document.createElement('span');
285
+ unavailable.className = 'agent-row__muted';
286
+ unavailable.textContent = 'Manual setup';
287
+ actionSpan.appendChild(unavailable);
254
288
  }
255
-
256
- li.appendChild(nameSpan);
257
289
  li.appendChild(actionSpan);
258
290
  list.appendChild(li);
259
291
  }
260
-
261
292
  pane.appendChild(list);
262
293
 
263
- const otherNote = document.createElement('p');
264
- otherNote.className = 'pane-copy';
265
- otherNote.innerHTML = 'Using a different tool? Run <code>npx fraim add-ide</code> from your project directory after setup.';
266
- pane.appendChild(otherNote);
267
-
268
- const rescanBtn = button('Rescan installed tools', 'ghost');
294
+ const rescanBtn = button('Rescan supported tools', 'ghost');
269
295
  rescanBtn.setAttribute('data-testid', 'rescan-agents');
270
296
  rescanBtn.addEventListener('click', async () => {
271
297
  rescanBtn.disabled = true;
@@ -277,6 +303,53 @@
277
303
  });
278
304
  pane.appendChild(rescanBtn);
279
305
 
306
+ const hubHeading = document.createElement('h3');
307
+ hubHeading.className = 'agent-section-heading';
308
+ hubHeading.textContent = 'Hub-ready CLI agents';
309
+ pane.appendChild(hubHeading);
310
+
311
+ const hubCopy = document.createElement('p');
312
+ hubCopy.className = 'pane-copy';
313
+ hubCopy.textContent = 'FRAIM Hub runs jobs through CLI agents. Set up at least one of these if you plan to launch jobs in Hub.';
314
+ pane.appendChild(hubCopy);
315
+
316
+ const grid = document.createElement('div');
317
+ grid.className = 'agent-grid';
318
+ const agentOptions = state.session.agentOptions || [];
319
+ for (const opt of agentOptions) {
320
+ const card = document.createElement('div');
321
+ card.className = 'user-type-card recruit-card';
322
+ card.setAttribute('data-agent-id', opt.id);
323
+ card.setAttribute('data-testid', `agent-card-${opt.id}`);
324
+ const agentReady = isAgentReady(opt);
325
+
326
+ const title = document.createElement('strong');
327
+ title.className = 'card-title';
328
+ title.textContent = opt.label;
329
+
330
+ const desc = document.createElement('p');
331
+ desc.className = 'card-desc';
332
+ desc.textContent = agentReady
333
+ ? 'Already installed and ready for FRAIM.'
334
+ : `Available to set up: install, sign in, and verify ${opt.label}.`;
335
+
336
+ const action = button(agentReady ? 'Ready' : 'Set up', 'secondary');
337
+ action.disabled = agentReady;
338
+ action.setAttribute('data-testid', `install-${opt.id}`);
339
+ action.addEventListener('click', () => openAgentModal(opt));
340
+
341
+ card.appendChild(title);
342
+ card.appendChild(desc);
343
+ card.appendChild(action);
344
+ grid.appendChild(card);
345
+ }
346
+
347
+ const byoa = document.createElement('div');
348
+ byoa.className = 'user-type-card recruit-card byoa-card';
349
+ 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>';
350
+ grid.appendChild(byoa);
351
+ pane.appendChild(grid);
352
+
280
353
  const done = button('Next', 'primary');
281
354
  done.setAttribute('data-testid', 'done-installing-agents');
282
355
  done.disabled = detectedAgentCount() < 1;
@@ -531,15 +531,22 @@ body {
531
531
  gap: 12px;
532
532
  }
533
533
 
534
- /* Compact server-driven agent list */
534
+ .agent-section-heading {
535
+ margin: 8px 0 -6px;
536
+ font-size: 13px;
537
+ font-weight: 700;
538
+ color: var(--text);
539
+ }
540
+
535
541
  .agent-list {
536
542
  list-style: none;
537
- margin: 0 0 12px;
543
+ margin: 0;
538
544
  padding: 0;
539
545
  border: 1px solid var(--line);
540
546
  border-radius: 10px;
541
547
  overflow: hidden;
542
548
  }
549
+
543
550
  .agent-row {
544
551
  display: flex;
545
552
  align-items: center;
@@ -549,32 +556,51 @@ body {
549
556
  background: var(--surface);
550
557
  gap: 12px;
551
558
  }
559
+
552
560
  .agent-row:last-child { border-bottom: 0; }
553
561
  .agent-row--detected { background: var(--accent-soft); }
562
+
554
563
  .agent-row__name {
564
+ min-width: 0;
565
+ flex: 1;
555
566
  font-size: 14px;
556
567
  font-weight: 500;
557
568
  color: var(--text);
558
- flex: 1;
559
569
  }
570
+
560
571
  .agent-row--detected .agent-row__name { color: var(--accent-strong); }
561
- .agent-row__action { flex-shrink: 0; }
562
- .agent-badge {
572
+
573
+ .agent-row__action {
574
+ flex: 0 0 auto;
575
+ }
576
+
577
+ .agent-row__muted {
563
578
  font-size: 12px;
564
- font-weight: 600;
579
+ color: var(--muted);
580
+ }
581
+
582
+ .agent-badge {
583
+ display: inline-flex;
584
+ align-items: center;
585
+ min-height: 24px;
565
586
  padding: 3px 10px;
566
- border-radius: 20px;
587
+ border-radius: 999px;
588
+ font-size: 12px;
589
+ font-weight: 700;
567
590
  }
591
+
568
592
  .agent-badge--installed {
569
593
  background: var(--accent);
570
594
  color: #fff;
571
595
  }
596
+
572
597
  .agent-download-link {
573
598
  font-size: 13px;
574
- font-weight: 500;
599
+ font-weight: 600;
575
600
  color: var(--accent-strong);
576
601
  text-decoration: none;
577
602
  }
603
+
578
604
  .agent-download-link:hover { text-decoration: underline; }
579
605
 
580
606
  .ready-strip {