bosun 0.28.3 → 0.28.4
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/.env.example +68 -0
- package/agent-prompts.mjs +12 -6
- package/agent-work-analyzer.mjs +39 -15
- package/cli.mjs +4 -1
- package/codex-config.mjs +7 -0
- package/monitor.mjs +40 -5
- package/package.json +1 -1
- package/primary-agent.mjs +5 -1
- package/setup.mjs +6 -0
- package/task-executor.mjs +125 -2
- package/ui/app.js +2 -16
- package/ui/components/workspace-switcher.js +25 -32
- package/ui/modules/settings-schema.js +7 -0
- package/ui/styles/base.css +3 -28
- package/ui/styles/components.css +309 -73
- package/ui/styles/kanban.css +10 -16
- package/ui/styles/layout.css +81 -101
- package/ui/styles/sessions.css +27 -32
- package/ui/styles/variables.css +8 -8
- package/ui/styles/workspace-switcher.css +2 -4
- package/ui/tabs/control.js +39 -115
- package/ui/tabs/settings.js +207 -0
- package/ui/tabs/tasks.js +116 -129
- package/ui-server.mjs +487 -0
package/ui/tabs/control.js
CHANGED
|
@@ -64,9 +64,6 @@ export function ControlTab() {
|
|
|
64
64
|
const [commandInput, setCommandInput] = useState("");
|
|
65
65
|
const [startTaskId, setStartTaskId] = useState("");
|
|
66
66
|
const [retryTaskId, setRetryTaskId] = useState("");
|
|
67
|
-
const [retryReason, setRetryReason] = useState("");
|
|
68
|
-
const [askInput, setAskInput] = useState("");
|
|
69
|
-
const [steerInput, setSteerInput] = useState("");
|
|
70
67
|
const [quickCmdInput, setQuickCmdInput] = useState("");
|
|
71
68
|
const [quickCmdPrefix, setQuickCmdPrefix] = useState("shell");
|
|
72
69
|
const [quickCmdFeedback, setQuickCmdFeedback] = useState("");
|
|
@@ -83,7 +80,7 @@ export function ControlTab() {
|
|
|
83
80
|
const [tasksLoading, setTasksLoading] = useState(false);
|
|
84
81
|
const [startTaskError, setStartTaskError] = useState("");
|
|
85
82
|
const [retryTaskError, setRetryTaskError] = useState("");
|
|
86
|
-
const [
|
|
83
|
+
const [planFocus, setPlanFocus] = useState(""); // chip selection — no typing needed
|
|
87
84
|
const [planCount, setPlanCount] = useState("5");
|
|
88
85
|
const startTaskIdRef = useRef("");
|
|
89
86
|
const retryTaskIdRef = useRef("");
|
|
@@ -504,19 +501,15 @@ export function ControlTab() {
|
|
|
504
501
|
try {
|
|
505
502
|
await apiFetch("/api/tasks/retry", {
|
|
506
503
|
method: "POST",
|
|
507
|
-
body: JSON.stringify({
|
|
508
|
-
taskId,
|
|
509
|
-
retryReason: retryReason.trim() || undefined,
|
|
510
|
-
}),
|
|
504
|
+
body: JSON.stringify({ taskId }),
|
|
511
505
|
});
|
|
512
506
|
showToast("Task retried", "success");
|
|
513
|
-
setRetryReason("");
|
|
514
507
|
refreshTaskOptions();
|
|
515
508
|
scheduleRefresh(150);
|
|
516
509
|
} catch {
|
|
517
510
|
/* toast via apiFetch */
|
|
518
511
|
}
|
|
519
|
-
}, [retryTaskId,
|
|
512
|
+
}, [retryTaskId, refreshTaskOptions]);
|
|
520
513
|
|
|
521
514
|
return html`
|
|
522
515
|
${!executor && !config && html`<${Card} title="Loading…"><${SkeletonCard} /><//>`}
|
|
@@ -709,58 +702,6 @@ export function ControlTab() {
|
|
|
709
702
|
<//>
|
|
710
703
|
<//>
|
|
711
704
|
|
|
712
|
-
<${Card} className="agent-control-card">
|
|
713
|
-
<${Collapsible} title="Agent Control" defaultOpen=${!isCompact}>
|
|
714
|
-
<div class="meta-text mb-sm">Ask or steer the active agent.</div>
|
|
715
|
-
<div class="agent-control-grid">
|
|
716
|
-
<div>
|
|
717
|
-
<div class="form-label">Ask agent</div>
|
|
718
|
-
<textarea
|
|
719
|
-
class="input mb-sm"
|
|
720
|
-
rows="2"
|
|
721
|
-
placeholder="Ask the agent…"
|
|
722
|
-
value=${askInput}
|
|
723
|
-
onInput=${(e) => setAskInput(e.target.value)}
|
|
724
|
-
></textarea>
|
|
725
|
-
<div class="btn-row">
|
|
726
|
-
<button
|
|
727
|
-
class="btn btn-primary btn-sm"
|
|
728
|
-
onClick=${() => {
|
|
729
|
-
if (askInput.trim()) {
|
|
730
|
-
sendCmd(`/ask ${askInput.trim()}`);
|
|
731
|
-
setAskInput("");
|
|
732
|
-
}
|
|
733
|
-
}}
|
|
734
|
-
>
|
|
735
|
-
💬 Ask
|
|
736
|
-
</button>
|
|
737
|
-
</div>
|
|
738
|
-
</div>
|
|
739
|
-
<div>
|
|
740
|
-
<div class="form-label">Steer prompt</div>
|
|
741
|
-
<div class="input-row mb-sm">
|
|
742
|
-
<input
|
|
743
|
-
class="input"
|
|
744
|
-
placeholder="Steer prompt (focus on…)"
|
|
745
|
-
value=${steerInput}
|
|
746
|
-
onInput=${(e) => setSteerInput(e.target.value)}
|
|
747
|
-
/>
|
|
748
|
-
<button
|
|
749
|
-
class="btn btn-secondary btn-sm"
|
|
750
|
-
onClick=${() => {
|
|
751
|
-
if (steerInput.trim()) {
|
|
752
|
-
sendCmd(`/steer ${steerInput.trim()}`);
|
|
753
|
-
setSteerInput("");
|
|
754
|
-
}
|
|
755
|
-
}}
|
|
756
|
-
>
|
|
757
|
-
🎯 Steer
|
|
758
|
-
</button>
|
|
759
|
-
</div>
|
|
760
|
-
</div>
|
|
761
|
-
</div>
|
|
762
|
-
<//>
|
|
763
|
-
<//>
|
|
764
705
|
</div>
|
|
765
706
|
|
|
766
707
|
<div class="control-side">
|
|
@@ -815,6 +756,7 @@ export function ControlTab() {
|
|
|
815
756
|
<div class="field-group">
|
|
816
757
|
<div class="form-label">Retry task</div>
|
|
817
758
|
<div class="input-row">
|
|
759
|
+
<div class="input-row">
|
|
818
760
|
<select
|
|
819
761
|
class=${retryTaskError ? "input input-error" : "input"}
|
|
820
762
|
value=${retryTaskId}
|
|
@@ -833,70 +775,52 @@ export function ControlTab() {
|
|
|
833
775
|
`,
|
|
834
776
|
)}
|
|
835
777
|
</select>
|
|
836
|
-
<input
|
|
837
|
-
class="input"
|
|
838
|
-
placeholder="Retry reason (optional)"
|
|
839
|
-
value=${retryReason}
|
|
840
|
-
onInput=${(e) => setRetryReason(e.target.value)}
|
|
841
|
-
/>
|
|
842
778
|
<button
|
|
843
779
|
class="btn btn-secondary btn-sm"
|
|
844
780
|
disabled=${!retryTaskId}
|
|
845
781
|
onClick=${handleRetryTask}
|
|
846
782
|
>
|
|
847
|
-
Retry
|
|
783
|
+
↻ Retry
|
|
848
784
|
</button>
|
|
849
|
-
<div class="form-group" style="margin-top:0.5rem">
|
|
850
|
-
<div class="card-subtitle" style="margin-bottom:0.25rem">Task Planner</div>
|
|
851
|
-
<div class="input-row" style="display:flex;gap:0.4rem;align-items:center;flex-wrap:wrap">
|
|
852
|
-
<input
|
|
853
|
-
type="number"
|
|
854
|
-
class="input"
|
|
855
|
-
style="width:4.5rem;flex-shrink:0"
|
|
856
|
-
min="1"
|
|
857
|
-
max="50"
|
|
858
|
-
placeholder="5"
|
|
859
|
-
value=${planCount}
|
|
860
|
-
onInput=${(e) => setPlanCount(e.target.value)}
|
|
861
|
-
title="Number of tasks to generate"
|
|
862
|
-
/>
|
|
863
|
-
<input
|
|
864
|
-
class="input"
|
|
865
|
-
style="flex:1;min-width:10rem"
|
|
866
|
-
placeholder="Optional: focus on X, fix Y issues…"
|
|
867
|
-
value=${planPrompt}
|
|
868
|
-
onInput=${(e) => setPlanPrompt(e.target.value)}
|
|
869
|
-
onKeyDown=${(e) => {
|
|
870
|
-
if (e.key === "Enter") {
|
|
871
|
-
const count = parseInt(planCount, 10);
|
|
872
|
-
const n = Number.isFinite(count) && count > 0 ? count : 5;
|
|
873
|
-
const cmd = planPrompt.trim()
|
|
874
|
-
? `/plan ${n} ${planPrompt.trim()}`
|
|
875
|
-
: `/plan ${n}`;
|
|
876
|
-
sendCmd(cmd);
|
|
877
|
-
}
|
|
878
|
-
}}
|
|
879
|
-
/>
|
|
880
|
-
<button
|
|
881
|
-
class="btn btn-ghost btn-sm"
|
|
882
|
-
onClick=${() => {
|
|
883
|
-
const count = parseInt(planCount, 10);
|
|
884
|
-
const n = Number.isFinite(count) && count > 0 ? count : 5;
|
|
885
|
-
const cmd = planPrompt.trim()
|
|
886
|
-
? `/plan ${n} ${planPrompt.trim()}`
|
|
887
|
-
: `/plan ${n}`;
|
|
888
|
-
sendCmd(cmd);
|
|
889
|
-
}}
|
|
890
|
-
>
|
|
891
|
-
📋 Plan
|
|
892
|
-
</button>
|
|
893
|
-
</div>
|
|
894
|
-
</div>
|
|
895
785
|
</div>
|
|
896
786
|
${retryTaskError
|
|
897
787
|
? html`<div class="form-hint error">${retryTaskError}</div>`
|
|
898
788
|
: null}
|
|
899
789
|
</div>
|
|
790
|
+
|
|
791
|
+
<div class="field-group">
|
|
792
|
+
<div class="form-label">Task Planner</div>
|
|
793
|
+
<div class="plan-chips">
|
|
794
|
+
${["fix bugs", "add tests", "security", "refactor", "add docs", "performance"].map((chip) => html`
|
|
795
|
+
<button
|
|
796
|
+
key=${chip}
|
|
797
|
+
class=${`chip ${planFocus === chip ? "active" : ""}`}
|
|
798
|
+
onClick=${() => { haptic("light"); setPlanFocus(planFocus === chip ? "" : chip); }}
|
|
799
|
+
>${chip}</button>
|
|
800
|
+
`)}
|
|
801
|
+
</div>
|
|
802
|
+
<div class="input-row mt-sm">
|
|
803
|
+
<span class="form-hint" style="flex:1;margin:0">Generate
|
|
804
|
+
<input
|
|
805
|
+
type="number"
|
|
806
|
+
class="input plan-count-input"
|
|
807
|
+
min="1" max="50"
|
|
808
|
+
value=${planCount}
|
|
809
|
+
onInput=${(e) => setPlanCount(e.target.value)}
|
|
810
|
+
/>
|
|
811
|
+
tasks${planFocus ? ` · ${planFocus}` : ""}
|
|
812
|
+
</span>
|
|
813
|
+
<button
|
|
814
|
+
class="btn btn-ghost btn-sm"
|
|
815
|
+
onClick=${() => {
|
|
816
|
+
const n = Math.max(1, parseInt(planCount, 10) || 5);
|
|
817
|
+
sendCmd(planFocus ? `/plan ${n} ${planFocus}` : `/plan ${n}`);
|
|
818
|
+
}}
|
|
819
|
+
>
|
|
820
|
+
📋 Plan
|
|
821
|
+
</button>
|
|
822
|
+
</div>
|
|
823
|
+
</div>
|
|
900
824
|
<//>
|
|
901
825
|
<//>
|
|
902
826
|
|
package/ui/tabs/settings.js
CHANGED
|
@@ -1028,6 +1028,9 @@ function ServerConfigMode() {
|
|
|
1028
1028
|
${activeCat?.description &&
|
|
1029
1029
|
html`<div class="settings-cat-desc">${activeCat.description}</div>`}
|
|
1030
1030
|
|
|
1031
|
+
<!-- GitHub Device Flow login card -->
|
|
1032
|
+
${activeCategory === "github" && html`<${GitHubDeviceFlowCard} config=${serverData} />`}
|
|
1033
|
+
|
|
1031
1034
|
<!-- Settings list for active category -->
|
|
1032
1035
|
${catDefs.length === 0
|
|
1033
1036
|
? html`
|
|
@@ -1567,6 +1570,210 @@ function AppPreferencesMode() {
|
|
|
1567
1570
|
`;
|
|
1568
1571
|
}
|
|
1569
1572
|
|
|
1573
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
1574
|
+
* GitHubDeviceFlowCard — "Sign in with GitHub" (like VS Code / Roo Code)
|
|
1575
|
+
* Uses OAuth Device Flow: no public URL, no callback needed.
|
|
1576
|
+
* ═══════════════════════════════════════════════════════════════ */
|
|
1577
|
+
function GitHubDeviceFlowCard({ config }) {
|
|
1578
|
+
const [phase, setPhase] = useState("idle"); // idle | loading | code | polling | done | error
|
|
1579
|
+
const [userCode, setUserCode] = useState("");
|
|
1580
|
+
const [verificationUri, setVerificationUri] = useState("");
|
|
1581
|
+
const [deviceCode, setDeviceCode] = useState("");
|
|
1582
|
+
const [pollInterval, setPollInterval] = useState(5);
|
|
1583
|
+
const [ghUser, setGhUser] = useState("");
|
|
1584
|
+
const [error, setError] = useState("");
|
|
1585
|
+
const pollRef = useRef(null);
|
|
1586
|
+
|
|
1587
|
+
// Check if already authenticated
|
|
1588
|
+
const hasToken = Boolean(
|
|
1589
|
+
config?.GH_TOKEN || config?.GITHUB_TOKEN
|
|
1590
|
+
);
|
|
1591
|
+
|
|
1592
|
+
// Cleanup polling on unmount
|
|
1593
|
+
useEffect(() => {
|
|
1594
|
+
return () => { if (pollRef.current) clearTimeout(pollRef.current); };
|
|
1595
|
+
}, []);
|
|
1596
|
+
|
|
1597
|
+
async function startFlow() {
|
|
1598
|
+
setPhase("loading");
|
|
1599
|
+
setError("");
|
|
1600
|
+
try {
|
|
1601
|
+
const res = await apiFetch("/api/github/device/start", { method: "POST" });
|
|
1602
|
+
if (!res.ok) throw new Error(res.error || "Failed to start device flow");
|
|
1603
|
+
const d = res.data;
|
|
1604
|
+
setUserCode(d.userCode);
|
|
1605
|
+
setVerificationUri(d.verificationUri);
|
|
1606
|
+
setDeviceCode(d.deviceCode);
|
|
1607
|
+
setPollInterval(d.interval || 5);
|
|
1608
|
+
setPhase("code");
|
|
1609
|
+
|
|
1610
|
+
// Open GitHub in a new tab automatically
|
|
1611
|
+
try {
|
|
1612
|
+
window.open(d.verificationUri, "_blank");
|
|
1613
|
+
} catch {
|
|
1614
|
+
// may be blocked by popup blocker
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// Start polling
|
|
1618
|
+
startPolling(d.deviceCode, (d.interval || 5) * 1000);
|
|
1619
|
+
} catch (err) {
|
|
1620
|
+
setError(err.message);
|
|
1621
|
+
setPhase("error");
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
function startPolling(dc, intervalMs) {
|
|
1626
|
+
if (pollRef.current) clearTimeout(pollRef.current);
|
|
1627
|
+
setPhase("polling");
|
|
1628
|
+
|
|
1629
|
+
async function tick() {
|
|
1630
|
+
try {
|
|
1631
|
+
const res = await apiFetch("/api/github/device/poll", {
|
|
1632
|
+
method: "POST",
|
|
1633
|
+
body: { deviceCode: dc },
|
|
1634
|
+
});
|
|
1635
|
+
if (!res.ok) {
|
|
1636
|
+
pollRef.current = setTimeout(tick, intervalMs);
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
const d = res.data;
|
|
1640
|
+
if (d.status === "complete") {
|
|
1641
|
+
pollRef.current = null;
|
|
1642
|
+
setGhUser(d.login);
|
|
1643
|
+
setPhase("done");
|
|
1644
|
+
haptic("success");
|
|
1645
|
+
showToast("success", `Signed in as ${d.login}`);
|
|
1646
|
+
return;
|
|
1647
|
+
} else if (d.status === "slow_down") {
|
|
1648
|
+
// Increase interval as requested by GitHub
|
|
1649
|
+
const newInterval = (d.interval || 10) * 1000;
|
|
1650
|
+
setPollInterval(d.interval || 10);
|
|
1651
|
+
intervalMs = newInterval;
|
|
1652
|
+
} else if (d.status === "expired") {
|
|
1653
|
+
pollRef.current = null;
|
|
1654
|
+
setError("Code expired. Please try again.");
|
|
1655
|
+
setPhase("error");
|
|
1656
|
+
return;
|
|
1657
|
+
} else if (d.status === "error") {
|
|
1658
|
+
pollRef.current = null;
|
|
1659
|
+
setError(d.description || d.error || "Authorization failed");
|
|
1660
|
+
setPhase("error");
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
// "pending" or "slow_down" → schedule next tick
|
|
1664
|
+
pollRef.current = setTimeout(tick, intervalMs);
|
|
1665
|
+
} catch {
|
|
1666
|
+
// network error — keep polling, it may recover
|
|
1667
|
+
pollRef.current = setTimeout(tick, intervalMs);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
pollRef.current = setTimeout(tick, intervalMs);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function copyCode() {
|
|
1675
|
+
try {
|
|
1676
|
+
navigator.clipboard.writeText(userCode);
|
|
1677
|
+
haptic("light");
|
|
1678
|
+
showToast("info", "Code copied!");
|
|
1679
|
+
} catch {
|
|
1680
|
+
// clipboard not available
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// Already authenticated — compact info
|
|
1685
|
+
if (hasToken && phase !== "done") {
|
|
1686
|
+
return html`
|
|
1687
|
+
<${Card}>
|
|
1688
|
+
<div style="display:flex;align-items:center;gap:10px;padding:4px 0">
|
|
1689
|
+
<span style="font-size:20px">🐙</span>
|
|
1690
|
+
<div style="flex:1;min-width:0">
|
|
1691
|
+
<div style="font-size:13px;font-weight:600;color:var(--text-primary)">GitHub Connected</div>
|
|
1692
|
+
<div style="font-size:12px;color:var(--text-secondary)">Token is configured. Re-authenticate below if needed.</div>
|
|
1693
|
+
</div>
|
|
1694
|
+
<button class="btn btn-sm btn-secondary" onClick=${startFlow}>
|
|
1695
|
+
Re-auth
|
|
1696
|
+
</button>
|
|
1697
|
+
</div>
|
|
1698
|
+
<//>
|
|
1699
|
+
`;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// Done — just authorized
|
|
1703
|
+
if (phase === "done") {
|
|
1704
|
+
return html`
|
|
1705
|
+
<${Card}>
|
|
1706
|
+
<div style="text-align:center;padding:12px 0">
|
|
1707
|
+
<div style="font-size:32px;margin-bottom:8px">✅</div>
|
|
1708
|
+
<div style="font-size:15px;font-weight:600;color:var(--text-primary)">Signed in as ${ghUser}</div>
|
|
1709
|
+
<div style="font-size:12px;color:var(--text-secondary);margin-top:4px">GitHub token saved to .env</div>
|
|
1710
|
+
</div>
|
|
1711
|
+
<//>
|
|
1712
|
+
`;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// Show device code to enter
|
|
1716
|
+
if (phase === "code" || phase === "polling") {
|
|
1717
|
+
return html`
|
|
1718
|
+
<${Card}>
|
|
1719
|
+
<div style="text-align:center;padding:8px 0">
|
|
1720
|
+
<div style="font-size:13px;color:var(--text-secondary);margin-bottom:12px">
|
|
1721
|
+
Go to <a href=${verificationUri} target="_blank" rel="noopener"
|
|
1722
|
+
style="color:var(--accent);font-weight:600;text-decoration:underline">${verificationUri}</a>
|
|
1723
|
+
and enter this code:
|
|
1724
|
+
</div>
|
|
1725
|
+
<button onClick=${copyCode}
|
|
1726
|
+
style="font-size:28px;font-weight:700;letter-spacing:0.15em;font-family:var(--font-mono,'SF Mono',monospace);
|
|
1727
|
+
padding:12px 24px;border-radius:var(--radius-md);background:var(--surface-1);
|
|
1728
|
+
border:2px dashed var(--accent);color:var(--text-primary);cursor:pointer;
|
|
1729
|
+
transition:background 0.15s ease"
|
|
1730
|
+
title="Click to copy">
|
|
1731
|
+
${userCode}
|
|
1732
|
+
</button>
|
|
1733
|
+
<div style="font-size:12px;color:var(--text-hint);margin-top:10px;display:flex;align-items:center;justify-content:center;gap:6px">
|
|
1734
|
+
<span class="spinner" style="width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%"></span>
|
|
1735
|
+
Waiting for authorization…
|
|
1736
|
+
</div>
|
|
1737
|
+
</div>
|
|
1738
|
+
<//>
|
|
1739
|
+
`;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// Error state
|
|
1743
|
+
if (phase === "error") {
|
|
1744
|
+
return html`
|
|
1745
|
+
<${Card}>
|
|
1746
|
+
<div style="text-align:center;padding:12px 0">
|
|
1747
|
+
<div style="font-size:24px;margin-bottom:8px">⚠️</div>
|
|
1748
|
+
<div style="font-size:13px;color:var(--color-error);margin-bottom:12px">${error}</div>
|
|
1749
|
+
<button class="btn btn-sm btn-primary" onClick=${startFlow}>Try Again</button>
|
|
1750
|
+
</div>
|
|
1751
|
+
<//>
|
|
1752
|
+
`;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// Idle — show sign-in button
|
|
1756
|
+
return html`
|
|
1757
|
+
<${Card}>
|
|
1758
|
+
<div style="text-align:center;padding:16px 0">
|
|
1759
|
+
<div style="font-size:32px;margin-bottom:8px">🐙</div>
|
|
1760
|
+
<div style="font-size:15px;font-weight:600;margin-bottom:4px;color:var(--text-primary)">
|
|
1761
|
+
Sign in with GitHub
|
|
1762
|
+
</div>
|
|
1763
|
+
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:16px;max-width:280px;margin-inline:auto;line-height:1.5">
|
|
1764
|
+
Authorize Bosun to manage repos and issues on your behalf.
|
|
1765
|
+
No public URL needed — works entirely from your local machine.
|
|
1766
|
+
</div>
|
|
1767
|
+
<button class="btn btn-primary" onClick=${startFlow}
|
|
1768
|
+
disabled=${phase === "loading"}
|
|
1769
|
+
style="min-width:200px">
|
|
1770
|
+
${phase === "loading" ? html`<${Spinner} size=${14} /> Connecting…` : "Sign in with GitHub"}
|
|
1771
|
+
</button>
|
|
1772
|
+
</div>
|
|
1773
|
+
<//>
|
|
1774
|
+
`;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1570
1777
|
/* ═══════════════════════════════════════════════════════════════
|
|
1571
1778
|
* SettingsTab — Top-level with two-mode segmented control
|
|
1572
1779
|
* ═══════════════════════════════════════════════════════════════ */
|