vibespot 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -271
- package/assets/prompts.bundle.json +53 -0
- package/assets/readme/00-hero-banner.png +0 -0
- package/assets/readme/00-hero-banner.svg +59 -0
- package/assets/readme/01-vibe-coding-hero.png +0 -0
- package/assets/readme/02-plan-mode.png +0 -0
- package/assets/readme/03-figma-import.png +0 -0
- package/assets/readme/04-multi-page-sites.png +0 -0
- package/assets/readme/05-inline-wysiwyg.png +0 -0
- package/assets/readme/06-hubspot-upload.png +0 -0
- package/dist/index.js +420 -420
- package/dist/index.js.map +1 -1
- package/package.json +10 -3
- package/ui/chat.js +106 -0
- package/ui/docs/index.html +55 -16
- package/ui/docs/screenshots.zip +0 -0
- package/ui/index.html +1 -0
- package/ui/settings.js +339 -17
- package/ui/styles.css +22 -0
package/ui/settings.js
CHANGED
|
@@ -10,6 +10,98 @@ let settingsData = null;
|
|
|
10
10
|
let activeTab = "ai";
|
|
11
11
|
const activePolls = {};
|
|
12
12
|
|
|
13
|
+
// VIB-1835: /api/settings/status is now config-only and renders instantly. The
|
|
14
|
+
// expensive bits — live model lists and CLI/auth detection — are fetched on
|
|
15
|
+
// demand and cached here, layered over the fast /status response on each render.
|
|
16
|
+
let liveModels = null; // from /api/settings/models
|
|
17
|
+
let scannedTools = {}; // tool entries from /api/settings/tools
|
|
18
|
+
let scannedEngines = null; // availableEngines refined by an AI scan
|
|
19
|
+
const scannedGroups = { ai: false, platform: false };
|
|
20
|
+
let bgScanStarted = false; // one-time background scan guard (per open)
|
|
21
|
+
|
|
22
|
+
function resetScanState() {
|
|
23
|
+
liveModels = null;
|
|
24
|
+
scannedTools = {};
|
|
25
|
+
scannedEngines = null;
|
|
26
|
+
scannedGroups.ai = false;
|
|
27
|
+
scannedGroups.platform = false;
|
|
28
|
+
bgScanStarted = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Layer the on-demand caches over the fast /status payload so a re-render after
|
|
32
|
+
// any action keeps already-fetched models / scanned tool state.
|
|
33
|
+
function applyScanCaches(data) {
|
|
34
|
+
if (!data || !data.environment) return;
|
|
35
|
+
if (liveModels) data.models = liveModels;
|
|
36
|
+
if (Object.keys(scannedTools).length) {
|
|
37
|
+
data.environment.tools = { ...data.environment.tools, ...scannedTools };
|
|
38
|
+
}
|
|
39
|
+
if (scannedEngines) data.environment.availableEngines = scannedEngines;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function fetchModels(refresh) {
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch("/api/settings/models" + (refresh ? "?refresh=1" : ""));
|
|
45
|
+
const data = await res.json();
|
|
46
|
+
if (data && data.models) {
|
|
47
|
+
liveModels = data.models;
|
|
48
|
+
if (settingsData) settingsData.models = liveModels;
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
} catch { /* keep STATIC_MODELS */ }
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function fetchTools(group, refresh) {
|
|
56
|
+
try {
|
|
57
|
+
const qs = `?group=${group}` + (refresh ? "&refresh=1" : "");
|
|
58
|
+
const res = await fetch("/api/settings/tools" + qs);
|
|
59
|
+
const data = await res.json();
|
|
60
|
+
if (data && data.tools) {
|
|
61
|
+
scannedTools = { ...scannedTools, ...data.tools };
|
|
62
|
+
if (Array.isArray(data.availableEngines)) scannedEngines = data.availableEngines;
|
|
63
|
+
if (group === "ai" || group === "all") scannedGroups.ai = true;
|
|
64
|
+
if (group === "platform" || group === "all") scannedGroups.platform = true;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
} catch { /* leave "not scanned" */ }
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Hybrid default (Boris-approved): after the instant render, kick off one
|
|
72
|
+
// non-blocking scan of models + all tools, then re-render in place. Every later
|
|
73
|
+
// open repeats this once; explicit Refresh/Scan/Check buttons are always live.
|
|
74
|
+
function maybeStartBackgroundScan() {
|
|
75
|
+
if (bgScanStarted) return;
|
|
76
|
+
bgScanStarted = true;
|
|
77
|
+
Promise.all([fetchModels(false), fetchTools("all", false)]).then((results) => {
|
|
78
|
+
// Don't yank a re-render out from under someone mid-typing.
|
|
79
|
+
if (results.some(Boolean) && settingsData && !settingsHasFocusedInput()) {
|
|
80
|
+
renderSettings(settingsData);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function settingsHasFocusedInput() {
|
|
86
|
+
const body = document.getElementById("settings-body");
|
|
87
|
+
const ae = document.activeElement;
|
|
88
|
+
return !!(body && ae && body.contains(ae) && (ae.tagName === "INPUT" || ae.tagName === "SELECT"));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Small button that runs an on-demand scan with a spinner, then re-renders the
|
|
92
|
+
// active tab (which recreates this button in its resolved state).
|
|
93
|
+
function makeScanButton(label, onScan, extraClass) {
|
|
94
|
+
const btn = el("button", "settings__btn settings__btn--small" + (extraClass ? " " + extraClass : ""));
|
|
95
|
+
btn.textContent = label;
|
|
96
|
+
btn.addEventListener("click", async () => {
|
|
97
|
+
btn.disabled = true;
|
|
98
|
+
btn.innerHTML = '<span class="settings__spinner"></span> ' + escSettings(label) + "…";
|
|
99
|
+
await onScan();
|
|
100
|
+
if (settingsData) renderSettings(settingsData);
|
|
101
|
+
});
|
|
102
|
+
return btn;
|
|
103
|
+
}
|
|
104
|
+
|
|
13
105
|
const ENGINE_LABELS = {
|
|
14
106
|
"claude-code": "Claude Code",
|
|
15
107
|
"anthropic-api": "Anthropic API",
|
|
@@ -34,6 +126,7 @@ function openSettings(tab) {
|
|
|
34
126
|
}
|
|
35
127
|
const overlay = document.getElementById("settings-overlay");
|
|
36
128
|
if (overlay) overlay.classList.remove("hidden");
|
|
129
|
+
resetScanState();
|
|
37
130
|
refreshSettings();
|
|
38
131
|
}
|
|
39
132
|
|
|
@@ -70,14 +163,17 @@ async function refreshSettings() {
|
|
|
70
163
|
const body = document.getElementById("settings-body");
|
|
71
164
|
body.innerHTML = `<div class="settings__loading"><div class="settings__spinner-lg"></div><span>Loading environment...</span></div>`;
|
|
72
165
|
|
|
166
|
+
// /status is config-only and returns in single-digit ms; this is just a relaxed
|
|
167
|
+
// safety net for an unreachable server, not the old 3s budget that timed out.
|
|
73
168
|
const controller = new AbortController();
|
|
74
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
169
|
+
const timeout = setTimeout(() => controller.abort(), 12000);
|
|
75
170
|
|
|
76
171
|
try {
|
|
77
172
|
const res = await fetch("/api/settings/status", { signal: controller.signal });
|
|
78
173
|
clearTimeout(timeout);
|
|
79
174
|
settingsData = await res.json();
|
|
80
175
|
renderSettings(settingsData);
|
|
176
|
+
maybeStartBackgroundScan();
|
|
81
177
|
} catch (err) {
|
|
82
178
|
clearTimeout(timeout);
|
|
83
179
|
const aborted = err && err.name === "AbortError";
|
|
@@ -108,6 +204,7 @@ function renderSettingsError(body, timedOut) {
|
|
|
108
204
|
}
|
|
109
205
|
|
|
110
206
|
function renderSettings(data) {
|
|
207
|
+
applyScanCaches(data);
|
|
111
208
|
const body = document.getElementById("settings-body");
|
|
112
209
|
body.innerHTML = "";
|
|
113
210
|
|
|
@@ -156,8 +253,43 @@ function renderAITab(body, data) {
|
|
|
156
253
|
}
|
|
157
254
|
section.appendChild(selectEl);
|
|
158
255
|
|
|
159
|
-
//
|
|
256
|
+
// Langdock provider selector — shown only when Langdock is the active engine
|
|
160
257
|
const activeEngine = config.aiEngine || (env.availableEngines.length > 0 ? env.availableEngines[0] : null);
|
|
258
|
+
const langdockProvider = config.langdockProvider || "anthropic";
|
|
259
|
+
|
|
260
|
+
if (activeEngine === "langdock-api") {
|
|
261
|
+
const providerRow = el("div", "settings__model-row");
|
|
262
|
+
const providerLabel = el("span", "settings__card-label");
|
|
263
|
+
providerLabel.textContent = "Provider";
|
|
264
|
+
providerRow.appendChild(providerLabel);
|
|
265
|
+
|
|
266
|
+
const providerSelect = el("select", "settings__model-select");
|
|
267
|
+
const providers = [
|
|
268
|
+
{ id: "anthropic", label: "Anthropic (Claude)" },
|
|
269
|
+
{ id: "openai", label: "OpenAI (GPT)" },
|
|
270
|
+
{ id: "google", label: "Google (Gemini)" },
|
|
271
|
+
{ id: "mistral", label: "Mistral" },
|
|
272
|
+
];
|
|
273
|
+
for (const p of providers) {
|
|
274
|
+
const opt = document.createElement("option");
|
|
275
|
+
opt.value = p.id;
|
|
276
|
+
opt.textContent = p.label;
|
|
277
|
+
if (p.id === langdockProvider) opt.selected = true;
|
|
278
|
+
providerSelect.appendChild(opt);
|
|
279
|
+
}
|
|
280
|
+
providerSelect.addEventListener("change", async () => {
|
|
281
|
+
await fetch("/api/settings", {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: { "Content-Type": "application/json" },
|
|
284
|
+
body: JSON.stringify({ langdockProvider: providerSelect.value }),
|
|
285
|
+
});
|
|
286
|
+
refreshSettings();
|
|
287
|
+
});
|
|
288
|
+
providerRow.appendChild(providerSelect);
|
|
289
|
+
section.appendChild(providerRow);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Model selector
|
|
161
293
|
if (activeEngine) {
|
|
162
294
|
const modelRow = el("div", "settings__model-row");
|
|
163
295
|
const modelLabel = el("span", "settings__card-label");
|
|
@@ -195,6 +327,9 @@ function renderAITab(body, data) {
|
|
|
195
327
|
});
|
|
196
328
|
|
|
197
329
|
modelRow.appendChild(modelSelect);
|
|
330
|
+
// Dropdowns populate instantly from STATIC_MODELS; Refresh pulls the live
|
|
331
|
+
// provider catalog on demand (VIB-1835).
|
|
332
|
+
modelRow.appendChild(makeScanButton("Refresh", () => fetchModels(true)));
|
|
198
333
|
section.appendChild(modelRow);
|
|
199
334
|
}
|
|
200
335
|
|
|
@@ -341,7 +476,14 @@ function renderAITab(body, data) {
|
|
|
341
476
|
// CLI Tools section with toggles
|
|
342
477
|
const cliSection = el("section", "settings__section");
|
|
343
478
|
cliSection.appendChild(sectionTitle("CLI Tools"));
|
|
344
|
-
cliSection.appendChild(desc("Enable CLI tools you have installed. Install status is
|
|
479
|
+
cliSection.appendChild(desc("Enable the CLI tools you have installed. Install and auth status is detected on demand — scan to check or refresh it."));
|
|
480
|
+
|
|
481
|
+
const cliScanRow = el("div", "settings__card-row");
|
|
482
|
+
const cliScanHint = el("span", "settings__card-meta");
|
|
483
|
+
cliScanHint.textContent = scannedGroups.ai ? "Status scanned" : "Not scanned yet";
|
|
484
|
+
cliScanRow.appendChild(cliScanHint);
|
|
485
|
+
cliScanRow.appendChild(makeScanButton(scannedGroups.ai ? "Rescan" : "Scan AI tools", () => fetchTools("ai", true)));
|
|
486
|
+
cliSection.appendChild(cliScanRow);
|
|
345
487
|
|
|
346
488
|
const cliTools = [
|
|
347
489
|
{ key: "claudeCode", id: "claude-code", name: "Claude Code", installId: "claude", url: "https://claude.ai/code" },
|
|
@@ -362,7 +504,12 @@ function renderAITab(body, data) {
|
|
|
362
504
|
label.textContent = tool.name;
|
|
363
505
|
labelWrap.appendChild(label);
|
|
364
506
|
|
|
365
|
-
if (enabled &&
|
|
507
|
+
if (enabled && !scannedGroups.ai) {
|
|
508
|
+
const sub = el("div", "settings__toggle-label-sub");
|
|
509
|
+
sub.textContent = "Not scanned";
|
|
510
|
+
sub.style.color = "var(--text-muted)";
|
|
511
|
+
labelWrap.appendChild(sub);
|
|
512
|
+
} else if (enabled && info.found) {
|
|
366
513
|
const sub = el("div", "settings__toggle-label-sub");
|
|
367
514
|
sub.textContent = `v${info.version}` + (info.authenticated ? " \u2014 authenticated" : " \u2014 not authenticated");
|
|
368
515
|
sub.style.color = info.authenticated ? "var(--success)" : "var(--warning)";
|
|
@@ -390,8 +537,9 @@ function renderAITab(body, data) {
|
|
|
390
537
|
|
|
391
538
|
row.appendChild(toggleRow);
|
|
392
539
|
|
|
393
|
-
// If enabled but not installed, show install button
|
|
394
|
-
|
|
540
|
+
// If enabled but not installed, show install button (only once we've scanned
|
|
541
|
+
// — before that, info.found is just an unscanned placeholder).
|
|
542
|
+
if (enabled && scannedGroups.ai && !info.found) {
|
|
395
543
|
const installRow = el("div", "settings__card-row");
|
|
396
544
|
const installBtn = el("button", "settings__btn settings__btn--primary");
|
|
397
545
|
installBtn.textContent = "Install";
|
|
@@ -409,7 +557,7 @@ function renderAITab(body, data) {
|
|
|
409
557
|
}
|
|
410
558
|
|
|
411
559
|
// If enabled, installed, but not authenticated — show sign in
|
|
412
|
-
if (enabled && info.found && !info.authenticated) {
|
|
560
|
+
if (enabled && scannedGroups.ai && info.found && !info.authenticated) {
|
|
413
561
|
const authRow = el("div", "settings__card-row");
|
|
414
562
|
const authBtn = el("button", "settings__btn settings__btn--primary");
|
|
415
563
|
authBtn.textContent = "Sign in";
|
|
@@ -421,6 +569,105 @@ function renderAITab(body, data) {
|
|
|
421
569
|
cliSection.appendChild(row);
|
|
422
570
|
}
|
|
423
571
|
body.appendChild(cliSection);
|
|
572
|
+
|
|
573
|
+
// Observability section — opt-in Langfuse keys, base URL, and enable toggle
|
|
574
|
+
body.appendChild(renderObservabilitySection(env, config));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ---------------------------------------------------------------------------
|
|
578
|
+
// Observability — Langfuse keys + base URL + enable toggle
|
|
579
|
+
// ---------------------------------------------------------------------------
|
|
580
|
+
|
|
581
|
+
function renderObservabilitySection(env, config) {
|
|
582
|
+
const section = el("section", "settings__section");
|
|
583
|
+
section.appendChild(sectionTitle("Observability"));
|
|
584
|
+
section.appendChild(desc("Langfuse captures token usage, estimated cost, and traces for every API model call. Off by default — turn it on with the toggle and set both keys to start sending traces. Keys are stored locally in ~/.vibespot/config.json and sent only to your Langfuse instance."));
|
|
585
|
+
|
|
586
|
+
const pub = env.apiKeys.langfusePublic || { configured: false, masked: "" };
|
|
587
|
+
const sec = env.apiKeys.langfuseSecret || { configured: false, masked: "" };
|
|
588
|
+
const hasKeys = pub.configured && sec.configured;
|
|
589
|
+
const enabledFlag = config.langfuseEnabled; // undefined | true | false
|
|
590
|
+
const isOn = enabledFlag === true; // off by default; explicit opt-in required
|
|
591
|
+
|
|
592
|
+
// Enable toggle
|
|
593
|
+
const toggleRow = el("div", "settings__toggle-row");
|
|
594
|
+
const labelWrap = el("div", "");
|
|
595
|
+
const label = el("div", "settings__toggle-label");
|
|
596
|
+
label.textContent = "Send traces to Langfuse";
|
|
597
|
+
labelWrap.appendChild(label);
|
|
598
|
+
|
|
599
|
+
const sub = el("div", "settings__toggle-label-sub");
|
|
600
|
+
if (!isOn) {
|
|
601
|
+
sub.textContent = "Off — no traces or usage sent";
|
|
602
|
+
sub.style.color = "var(--text-muted)";
|
|
603
|
+
} else if (hasKeys) {
|
|
604
|
+
sub.textContent = "Active — traces, token usage & cost sent to Langfuse";
|
|
605
|
+
sub.style.color = "var(--success)";
|
|
606
|
+
} else {
|
|
607
|
+
sub.textContent = "Enabled — add a public + secret key below to start sending traces";
|
|
608
|
+
sub.style.color = "var(--warning)";
|
|
609
|
+
}
|
|
610
|
+
labelWrap.appendChild(sub);
|
|
611
|
+
toggleRow.appendChild(labelWrap);
|
|
612
|
+
|
|
613
|
+
const toggle = el("button", "settings__toggle" + (isOn ? " active" : ""));
|
|
614
|
+
toggle.addEventListener("click", async () => {
|
|
615
|
+
await fetch("/api/settings", {
|
|
616
|
+
method: "POST",
|
|
617
|
+
headers: { "Content-Type": "application/json" },
|
|
618
|
+
body: JSON.stringify({ langfuseEnabled: !isOn }),
|
|
619
|
+
});
|
|
620
|
+
refreshSettings();
|
|
621
|
+
});
|
|
622
|
+
toggleRow.appendChild(toggle);
|
|
623
|
+
section.appendChild(toggleRow);
|
|
624
|
+
|
|
625
|
+
// Keys — masked display, persisted via the shared /api/settings/apikey route
|
|
626
|
+
section.appendChild(createApiKeyCard("langfuse-public", "Public Key", "pk-lf-...", pub));
|
|
627
|
+
section.appendChild(createApiKeyCard("langfuse-secret", "Secret Key", "sk-lf-...", sec));
|
|
628
|
+
|
|
629
|
+
// Base URL
|
|
630
|
+
const urlCard = el("div", "settings__card");
|
|
631
|
+
const urlRow = el("div", "settings__card-row");
|
|
632
|
+
urlRow.style.gap = "8px";
|
|
633
|
+
const urlLabel = el("span", "settings__card-label");
|
|
634
|
+
urlLabel.textContent = "Base URL";
|
|
635
|
+
urlRow.appendChild(urlLabel);
|
|
636
|
+
|
|
637
|
+
const urlInput = el("input", "settings__apikey-input");
|
|
638
|
+
urlInput.type = "text";
|
|
639
|
+
urlInput.placeholder = "https://cloud.langfuse.com";
|
|
640
|
+
urlInput.value = config.langfuseBaseUrl || "";
|
|
641
|
+
urlRow.appendChild(urlInput);
|
|
642
|
+
|
|
643
|
+
const urlSaveBtn = el("button", "settings__btn settings__btn--primary");
|
|
644
|
+
urlSaveBtn.textContent = "Save";
|
|
645
|
+
const saveBaseUrl = async () => {
|
|
646
|
+
urlSaveBtn.disabled = true;
|
|
647
|
+
urlSaveBtn.textContent = "Saving...";
|
|
648
|
+
await fetch("/api/settings", {
|
|
649
|
+
method: "POST",
|
|
650
|
+
headers: { "Content-Type": "application/json" },
|
|
651
|
+
body: JSON.stringify({ langfuseBaseUrl: urlInput.value.trim() }),
|
|
652
|
+
});
|
|
653
|
+
refreshSettings();
|
|
654
|
+
};
|
|
655
|
+
urlSaveBtn.addEventListener("click", saveBaseUrl);
|
|
656
|
+
urlInput.addEventListener("keydown", (e) => {
|
|
657
|
+
if (e.key === "Enter") { e.preventDefault(); saveBaseUrl(); }
|
|
658
|
+
});
|
|
659
|
+
urlRow.appendChild(urlSaveBtn);
|
|
660
|
+
urlCard.appendChild(urlRow);
|
|
661
|
+
|
|
662
|
+
const urlHint = el("div", "settings__toggle-label-sub");
|
|
663
|
+
urlHint.textContent = "cloud.langfuse.com (EU) · us.cloud.langfuse.com (US) · or your self-host URL. Leave blank for EU cloud.";
|
|
664
|
+
urlHint.style.color = "var(--text-muted)";
|
|
665
|
+
urlHint.style.marginTop = "6px";
|
|
666
|
+
urlCard.appendChild(urlHint);
|
|
667
|
+
|
|
668
|
+
section.appendChild(urlCard);
|
|
669
|
+
|
|
670
|
+
return section;
|
|
424
671
|
}
|
|
425
672
|
|
|
426
673
|
// ---------------------------------------------------------------------------
|
|
@@ -433,7 +680,10 @@ function renderAICapabilitiesSection(activeEngine, config) {
|
|
|
433
680
|
section.appendChild(desc("Advanced model features. Some are configurable directly; some auto-engage based on the active engine."));
|
|
434
681
|
|
|
435
682
|
// Engine classification — different engines have different feature surfaces.
|
|
436
|
-
|
|
683
|
+
// Langdock inherits capabilities from the selected upstream provider.
|
|
684
|
+
const ldProv = (config.langdockProvider || "anthropic");
|
|
685
|
+
const isLangdockAnthropic = activeEngine === "langdock-api" && ldProv === "anthropic";
|
|
686
|
+
const isAnthropicAPI = activeEngine === "anthropic-api" || activeEngine === "claude-oauth" || isLangdockAnthropic;
|
|
437
687
|
const isClaudeCode = activeEngine === "claude-code";
|
|
438
688
|
const isAnthropicAny = isAnthropicAPI || isClaudeCode;
|
|
439
689
|
|
|
@@ -733,6 +983,28 @@ function renderHubSpotTab(body, data) {
|
|
|
733
983
|
// CLI mode — show CLI status and accounts from hs accounts list
|
|
734
984
|
acctSection.appendChild(desc("HubSpot CLI accounts are managed by the hs command. Use \u201chs auth\u201d to add accounts."));
|
|
735
985
|
|
|
986
|
+
// The hs CLI + accounts probe is a subprocess, so it runs on demand rather
|
|
987
|
+
// than on every settings open (VIB-1835).
|
|
988
|
+
const hsCheckRow = el("div", "settings__card-row");
|
|
989
|
+
const hsCheckHint = el("span", "settings__card-meta");
|
|
990
|
+
hsCheckHint.textContent = scannedGroups.platform ? "Status checked" : "Not checked yet";
|
|
991
|
+
hsCheckRow.appendChild(hsCheckHint);
|
|
992
|
+
hsCheckRow.appendChild(makeScanButton(scannedGroups.platform ? "Recheck" : "Check", () => fetchTools("platform", true)));
|
|
993
|
+
acctSection.appendChild(hsCheckRow);
|
|
994
|
+
|
|
995
|
+
if (!scannedGroups.platform) {
|
|
996
|
+
const pending = el("div", "settings__card");
|
|
997
|
+
const prow = el("div", "settings__card-row");
|
|
998
|
+
prow.appendChild(dot("muted"));
|
|
999
|
+
const plabel = el("span", "settings__card-label");
|
|
1000
|
+
plabel.textContent = "HubSpot CLI — not scanned";
|
|
1001
|
+
prow.appendChild(plabel);
|
|
1002
|
+
pending.appendChild(prow);
|
|
1003
|
+
acctSection.appendChild(pending);
|
|
1004
|
+
body.appendChild(acctSection);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
736
1008
|
const cliCard = el("div", "settings__card");
|
|
737
1009
|
const cliRow = el("div", "settings__card-row");
|
|
738
1010
|
cliRow.appendChild(dot(hs.found ? "success" : "warn"));
|
|
@@ -865,6 +1137,28 @@ function renderGitHubTab(body, data) {
|
|
|
865
1137
|
section.appendChild(sectionTitle("GitHub CLI"));
|
|
866
1138
|
section.appendChild(desc("GitHub CLI enables pushing your theme to a repository. Optional \u2014 not needed for HubSpot deployment."));
|
|
867
1139
|
|
|
1140
|
+
// GitHub install/auth is detected via subprocess, so it's checked on demand
|
|
1141
|
+
// rather than on every settings open (VIB-1835).
|
|
1142
|
+
const checkRow = el("div", "settings__card-row");
|
|
1143
|
+
const checkHint = el("span", "settings__card-meta");
|
|
1144
|
+
checkHint.textContent = scannedGroups.platform ? "Status checked" : "Not checked yet";
|
|
1145
|
+
checkRow.appendChild(checkHint);
|
|
1146
|
+
checkRow.appendChild(makeScanButton(scannedGroups.platform ? "Recheck" : "Check", () => fetchTools("platform", true)));
|
|
1147
|
+
section.appendChild(checkRow);
|
|
1148
|
+
|
|
1149
|
+
if (!scannedGroups.platform) {
|
|
1150
|
+
const pending = el("div", "settings__card");
|
|
1151
|
+
const pendingRow = el("div", "settings__card-row");
|
|
1152
|
+
pendingRow.appendChild(dot("muted"));
|
|
1153
|
+
const pendingLabel = el("span", "settings__card-label");
|
|
1154
|
+
pendingLabel.textContent = "GitHub CLI \u2014 not scanned";
|
|
1155
|
+
pendingRow.appendChild(pendingLabel);
|
|
1156
|
+
pending.appendChild(pendingRow);
|
|
1157
|
+
section.appendChild(pending);
|
|
1158
|
+
body.appendChild(section);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
868
1162
|
const card = el("div", "settings__card");
|
|
869
1163
|
|
|
870
1164
|
// CLI status
|
|
@@ -1473,14 +1767,38 @@ function getModelsForEngine(engine) {
|
|
|
1473
1767
|
{ id: "gpt-5.4-nano", label: "GPT-5.4 Nano" },
|
|
1474
1768
|
{ id: "codex-mini-latest", label: "Codex Mini (latest)" },
|
|
1475
1769
|
];
|
|
1476
|
-
case "langdock-api":
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1770
|
+
case "langdock-api": {
|
|
1771
|
+
const ldProvider = (settingsData && settingsData.config && settingsData.config.langdockProvider) || "anthropic";
|
|
1772
|
+
const ldModels = {
|
|
1773
|
+
anthropic: [
|
|
1774
|
+
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6 (default)" },
|
|
1775
|
+
{ id: "claude-opus-4-7", label: "Claude Opus 4.7" },
|
|
1776
|
+
{ id: "claude-opus-4-6", label: "Claude Opus 4.6" },
|
|
1777
|
+
{ id: "claude-sonnet-4-5", label: "Claude Sonnet 4.5" },
|
|
1778
|
+
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" },
|
|
1779
|
+
],
|
|
1780
|
+
openai: [
|
|
1781
|
+
{ id: "gpt-4.1", label: "GPT-4.1 (default)" },
|
|
1782
|
+
{ id: "gpt-4.1-mini", label: "GPT-4.1 Mini" },
|
|
1783
|
+
{ id: "gpt-4.1-nano", label: "GPT-4.1 Nano" },
|
|
1784
|
+
{ id: "gpt-4o", label: "GPT-4o" },
|
|
1785
|
+
{ id: "o3-mini", label: "o3 Mini" },
|
|
1786
|
+
],
|
|
1787
|
+
google: [
|
|
1788
|
+
{ id: "gemini-2.5-pro", label: "Gemini 2.5 Pro (default)" },
|
|
1789
|
+
{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash" },
|
|
1790
|
+
{ id: "gemini-2.0-flash", label: "Gemini 2.0 Flash" },
|
|
1791
|
+
],
|
|
1792
|
+
mistral: [
|
|
1793
|
+
{ id: "mistral-large-latest", label: "Mistral Large (default)" },
|
|
1794
|
+
{ id: "mistral-medium-latest", label: "Mistral Medium" },
|
|
1795
|
+
{ id: "mistral-small-latest", label: "Mistral Small" },
|
|
1796
|
+
{ id: "codestral-latest", label: "Codestral" },
|
|
1797
|
+
{ id: "pixtral-large-latest", label: "Pixtral Large" },
|
|
1798
|
+
],
|
|
1799
|
+
};
|
|
1800
|
+
return ldModels[ldProvider] || ldModels.anthropic;
|
|
1801
|
+
}
|
|
1484
1802
|
default:
|
|
1485
1803
|
return [];
|
|
1486
1804
|
}
|
|
@@ -1495,7 +1813,11 @@ function getCurrentModel(engine, config) {
|
|
|
1495
1813
|
case "codex-cli": return config.codexCliModel || "gpt-5.5";
|
|
1496
1814
|
case "gemini-cli": return config.geminiCliModel || "gemini-2.5-pro";
|
|
1497
1815
|
case "gemini-api": return config.geminiApiModel || "gemini-2.5-pro";
|
|
1498
|
-
case "langdock-api":
|
|
1816
|
+
case "langdock-api": {
|
|
1817
|
+
if (config.langdockApiModel) return config.langdockApiModel;
|
|
1818
|
+
const defaults = { anthropic: "claude-sonnet-4-6", openai: "gpt-4.1", google: "gemini-2.5-pro", mistral: "mistral-large-latest" };
|
|
1819
|
+
return defaults[config.langdockProvider || "anthropic"] || "claude-sonnet-4-6";
|
|
1820
|
+
}
|
|
1499
1821
|
default: return null;
|
|
1500
1822
|
}
|
|
1501
1823
|
}
|
package/ui/styles.css
CHANGED
|
@@ -4184,6 +4184,19 @@ body { display: flex; }
|
|
|
4184
4184
|
font-size: var(--text-sm);
|
|
4185
4185
|
color: var(--text-dim);
|
|
4186
4186
|
}
|
|
4187
|
+
/* Per-project running generation cost chip (VIB-1770) */
|
|
4188
|
+
.chat__cost-total {
|
|
4189
|
+
margin-left: auto;
|
|
4190
|
+
font-size: var(--text-xs, 11px);
|
|
4191
|
+
font-variant-numeric: tabular-nums;
|
|
4192
|
+
color: var(--text-dim);
|
|
4193
|
+
background: var(--bg-subtle, rgba(127, 127, 127, 0.12));
|
|
4194
|
+
border: 1px solid var(--border);
|
|
4195
|
+
border-radius: 999px;
|
|
4196
|
+
padding: 2px 8px;
|
|
4197
|
+
cursor: default;
|
|
4198
|
+
white-space: nowrap;
|
|
4199
|
+
}
|
|
4187
4200
|
|
|
4188
4201
|
.chat__messages {
|
|
4189
4202
|
flex: 1;
|
|
@@ -5032,6 +5045,15 @@ body { display: flex; }
|
|
|
5032
5045
|
color: var(--warning, #f59e0b);
|
|
5033
5046
|
}
|
|
5034
5047
|
|
|
5048
|
+
/* Per-page estimated generation cost (VIB-1770) */
|
|
5049
|
+
.pipeline-cost {
|
|
5050
|
+
font-size: var(--text-xs, 11px);
|
|
5051
|
+
color: var(--text-dim);
|
|
5052
|
+
font-variant-numeric: tabular-nums;
|
|
5053
|
+
padding: 0 0 2px;
|
|
5054
|
+
cursor: default;
|
|
5055
|
+
}
|
|
5056
|
+
|
|
5035
5057
|
.pipeline-footer {
|
|
5036
5058
|
display: flex;
|
|
5037
5059
|
justify-content: space-between;
|