privateboard 0.1.0 → 0.1.3
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/dist/cli.js +5281 -989
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/public/agent-overlay.js +3 -3
- package/public/agent-profile.css +5 -4
- package/public/agent-profile.js +35 -9
- package/public/app.js +4408 -580
- package/public/avatar-skill.js +6 -9
- package/public/home.html +1750 -0
- package/public/{prototype-dashboard.html → index.html} +2426 -321
- package/public/onboarding.js +40 -13
- package/public/quote-cta.css +269 -0
- package/public/quote-cta.js +553 -0
- package/public/report/spines/a16z-thesis.css +234 -87
- package/public/report/spines/anthropic-essay.css +587 -191
- package/public/report/spines/boardroom-dark.css +141 -67
- package/public/report/spines/gartner-note.css +105 -23
- package/public/report/spines/mckinsey-deck.css +102 -15
- package/public/report/spines/openai-paper.css +117 -20
- package/public/report.html +3882 -148
- package/public/room-settings.css +6 -4
- package/public/room-settings.js +19 -13
- package/public/themes.css +15 -2
- package/public/user-settings.css +37 -8
- package/public/user-settings.js +68 -164
package/public/room-settings.css
CHANGED
|
@@ -183,13 +183,15 @@
|
|
|
183
183
|
.rs-config-row { grid-template-columns: 1fr; gap: 6px; }
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
/* Tone + intensity chip rows · explicit grid columns (
|
|
186
|
+
/* Tone + intensity chip rows · explicit grid columns (5 / 3) so the
|
|
187
187
|
chips always sit in a single row at content width. flex-wrap was
|
|
188
188
|
the prior approach but allowed chips to fold onto a second line on
|
|
189
|
-
tight viewports — the user wants them locked to one row.
|
|
189
|
+
tight viewports — the user wants them locked to one row. Tone count
|
|
190
|
+
bumped from 4 → 5 when `research` and `critique` were added; if
|
|
191
|
+
you add more modes, bump this number too. */
|
|
190
192
|
.rs-mode-grid {
|
|
191
193
|
display: grid;
|
|
192
|
-
grid-template-columns: repeat(
|
|
194
|
+
grid-template-columns: repeat(5, max-content);
|
|
193
195
|
gap: 3px;
|
|
194
196
|
}
|
|
195
197
|
.rs-intensity-chips {
|
|
@@ -439,7 +441,7 @@
|
|
|
439
441
|
/* Section label · matches the new composer's mono micro-type kicker.
|
|
440
442
|
No leading lime bullet — the inline count chip carries enough
|
|
441
443
|
visual weight, and dropping the bullet lets the labels sit flush
|
|
442
|
-
left like the rest of the
|
|
444
|
+
left like the rest of the app's section heads. */
|
|
443
445
|
.rs-label {
|
|
444
446
|
font-size: 9px;
|
|
445
447
|
font-weight: 700;
|
package/public/room-settings.js
CHANGED
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
"Co-creator. Directors stand with you and push the idea outward — yes-and a contribution, name a concrete adjacent variant (\"what if we instead…\"), borrow pieces from another director's turn into new combinations. May end with one curious question, never a defense-demanding one.",
|
|
17
17
|
constructive:
|
|
18
18
|
"Sympathetic interrogator. They want you to win, but only via the strongest version. Each turn picks ONE load-bearing assumption and proposes the candidate stronger version that would stand. Disagreement is allowed, but every objection comes packaged with a forward path.",
|
|
19
|
+
research:
|
|
20
|
+
"Collaborative inquiry. The room mines the materials in front of it (your brief, web-search results, prior turns) for what's actually there. Each turn must cite a specific source piece, label it OBSERVATION / INFERENCE / SPECULATION, then extract the insight your lens makes salient. Defaults web search ON when a Brave key is configured.",
|
|
19
21
|
debate:
|
|
20
22
|
"Peer reviewer. Each turn opens by steelmanning your strongest claim (\"the strongest read of your point is…\") and only then attacks THAT version — naming a specific risk, demanding evidence, exposing the trade-off you're hiding. Sharp but professional. Skipping the steelman is a protocol violation.",
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
+
critique:
|
|
24
|
+
"Review board. The room audits a finished deliverable systematically — each turn names the dimension being audited (logic / evidence / scope / risk / etc.), surfaces 2–3 specific flaws labelled BLOCKER · MAJOR · MINOR, points at the load-bearing piece, and indicates the direction a fix would lie. At least one BLOCKER or MAJOR per turn is mandatory.",
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
/** Intensity tooltips · what each pick does to the directors' default
|
|
@@ -29,8 +31,8 @@
|
|
|
29
31
|
"Long-form thinking aloud. Directors take the room slowly — pause to think, surface caveats, sit with ambiguity rather than rushing to resolve. Best for novel / ambiguous problems where premature conclusions cost more than slow ones.",
|
|
30
32
|
sharp:
|
|
31
33
|
"No hedging. Directors land each turn on a load-bearing claim and back it with the load-bearing reason. They cut the qualifying language (\"perhaps,\" \"could be,\" \"in some cases\") in favour of clear, falsifiable statements. Default for most rooms.",
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
+
terse:
|
|
35
|
+
"Telegraphic. One paragraph, often one sentence. Directors cut every warm-up, every diplomatic packaging, every \"I think\" — they state the conclusion and only justify if pressed. NOTE · this is the LENGTH dial, not the harshness dial. Whether a director pushes back hard or builds with you is set by Tone (brainstorm vs critique etc); Terse only decides how long they take saying it.",
|
|
34
36
|
};
|
|
35
37
|
|
|
36
38
|
/** Generic info popover · single floating element, hover-driven.
|
|
@@ -146,8 +148,8 @@
|
|
|
146
148
|
const NAMES = {};
|
|
147
149
|
|
|
148
150
|
// Baseline state — synced from window.app.currentRoom each time the
|
|
149
|
-
// overlay opens. The fallback values keep the
|
|
150
|
-
//
|
|
151
|
+
// overlay opens. The fallback values keep the page usable in standalone
|
|
152
|
+
// preview (where window.app is absent).
|
|
151
153
|
const ROOM_STATE = {
|
|
152
154
|
title: "the minimum viable structure of a data flywheel",
|
|
153
155
|
topic: "I want to build an AI assistant for enterprise HR teams — automated resume screening + interview guides. Does this idea hold up under three-director scrutiny?",
|
|
@@ -201,8 +203,9 @@
|
|
|
201
203
|
const MODES = [
|
|
202
204
|
{ v: "brainstorm", label: "Brainstorm", desc: "yes-and" },
|
|
203
205
|
{ v: "constructive", label: "Constructive", desc: "push & sharpen" },
|
|
206
|
+
{ v: "research", label: "Research", desc: "mine the material" },
|
|
204
207
|
{ v: "debate", label: "Debate", desc: "find holes" },
|
|
205
|
-
{ v: "
|
|
208
|
+
{ v: "critique", label: "Critique", desc: "audit the deliverable" }
|
|
206
209
|
];
|
|
207
210
|
|
|
208
211
|
|
|
@@ -335,9 +338,9 @@
|
|
|
335
338
|
<span class="rs-chip-label">Sharp</span>
|
|
336
339
|
<span class="rs-chip-info rs-info-trigger" data-info-title="Sharp" data-info-body="${escape(INTENSITY_TIPS.sharp)}" tabindex="-1" aria-label="What 'Sharp' means">i</span>
|
|
337
340
|
</button>
|
|
338
|
-
<button type="button" class="rs-chip rs-chip-mini" data-rs-intensity-pick="
|
|
339
|
-
<span class="rs-chip-label">
|
|
340
|
-
<span class="rs-chip-info rs-info-trigger" data-info-title="
|
|
341
|
+
<button type="button" class="rs-chip rs-chip-mini" data-rs-intensity-pick="terse">
|
|
342
|
+
<span class="rs-chip-label">Terse</span>
|
|
343
|
+
<span class="rs-chip-info rs-info-trigger" data-info-title="Terse" data-info-body="${escape(INTENSITY_TIPS.terse)}" tabindex="-1" aria-label="What 'Terse' means">i</span>
|
|
341
344
|
</button>
|
|
342
345
|
</div>
|
|
343
346
|
</div>
|
|
@@ -536,7 +539,7 @@
|
|
|
536
539
|
}
|
|
537
540
|
|
|
538
541
|
function renderIntensity() {
|
|
539
|
-
// Intensity is now a 3-chip row (Calm / Sharp /
|
|
542
|
+
// Intensity is now a 3-chip row (Calm / Sharp / Terse) instead of
|
|
540
543
|
// a slider · highlight the active chip. The hint line above shows
|
|
541
544
|
// "currently: <value>" so the picked state stays self-evident.
|
|
542
545
|
const cur = effective("intensity");
|
|
@@ -693,7 +696,10 @@
|
|
|
693
696
|
renderConfirmState();
|
|
694
697
|
}
|
|
695
698
|
function stageIntensity(next) {
|
|
696
|
-
|
|
699
|
+
// Accept legacy `brutal` from any code path that still emits it
|
|
700
|
+
// (cached HTML, third-party clients) and normalize to `terse`.
|
|
701
|
+
if (next === "brutal") next = "terse";
|
|
702
|
+
if (!["calm", "sharp", "terse"].includes(next)) return;
|
|
697
703
|
STAGED.intensity = next === ROOM_STATE.intensity ? null : next;
|
|
698
704
|
renderIntensity();
|
|
699
705
|
renderConfirmState();
|
|
@@ -998,7 +1004,7 @@
|
|
|
998
1004
|
const rect = bar.getBoundingClientRect();
|
|
999
1005
|
if (rect.width <= 0) return "sharp";
|
|
1000
1006
|
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
1001
|
-
return ratio < 0.33 ? "calm" : ratio < 0.67 ? "sharp" : "
|
|
1007
|
+
return ratio < 0.33 ? "calm" : ratio < 0.67 ? "sharp" : "terse";
|
|
1002
1008
|
};
|
|
1003
1009
|
bar.addEventListener("pointerdown", (e) => {
|
|
1004
1010
|
e.preventDefault();
|
package/public/themes.css
CHANGED
|
@@ -45,12 +45,25 @@
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
:root {
|
|
48
|
-
|
|
48
|
+
/* CJK fallback chain · PingFang SC anchors every stack so Chinese
|
|
49
|
+
glyphs always render as PingFang on macOS rather than falling
|
|
50
|
+
through to whatever `system-ui` resolves to (Songti / Microsoft
|
|
51
|
+
YaHei / Source Han depending on platform). Latin fonts stay
|
|
52
|
+
FIRST so mixed CN/EN prose still picks Human Sans / Inter / Agent
|
|
53
|
+
for English glyphs and only Chinese characters land on PingFang
|
|
54
|
+
via per-glyph fallback. */
|
|
55
|
+
--font-human: "Human Sans", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue",
|
|
56
|
+
"PingFang SC", "PingFang TC", "Hiragino Sans GB", "Microsoft YaHei",
|
|
57
|
+
"Source Han Sans CN", "Noto Sans CJK SC",
|
|
58
|
+
system-ui, sans-serif;
|
|
49
59
|
/* Agent italic-faced font. ui-sans-serif is a CSS generic keyword
|
|
50
60
|
(must be unquoted) and resolves to the OS default sans-serif before
|
|
51
61
|
hitting -apple-system / system-ui — gives a clean italic synth on
|
|
52
62
|
systems where the bundled "Agent" face fails to load. */
|
|
53
|
-
--font-agent: "Agent", ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue",
|
|
63
|
+
--font-agent: "Agent", ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue",
|
|
64
|
+
"PingFang SC", "PingFang TC", "Hiragino Sans GB", "Microsoft YaHei",
|
|
65
|
+
"Source Han Sans CN", "Noto Sans CJK SC",
|
|
66
|
+
system-ui, sans-serif;
|
|
54
67
|
/* --sans defaults to the human face — chat input, topic question,
|
|
55
68
|
replies, and live notes use it unless an agent override applies. */
|
|
56
69
|
--sans: var(--font-human);
|
package/public/user-settings.css
CHANGED
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
}
|
|
176
176
|
.us-pane-tag {
|
|
177
177
|
font-family: var(--mono);
|
|
178
|
-
font-size:
|
|
178
|
+
font-size: 14px;
|
|
179
179
|
font-weight: 700;
|
|
180
180
|
letter-spacing: 0.2em;
|
|
181
181
|
text-transform: uppercase;
|
|
@@ -321,6 +321,17 @@
|
|
|
321
321
|
letter-spacing: -0.003em;
|
|
322
322
|
width: 100%;
|
|
323
323
|
}
|
|
324
|
+
/* API-key masking · the input is `type="text"` so browsers don't pop
|
|
325
|
+
up "Save password?" when the user types a key and navigates away.
|
|
326
|
+
`-webkit-text-security: disc` shows dots instead of the actual
|
|
327
|
+
characters — visually equivalent to a password field, but invisible
|
|
328
|
+
to password managers. Toggle (eye button) just removes this class.
|
|
329
|
+
`text-security` is the standard property name; only Safari + Chrome
|
|
330
|
+
actually implement it under the `-webkit-` prefix today. */
|
|
331
|
+
.us-input-masked {
|
|
332
|
+
-webkit-text-security: disc;
|
|
333
|
+
text-security: disc;
|
|
334
|
+
}
|
|
324
335
|
.us-input::placeholder { color: var(--text-faint, #3A382F); }
|
|
325
336
|
/* When a key is on file, we surface a 4+4 masked preview AS the
|
|
326
337
|
placeholder. Default placeholder colour is the dim "hint" tier,
|
|
@@ -394,7 +405,7 @@
|
|
|
394
405
|
.us-theme-info { min-width: 0; }
|
|
395
406
|
.us-theme-name {
|
|
396
407
|
font-family: var(--mono);
|
|
397
|
-
font-size:
|
|
408
|
+
font-size: 14px;
|
|
398
409
|
font-weight: 700;
|
|
399
410
|
color: var(--text, #C8C5BE);
|
|
400
411
|
letter-spacing: -0.005em;
|
|
@@ -442,7 +453,7 @@
|
|
|
442
453
|
}
|
|
443
454
|
.us-key-label {
|
|
444
455
|
font-family: var(--mono);
|
|
445
|
-
font-size:
|
|
456
|
+
font-size: 14px;
|
|
446
457
|
font-weight: 700;
|
|
447
458
|
color: var(--text, #C8C5BE);
|
|
448
459
|
letter-spacing: -0.003em;
|
|
@@ -656,6 +667,24 @@
|
|
|
656
667
|
}
|
|
657
668
|
.us-foot .us-done:hover { border-color: var(--lime, #6FB572); color: var(--lime, #6FB572); }
|
|
658
669
|
|
|
670
|
+
/* Right-side cluster · website link + Done button. */
|
|
671
|
+
.us-foot-right {
|
|
672
|
+
display: flex;
|
|
673
|
+
align-items: center;
|
|
674
|
+
gap: 14px;
|
|
675
|
+
}
|
|
676
|
+
.us-foot .us-website {
|
|
677
|
+
font-family: var(--mono);
|
|
678
|
+
font-size: 9.5px;
|
|
679
|
+
font-weight: 600;
|
|
680
|
+
letter-spacing: 0.1em;
|
|
681
|
+
text-transform: uppercase;
|
|
682
|
+
color: var(--text-soft, #8E8B83);
|
|
683
|
+
text-decoration: none;
|
|
684
|
+
transition: color 0.12s;
|
|
685
|
+
}
|
|
686
|
+
.us-foot .us-website:hover { color: var(--lime, #6FB572); }
|
|
687
|
+
|
|
659
688
|
@media (max-width: 600px) {
|
|
660
689
|
.user-settings-overlay { padding: 12px; }
|
|
661
690
|
.us-head { padding-left: 14px; padding-right: 14px; }
|
|
@@ -806,7 +835,7 @@
|
|
|
806
835
|
flex: 0 0 auto;
|
|
807
836
|
}
|
|
808
837
|
.us-model-name {
|
|
809
|
-
font-size:
|
|
838
|
+
font-size: 14px;
|
|
810
839
|
color: var(--text, #C8C5BE);
|
|
811
840
|
font-weight: 600;
|
|
812
841
|
letter-spacing: -0.005em;
|
|
@@ -876,7 +905,7 @@
|
|
|
876
905
|
.us-agent-row:last-child { border-bottom: none; }
|
|
877
906
|
.us-agent-name-col { display: inline-flex; align-items: baseline; gap: 8px; min-width: 0; }
|
|
878
907
|
.us-agent-name {
|
|
879
|
-
font-size:
|
|
908
|
+
font-size: 14px;
|
|
880
909
|
color: var(--text, #C8C5BE);
|
|
881
910
|
font-weight: 600;
|
|
882
911
|
white-space: nowrap;
|
|
@@ -1055,7 +1084,7 @@
|
|
|
1055
1084
|
.us-models-row:last-child { border-bottom: none; }
|
|
1056
1085
|
.us-models-name {
|
|
1057
1086
|
font-family: var(--mono, "Inter", system-ui, sans-serif);
|
|
1058
|
-
font-size:
|
|
1087
|
+
font-size: 14px;
|
|
1059
1088
|
font-weight: 700;
|
|
1060
1089
|
color: var(--text, #C8C5BE);
|
|
1061
1090
|
letter-spacing: -0.005em;
|
|
@@ -1130,7 +1159,7 @@
|
|
|
1130
1159
|
}
|
|
1131
1160
|
.us-models-default-name {
|
|
1132
1161
|
font-family: var(--mono, "Inter", system-ui, sans-serif);
|
|
1133
|
-
font-size:
|
|
1162
|
+
font-size: 14px;
|
|
1134
1163
|
font-weight: 700;
|
|
1135
1164
|
color: var(--text, #C8C5BE);
|
|
1136
1165
|
letter-spacing: -0.005em;
|
|
@@ -1215,7 +1244,7 @@
|
|
|
1215
1244
|
.us-default-row-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
1216
1245
|
.us-default-row-name {
|
|
1217
1246
|
font-family: var(--mono, "Inter", system-ui, sans-serif);
|
|
1218
|
-
font-size:
|
|
1247
|
+
font-size: 14px;
|
|
1219
1248
|
font-weight: 700;
|
|
1220
1249
|
color: var(--text, #C8C5BE);
|
|
1221
1250
|
letter-spacing: -0.005em;
|
package/public/user-settings.js
CHANGED
|
@@ -156,48 +156,12 @@
|
|
|
156
156
|
} catch (e) { /* swallow · UI is optimistic */ }
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
anthropic: "opus-4-7",
|
|
166
|
-
openai: "gpt-5-5",
|
|
167
|
-
google: "gemini-3-flash",
|
|
168
|
-
xai: "grok-4-3",
|
|
169
|
-
deepseek: "deepseek-v4-pro",
|
|
170
|
-
openrouter: "opus-4-7",
|
|
171
|
-
};
|
|
172
|
-
function primaryModelForProvider(provider) {
|
|
173
|
-
const snap = modelsSnapshot();
|
|
174
|
-
const reachable = (snap && snap.reachable) || [];
|
|
175
|
-
// 1. Curated primary, if reachable.
|
|
176
|
-
const curated = PRIMARY_BY_PROVIDER[provider];
|
|
177
|
-
if (curated && reachable.find((m) => m.modelV === curated && m.provider === provider)) {
|
|
178
|
-
return curated;
|
|
179
|
-
}
|
|
180
|
-
// 2. First reachable model from that provider.
|
|
181
|
-
const first = reachable.find((m) => m.provider === provider);
|
|
182
|
-
return first ? first.modelV : null;
|
|
183
|
-
}
|
|
184
|
-
/** Look up which provider currently owns the saved default model. */
|
|
185
|
-
function currentDefaultProvider() {
|
|
186
|
-
const snap = modelsSnapshot();
|
|
187
|
-
if (!snap || !snap.defaultModelV) return null;
|
|
188
|
-
const m = (snap.reachable || []).find((x) => x.modelV === snap.defaultModelV);
|
|
189
|
-
return m ? m.provider : null;
|
|
190
|
-
}
|
|
191
|
-
/** Click handler for the per-row "Set as default" button. Picks the
|
|
192
|
-
* provider's primary model + persists it as defaultModelV. The row
|
|
193
|
-
* re-renders so the badge moves. */
|
|
194
|
-
async function setProviderAsDefault(provider) {
|
|
195
|
-
const modelV = primaryModelForProvider(provider);
|
|
196
|
-
if (!modelV) return;
|
|
197
|
-
await saveDefaultModel(modelV);
|
|
198
|
-
// Re-render the keys section so every LLM row updates its badge.
|
|
199
|
-
if (currentSection === "keys") renderSection("keys");
|
|
200
|
-
}
|
|
159
|
+
// Provider→primary-model helpers (`primaryModelForProvider`,
|
|
160
|
+
// `currentDefaultProvider`, `setProviderAsDefault`) lived here to
|
|
161
|
+
// power the per-row "set as default" button on the API Key pane.
|
|
162
|
+
// That button + its companion bottom-of-pane Default Model picker
|
|
163
|
+
// were removed in favour of the dedicated "Default Model" sidebar
|
|
164
|
+
// pane (single source of truth). Helpers deleted as dead code.
|
|
201
165
|
|
|
202
166
|
// Set / clear a single provider key. The server applies the trim+empty-=delete
|
|
203
167
|
// semantic; we mirror the resulting meta into our local cache.
|
|
@@ -500,35 +464,29 @@
|
|
|
500
464
|
const placeholder = has
|
|
501
465
|
? (preview || "••••••••")
|
|
502
466
|
: p.placeholder;
|
|
503
|
-
// Default-
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
|
|
509
|
-
const isDefault = isLlm && currentDefaultProvider() === p.id;
|
|
510
|
-
const canSetDefault = isLlm && has && !isDefault && !!primaryModelForProvider(p.id);
|
|
511
|
-
const labelExtras = isDefault
|
|
512
|
-
? ' <span class="badge us-key-default-badge">default</span>'
|
|
513
|
-
: "";
|
|
467
|
+
// Default-model selection lives entirely in the dedicated
|
|
468
|
+
// "Default Model" sidebar pane. The previous in-row "default"
|
|
469
|
+
// badge + "set as default" button on each LLM provider was a
|
|
470
|
+
// duplicate UX that also competed with the bottom-of-pane
|
|
471
|
+
// "Default model" picker · all three controls did the same
|
|
472
|
+
// thing. The single source of truth is now the sidebar pane.
|
|
514
473
|
return `
|
|
515
|
-
<div class="us-key-row
|
|
474
|
+
<div class="us-key-row" data-provider="${p.id}">
|
|
516
475
|
<div class="us-key-head">
|
|
517
|
-
<div class="us-key-label">${escape(p.label)}
|
|
476
|
+
<div class="us-key-label">${escape(p.label)}</div>
|
|
518
477
|
<div class="us-key-status ${has ? "on" : "off"}" data-status>${has ? "● configured" : "○ not set"}</div>
|
|
519
|
-
${canSetDefault ? `<button type="button" class="us-key-set-default" data-set-default-provider="${p.id}" title="Use ${escape(p.label)} as the default model provider for new agents">set as default</button>` : ""}
|
|
520
478
|
${removable ? `<button type="button" class="us-key-remove" data-remove-provider="${p.id}" title="Remove">✕</button>` : ""}
|
|
521
479
|
</div>
|
|
522
480
|
<div class="us-key-hint">${escape(p.hint)}</div>
|
|
523
481
|
<div class="us-input-wrap">
|
|
524
482
|
<input
|
|
525
|
-
type="
|
|
526
|
-
class="us-input${has ? " has-preview" : ""}"
|
|
483
|
+
type="text"
|
|
484
|
+
class="us-input us-input-masked${has ? " has-preview" : ""}"
|
|
527
485
|
data-key-input
|
|
528
486
|
name="bk-${p.id}"
|
|
529
487
|
placeholder="${escape(placeholder)}"
|
|
530
488
|
value=""
|
|
531
|
-
autocomplete="
|
|
489
|
+
autocomplete="off"
|
|
532
490
|
data-lpignore="true"
|
|
533
491
|
data-1p-ignore="true"
|
|
534
492
|
data-form-type="other"
|
|
@@ -659,43 +617,15 @@
|
|
|
659
617
|
`;
|
|
660
618
|
}).join("");
|
|
661
619
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const models = byProvider.get(p);
|
|
667
|
-
return `<optgroup label="${escape(providerLabel(p))}">${
|
|
668
|
-
models.map((m) => `<option value="${escape(m.modelV)}"${m.modelV === def ? " selected" : ""}>${escape(m.displayName)}</option>`).join("")
|
|
669
|
-
}</optgroup>`;
|
|
670
|
-
}).join("");
|
|
671
|
-
defaultBlock = `
|
|
672
|
-
<div class="us-models-default">
|
|
673
|
-
<div class="us-models-default-label">Default model</div>
|
|
674
|
-
<div class="us-models-default-hint">new agents inherit this. when an agent's model goes unreachable, it falls back here too.</div>
|
|
675
|
-
<div class="us-input-wrap us-models-default-wrap">
|
|
676
|
-
<select class="us-input us-models-default-select" data-default-model>${optgroups}</select>
|
|
677
|
-
</div>
|
|
678
|
-
</div>
|
|
679
|
-
`;
|
|
680
|
-
} else {
|
|
681
|
-
const m = reachable[0];
|
|
682
|
-
defaultBlock = `
|
|
683
|
-
<div class="us-models-default">
|
|
684
|
-
<div class="us-models-default-label">Default model</div>
|
|
685
|
-
<div class="us-models-default-static">
|
|
686
|
-
<span class="us-models-default-name">${escape(m.displayName)}</span>
|
|
687
|
-
<span class="us-models-default-note">only reachable model</span>
|
|
688
|
-
</div>
|
|
689
|
-
</div>
|
|
690
|
-
`;
|
|
691
|
-
}
|
|
692
|
-
|
|
620
|
+
// Default-model selection moved to the sidebar's "Default Model"
|
|
621
|
+
// pane · the previous bottom-of-pane select duplicated that flow.
|
|
622
|
+
// The Available Models block is now read-only (which models are
|
|
623
|
+
// reachable + how they route), nothing else.
|
|
693
624
|
return `
|
|
694
625
|
<div class="us-key-group us-key-group-models">
|
|
695
626
|
<div class="us-key-group-tag">Available models</div>
|
|
696
627
|
<div class="us-key-group-deck">${reachable.length} model${reachable.length === 1 ? "" : "s"} reachable across ${providers.length} provider${providers.length === 1 ? "" : "s"}. <code>direct</code> uses the provider key, <code>OR</code> routes through OpenRouter.</div>
|
|
697
628
|
<div class="us-models-list">${blocks}</div>
|
|
698
|
-
${defaultBlock}
|
|
699
629
|
</div>
|
|
700
630
|
`;
|
|
701
631
|
}
|
|
@@ -807,61 +737,10 @@
|
|
|
807
737
|
const slot = paneEl.querySelector("[data-models-summary]");
|
|
808
738
|
if (!slot) return;
|
|
809
739
|
slot.innerHTML = modelsSummaryHTML();
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
// default-related controls in place so we don't disturb the
|
|
815
|
-
// input field the user is typing into.
|
|
816
|
-
const defaultProvider = currentDefaultProvider();
|
|
817
|
-
paneEl.querySelectorAll(".us-key-row").forEach((row) => {
|
|
818
|
-
const id = row.dataset.provider;
|
|
819
|
-
const p = PROVIDERS.find((x) => x.id === id);
|
|
820
|
-
if (!p || p.group !== "llm") return;
|
|
821
|
-
const meta = _keysMeta[id];
|
|
822
|
-
const has = !!(meta && meta.configured);
|
|
823
|
-
const isDefault = defaultProvider === id;
|
|
824
|
-
const canSet = has && !isDefault && !!primaryModelForProvider(id);
|
|
825
|
-
// Toggle .is-default on the row.
|
|
826
|
-
row.classList.toggle("is-default", isDefault);
|
|
827
|
-
// Sync the badge in the label.
|
|
828
|
-
const label = row.querySelector(".us-key-label");
|
|
829
|
-
if (label) {
|
|
830
|
-
const existing = label.querySelector(".us-key-default-badge");
|
|
831
|
-
if (isDefault && !existing) {
|
|
832
|
-
label.insertAdjacentHTML("beforeend", ' <span class="badge us-key-default-badge">default</span>');
|
|
833
|
-
} else if (!isDefault && existing) {
|
|
834
|
-
existing.remove();
|
|
835
|
-
// Cleanup adjacent whitespace text node so we don't accumulate
|
|
836
|
-
// spaces over time.
|
|
837
|
-
const next = label.lastChild;
|
|
838
|
-
if (next && next.nodeType === 3 && /\s+/.test(next.nodeValue || "")) next.remove();
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
// Sync the "set as default" button.
|
|
842
|
-
const head = row.querySelector(".us-key-head");
|
|
843
|
-
if (head) {
|
|
844
|
-
const existing = head.querySelector("[data-set-default-provider]");
|
|
845
|
-
if (canSet && !existing) {
|
|
846
|
-
// Insert just before the remove button (or at the end).
|
|
847
|
-
const btn = document.createElement("button");
|
|
848
|
-
btn.type = "button";
|
|
849
|
-
btn.className = "us-key-set-default";
|
|
850
|
-
btn.dataset.setDefaultProvider = id;
|
|
851
|
-
btn.title = `Use ${p.label} as the default model provider for new agents`;
|
|
852
|
-
btn.textContent = "set as default";
|
|
853
|
-
btn.addEventListener("click", async (e) => {
|
|
854
|
-
e.preventDefault();
|
|
855
|
-
await setProviderAsDefault(id);
|
|
856
|
-
});
|
|
857
|
-
const removeBtn = head.querySelector("[data-remove-provider]");
|
|
858
|
-
if (removeBtn) head.insertBefore(btn, removeBtn);
|
|
859
|
-
else head.appendChild(btn);
|
|
860
|
-
} else if (!canSet && existing) {
|
|
861
|
-
existing.remove();
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
});
|
|
740
|
+
// Default-model state lives in the sidebar's "Default Model"
|
|
741
|
+
// pane · this refresh used to also patch each LLM row's
|
|
742
|
+
// badge / "set as default" button, but those controls were
|
|
743
|
+
// removed to eliminate the duplicate flow.
|
|
865
744
|
}
|
|
866
745
|
|
|
867
746
|
/* Avatar generation · same flow as the agent profile's regenerate
|
|
@@ -903,7 +782,10 @@
|
|
|
903
782
|
|
|
904
783
|
<footer class="us-foot">
|
|
905
784
|
<span class="saved">changes save automatically</span>
|
|
906
|
-
<
|
|
785
|
+
<div class="us-foot-right">
|
|
786
|
+
<a class="us-website" href="/home.html" target="_blank" rel="noopener">website ↗</a>
|
|
787
|
+
<button type="button" class="us-done">[ Done ]</button>
|
|
788
|
+
</div>
|
|
907
789
|
</footer>
|
|
908
790
|
|
|
909
791
|
</div>
|
|
@@ -971,7 +853,6 @@
|
|
|
971
853
|
if (typeof window.app.renderUserBlock === "function") window.app.renderUserBlock();
|
|
972
854
|
} else {
|
|
973
855
|
document.querySelectorAll(".sidebar-foot .user-name").forEach((el) => { el.textContent = (u.name || "Kay").toUpperCase(); });
|
|
974
|
-
document.querySelectorAll(".sidebar-foot .user-menu .name").forEach((el) => { el.textContent = u.name || "Kay"; });
|
|
975
856
|
}
|
|
976
857
|
}
|
|
977
858
|
|
|
@@ -1011,11 +892,18 @@
|
|
|
1011
892
|
}
|
|
1012
893
|
|
|
1013
894
|
function wireKeysSection() {
|
|
895
|
+
// Show/hide toggle · the input is permanently `type="text"` so
|
|
896
|
+
// browsers don't trigger their "Save password?" popup when the
|
|
897
|
+
// user navigates away from a typed-in key (e.g., clicking another
|
|
898
|
+
// sidebar tab in user prefs). Masking is done via the CSS
|
|
899
|
+
// `-webkit-text-security: disc` rule on `.us-input-masked` —
|
|
900
|
+
// visually identical to a password input but invisible to
|
|
901
|
+
// password managers. Toggle = add/remove the masking class.
|
|
1014
902
|
paneEl.querySelectorAll("[data-key-eye]").forEach((btn) => {
|
|
1015
903
|
btn.addEventListener("click", (e) => {
|
|
1016
904
|
e.preventDefault();
|
|
1017
905
|
const input = btn.parentElement.querySelector("input");
|
|
1018
|
-
if (input) input.
|
|
906
|
+
if (input) input.classList.toggle("us-input-masked");
|
|
1019
907
|
});
|
|
1020
908
|
});
|
|
1021
909
|
|
|
@@ -1041,21 +929,10 @@
|
|
|
1041
929
|
});
|
|
1042
930
|
});
|
|
1043
931
|
|
|
1044
|
-
//
|
|
1045
|
-
//
|
|
1046
|
-
//
|
|
1047
|
-
|
|
1048
|
-
btn.addEventListener("click", async (e) => {
|
|
1049
|
-
e.preventDefault();
|
|
1050
|
-
const id = btn.dataset.setDefaultProvider;
|
|
1051
|
-
await setProviderAsDefault(id);
|
|
1052
|
-
});
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
// Default-model picker · persists to /api/prefs.
|
|
1056
|
-
paneEl.querySelectorAll("[data-default-model]").forEach((sel) => {
|
|
1057
|
-
sel.addEventListener("change", () => { saveDefaultModel(sel.value); });
|
|
1058
|
-
});
|
|
932
|
+
// Default-model controls live in the sidebar's "Default Model"
|
|
933
|
+
// pane only · the previous in-row "set as default" button and
|
|
934
|
+
// the bottom-of-pane Default Model picker were removed because
|
|
935
|
+
// they duplicated that flow.
|
|
1059
936
|
|
|
1060
937
|
// Auto-save: every keystroke / paste persists immediately, no Save button.
|
|
1061
938
|
// We debounce slightly so we don't fire a server PUT on every character —
|
|
@@ -1124,6 +1001,26 @@
|
|
|
1124
1001
|
// wireKeysSection again.
|
|
1125
1002
|
fetchKeyMeta().then(() => {
|
|
1126
1003
|
if (currentSection !== "keys") return;
|
|
1004
|
+
|
|
1005
|
+
// After-onboarding sync · the bootstrap fetchKeyMeta ran before
|
|
1006
|
+
// the user wrote their first key (during onboarding). When the
|
|
1007
|
+
// user opens settings without a page refresh, _keysMeta was
|
|
1008
|
+
// empty at first render, so activeProviders was derived without
|
|
1009
|
+
// the just-configured provider — and the keys tab paints with
|
|
1010
|
+
// no row for it (e.g. "no OpenRouter section visible until
|
|
1011
|
+
// refresh"). Detect that drift here and rebuild the section
|
|
1012
|
+
// when a configured provider is missing its row. Inline pill
|
|
1013
|
+
// refresh below handles the simpler case where the row already
|
|
1014
|
+
// exists and only its `● configured` state needs flipping.
|
|
1015
|
+
const missingActive = LLM_PROVIDER_IDS.filter(
|
|
1016
|
+
(id) => _keysMeta[id] && _keysMeta[id].configured && !activeProviders.includes(id),
|
|
1017
|
+
);
|
|
1018
|
+
if (missingActive.length > 0) {
|
|
1019
|
+
activeProviders = null;
|
|
1020
|
+
rerenderKeysSection();
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1127
1024
|
paneEl.querySelectorAll(".us-key-row").forEach((row) => {
|
|
1128
1025
|
const provider = row.dataset.provider;
|
|
1129
1026
|
const meta = _keysMeta[provider];
|
|
@@ -1179,6 +1076,13 @@
|
|
|
1179
1076
|
if (window.app && typeof window.app.refreshKeys === "function") {
|
|
1180
1077
|
window.app.refreshKeys();
|
|
1181
1078
|
}
|
|
1079
|
+
// If an agent profile is open, its skill rows have data-key-
|
|
1080
|
+
// configured cached from first paint. Re-fetch so the web-search
|
|
1081
|
+
// toggle no longer shows the "configure key" prompt after the
|
|
1082
|
+
// user added the Brave key here.
|
|
1083
|
+
if (typeof window.refreshAgentProfileSkills === "function") {
|
|
1084
|
+
window.refreshAgentProfileSkills();
|
|
1085
|
+
}
|
|
1182
1086
|
}
|
|
1183
1087
|
|
|
1184
1088
|
function init() {
|