bosun 0.30.0 → 0.31.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/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/setup.html +13 -7
- package/ui/styles/components.css +16 -1
- package/ui/styles/sessions.css +79 -44
- package/ui/tabs/chat.js +12 -3
- package/ui/tabs/tasks.js +25 -7
- package/ui-server.mjs +18 -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.0",
|
|
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/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/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/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() {
|
|
@@ -660,7 +660,7 @@ const SETTINGS_KNOWN_SET = new Set(SETTINGS_KNOWN_KEYS);
|
|
|
660
660
|
let _settingsLastUpdateTime = 0;
|
|
661
661
|
|
|
662
662
|
function updateEnvFile(changes) {
|
|
663
|
-
const envPath = resolve(
|
|
663
|
+
const envPath = resolve(resolveUiConfigDir(), '.env');
|
|
664
664
|
let content = '';
|
|
665
665
|
try { content = readFileSync(envPath, 'utf8'); } catch { content = ''; }
|
|
666
666
|
|
|
@@ -4339,7 +4339,7 @@ async function handleApi(req, res, url) {
|
|
|
4339
4339
|
data[key] = val || "";
|
|
4340
4340
|
}
|
|
4341
4341
|
}
|
|
4342
|
-
const envPath = resolve(
|
|
4342
|
+
const envPath = resolve(resolveUiConfigDir(), ".env");
|
|
4343
4343
|
const configPath = resolveConfigPath();
|
|
4344
4344
|
const configExists = existsSync(configPath);
|
|
4345
4345
|
const configSchema = getConfigSchema();
|
|
@@ -5468,6 +5468,21 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
5468
5468
|
console.log(`[telegram-ui] LAN access: ${protocol}://${lanIp}:${actualPort}`);
|
|
5469
5469
|
console.log(`[telegram-ui] Browser access: ${protocol}://${lanIp}:${actualPort}/?token=${sessionToken}`);
|
|
5470
5470
|
|
|
5471
|
+
// Auto-open browser (skip in desktop/embedded mode)
|
|
5472
|
+
if (process.env.BOSUN_DESKTOP !== "1" && !options.skipAutoOpen) {
|
|
5473
|
+
const openUrl = `${protocol}://${lanIp}:${actualPort}/?token=${sessionToken}`;
|
|
5474
|
+
try {
|
|
5475
|
+
const { exec } = await import("node:child_process");
|
|
5476
|
+
if (process.platform === "win32") {
|
|
5477
|
+
exec(`start "" "${openUrl}"`);
|
|
5478
|
+
} else if (process.platform === "darwin") {
|
|
5479
|
+
exec(`open "${openUrl}"`);
|
|
5480
|
+
} else {
|
|
5481
|
+
exec(`xdg-open "${openUrl}"`);
|
|
5482
|
+
}
|
|
5483
|
+
} catch { /* ignore auto-open failure */ }
|
|
5484
|
+
}
|
|
5485
|
+
|
|
5471
5486
|
// Check firewall rules for the UI port
|
|
5472
5487
|
firewallState = await checkFirewall(actualPort);
|
|
5473
5488
|
if (firewallState) {
|