bosun 0.29.4 → 0.29.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/cli.mjs +10 -2
- package/codex-model-profiles.mjs +34 -0
- package/config.mjs +26 -10
- package/desktop/launch.mjs +2 -0
- package/desktop/main.mjs +2 -0
- package/github-reconciler.mjs +7 -0
- package/monitor.mjs +5 -1
- package/package.json +1 -1
- package/task-executor.mjs +1 -1
- package/ui/components/workspace-switcher.js +43 -68
- package/ui/index.html +1 -0
- package/ui/styles/components.css +1 -14
- package/ui/styles/sessions.css +14 -4
- package/ui/styles/variables.css +5 -5
- package/ui/styles/workspace-switcher.css +35 -0
- package/ui/tabs/settings.js +33 -17
package/README.md
CHANGED
package/cli.mjs
CHANGED
|
@@ -721,15 +721,23 @@ async function main() {
|
|
|
721
721
|
);
|
|
722
722
|
}
|
|
723
723
|
|
|
724
|
+
// Auto-start sentinel in daemon mode when Telegram credentials are available
|
|
725
|
+
const hasTelegramCreds = !!(
|
|
726
|
+
(process.env.TELEGRAM_BOT_TOKEN || readEnvCredentials().TELEGRAM_BOT_TOKEN) &&
|
|
727
|
+
(process.env.TELEGRAM_CHAT_ID || readEnvCredentials().TELEGRAM_CHAT_ID)
|
|
728
|
+
);
|
|
724
729
|
const sentinelRequested =
|
|
725
730
|
args.includes("--sentinel") ||
|
|
726
|
-
parseBoolEnv(process.env.BOSUN_SENTINEL_AUTO_START, false)
|
|
731
|
+
parseBoolEnv(process.env.BOSUN_SENTINEL_AUTO_START, false) ||
|
|
732
|
+
(IS_DAEMON_CHILD && hasTelegramCreds);
|
|
727
733
|
if (sentinelRequested) {
|
|
728
734
|
const sentinel = await ensureSentinelRunning({ quiet: false });
|
|
729
735
|
if (!sentinel.ok) {
|
|
730
736
|
const mode = args.includes("--sentinel")
|
|
731
737
|
? "requested by --sentinel"
|
|
732
|
-
:
|
|
738
|
+
: IS_DAEMON_CHILD && hasTelegramCreds
|
|
739
|
+
? "auto-started in daemon mode (Telegram credentials detected)"
|
|
740
|
+
: "requested by BOSUN_SENTINEL_AUTO_START";
|
|
733
741
|
const strictSentinel = parseBoolEnv(
|
|
734
742
|
process.env.BOSUN_SENTINEL_STRICT,
|
|
735
743
|
false,
|
package/codex-model-profiles.mjs
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
1
5
|
const DEFAULT_ACTIVE_PROFILE = "xl";
|
|
2
6
|
const DEFAULT_SUBAGENT_PROFILE = "m";
|
|
3
7
|
|
|
@@ -63,6 +67,19 @@ function profileRecord(env, profileName, globalProvider) {
|
|
|
63
67
|
};
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
function readCodexConfigTopLevelModel() {
|
|
71
|
+
try {
|
|
72
|
+
const configPath = resolve(homedir(), ".codex", "config.toml");
|
|
73
|
+
if (!existsSync(configPath)) return "";
|
|
74
|
+
const content = readFileSync(configPath, "utf8");
|
|
75
|
+
const head = content.split(/\n\[/)[0] || "";
|
|
76
|
+
const match = head.match(/^\s*model\s*=\s*"([^"]+)"/m);
|
|
77
|
+
return match ? match[1].trim() : "";
|
|
78
|
+
} catch {
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
66
83
|
/**
|
|
67
84
|
* Resolve codex model/provider profile configuration from env vars.
|
|
68
85
|
* Applies active profile values onto runtime env keys (`CODEX_MODEL`,
|
|
@@ -86,6 +103,8 @@ export function resolveCodexProfileRuntime(envInput = process.env) {
|
|
|
86
103
|
|
|
87
104
|
const env = { ...sourceEnv };
|
|
88
105
|
|
|
106
|
+
const configModel = readCodexConfigTopLevelModel();
|
|
107
|
+
|
|
89
108
|
if (active.model) {
|
|
90
109
|
env.CODEX_MODEL = active.model;
|
|
91
110
|
}
|
|
@@ -96,6 +115,21 @@ export function resolveCodexProfileRuntime(envInput = process.env) {
|
|
|
96
115
|
const profileApiKey = active.apiKey;
|
|
97
116
|
const resolvedProvider = active.provider || globalProvider;
|
|
98
117
|
|
|
118
|
+
// Azure deployments often differ from default model names.
|
|
119
|
+
// If the env is using Azure and the model is still the default,
|
|
120
|
+
// prefer the top-level ~/.codex/config.toml model when present.
|
|
121
|
+
const activeModelExplicit =
|
|
122
|
+
Boolean(readProfileField(sourceEnv, activeProfile, "MODEL")) ||
|
|
123
|
+
Boolean(clean(sourceEnv.CODEX_MODEL));
|
|
124
|
+
if (
|
|
125
|
+
resolvedProvider === "azure" &&
|
|
126
|
+
configModel &&
|
|
127
|
+
(!activeModelExplicit || clean(env.CODEX_MODEL) === "gpt-5.3-codex")
|
|
128
|
+
) {
|
|
129
|
+
env.CODEX_MODEL = configModel;
|
|
130
|
+
active.model = configModel;
|
|
131
|
+
}
|
|
132
|
+
|
|
99
133
|
if (profileApiKey) {
|
|
100
134
|
if (resolvedProvider === "azure") {
|
|
101
135
|
env.AZURE_OPENAI_API_KEY = profileApiKey;
|
package/config.mjs
CHANGED
|
@@ -265,17 +265,33 @@ function isEnvEnabled(value, defaultValue = false) {
|
|
|
265
265
|
|
|
266
266
|
// ── Git helpers ──────────────────────────────────────────────────────────────
|
|
267
267
|
|
|
268
|
-
function detectRepoSlug() {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
268
|
+
function detectRepoSlug(repoRoot = "") {
|
|
269
|
+
const tryResolve = (cwd) => {
|
|
270
|
+
try {
|
|
271
|
+
const remote = execSync("git remote get-url origin", {
|
|
272
|
+
cwd,
|
|
273
|
+
encoding: "utf8",
|
|
274
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
275
|
+
}).trim();
|
|
276
|
+
const match = remote.match(/github\.com[/:]([^/]+\/[^/.]+)/);
|
|
277
|
+
return match ? match[1] : null;
|
|
278
|
+
} catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// First try current working directory
|
|
284
|
+
const direct = tryResolve(process.cwd());
|
|
285
|
+
if (direct) return direct;
|
|
286
|
+
|
|
287
|
+
// Fall back to detected repo root if provided (or detectable)
|
|
288
|
+
const root = repoRoot || detectRepoRoot();
|
|
289
|
+
if (root) {
|
|
290
|
+
const viaRoot = tryResolve(root);
|
|
291
|
+
if (viaRoot) return viaRoot;
|
|
278
292
|
}
|
|
293
|
+
|
|
294
|
+
return null;
|
|
279
295
|
}
|
|
280
296
|
|
|
281
297
|
function detectRepoRoot() {
|
package/desktop/launch.mjs
CHANGED
|
@@ -15,6 +15,8 @@ const chromeSandbox = resolve(
|
|
|
15
15
|
"chrome-sandbox",
|
|
16
16
|
);
|
|
17
17
|
|
|
18
|
+
process.title = "bosun-desktop-launcher";
|
|
19
|
+
|
|
18
20
|
function shouldDisableSandbox() {
|
|
19
21
|
if (process.env.BOSUN_DESKTOP_DISABLE_SANDBOX === "1") return true;
|
|
20
22
|
if (process.platform !== "linux") return false;
|
package/desktop/main.mjs
CHANGED
package/github-reconciler.mjs
CHANGED
|
@@ -216,6 +216,9 @@ export class GitHubReconciler {
|
|
|
216
216
|
if (backend !== "github") {
|
|
217
217
|
return { status: "skipped", reason: `backend=${backend || "unknown"}` };
|
|
218
218
|
}
|
|
219
|
+
if (!this.repoSlug || this.repoSlug === "unknown/unknown") {
|
|
220
|
+
return { status: "skipped", reason: "missing-repo" };
|
|
221
|
+
}
|
|
219
222
|
|
|
220
223
|
const summary = {
|
|
221
224
|
status: "ok",
|
|
@@ -350,6 +353,10 @@ export class GitHubReconciler {
|
|
|
350
353
|
start() {
|
|
351
354
|
if (this.running) return this;
|
|
352
355
|
this.running = true;
|
|
356
|
+
if (!this.repoSlug || this.repoSlug === "unknown/unknown") {
|
|
357
|
+
console.warn(`${TAG} disabled (missing repo slug)`);
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
353
360
|
console.log(
|
|
354
361
|
`${TAG} started (repo=${this.repoSlug}, interval=${this.intervalMs}ms, lookback=${this.mergedLookbackHours}h)`,
|
|
355
362
|
);
|
package/monitor.mjs
CHANGED
|
@@ -5863,7 +5863,7 @@ const dependabotMergeAttempted = new Set();
|
|
|
5863
5863
|
*/
|
|
5864
5864
|
async function checkAndMergeDependabotPRs() {
|
|
5865
5865
|
if (!dependabotAutoMerge) return;
|
|
5866
|
-
if (!repoSlug) {
|
|
5866
|
+
if (!repoSlug || repoSlug === "unknown/unknown") {
|
|
5867
5867
|
console.warn("[dependabot] auto-merge disabled — no repo slug configured");
|
|
5868
5868
|
return;
|
|
5869
5869
|
}
|
|
@@ -12035,6 +12035,10 @@ function restartGitHubReconciler() {
|
|
|
12035
12035
|
: "") ||
|
|
12036
12036
|
repoSlug ||
|
|
12037
12037
|
"unknown/unknown";
|
|
12038
|
+
if (!repo || repo === "unknown/unknown") {
|
|
12039
|
+
console.warn("[gh-reconciler] disabled — missing repo slug");
|
|
12040
|
+
return;
|
|
12041
|
+
}
|
|
12038
12042
|
|
|
12039
12043
|
ghReconciler = startGitHubReconciler({
|
|
12040
12044
|
repoSlug: repo,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.6",
|
|
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/task-executor.mjs
CHANGED
|
@@ -3168,7 +3168,7 @@ class TaskExecutor {
|
|
|
3168
3168
|
tasks = tasks.filter((t) => t.status === "todo");
|
|
3169
3169
|
if (tasks.length !== before) {
|
|
3170
3170
|
console.debug(
|
|
3171
|
-
`${TAG} filtered ${before - tasks.length} non-todo tasks (
|
|
3171
|
+
`${TAG} filtered ${before - tasks.length} non-todo tasks (API returned ${before}, kept ${tasks.length})`,
|
|
3172
3172
|
);
|
|
3173
3173
|
}
|
|
3174
3174
|
}
|
|
@@ -448,87 +448,62 @@ export function WorkspaceManager({ open, onClose }) {
|
|
|
448
448
|
`;
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
-
// ─── Main component:
|
|
451
|
+
// ─── Main component: native <select> dropdown + manage trigger ─────
|
|
452
452
|
export function WorkspaceSwitcher() {
|
|
453
|
-
const [open, setOpen] = useState(false);
|
|
454
453
|
const [managerOpen, setManagerOpen] = useState(false);
|
|
455
454
|
|
|
456
455
|
useEffect(() => {
|
|
457
456
|
loadWorkspaces();
|
|
458
457
|
}, []);
|
|
459
458
|
|
|
460
|
-
// Close dropdown on outside click
|
|
461
|
-
useEffect(() => {
|
|
462
|
-
if (!open) return;
|
|
463
|
-
const handler = (e) => {
|
|
464
|
-
if (!e.target.closest?.(".ws-switcher")) setOpen(false);
|
|
465
|
-
};
|
|
466
|
-
document.addEventListener("click", handler, true);
|
|
467
|
-
return () => document.removeEventListener("click", handler, true);
|
|
468
|
-
}, [open]);
|
|
469
|
-
|
|
470
|
-
const activeWs = workspaces.value.find((ws) => ws.id === activeWorkspaceId.value);
|
|
471
459
|
const wsList = workspaces.value;
|
|
472
|
-
const
|
|
460
|
+
const currentId = activeWorkspaceId.value;
|
|
461
|
+
|
|
462
|
+
const handleChange = async (e) => {
|
|
463
|
+
const wsId = e.target.value;
|
|
464
|
+
if (wsId === "__manage__") {
|
|
465
|
+
e.target.value = currentId || "";
|
|
466
|
+
haptic("medium");
|
|
467
|
+
setManagerOpen(true);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (!wsId || wsId === currentId) return;
|
|
471
|
+
haptic("light");
|
|
472
|
+
await switchWorkspace(wsId);
|
|
473
|
+
};
|
|
473
474
|
|
|
474
|
-
|
|
475
|
+
if (!wsList.length && !workspacesLoading.value) {
|
|
476
|
+
return html`
|
|
477
|
+
<div class="ws-switcher">
|
|
478
|
+
<button
|
|
479
|
+
class="ws-switcher-btn ws-switcher-btn-empty"
|
|
480
|
+
onClick=${() => { haptic("medium"); setManagerOpen(true); }}
|
|
481
|
+
title="Set up a workspace"
|
|
482
|
+
>
|
|
483
|
+
<span class="ws-switcher-icon">⬡</span>
|
|
484
|
+
<span class="ws-switcher-name">Set up workspace</span>
|
|
485
|
+
</button>
|
|
486
|
+
<${WorkspaceManager}
|
|
487
|
+
open=${managerOpen}
|
|
488
|
+
onClose=${() => setManagerOpen(false)}
|
|
489
|
+
/>
|
|
490
|
+
</div>
|
|
491
|
+
`;
|
|
492
|
+
}
|
|
475
493
|
|
|
476
494
|
return html`
|
|
477
495
|
<div class="ws-switcher">
|
|
478
|
-
<
|
|
479
|
-
class="ws-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
haptic("light");
|
|
483
|
-
if (!wsList.length) {
|
|
484
|
-
setManagerOpen(true);
|
|
485
|
-
} else {
|
|
486
|
-
setOpen(!open);
|
|
487
|
-
}
|
|
488
|
-
}}
|
|
489
|
-
title=${wsList.length ? "Switch workspace" : "Set up a workspace"}
|
|
496
|
+
<select
|
|
497
|
+
class="ws-native-select"
|
|
498
|
+
value=${currentId || ""}
|
|
499
|
+
onChange=${handleChange}
|
|
490
500
|
>
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
</
|
|
497
|
-
|
|
498
|
-
${open && html`
|
|
499
|
-
<div class="ws-switcher-dropdown">
|
|
500
|
-
<div class="ws-switcher-header">Workspaces</div>
|
|
501
|
-
${wsList.map((ws) => html`
|
|
502
|
-
<button
|
|
503
|
-
key=${ws.id}
|
|
504
|
-
class="ws-switcher-item ${ws.id === activeWorkspaceId.value ? "active" : ""}"
|
|
505
|
-
onClick=${() => { haptic("light"); switchWorkspace(ws.id); setOpen(false); }}
|
|
506
|
-
>
|
|
507
|
-
<div class="ws-switcher-item-main">
|
|
508
|
-
<span class="ws-switcher-item-name">${ws.name}</span>
|
|
509
|
-
${ws.id === activeWorkspaceId.value
|
|
510
|
-
? html`<span class="ws-switcher-badge">Active</span>`
|
|
511
|
-
: null}
|
|
512
|
-
</div>
|
|
513
|
-
<div class="ws-switcher-item-repos">
|
|
514
|
-
${(ws.repos || []).map((r) => html`
|
|
515
|
-
<span key=${r.name} class="ws-switcher-repo ${r.exists ? "" : "missing"}" title=${r.slug || r.name}>
|
|
516
|
-
${r.primary ? "★ " : ""}${r.name}
|
|
517
|
-
</span>
|
|
518
|
-
`)}
|
|
519
|
-
</div>
|
|
520
|
-
</button>
|
|
521
|
-
`)}
|
|
522
|
-
<div class="ws-switcher-divider" />
|
|
523
|
-
<button
|
|
524
|
-
class="ws-switcher-item ws-switcher-manage-btn"
|
|
525
|
-
onClick=${() => { haptic("medium"); setOpen(false); setManagerOpen(true); }}
|
|
526
|
-
>
|
|
527
|
-
<span class="ws-switcher-manage-icon">⚙</span>
|
|
528
|
-
<span>Manage Workspaces</span>
|
|
529
|
-
</button>
|
|
530
|
-
</div>
|
|
531
|
-
`}
|
|
501
|
+
${wsList.map((ws) => html`
|
|
502
|
+
<option key=${ws.id} value=${ws.id}>${ws.name || ws.id}</option>
|
|
503
|
+
`)}
|
|
504
|
+
<option disabled>──────────</option>
|
|
505
|
+
<option value="__manage__">⚙ Manage Workspaces</option>
|
|
506
|
+
</select>
|
|
532
507
|
|
|
533
508
|
<${WorkspaceManager}
|
|
534
509
|
open=${managerOpen}
|
package/ui/index.html
CHANGED
package/ui/styles/components.css
CHANGED
|
@@ -2478,20 +2478,7 @@ select.input {
|
|
|
2478
2478
|
|
|
2479
2479
|
@media (min-width: 760px) {
|
|
2480
2480
|
.dashboard-grid {
|
|
2481
|
-
grid-template-columns: repeat(
|
|
2482
|
-
}
|
|
2483
|
-
|
|
2484
|
-
.dashboard-health,
|
|
2485
|
-
.dashboard-active,
|
|
2486
|
-
.dashboard-project {
|
|
2487
|
-
grid-column: 1 / span 7;
|
|
2488
|
-
}
|
|
2489
|
-
|
|
2490
|
-
.dashboard-overview,
|
|
2491
|
-
.dashboard-alerts,
|
|
2492
|
-
.dashboard-actions,
|
|
2493
|
-
.dashboard-quality {
|
|
2494
|
-
grid-column: 8 / span 5;
|
|
2481
|
+
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
|
2495
2482
|
}
|
|
2496
2483
|
|
|
2497
2484
|
.dashboard-activity {
|
package/ui/styles/sessions.css
CHANGED
|
@@ -56,6 +56,14 @@
|
|
|
56
56
|
.session-split > * {
|
|
57
57
|
min-height: 0;
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
/* Ensure session sidebar is always visible on desktop regardless of drawer state */
|
|
61
|
+
.session-pane {
|
|
62
|
+
position: static !important;
|
|
63
|
+
transform: none !important;
|
|
64
|
+
width: auto !important;
|
|
65
|
+
z-index: auto !important;
|
|
66
|
+
}
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
@media (min-width: 1200px) {
|
|
@@ -1422,7 +1430,9 @@ ul.md-list li::before {
|
|
|
1422
1430
|
|
|
1423
1431
|
/* ═══ Chat Input Area (bottom bar) ═══ */
|
|
1424
1432
|
.chat-input-area {
|
|
1425
|
-
position:
|
|
1433
|
+
position: sticky;
|
|
1434
|
+
bottom: 0;
|
|
1435
|
+
z-index: 10;
|
|
1426
1436
|
padding: 16px 16px;
|
|
1427
1437
|
border-top: 1px solid var(--border);
|
|
1428
1438
|
background: var(--bg-surface, var(--bg-card));
|
|
@@ -1441,14 +1451,14 @@ ul.md-list li::before {
|
|
|
1441
1451
|
border: none;
|
|
1442
1452
|
border-radius: 0;
|
|
1443
1453
|
background: transparent;
|
|
1444
|
-
padding:
|
|
1454
|
+
padding: 20px 24px;
|
|
1445
1455
|
}
|
|
1446
1456
|
|
|
1447
1457
|
/* ═══ Session Detail as flex column (messages grow, input pinned) ═══ */
|
|
1448
1458
|
.session-detail > .chat-view-embedded {
|
|
1449
1459
|
flex: 1;
|
|
1450
1460
|
min-height: 0;
|
|
1451
|
-
overflow:
|
|
1461
|
+
overflow-y: auto;
|
|
1452
1462
|
display: flex;
|
|
1453
1463
|
flex-direction: column;
|
|
1454
1464
|
}
|
|
@@ -1463,7 +1473,7 @@ ul.md-list li::before {
|
|
|
1463
1473
|
}
|
|
1464
1474
|
|
|
1465
1475
|
.app-shell[data-tab="chat"] .chat-view-embedded .chat-messages {
|
|
1466
|
-
padding:
|
|
1476
|
+
padding: 16px 20px 16px;
|
|
1467
1477
|
}
|
|
1468
1478
|
|
|
1469
1479
|
.app-shell[data-tab="chat"] .chat-input-area {
|
package/ui/styles/variables.css
CHANGED
|
@@ -94,11 +94,11 @@
|
|
|
94
94
|
--inspector-width: 300px;
|
|
95
95
|
--content-max: 1200px;
|
|
96
96
|
--shell-gap: 0px;
|
|
97
|
-
--radius-xs:
|
|
98
|
-
--radius-sm:
|
|
99
|
-
--radius-md:
|
|
100
|
-
--radius-lg:
|
|
101
|
-
--radius-xl:
|
|
97
|
+
--radius-xs: 2px;
|
|
98
|
+
--radius-sm: 3px;
|
|
99
|
+
--radius-md: 4px;
|
|
100
|
+
--radius-lg: 5px;
|
|
101
|
+
--radius-xl: 6px;
|
|
102
102
|
--radius-full: 9999px;
|
|
103
103
|
|
|
104
104
|
/* Shadows */
|
|
@@ -5,6 +5,41 @@
|
|
|
5
5
|
z-index: 100;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
.ws-native-select {
|
|
9
|
+
background: var(--bg-card);
|
|
10
|
+
color: var(--text-primary);
|
|
11
|
+
border: 1px solid var(--border);
|
|
12
|
+
border-radius: var(--radius-sm);
|
|
13
|
+
padding: 6px 28px 6px 10px;
|
|
14
|
+
font-size: 13px;
|
|
15
|
+
font-family: inherit;
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
outline: none;
|
|
18
|
+
appearance: none;
|
|
19
|
+
-webkit-appearance: none;
|
|
20
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23b5b0a6'%3E%3Cpath d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
|
21
|
+
background-repeat: no-repeat;
|
|
22
|
+
background-position: right 8px center;
|
|
23
|
+
max-width: 180px;
|
|
24
|
+
text-overflow: ellipsis;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
white-space: nowrap;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.ws-native-select:hover {
|
|
30
|
+
border-color: var(--border-strong);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.ws-native-select:focus {
|
|
34
|
+
border-color: var(--accent);
|
|
35
|
+
box-shadow: 0 0 0 2px var(--accent-soft);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ws-native-select option {
|
|
39
|
+
background: var(--bg-elevated);
|
|
40
|
+
color: var(--text-primary);
|
|
41
|
+
}
|
|
42
|
+
|
|
8
43
|
.ws-switcher-btn {
|
|
9
44
|
display: flex;
|
|
10
45
|
align-items: center;
|
package/ui/tabs/settings.js
CHANGED
|
@@ -352,6 +352,14 @@ const SETTINGS_STYLES = `
|
|
|
352
352
|
.settings-content-scroll {
|
|
353
353
|
padding-bottom: 160px;
|
|
354
354
|
}
|
|
355
|
+
/* Constrain settings content width on wide viewports */
|
|
356
|
+
.settings-content-constrained {
|
|
357
|
+
max-width: 900px;
|
|
358
|
+
margin-left: auto;
|
|
359
|
+
margin-right: auto;
|
|
360
|
+
width: 100%;
|
|
361
|
+
box-sizing: border-box;
|
|
362
|
+
}
|
|
355
363
|
|
|
356
364
|
body.settings-save-open .main-content {
|
|
357
365
|
padding-bottom: calc(var(--nav-height) + var(--safe-bottom) + 110px);
|
|
@@ -1204,6 +1212,7 @@ function AppPreferencesMode() {
|
|
|
1204
1212
|
setter(next);
|
|
1205
1213
|
cloudSet(key, next);
|
|
1206
1214
|
haptic();
|
|
1215
|
+
showToast("Preference saved", "success");
|
|
1207
1216
|
}, []);
|
|
1208
1217
|
|
|
1209
1218
|
const handleFontSize = (v) => {
|
|
@@ -1211,6 +1220,7 @@ function AppPreferencesMode() {
|
|
|
1211
1220
|
cloudSet("fontSize", v);
|
|
1212
1221
|
haptic();
|
|
1213
1222
|
applyFontSize(v);
|
|
1223
|
+
showToast("Font size saved", "success");
|
|
1214
1224
|
};
|
|
1215
1225
|
|
|
1216
1226
|
const handleColorTheme = (v) => {
|
|
@@ -1218,6 +1228,7 @@ function AppPreferencesMode() {
|
|
|
1218
1228
|
cloudSet("colorTheme", v);
|
|
1219
1229
|
haptic();
|
|
1220
1230
|
applyColorTheme(v);
|
|
1231
|
+
showToast("Theme saved", "success");
|
|
1221
1232
|
};
|
|
1222
1233
|
|
|
1223
1234
|
const handleDefaultMaxParallel = (v) => {
|
|
@@ -1225,18 +1236,21 @@ function AppPreferencesMode() {
|
|
|
1225
1236
|
setDefaultMaxParallel(val);
|
|
1226
1237
|
cloudSet("defaultMaxParallel", val);
|
|
1227
1238
|
haptic();
|
|
1239
|
+
showToast("Preference saved", "success");
|
|
1228
1240
|
};
|
|
1229
1241
|
|
|
1230
1242
|
const handleDefaultSdk = (v) => {
|
|
1231
1243
|
setDefaultSdk(v);
|
|
1232
1244
|
cloudSet("defaultSdk", v);
|
|
1233
1245
|
haptic();
|
|
1246
|
+
showToast("Preference saved", "success");
|
|
1234
1247
|
};
|
|
1235
1248
|
|
|
1236
1249
|
const handleDefaultRegion = (v) => {
|
|
1237
1250
|
setDefaultRegion(v);
|
|
1238
1251
|
cloudSet("defaultRegion", v);
|
|
1239
1252
|
haptic();
|
|
1253
|
+
showToast("Preference saved", "success");
|
|
1240
1254
|
};
|
|
1241
1255
|
|
|
1242
1256
|
/* Clear cache */
|
|
@@ -1785,23 +1799,25 @@ export function SettingsTab() {
|
|
|
1785
1799
|
useEffect(() => { injectStyles(); }, []);
|
|
1786
1800
|
|
|
1787
1801
|
return html`
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
{
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
+
<div class="settings-content-constrained">
|
|
1803
|
+
<!-- Top-level mode switcher -->
|
|
1804
|
+
<div style="margin-bottom:12px">
|
|
1805
|
+
<${SegmentedControl}
|
|
1806
|
+
options=${[
|
|
1807
|
+
{ value: "preferences", label: "App Preferences" },
|
|
1808
|
+
{ value: "server", label: "Server Config" },
|
|
1809
|
+
]}
|
|
1810
|
+
value=${mode}
|
|
1811
|
+
onChange=${(v) => {
|
|
1812
|
+
setMode(v);
|
|
1813
|
+
haptic("light");
|
|
1814
|
+
}}
|
|
1815
|
+
/>
|
|
1816
|
+
</div>
|
|
1802
1817
|
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1818
|
+
${mode === "preferences"
|
|
1819
|
+
? html`<${AppPreferencesMode} />`
|
|
1820
|
+
: html`<${ServerConfigMode} />`}
|
|
1821
|
+
</div>
|
|
1806
1822
|
`;
|
|
1807
1823
|
}
|