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.
@@ -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 [planPrompt, setPlanPrompt] = useState("");
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, retryReason, refreshTaskOptions]);
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 Task
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
 
@@ -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
  * ═══════════════════════════════════════════════════════════════ */