bosun 0.30.0 → 0.31.2
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/desktop/launch.mjs +2 -0
- package/package.json +1 -1
- package/setup-web-server.mjs +33 -11
- package/ui/components/agent-selector.js +54 -89
- package/ui/components/chat-view.js +4 -6
- package/ui/components/shared.js +3 -1
- package/ui/setup.html +13 -7
- package/ui/styles/components.css +16 -1
- package/ui/styles/sessions.css +79 -44
- package/ui/tabs/agents.js +0 -3
- package/ui/tabs/chat.js +12 -3
- package/ui/tabs/settings.js +76 -47
- package/ui/tabs/tasks.js +25 -7
- package/ui-server.mjs +31 -3
package/desktop/launch.mjs
CHANGED
|
@@ -42,6 +42,7 @@ function ensureElectronInstalled() {
|
|
|
42
42
|
const result = spawnSync("npm", ["install"], {
|
|
43
43
|
cwd: desktopDir,
|
|
44
44
|
stdio: "inherit",
|
|
45
|
+
shell: process.platform === "win32",
|
|
45
46
|
env: process.env,
|
|
46
47
|
});
|
|
47
48
|
return result.status === 0 && existsSync(electronBin);
|
|
@@ -60,6 +61,7 @@ function launch() {
|
|
|
60
61
|
|
|
61
62
|
const child = spawn(electronBin, args, {
|
|
62
63
|
stdio: "inherit",
|
|
64
|
+
shell: process.platform === "win32",
|
|
63
65
|
env: {
|
|
64
66
|
...process.env,
|
|
65
67
|
BOSUN_DESKTOP: "1",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.2",
|
|
4
4
|
"description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache 2.0",
|
package/setup-web-server.mjs
CHANGED
|
@@ -28,21 +28,43 @@ const MAX_PORT_ATTEMPTS = 20;
|
|
|
28
28
|
|
|
29
29
|
const MODELS = {
|
|
30
30
|
copilot: [
|
|
31
|
-
{ value: "claude-
|
|
32
|
-
{ value: "claude-
|
|
33
|
-
{ value: "
|
|
34
|
-
{ value: "
|
|
35
|
-
{ value: "
|
|
31
|
+
{ value: "claude-opus-4.6", label: "claude-opus-4.6", recommended: true },
|
|
32
|
+
{ value: "claude-sonnet-4.6", label: "claude-sonnet-4.6" },
|
|
33
|
+
{ value: "claude-opus-4.5", label: "claude-opus-4.5" },
|
|
34
|
+
{ value: "claude-sonnet-4.5", label: "claude-sonnet-4.5" },
|
|
35
|
+
{ value: "claude-sonnet-4", label: "claude-sonnet-4" },
|
|
36
|
+
{ value: "claude-haiku-4.5", label: "claude-haiku-4.5" },
|
|
37
|
+
{ value: "gpt-5.2-codex", label: "gpt-5.2-codex" },
|
|
38
|
+
{ value: "gpt-5.3-codex", label: "gpt-5.3-codex" },
|
|
39
|
+
{ value: "gpt-5.1-codex", label: "gpt-5.1-codex" },
|
|
40
|
+
{ value: "gpt-5.1-codex-mini", label: "gpt-5.1-codex-mini" },
|
|
41
|
+
{ value: "gpt-5.1-codex-max", label: "gpt-5.1-codex-max" },
|
|
42
|
+
{ value: "gpt-5.2", label: "gpt-5.2" },
|
|
43
|
+
{ value: "gpt-5.1", label: "gpt-5.1" },
|
|
44
|
+
{ value: "gpt-5-mini", label: "gpt-5-mini" },
|
|
45
|
+
{ value: "gemini-3.1-pro", label: "gemini-3.1-pro" },
|
|
46
|
+
{ value: "gemini-3-pro", label: "gemini-3-pro" },
|
|
47
|
+
{ value: "gemini-3-flash", label: "gemini-3-flash" },
|
|
48
|
+
{ value: "gemini-2.5-pro", label: "gemini-2.5-pro" },
|
|
49
|
+
{ value: "grok-code-fast-1", label: "grok-code-fast-1" },
|
|
36
50
|
],
|
|
37
51
|
codex: [
|
|
38
|
-
{ value: "
|
|
39
|
-
{ value: "
|
|
40
|
-
{ value: "gpt-
|
|
41
|
-
{ value: "codex-mini
|
|
52
|
+
{ value: "gpt-5.3-codex", label: "gpt-5.3-codex", recommended: true },
|
|
53
|
+
{ value: "gpt-5.2-codex", label: "gpt-5.2-codex" },
|
|
54
|
+
{ value: "gpt-5.1-codex", label: "gpt-5.1-codex" },
|
|
55
|
+
{ value: "gpt-5.1-codex-mini", label: "gpt-5.1-codex-mini" },
|
|
56
|
+
{ value: "gpt-5.1-codex-max", label: "gpt-5.1-codex-max" },
|
|
57
|
+
{ value: "gpt-5.2", label: "gpt-5.2" },
|
|
58
|
+
{ value: "gpt-5.1", label: "gpt-5.1" },
|
|
59
|
+
{ value: "gpt-5-mini", label: "gpt-5-mini" },
|
|
42
60
|
],
|
|
43
61
|
claude: [
|
|
44
|
-
{ value: "claude-
|
|
45
|
-
{ value: "claude-
|
|
62
|
+
{ value: "claude-opus-4.6", label: "claude-opus-4.6", recommended: true },
|
|
63
|
+
{ value: "claude-sonnet-4.6", label: "claude-sonnet-4.6" },
|
|
64
|
+
{ value: "claude-opus-4.5", label: "claude-opus-4.5" },
|
|
65
|
+
{ value: "claude-sonnet-4.5", label: "claude-sonnet-4.5" },
|
|
66
|
+
{ value: "claude-sonnet-4", label: "claude-sonnet-4" },
|
|
67
|
+
{ value: "claude-haiku-4.5", label: "claude-haiku-4.5" },
|
|
46
68
|
],
|
|
47
69
|
};
|
|
48
70
|
|
|
@@ -361,6 +361,36 @@ const AGENT_SELECTOR_STYLES = `
|
|
|
361
361
|
padding: 8px 12px;
|
|
362
362
|
}
|
|
363
363
|
}
|
|
364
|
+
|
|
365
|
+
/* ── Native Agent Select ── */
|
|
366
|
+
.agent-picker-native {
|
|
367
|
+
appearance: none;
|
|
368
|
+
-webkit-appearance: none;
|
|
369
|
+
background: var(--tg-theme-secondary-bg-color, #1e1e2e);
|
|
370
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
371
|
+
border-radius: 8px;
|
|
372
|
+
color: var(--tg-theme-text-color, #fff);
|
|
373
|
+
font-size: 12px;
|
|
374
|
+
font-weight: 500;
|
|
375
|
+
padding: 5px 28px 5px 28px;
|
|
376
|
+
cursor: pointer;
|
|
377
|
+
min-width: 120px;
|
|
378
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath fill='%23999' d='M1 3l4 4 4-4'/%3E%3C/svg%3E");
|
|
379
|
+
background-repeat: no-repeat;
|
|
380
|
+
background-position: right 8px center;
|
|
381
|
+
transition: border-color 0.2s ease;
|
|
382
|
+
}
|
|
383
|
+
.agent-picker-native:hover {
|
|
384
|
+
border-color: rgba(255,255,255,0.15);
|
|
385
|
+
}
|
|
386
|
+
.agent-picker-native:focus {
|
|
387
|
+
outline: none;
|
|
388
|
+
border-color: var(--tg-theme-button-color, #3b82f6);
|
|
389
|
+
}
|
|
390
|
+
.agent-picker-native option {
|
|
391
|
+
background: #1a1a2e;
|
|
392
|
+
color: #fff;
|
|
393
|
+
}
|
|
364
394
|
`;
|
|
365
395
|
|
|
366
396
|
let _agentStylesInjected = false;
|
|
@@ -480,106 +510,41 @@ export function AgentModeSelector() {
|
|
|
480
510
|
* ═══════════════════════════════════════════════ */
|
|
481
511
|
|
|
482
512
|
export function AgentPicker() {
|
|
483
|
-
const [open, setOpen] = useState(false);
|
|
484
|
-
const wrapRef = useRef(null);
|
|
485
513
|
const agents = availableAgents.value;
|
|
486
|
-
const current =
|
|
514
|
+
const current = activeAgent.value;
|
|
487
515
|
const loading = agentSelectorLoading.value;
|
|
488
516
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
if (
|
|
492
|
-
const handler = (e) => {
|
|
493
|
-
if (wrapRef.current && !wrapRef.current.contains(e.target)) {
|
|
494
|
-
setOpen(false);
|
|
495
|
-
}
|
|
496
|
-
};
|
|
497
|
-
document.addEventListener("pointerdown", handler, true);
|
|
498
|
-
return () => document.removeEventListener("pointerdown", handler, true);
|
|
499
|
-
}, [open]);
|
|
500
|
-
|
|
501
|
-
// Close on Escape
|
|
502
|
-
useEffect(() => {
|
|
503
|
-
if (!open) return;
|
|
504
|
-
const handler = (e) => { if (e.key === "Escape") setOpen(false); };
|
|
505
|
-
document.addEventListener("keydown", handler);
|
|
506
|
-
return () => document.removeEventListener("keydown", handler);
|
|
507
|
-
}, [open]);
|
|
508
|
-
|
|
509
|
-
const handleToggle = useCallback(() => {
|
|
510
|
-
haptic("light");
|
|
511
|
-
setOpen((v) => !v);
|
|
512
|
-
}, []);
|
|
513
|
-
|
|
514
|
-
const handleSelect = useCallback((agentId) => {
|
|
515
|
-
if (agentId === activeAgent.value) {
|
|
516
|
-
setOpen(false);
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
517
|
+
const handleChange = useCallback((e) => {
|
|
518
|
+
const agentId = e.target.value;
|
|
519
|
+
if (agentId === activeAgent.value) return;
|
|
519
520
|
haptic("medium");
|
|
520
521
|
switchAgent(agentId);
|
|
521
|
-
setOpen(false);
|
|
522
522
|
}, []);
|
|
523
523
|
|
|
524
|
-
const currentIcon =
|
|
525
|
-
const currentName = current ? current.name : (loading ? "Loading…" : "Select Agent");
|
|
524
|
+
const currentIcon = AGENT_ICONS[current] || "⚡";
|
|
526
525
|
|
|
527
526
|
return html`
|
|
528
|
-
<div class="agent-picker-wrap"
|
|
529
|
-
<
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
527
|
+
<div class="agent-picker-wrap">
|
|
528
|
+
<span class="picker-icon" style="position:absolute;left:8px;top:50%;transform:translateY(-50%);font-size:13px;pointer-events:none;z-index:1">${currentIcon}</span>
|
|
529
|
+
<select
|
|
530
|
+
class="agent-picker-native"
|
|
531
|
+
value=${current}
|
|
532
|
+
onChange=${handleChange}
|
|
533
|
+
disabled=${loading}
|
|
534
534
|
title="Select AI agent"
|
|
535
535
|
>
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
`}
|
|
549
|
-
${agents.map((agent) => {
|
|
550
|
-
const isActive = agent.id === activeAgent.value;
|
|
551
|
-
const icon = AGENT_ICONS[agent.id] || "🔌";
|
|
552
|
-
const statusClass = agent.busy ? "busy" : agent.available ? "available" : "offline";
|
|
553
|
-
const providerColor = PROVIDER_COLORS[agent.provider] || "#6b7280";
|
|
554
|
-
|
|
555
|
-
return html`
|
|
556
|
-
<button
|
|
557
|
-
key=${agent.id}
|
|
558
|
-
class="agent-picker-item ${isActive ? "active" : ""}"
|
|
559
|
-
role="option"
|
|
560
|
-
aria-selected=${isActive}
|
|
561
|
-
onClick=${() => handleSelect(agent.id)}
|
|
562
|
-
>
|
|
563
|
-
<div class="agent-picker-item-icon">${icon}</div>
|
|
564
|
-
<div class="agent-picker-item-info">
|
|
565
|
-
<div class="agent-picker-item-name">${agent.name}</div>
|
|
566
|
-
<div class="agent-picker-item-provider">
|
|
567
|
-
<span
|
|
568
|
-
class="agent-provider-badge"
|
|
569
|
-
style="background: ${providerColor}"
|
|
570
|
-
>${agent.provider}</span>
|
|
571
|
-
</div>
|
|
572
|
-
</div>
|
|
573
|
-
<div class="agent-picker-item-end">
|
|
574
|
-
<span class="agent-picker-status-dot ${statusClass}"
|
|
575
|
-
title=${statusClass} />
|
|
576
|
-
${isActive && html`<span class="agent-picker-check">✓</span>`}
|
|
577
|
-
</div>
|
|
578
|
-
</button>
|
|
579
|
-
`;
|
|
580
|
-
})}
|
|
581
|
-
</div>
|
|
582
|
-
`}
|
|
536
|
+
${agents.length === 0 && html`
|
|
537
|
+
<option disabled>${loading ? "Loading…" : "No agents"}</option>
|
|
538
|
+
`}
|
|
539
|
+
${agents.map((agent) => {
|
|
540
|
+
const statusLabel = agent.busy ? " (busy)" : !agent.available ? " (offline)" : "";
|
|
541
|
+
return html`
|
|
542
|
+
<option key=${agent.id} value=${agent.id}>
|
|
543
|
+
${agent.name} · ${agent.provider}${statusLabel}
|
|
544
|
+
</option>
|
|
545
|
+
`;
|
|
546
|
+
})}
|
|
547
|
+
</select>
|
|
583
548
|
</div>
|
|
584
549
|
`;
|
|
585
550
|
}
|
|
@@ -268,6 +268,7 @@ export function ChatView({ sessionId, readOnly = false, embedded = false }) {
|
|
|
268
268
|
const [autoScroll, setAutoScroll] = useState(true);
|
|
269
269
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
270
270
|
const [visibleCount, setVisibleCount] = useState(200);
|
|
271
|
+
const [showStreamMeta, setShowStreamMeta] = useState(false);
|
|
271
272
|
const [filters, setFilters] = useState({
|
|
272
273
|
tool: false,
|
|
273
274
|
result: false,
|
|
@@ -704,15 +705,12 @@ export function ChatView({ sessionId, readOnly = false, embedded = false }) {
|
|
|
704
705
|
>
|
|
705
706
|
${paused ? "Resume" : "Pause"}
|
|
706
707
|
</button>
|
|
707
|
-
<button class="btn btn-ghost btn-xs" onClick=${
|
|
708
|
-
|
|
709
|
-
</button>
|
|
710
|
-
<button class="btn btn-ghost btn-xs" onClick=${handleExportStream} title="Export stream">
|
|
711
|
-
Export
|
|
708
|
+
<button class="btn btn-ghost btn-xs" onClick=${() => setShowStreamMeta((prev) => !prev)} title="Toggle filters">
|
|
709
|
+
${showStreamMeta ? "Hide filters" : "Filters"}
|
|
712
710
|
</button>
|
|
713
711
|
</div>
|
|
714
712
|
</div>
|
|
715
|
-
<div class="chat-stream-meta">
|
|
713
|
+
<div class="chat-stream-meta ${showStreamMeta ? 'expanded' : ''}">
|
|
716
714
|
<div class="chat-stream-filters">
|
|
717
715
|
<button
|
|
718
716
|
class="chat-filter-chip ${activeFilters.length === 0 ? "active" : ""}"
|
package/ui/components/shared.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* ────────────────────────────────────────────────────────────── */
|
|
5
5
|
|
|
6
6
|
import { h } from "preact";
|
|
7
|
+
import { createPortal } from "preact/compat";
|
|
7
8
|
import {
|
|
8
9
|
useState,
|
|
9
10
|
useEffect,
|
|
@@ -319,7 +320,7 @@ export function Modal({ title, open = true, onClose, children, contentClassName
|
|
|
319
320
|
? `transform: translateY(${dragY}px); opacity: ${Math.max(0.2, 1 - dragY / 400)}`
|
|
320
321
|
: "";
|
|
321
322
|
|
|
322
|
-
|
|
323
|
+
const content = html`
|
|
323
324
|
<div
|
|
324
325
|
class="modal-overlay ${visible ? "modal-overlay-visible" : ""}"
|
|
325
326
|
onClick=${(e) => {
|
|
@@ -354,6 +355,7 @@ export function Modal({ title, open = true, onClose, children, contentClassName
|
|
|
354
355
|
</div>
|
|
355
356
|
</div>
|
|
356
357
|
`;
|
|
358
|
+
return createPortal(content, document.body);
|
|
357
359
|
}
|
|
358
360
|
|
|
359
361
|
/* ═══════════════════════════════════════════════
|
package/ui/setup.html
CHANGED
|
@@ -410,6 +410,7 @@ function SelectWithCustom({ options, value, onChange, placeholder }) {
|
|
|
410
410
|
const handleSelect = (e) => {
|
|
411
411
|
if (e.target.value === "__custom__") {
|
|
412
412
|
setIsCustom(true);
|
|
413
|
+
setCustomValue(value || "");
|
|
413
414
|
setTimeout(() => inputRef.current?.focus(), 50);
|
|
414
415
|
} else {
|
|
415
416
|
setIsCustom(false);
|
|
@@ -418,9 +419,12 @@ function SelectWithCustom({ options, value, onChange, placeholder }) {
|
|
|
418
419
|
}
|
|
419
420
|
};
|
|
420
421
|
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
422
|
+
const commitCustom = () => {
|
|
423
|
+
if (customValue.trim()) onChange(customValue.trim());
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const handleCustomKeyDown = (e) => {
|
|
427
|
+
if (e.key === "Enter") { commitCustom(); e.target.blur(); }
|
|
424
428
|
};
|
|
425
429
|
|
|
426
430
|
return html`
|
|
@@ -435,8 +439,10 @@ function SelectWithCustom({ options, value, onChange, placeholder }) {
|
|
|
435
439
|
ref=${inputRef}
|
|
436
440
|
type="text"
|
|
437
441
|
value=${customValue}
|
|
438
|
-
oninput=${
|
|
439
|
-
|
|
442
|
+
oninput=${(e) => setCustomValue(e.target.value)}
|
|
443
|
+
onblur=${commitCustom}
|
|
444
|
+
onkeydown=${handleCustomKeyDown}
|
|
445
|
+
placeholder="Enter model slug..."
|
|
440
446
|
/>
|
|
441
447
|
<button class="btn btn-sm" onclick=${() => { setIsCustom(false); onChange(options[0]?.value || ""); }}>
|
|
442
448
|
Cancel
|
|
@@ -469,8 +475,8 @@ function App() {
|
|
|
469
475
|
const [repoSlug, setRepoSlug] = useState("");
|
|
470
476
|
const [repoRoot, setRepoRoot] = useState("");
|
|
471
477
|
const [executors, setExecutors] = useState([
|
|
472
|
-
{ name: "primary", executor: "COPILOT", model: "claude-
|
|
473
|
-
{ name: "backup", executor: "CODEX", model: "
|
|
478
|
+
{ name: "primary", executor: "COPILOT", model: "claude-opus-4.6", weight: 70, role: "primary" },
|
|
479
|
+
{ name: "backup", executor: "CODEX", model: "gpt-5.2-codex", weight: 30, role: "backup" },
|
|
474
480
|
]);
|
|
475
481
|
const [repos, setRepos] = useState([""]);
|
|
476
482
|
const [kanbanBackend, setKanbanBackend] = useState("internal");
|
package/ui/styles/components.css
CHANGED
|
@@ -845,7 +845,7 @@ select.input {
|
|
|
845
845
|
background: var(--backdrop);
|
|
846
846
|
backdrop-filter: var(--backdrop-blur);
|
|
847
847
|
-webkit-backdrop-filter: var(--backdrop-blur);
|
|
848
|
-
z-index:
|
|
848
|
+
z-index: 10000;
|
|
849
849
|
display: flex;
|
|
850
850
|
align-items: flex-end;
|
|
851
851
|
justify-content: center;
|
|
@@ -882,6 +882,21 @@ select.input {
|
|
|
882
882
|
max-width: 860px;
|
|
883
883
|
}
|
|
884
884
|
|
|
885
|
+
/* ── Desktop: Center modal as dialog ── */
|
|
886
|
+
@media (min-width: 769px) {
|
|
887
|
+
.modal-overlay {
|
|
888
|
+
align-items: center;
|
|
889
|
+
padding: 24px;
|
|
890
|
+
z-index: 10000;
|
|
891
|
+
}
|
|
892
|
+
.modal-content {
|
|
893
|
+
border-radius: var(--radius-xl);
|
|
894
|
+
border-bottom: 1px solid var(--border);
|
|
895
|
+
max-height: 85vh;
|
|
896
|
+
animation: springUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
885
900
|
.modal-form-grid {
|
|
886
901
|
display: flex;
|
|
887
902
|
flex-direction: column;
|
package/ui/styles/sessions.css
CHANGED
|
@@ -66,19 +66,7 @@
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
.app-shell[data-has-rail="true"] .session-split {
|
|
71
|
-
grid-template-columns: 1fr;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.app-shell[data-has-rail="true"] .session-pane {
|
|
75
|
-
display: none;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.app-shell[data-has-rail="true"] .chat-shell-header .session-drawer-btn {
|
|
79
|
-
display: none;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
69
|
+
/* Rail no longer hides the session pane on desktop — sessions always visible */
|
|
82
70
|
|
|
83
71
|
@media (max-width: 768px) {
|
|
84
72
|
.session-split {
|
|
@@ -173,7 +161,7 @@
|
|
|
173
161
|
width: 100%;
|
|
174
162
|
max-width: 980px;
|
|
175
163
|
margin: 0 auto;
|
|
176
|
-
padding:
|
|
164
|
+
padding: 8px 16px;
|
|
177
165
|
}
|
|
178
166
|
|
|
179
167
|
.session-drawer-btn {
|
|
@@ -567,10 +555,10 @@
|
|
|
567
555
|
display: flex;
|
|
568
556
|
align-items: center;
|
|
569
557
|
justify-content: space-between;
|
|
570
|
-
gap:
|
|
571
|
-
padding:
|
|
558
|
+
gap: 8px;
|
|
559
|
+
padding: 6px 16px;
|
|
572
560
|
border-bottom: 1px solid var(--border);
|
|
573
|
-
background: var(--bg-
|
|
561
|
+
background: var(--bg-primary);
|
|
574
562
|
}
|
|
575
563
|
|
|
576
564
|
.chat-stream-status {
|
|
@@ -581,11 +569,11 @@
|
|
|
581
569
|
}
|
|
582
570
|
|
|
583
571
|
.chat-stream-dot {
|
|
584
|
-
width:
|
|
585
|
-
height:
|
|
572
|
+
width: 8px;
|
|
573
|
+
height: 8px;
|
|
586
574
|
border-radius: 999px;
|
|
587
575
|
background: var(--text-hint);
|
|
588
|
-
box-shadow: 0 0 0
|
|
576
|
+
box-shadow: 0 0 0 3px rgba(218, 119, 86, 0.1);
|
|
589
577
|
}
|
|
590
578
|
|
|
591
579
|
.chat-stream-dot.thinking,
|
|
@@ -641,12 +629,16 @@
|
|
|
641
629
|
}
|
|
642
630
|
|
|
643
631
|
.chat-stream-meta {
|
|
644
|
-
display:
|
|
632
|
+
display: none;
|
|
645
633
|
flex-direction: column;
|
|
646
|
-
gap:
|
|
647
|
-
padding:
|
|
634
|
+
gap: 6px;
|
|
635
|
+
padding: 4px 16px 6px;
|
|
648
636
|
border-bottom: 1px solid var(--border);
|
|
649
|
-
background: var(--bg-
|
|
637
|
+
background: var(--bg-primary);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.chat-stream-meta.expanded {
|
|
641
|
+
display: flex;
|
|
650
642
|
}
|
|
651
643
|
|
|
652
644
|
.chat-stream-filters {
|
|
@@ -784,11 +776,14 @@
|
|
|
784
776
|
scroll-padding-bottom: 80px;
|
|
785
777
|
scrollbar-width: thin;
|
|
786
778
|
scrollbar-color: rgba(255, 255, 255, 0.08) transparent;
|
|
787
|
-
padding:
|
|
779
|
+
padding: 24px 24px 48px;
|
|
788
780
|
display: flex;
|
|
789
781
|
flex-direction: column;
|
|
790
|
-
gap:
|
|
782
|
+
gap: 6px;
|
|
791
783
|
align-items: stretch;
|
|
784
|
+
max-width: 860px;
|
|
785
|
+
margin: 0 auto;
|
|
786
|
+
width: 100%;
|
|
792
787
|
}
|
|
793
788
|
|
|
794
789
|
.chat-messages::-webkit-scrollbar {
|
|
@@ -862,16 +857,16 @@
|
|
|
862
857
|
/* ─── Bubbles ─── */
|
|
863
858
|
.chat-bubble {
|
|
864
859
|
width: 100%;
|
|
865
|
-
max-width:
|
|
866
|
-
padding:
|
|
867
|
-
border-radius:
|
|
868
|
-
font-size:
|
|
869
|
-
line-height: 1.
|
|
860
|
+
max-width: 100%;
|
|
861
|
+
padding: 12px 16px;
|
|
862
|
+
border-radius: 18px;
|
|
863
|
+
font-size: 14px;
|
|
864
|
+
line-height: 1.6;
|
|
870
865
|
word-wrap: break-word;
|
|
871
|
-
border:
|
|
866
|
+
border: none;
|
|
872
867
|
background: var(--bg-card);
|
|
873
868
|
align-self: flex-start;
|
|
874
|
-
box-shadow:
|
|
869
|
+
box-shadow: none;
|
|
875
870
|
/* CSS containment — tells the browser each bubble is layout-independent,
|
|
876
871
|
reducing style recalc and layout thrashing on long message lists */
|
|
877
872
|
content-visibility: auto;
|
|
@@ -882,17 +877,20 @@
|
|
|
882
877
|
.chat-bubble.user {
|
|
883
878
|
align-self: flex-end;
|
|
884
879
|
width: auto;
|
|
885
|
-
max-width: min(560px
|
|
886
|
-
background: rgba(218, 119, 86, 0.
|
|
887
|
-
border
|
|
880
|
+
max-width: min(75%, 560px);
|
|
881
|
+
background: rgba(218, 119, 86, 0.14);
|
|
882
|
+
border: none;
|
|
888
883
|
color: var(--text-primary);
|
|
884
|
+
border-radius: 18px 18px 4px 18px;
|
|
889
885
|
}
|
|
890
886
|
|
|
891
887
|
.chat-bubble.assistant {
|
|
892
888
|
align-self: flex-start;
|
|
893
|
-
background:
|
|
889
|
+
background: rgba(255, 255, 255, 0.03);
|
|
894
890
|
color: var(--text-primary);
|
|
895
|
-
border
|
|
891
|
+
border: none;
|
|
892
|
+
border-radius: 18px 18px 18px 4px;
|
|
893
|
+
padding: 12px 4px;
|
|
896
894
|
}
|
|
897
895
|
|
|
898
896
|
.chat-bubble.system {
|
|
@@ -901,21 +899,25 @@
|
|
|
901
899
|
background: transparent;
|
|
902
900
|
padding: 6px 12px;
|
|
903
901
|
box-shadow: none;
|
|
902
|
+
border: none;
|
|
904
903
|
}
|
|
905
904
|
|
|
906
905
|
.chat-bubble.tool {
|
|
907
906
|
align-self: center;
|
|
908
|
-
background: rgba(218, 119, 86, 0.
|
|
909
|
-
border: 1px solid rgba(218, 119, 86, 0.
|
|
907
|
+
background: rgba(218, 119, 86, 0.06);
|
|
908
|
+
border: 1px solid rgba(218, 119, 86, 0.15);
|
|
910
909
|
color: var(--text-primary);
|
|
911
910
|
font-family: var(--font-mono);
|
|
911
|
+
border-radius: 12px;
|
|
912
|
+
font-size: 12px;
|
|
912
913
|
}
|
|
913
914
|
|
|
914
915
|
.chat-bubble.error {
|
|
915
916
|
align-self: center;
|
|
916
|
-
background: rgba(229, 83, 75, 0.
|
|
917
|
-
border: 1px solid rgba(229, 83, 75, 0.
|
|
917
|
+
background: rgba(229, 83, 75, 0.08);
|
|
918
|
+
border: 1px solid rgba(229, 83, 75, 0.25);
|
|
918
919
|
color: var(--text-primary);
|
|
920
|
+
border-radius: 12px;
|
|
919
921
|
}
|
|
920
922
|
|
|
921
923
|
.chat-system-text {
|
|
@@ -926,6 +928,7 @@
|
|
|
926
928
|
}
|
|
927
929
|
|
|
928
930
|
.chat-bubble-label {
|
|
931
|
+
display: none;
|
|
929
932
|
font-size: 10px;
|
|
930
933
|
font-weight: 600;
|
|
931
934
|
text-transform: uppercase;
|
|
@@ -934,14 +937,19 @@
|
|
|
934
937
|
margin-bottom: 4px;
|
|
935
938
|
}
|
|
936
939
|
|
|
940
|
+
.chat-bubble.tool .chat-bubble-label,
|
|
941
|
+
.chat-bubble.error .chat-bubble-label {
|
|
942
|
+
display: block;
|
|
943
|
+
}
|
|
944
|
+
|
|
937
945
|
.chat-bubble-content {
|
|
938
946
|
white-space: pre-wrap;
|
|
939
947
|
}
|
|
940
948
|
|
|
941
949
|
.chat-bubble-time {
|
|
942
950
|
font-size: 10px;
|
|
943
|
-
opacity: 0.
|
|
944
|
-
margin-top:
|
|
951
|
+
opacity: 0.4;
|
|
952
|
+
margin-top: 6px;
|
|
945
953
|
}
|
|
946
954
|
|
|
947
955
|
/* Subtle entrance for bubbles — only the last few get the animation
|
|
@@ -1908,3 +1916,30 @@ ul.md-list li::before {
|
|
|
1908
1916
|
max-width: min(840px, 100%);
|
|
1909
1917
|
}
|
|
1910
1918
|
}
|
|
1919
|
+
|
|
1920
|
+
/* ── Focus mode exit FAB ── */
|
|
1921
|
+
.focus-exit-fab {
|
|
1922
|
+
position: fixed;
|
|
1923
|
+
top: 16px;
|
|
1924
|
+
right: 16px;
|
|
1925
|
+
z-index: 10001;
|
|
1926
|
+
width: 40px;
|
|
1927
|
+
height: 40px;
|
|
1928
|
+
border-radius: 50%;
|
|
1929
|
+
border: none;
|
|
1930
|
+
background: #ef4444;
|
|
1931
|
+
color: #fff;
|
|
1932
|
+
font-size: 18px;
|
|
1933
|
+
font-weight: 700;
|
|
1934
|
+
cursor: pointer;
|
|
1935
|
+
display: flex;
|
|
1936
|
+
align-items: center;
|
|
1937
|
+
justify-content: center;
|
|
1938
|
+
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.4);
|
|
1939
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
1940
|
+
-webkit-tap-highlight-color: transparent;
|
|
1941
|
+
}
|
|
1942
|
+
.focus-exit-fab:hover {
|
|
1943
|
+
transform: scale(1.1);
|
|
1944
|
+
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.5);
|
|
1945
|
+
}
|
package/ui/tabs/agents.js
CHANGED
package/ui/tabs/chat.js
CHANGED
|
@@ -691,9 +691,11 @@ export function ChatTab() {
|
|
|
691
691
|
html`
|
|
692
692
|
<div class="chat-shell-header">
|
|
693
693
|
<div class="chat-shell-inner">
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
694
|
+
${isMobile && html`
|
|
695
|
+
<button class="session-drawer-btn" onClick=${handleBack}>
|
|
696
|
+
☰ Sessions
|
|
697
|
+
</button>
|
|
698
|
+
`}
|
|
697
699
|
<div class="chat-shell-title">
|
|
698
700
|
<div class="chat-shell-name">${sessionTitle}</div>
|
|
699
701
|
<div class="chat-shell-meta">${sessionMeta || "Session"}</div>
|
|
@@ -784,6 +786,13 @@ export function ChatTab() {
|
|
|
784
786
|
</div>
|
|
785
787
|
</div>
|
|
786
788
|
</div>
|
|
789
|
+
${focusMode && html`
|
|
790
|
+
<button
|
|
791
|
+
class="focus-exit-fab"
|
|
792
|
+
onClick=${() => setFocusMode(false)}
|
|
793
|
+
title="Exit focus mode"
|
|
794
|
+
>✕</button>
|
|
795
|
+
`}
|
|
787
796
|
${isMobile &&
|
|
788
797
|
html`
|
|
789
798
|
<div
|
package/ui/tabs/settings.js
CHANGED
|
@@ -90,26 +90,44 @@ const SETTINGS_STYLES = `
|
|
|
90
90
|
/* Floating save bar */
|
|
91
91
|
.settings-save-bar {
|
|
92
92
|
position: fixed;
|
|
93
|
-
bottom: calc(var(--nav-height) + var(--safe-bottom) +
|
|
94
|
-
left:
|
|
95
|
-
|
|
93
|
+
bottom: calc(var(--nav-height, 60px) + var(--safe-bottom, 0px) + 12px);
|
|
94
|
+
left: 50%;
|
|
95
|
+
transform: translateX(-50%);
|
|
96
|
+
z-index: 999;
|
|
96
97
|
display: flex;
|
|
97
98
|
align-items: center;
|
|
98
99
|
justify-content: space-between;
|
|
99
100
|
gap: 12px;
|
|
100
101
|
flex-wrap: wrap;
|
|
101
102
|
row-gap: 8px;
|
|
102
|
-
padding:
|
|
103
|
-
|
|
103
|
+
padding: 10px 16px;
|
|
104
|
+
min-width: 240px;
|
|
105
|
+
max-width: 480px;
|
|
106
|
+
width: auto;
|
|
104
107
|
background: var(--glass-bg, rgba(30,30,46,0.95));
|
|
105
108
|
backdrop-filter: blur(20px);
|
|
106
109
|
-webkit-backdrop-filter: blur(20px);
|
|
107
|
-
border
|
|
110
|
+
border: 1px solid var(--border, rgba(255,255,255,0.08));
|
|
111
|
+
border-radius: 12px;
|
|
112
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
|
113
|
+
transition: all 0.2s ease;
|
|
108
114
|
animation: slideUp 0.25s ease;
|
|
109
115
|
}
|
|
116
|
+
.settings-save-bar--dirty {
|
|
117
|
+
border-color: var(--accent, #5b6eae);
|
|
118
|
+
box-shadow: 0 4px 20px rgba(91, 110, 174, 0.25);
|
|
119
|
+
}
|
|
120
|
+
.settings-save-bar--clean .save-bar-info {
|
|
121
|
+
color: var(--text-hint, #666);
|
|
122
|
+
font-size: 12px;
|
|
123
|
+
}
|
|
124
|
+
.setting-modified-dot--clean {
|
|
125
|
+
background: var(--text-hint, #666) !important;
|
|
126
|
+
opacity: 0.4;
|
|
127
|
+
}
|
|
110
128
|
@keyframes slideUp {
|
|
111
|
-
from { transform:
|
|
112
|
-
to { transform: translateY(0); opacity: 1; }
|
|
129
|
+
from { transform: translateX(-50%) translateY(20px); opacity: 0; }
|
|
130
|
+
to { transform: translateX(-50%) translateY(0); opacity: 1; }
|
|
113
131
|
}
|
|
114
132
|
.settings-save-bar .save-bar-info {
|
|
115
133
|
display: flex;
|
|
@@ -129,7 +147,7 @@ const SETTINGS_STYLES = `
|
|
|
129
147
|
}
|
|
130
148
|
@media (min-width: 1200px) {
|
|
131
149
|
.settings-save-bar {
|
|
132
|
-
bottom:
|
|
150
|
+
bottom: 16px;
|
|
133
151
|
}
|
|
134
152
|
}
|
|
135
153
|
/* Individual setting row */
|
|
@@ -359,6 +377,7 @@ const SETTINGS_STYLES = `
|
|
|
359
377
|
margin-right: auto;
|
|
360
378
|
width: 100%;
|
|
361
379
|
box-sizing: border-box;
|
|
380
|
+
padding-bottom: 80px;
|
|
362
381
|
}
|
|
363
382
|
|
|
364
383
|
body.settings-save-open .main-content {
|
|
@@ -1069,15 +1088,14 @@ function ServerConfigMode() {
|
|
|
1069
1088
|
<//>
|
|
1070
1089
|
`}
|
|
1071
1090
|
|
|
1072
|
-
<!-- Floating save bar -->
|
|
1073
|
-
${changeCount > 0
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
<
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
<div class="save-bar-actions">
|
|
1091
|
+
<!-- Floating save bar - always visible -->
|
|
1092
|
+
<div class=${`settings-save-bar ${changeCount > 0 ? 'settings-save-bar--dirty' : 'settings-save-bar--clean'}`}>
|
|
1093
|
+
<div class="save-bar-info">
|
|
1094
|
+
<span class=${`setting-modified-dot ${changeCount === 0 ? 'setting-modified-dot--clean' : ''}`}></span>
|
|
1095
|
+
<span>${changeCount > 0 ? `${changeCount} unsaved change${changeCount !== 1 ? "s" : ""}` : "All changes saved"}</span>
|
|
1096
|
+
</div>
|
|
1097
|
+
<div class="save-bar-actions">
|
|
1098
|
+
${changeCount > 0 && html`
|
|
1081
1099
|
<button class="btn btn-ghost btn-sm" onClick=${handleDiscard}>
|
|
1082
1100
|
Discard
|
|
1083
1101
|
</button>
|
|
@@ -1088,9 +1106,9 @@ function ServerConfigMode() {
|
|
|
1088
1106
|
>
|
|
1089
1107
|
${saving ? html`<${Spinner} size=${14} /> Saving…` : "Save Changes"}
|
|
1090
1108
|
</button>
|
|
1091
|
-
|
|
1109
|
+
`}
|
|
1092
1110
|
</div>
|
|
1093
|
-
|
|
1111
|
+
</div>
|
|
1094
1112
|
|
|
1095
1113
|
<!-- Confirm dialog with diff -->
|
|
1096
1114
|
${confirmOpen &&
|
|
@@ -1176,33 +1194,38 @@ function AppPreferencesMode() {
|
|
|
1176
1194
|
/* Load prefs from CloudStorage on mount */
|
|
1177
1195
|
useEffect(() => {
|
|
1178
1196
|
(async () => {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
+
try {
|
|
1198
|
+
const [fs, ct, nu, ne, nc, dm, dmp, ds, dr] = await Promise.all([
|
|
1199
|
+
cloudGet("fontSize"),
|
|
1200
|
+
cloudGet("colorTheme"),
|
|
1201
|
+
cloudGet("notifyUpdates"),
|
|
1202
|
+
cloudGet("notifyErrors"),
|
|
1203
|
+
cloudGet("notifyComplete"),
|
|
1204
|
+
cloudGet("debugMode"),
|
|
1205
|
+
cloudGet("defaultMaxParallel"),
|
|
1206
|
+
cloudGet("defaultSdk"),
|
|
1207
|
+
cloudGet("defaultRegion"),
|
|
1208
|
+
]);
|
|
1209
|
+
if (fs) {
|
|
1210
|
+
setFontSize(fs);
|
|
1211
|
+
applyFontSize(fs);
|
|
1212
|
+
}
|
|
1213
|
+
if (ct) {
|
|
1214
|
+
setColorTheme(ct);
|
|
1215
|
+
applyColorTheme(ct);
|
|
1216
|
+
}
|
|
1217
|
+
if (nu != null) setNotifyUpdates(nu);
|
|
1218
|
+
if (ne != null) setNotifyErrors(ne);
|
|
1219
|
+
if (nc != null) setNotifyComplete(nc);
|
|
1220
|
+
if (dm != null) setDebugMode(dm);
|
|
1221
|
+
if (dmp != null) setDefaultMaxParallel(dmp);
|
|
1222
|
+
if (ds) setDefaultSdk(ds);
|
|
1223
|
+
if (dr) setDefaultRegion(dr);
|
|
1224
|
+
} catch (err) {
|
|
1225
|
+
console.warn('[AppPrefs] Failed to load preferences:', err);
|
|
1226
|
+
} finally {
|
|
1227
|
+
setLoaded(true);
|
|
1197
1228
|
}
|
|
1198
|
-
if (nu != null) setNotifyUpdates(nu);
|
|
1199
|
-
if (ne != null) setNotifyErrors(ne);
|
|
1200
|
-
if (nc != null) setNotifyComplete(nc);
|
|
1201
|
-
if (dm != null) setDebugMode(dm);
|
|
1202
|
-
if (dmp != null) setDefaultMaxParallel(dmp);
|
|
1203
|
-
if (ds) setDefaultSdk(ds);
|
|
1204
|
-
if (dr) setDefaultRegion(dr);
|
|
1205
|
-
setLoaded(true);
|
|
1206
1229
|
})();
|
|
1207
1230
|
}, []);
|
|
1208
1231
|
|
|
@@ -1211,6 +1234,7 @@ function AppPreferencesMode() {
|
|
|
1211
1234
|
const next = !getter;
|
|
1212
1235
|
setter(next);
|
|
1213
1236
|
cloudSet(key, next);
|
|
1237
|
+
console.log('[AppPrefs] Saved:', key, next);
|
|
1214
1238
|
haptic();
|
|
1215
1239
|
showToast("Preference saved", "success");
|
|
1216
1240
|
}, []);
|
|
@@ -1218,6 +1242,7 @@ function AppPreferencesMode() {
|
|
|
1218
1242
|
const handleFontSize = (v) => {
|
|
1219
1243
|
setFontSize(v);
|
|
1220
1244
|
cloudSet("fontSize", v);
|
|
1245
|
+
console.log('[AppPrefs] Saved: fontSize', v);
|
|
1221
1246
|
haptic();
|
|
1222
1247
|
applyFontSize(v);
|
|
1223
1248
|
showToast("Font size saved", "success");
|
|
@@ -1226,6 +1251,7 @@ function AppPreferencesMode() {
|
|
|
1226
1251
|
const handleColorTheme = (v) => {
|
|
1227
1252
|
setColorTheme(v);
|
|
1228
1253
|
cloudSet("colorTheme", v);
|
|
1254
|
+
console.log('[AppPrefs] Saved: colorTheme', v);
|
|
1229
1255
|
haptic();
|
|
1230
1256
|
applyColorTheme(v);
|
|
1231
1257
|
showToast("Theme saved", "success");
|
|
@@ -1235,6 +1261,7 @@ function AppPreferencesMode() {
|
|
|
1235
1261
|
const val = Math.max(1, Math.min(20, Number(v)));
|
|
1236
1262
|
setDefaultMaxParallel(val);
|
|
1237
1263
|
cloudSet("defaultMaxParallel", val);
|
|
1264
|
+
console.log('[AppPrefs] Saved: defaultMaxParallel', val);
|
|
1238
1265
|
haptic();
|
|
1239
1266
|
showToast("Preference saved", "success");
|
|
1240
1267
|
};
|
|
@@ -1242,6 +1269,7 @@ function AppPreferencesMode() {
|
|
|
1242
1269
|
const handleDefaultSdk = (v) => {
|
|
1243
1270
|
setDefaultSdk(v);
|
|
1244
1271
|
cloudSet("defaultSdk", v);
|
|
1272
|
+
console.log('[AppPrefs] Saved: defaultSdk', v);
|
|
1245
1273
|
haptic();
|
|
1246
1274
|
showToast("Preference saved", "success");
|
|
1247
1275
|
};
|
|
@@ -1249,6 +1277,7 @@ function AppPreferencesMode() {
|
|
|
1249
1277
|
const handleDefaultRegion = (v) => {
|
|
1250
1278
|
setDefaultRegion(v);
|
|
1251
1279
|
cloudSet("defaultRegion", v);
|
|
1280
|
+
console.log('[AppPrefs] Saved: defaultRegion', v);
|
|
1252
1281
|
haptic();
|
|
1253
1282
|
showToast("Preference saved", "success");
|
|
1254
1283
|
};
|
|
@@ -1312,7 +1341,7 @@ function AppPreferencesMode() {
|
|
|
1312
1341
|
: "";
|
|
1313
1342
|
|
|
1314
1343
|
return html`
|
|
1315
|
-
|
|
1344
|
+
|
|
1316
1345
|
|
|
1317
1346
|
<!-- ─── Account ─── -->
|
|
1318
1347
|
<${Collapsible} title="👤 Account" defaultOpen=${true}>
|
package/ui/tabs/tasks.js
CHANGED
|
@@ -214,6 +214,27 @@ export function StartTaskModal({
|
|
|
214
214
|
}, [task?.id, task?.meta?.codex?.isIgnored, task?.meta?.labels]);
|
|
215
215
|
|
|
216
216
|
const canModel = sdk && sdk !== "auto";
|
|
217
|
+
|
|
218
|
+
const EXECUTOR_MODELS = {
|
|
219
|
+
codex: [
|
|
220
|
+
"gpt-5.3-codex", "gpt-5.2-codex", "gpt-5.1-codex",
|
|
221
|
+
"gpt-5.1-codex-mini", "gpt-5.1-codex-max",
|
|
222
|
+
"gpt-5.2", "gpt-5.1", "gpt-5-mini",
|
|
223
|
+
],
|
|
224
|
+
copilot: [
|
|
225
|
+
"claude-opus-4.6", "claude-sonnet-4.6", "claude-opus-4.5", "claude-sonnet-4.5",
|
|
226
|
+
"claude-sonnet-4", "claude-haiku-4.5",
|
|
227
|
+
"gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.1-codex", "gpt-5.1-codex-mini",
|
|
228
|
+
"gpt-5.2", "gpt-5.1", "gpt-5-mini",
|
|
229
|
+
"gemini-3.1-pro", "gemini-3-pro", "gemini-3-flash", "gemini-2.5-pro",
|
|
230
|
+
"grok-code-fast-1",
|
|
231
|
+
],
|
|
232
|
+
claude: [
|
|
233
|
+
"claude-opus-4.6", "claude-sonnet-4.6", "claude-opus-4.5",
|
|
234
|
+
"claude-sonnet-4.5", "claude-sonnet-4", "claude-haiku-4.5",
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
|
|
217
238
|
const resolvedTaskId = (task?.id || taskIdInput || "").trim();
|
|
218
239
|
|
|
219
240
|
const handleStart = async () => {
|
|
@@ -272,13 +293,10 @@ export function StartTaskModal({
|
|
|
272
293
|
</div>
|
|
273
294
|
<div class="modal-form-field">
|
|
274
295
|
<div class="card-subtitle">Model Override (optional)</div>
|
|
275
|
-
<input
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
disabled=${!canModel}
|
|
280
|
-
onInput=${(e) => setModel(e.target.value)}
|
|
281
|
-
/>
|
|
296
|
+
<select class="input" value=${model} disabled=${!canModel} onChange=${(e) => setModel(e.target.value)}>
|
|
297
|
+
<option value="">Auto (default)</option>
|
|
298
|
+
${canModel && (EXECUTOR_MODELS[sdk] || []).map(m => html`<option value=${m}>${m}</option>`)}
|
|
299
|
+
</select>
|
|
282
300
|
</div>
|
|
283
301
|
<div class="modal-form-field modal-form-span">
|
|
284
302
|
<button
|
package/ui-server.mjs
CHANGED
|
@@ -110,7 +110,7 @@ let _configValidator = null;
|
|
|
110
110
|
function resolveConfigPath() {
|
|
111
111
|
return process.env.BOSUN_CONFIG_PATH
|
|
112
112
|
? resolve(process.env.BOSUN_CONFIG_PATH)
|
|
113
|
-
: resolve(
|
|
113
|
+
: resolve(resolveUiConfigDir(), "bosun.config.json");
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
function getConfigSchema() {
|
|
@@ -546,6 +546,8 @@ let uiServer = null;
|
|
|
546
546
|
let uiServerUrl = null;
|
|
547
547
|
let uiServerTls = false;
|
|
548
548
|
let wsServer = null;
|
|
549
|
+
/** Auto-open browser: only once per process, never during tests */
|
|
550
|
+
let _browserOpened = false;
|
|
549
551
|
const wsClients = new Set();
|
|
550
552
|
let sessionListenerAttached = false;
|
|
551
553
|
/** @type {ReturnType<typeof setInterval>|null} */
|
|
@@ -660,7 +662,7 @@ const SETTINGS_KNOWN_SET = new Set(SETTINGS_KNOWN_KEYS);
|
|
|
660
662
|
let _settingsLastUpdateTime = 0;
|
|
661
663
|
|
|
662
664
|
function updateEnvFile(changes) {
|
|
663
|
-
const envPath = resolve(
|
|
665
|
+
const envPath = resolve(resolveUiConfigDir(), '.env');
|
|
664
666
|
let content = '';
|
|
665
667
|
try { content = readFileSync(envPath, 'utf8'); } catch { content = ''; }
|
|
666
668
|
|
|
@@ -4339,7 +4341,7 @@ async function handleApi(req, res, url) {
|
|
|
4339
4341
|
data[key] = val || "";
|
|
4340
4342
|
}
|
|
4341
4343
|
}
|
|
4342
|
-
const envPath = resolve(
|
|
4344
|
+
const envPath = resolve(resolveUiConfigDir(), ".env");
|
|
4343
4345
|
const configPath = resolveConfigPath();
|
|
4344
4346
|
const configExists = existsSync(configPath);
|
|
4345
4347
|
const configSchema = getConfigSchema();
|
|
@@ -5468,6 +5470,32 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
5468
5470
|
console.log(`[telegram-ui] LAN access: ${protocol}://${lanIp}:${actualPort}`);
|
|
5469
5471
|
console.log(`[telegram-ui] Browser access: ${protocol}://${lanIp}:${actualPort}/?token=${sessionToken}`);
|
|
5470
5472
|
|
|
5473
|
+
// Auto-open browser:
|
|
5474
|
+
// - skip in desktop/Electron mode (BOSUN_DESKTOP=1)
|
|
5475
|
+
// - skip when caller passes skipAutoOpen
|
|
5476
|
+
// - skip during Vitest / Jest test runs (avoids opening 20+ tabs during `npm test`)
|
|
5477
|
+
// - only open ONCE per process (singleton guard — prevents loops on server restart)
|
|
5478
|
+
const isTestRun = process.env.VITEST || process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID;
|
|
5479
|
+
if (
|
|
5480
|
+
process.env.BOSUN_DESKTOP !== "1" &&
|
|
5481
|
+
!options.skipAutoOpen &&
|
|
5482
|
+
!_browserOpened &&
|
|
5483
|
+
!isTestRun
|
|
5484
|
+
) {
|
|
5485
|
+
_browserOpened = true;
|
|
5486
|
+
const openUrl = `${protocol}://${lanIp}:${actualPort}/?token=${sessionToken}`;
|
|
5487
|
+
try {
|
|
5488
|
+
const { exec } = await import("node:child_process");
|
|
5489
|
+
if (process.platform === "win32") {
|
|
5490
|
+
exec(`start "" "${openUrl}"`);
|
|
5491
|
+
} else if (process.platform === "darwin") {
|
|
5492
|
+
exec(`open "${openUrl}"`);
|
|
5493
|
+
} else {
|
|
5494
|
+
exec(`xdg-open "${openUrl}"`);
|
|
5495
|
+
}
|
|
5496
|
+
} catch { /* ignore auto-open failure */ }
|
|
5497
|
+
}
|
|
5498
|
+
|
|
5471
5499
|
// Check firewall rules for the UI port
|
|
5472
5500
|
firewallState = await checkFirewall(actualPort);
|
|
5473
5501
|
if (firewallState) {
|