bosun 0.28.2 → 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/README.md +1 -1
- 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 +83 -24
- package/package.json +2 -1
- package/preflight.mjs +3 -1
- package/primary-agent.mjs +5 -1
- package/pwsh-runtime.mjs +62 -0
- package/setup.mjs +70 -3
- package/task-executor.mjs +125 -2
- package/telegram-bot.mjs +45 -8
- 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 +40 -71
- package/ui/tabs/settings.js +207 -0
- package/ui/tabs/tasks.js +116 -129
- package/ui-server.mjs +487 -0
- package/workspace-manager.mjs +57 -11
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
|
* ═══════════════════════════════════════════════════════════════ */
|
package/ui/tabs/tasks.js
CHANGED
|
@@ -89,6 +89,8 @@ const SORT_OPTIONS = [
|
|
|
89
89
|
{ value: "title", label: "Title" },
|
|
90
90
|
];
|
|
91
91
|
|
|
92
|
+
const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3, "": 4 };
|
|
93
|
+
|
|
92
94
|
const SYSTEM_TAGS = new Set([
|
|
93
95
|
"draft",
|
|
94
96
|
"todo",
|
|
@@ -760,6 +762,8 @@ export function TasksTab() {
|
|
|
760
762
|
const [actionsOpen, setActionsOpen] = useState(false);
|
|
761
763
|
const [exporting, setExporting] = useState(false);
|
|
762
764
|
const [filtersOpen, setFiltersOpen] = useState(false);
|
|
765
|
+
const [listSortCol, setListSortCol] = useState(""); // active column sort in list mode
|
|
766
|
+
const [listSortDir, setListSortDir] = useState("desc"); // "asc" | "desc"
|
|
763
767
|
const [isCompact, setIsCompact] = useState(() => {
|
|
764
768
|
try { return globalThis.matchMedia?.("(max-width: 768px)")?.matches ?? false; }
|
|
765
769
|
catch { return false; }
|
|
@@ -978,6 +982,31 @@ export function TasksTab() {
|
|
|
978
982
|
];
|
|
979
983
|
}, [tasks]);
|
|
980
984
|
|
|
985
|
+
/* ── Client-side table sort (list mode) ── */
|
|
986
|
+
const sortedForTable = useMemo(() => {
|
|
987
|
+
if (!listSortCol) return visible;
|
|
988
|
+
return [...visible].sort((a, b) => {
|
|
989
|
+
let av, bv;
|
|
990
|
+
const dir = listSortDir === "asc" ? 1 : -1;
|
|
991
|
+
if (listSortCol === "priority") {
|
|
992
|
+
av = PRIORITY_ORDER[a.priority || ""] ?? 4;
|
|
993
|
+
bv = PRIORITY_ORDER[b.priority || ""] ?? 4;
|
|
994
|
+
return dir * (av - bv);
|
|
995
|
+
}
|
|
996
|
+
if (listSortCol === "updated") {
|
|
997
|
+
av = a.updated_at ? new Date(a.updated_at).getTime() : 0;
|
|
998
|
+
bv = b.updated_at ? new Date(b.updated_at).getTime() : 0;
|
|
999
|
+
return dir * (av - bv);
|
|
1000
|
+
}
|
|
1001
|
+
if (listSortCol === "status") { av = a.status || ""; bv = b.status || ""; }
|
|
1002
|
+
else if (listSortCol === "title") { av = (a.title || "").toLowerCase(); bv = (b.title || "").toLowerCase(); }
|
|
1003
|
+
else if (listSortCol === "repo") { av = a.repository || a.workspace || ""; bv = b.repository || b.workspace || ""; }
|
|
1004
|
+
else if (listSortCol === "branch") { av = getTaskBaseBranch(a); bv = getTaskBaseBranch(b); }
|
|
1005
|
+
else { return 0; }
|
|
1006
|
+
return dir * String(av).localeCompare(String(bv));
|
|
1007
|
+
});
|
|
1008
|
+
}, [visible, listSortCol, listSortDir]);
|
|
1009
|
+
|
|
981
1010
|
/* ── Handlers ── */
|
|
982
1011
|
const handleFilter = async (s) => {
|
|
983
1012
|
haptic();
|
|
@@ -1578,23 +1607,16 @@ export function TasksTab() {
|
|
|
1578
1607
|
`}
|
|
1579
1608
|
</div>
|
|
1580
1609
|
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
label=${metric.label}
|
|
1592
|
-
color=${metric.color}
|
|
1593
|
-
/>
|
|
1594
|
-
`,
|
|
1595
|
-
)}
|
|
1596
|
-
</div>
|
|
1597
|
-
<//>
|
|
1610
|
+
<div class="snapshot-bar">
|
|
1611
|
+
${summaryMetrics.map((m) => html`
|
|
1612
|
+
<span key=${m.label} class="snapshot-pill">
|
|
1613
|
+
<span class="snapshot-dot" style="background:${m.color};" />
|
|
1614
|
+
<strong class="snapshot-val">${m.value}</strong>
|
|
1615
|
+
<span class="snapshot-lbl">${m.label}</span>
|
|
1616
|
+
</span>
|
|
1617
|
+
`)}
|
|
1618
|
+
<span class="snapshot-view-tag">${isKanban ? "⬛ Board" : "☰ List"}</span>
|
|
1619
|
+
</div>
|
|
1598
1620
|
|
|
1599
1621
|
<style>
|
|
1600
1622
|
.actions-btn { display:inline-flex; align-items:center; gap:4px; }
|
|
@@ -1617,119 +1639,84 @@ export function TasksTab() {
|
|
|
1617
1639
|
|
|
1618
1640
|
${isKanban && html`<${KanbanBoard} onOpenTask=${openDetail} />`}
|
|
1619
1641
|
|
|
1620
|
-
${!isKanban &&
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
:
|
|
1662
|
-
${task.updated_at
|
|
1663
|
-
? html` · ${formatRelative(task.updated_at)}`
|
|
1664
|
-
: ""}
|
|
1665
|
-
</div>
|
|
1666
|
-
</div>
|
|
1667
|
-
<div class="task-card-badges">
|
|
1668
|
-
${isManual && html`<${Badge} status="warning" text="manual" />`}
|
|
1669
|
-
<${Badge} status=${task.status} text=${task.status} />
|
|
1670
|
-
</div>
|
|
1671
|
-
</div>
|
|
1672
|
-
<div class="meta-text">
|
|
1673
|
-
${task.description
|
|
1674
|
-
? truncate(task.description, descriptionLimit)
|
|
1675
|
-
: "No description."}
|
|
1676
|
-
</div>
|
|
1677
|
-
${getTaskBaseBranch(task) &&
|
|
1678
|
-
html`
|
|
1679
|
-
<div class="meta-text">
|
|
1680
|
-
Base: <code>${getTaskBaseBranch(task)}</code>
|
|
1681
|
-
</div>
|
|
1682
|
-
`}
|
|
1683
|
-
${(task.workspace || task.repository) &&
|
|
1684
|
-
html`
|
|
1685
|
-
<div class="meta-text" style="display:flex;gap:4px;align-items:center;flex-wrap:wrap">
|
|
1686
|
-
${task.workspace && html`<span class="pill" style="font-size:11px">📂 ${task.workspace}</span>`}
|
|
1687
|
-
${task.repository && html`<span class="pill" style="font-size:11px">📁 ${task.repository}</span>`}
|
|
1688
|
-
</div>
|
|
1689
|
-
`}
|
|
1690
|
-
${tags.length > 0 &&
|
|
1691
|
-
html`
|
|
1692
|
-
<div class="tag-row">
|
|
1693
|
-
${visibleTags.map(
|
|
1694
|
-
(tag) => html`<span class="tag-chip">#${tag}</span>`,
|
|
1695
|
-
)}
|
|
1696
|
-
${extraTagCount > 0 &&
|
|
1697
|
-
html`
|
|
1698
|
-
<span class="tag-chip tag-chip-muted">
|
|
1699
|
-
+${extraTagCount}
|
|
1700
|
-
</span>
|
|
1701
|
-
`}
|
|
1702
|
-
</div>
|
|
1703
|
-
`}
|
|
1704
|
-
${!batchMode &&
|
|
1705
|
-
html`
|
|
1706
|
-
<div class="btn-row mt-sm task-card-actions" onClick=${(e) => e.stopPropagation()}>
|
|
1707
|
-
${task.status === "todo" &&
|
|
1708
|
-
html`
|
|
1709
|
-
<button
|
|
1710
|
-
class="btn btn-primary btn-sm"
|
|
1711
|
-
onClick=${() => openStartModal(task)}
|
|
1642
|
+
${!isKanban && visible.length > 0 && html`
|
|
1643
|
+
<div class="task-table-wrap">
|
|
1644
|
+
<table class="task-table">
|
|
1645
|
+
<thead>
|
|
1646
|
+
<tr>
|
|
1647
|
+
${[
|
|
1648
|
+
{ col: "status", label: "Status" },
|
|
1649
|
+
{ col: "priority", label: "Pri" },
|
|
1650
|
+
{ col: "title", label: "Title", grow: true },
|
|
1651
|
+
{ col: "branch", label: "Branch" },
|
|
1652
|
+
{ col: "repo", label: "Repo" },
|
|
1653
|
+
{ col: "updated", label: "Updated" },
|
|
1654
|
+
].map(({ col, label, grow }) => {
|
|
1655
|
+
const active = listSortCol === col;
|
|
1656
|
+
const arrow = active ? (listSortDir === "asc" ? "▲" : "▼") : "⇅";
|
|
1657
|
+
return html`
|
|
1658
|
+
<th
|
|
1659
|
+
key=${col}
|
|
1660
|
+
class="task-th ${active ? "task-th-active" : ""} ${grow ? "task-th-grow" : ""}"
|
|
1661
|
+
onClick=${() => {
|
|
1662
|
+
if (listSortCol === col) {
|
|
1663
|
+
setListSortDir(listSortDir === "asc" ? "desc" : "asc");
|
|
1664
|
+
} else {
|
|
1665
|
+
setListSortCol(col);
|
|
1666
|
+
setListSortDir("desc");
|
|
1667
|
+
}
|
|
1668
|
+
}}
|
|
1669
|
+
>${label} <span class="task-th-arrow">${arrow}</span></th>
|
|
1670
|
+
`;
|
|
1671
|
+
})}
|
|
1672
|
+
</tr>
|
|
1673
|
+
</thead>
|
|
1674
|
+
<tbody>
|
|
1675
|
+
${sortedForTable.map((task) => {
|
|
1676
|
+
const isManual = isTaskManual(task);
|
|
1677
|
+
const branch = getTaskBaseBranch(task);
|
|
1678
|
+
return html`
|
|
1679
|
+
<tr
|
|
1680
|
+
key=${task.id}
|
|
1681
|
+
class="task-tr ${batchMode && selectedIds.has(task.id) ? "task-tr-selected" : ""}"
|
|
1682
|
+
data-status=${task.status || ""}
|
|
1683
|
+
onClick=${() => batchMode ? toggleSelect(task.id) : openDetail(task.id)}
|
|
1712
1684
|
>
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1685
|
+
<td class="task-td task-td-status">
|
|
1686
|
+
<${Badge} status=${task.status} text=${task.status} />
|
|
1687
|
+
${isManual && html`<${Badge} status="warning" text="⚑" />`}
|
|
1688
|
+
</td>
|
|
1689
|
+
<td class="task-td task-td-pri">
|
|
1690
|
+
${task.priority
|
|
1691
|
+
? html`<${Badge} status=${task.priority} text=${task.priority} />`
|
|
1692
|
+
: html`<span class="task-td-empty">—</span>`}
|
|
1693
|
+
</td>
|
|
1694
|
+
<td class="task-td task-td-title">
|
|
1695
|
+
<div class="task-td-title-text">${task.title || "(untitled)"}</div>
|
|
1696
|
+
${task.id && html`<div class="task-td-id">${task.id}</div>`}
|
|
1697
|
+
</td>
|
|
1698
|
+
<td class="task-td task-td-branch">
|
|
1699
|
+
${branch
|
|
1700
|
+
? html`<code class="task-td-code">${branch}</code>`
|
|
1701
|
+
: html`<span class="task-td-empty">—</span>`}
|
|
1702
|
+
</td>
|
|
1703
|
+
<td class="task-td task-td-repo">
|
|
1704
|
+
${(task.repository || task.workspace)
|
|
1705
|
+
? html`<span>${task.repository || task.workspace}</span>`
|
|
1706
|
+
: html`<span class="task-td-empty">—</span>`}
|
|
1707
|
+
</td>
|
|
1708
|
+
<td class="task-td task-td-updated">
|
|
1709
|
+
${task.updated_at
|
|
1710
|
+
? html`<span class="task-td-date">${formatRelative(task.updated_at)}</span>`
|
|
1711
|
+
: html`<span class="task-td-empty">—</span>`}
|
|
1712
|
+
</td>
|
|
1713
|
+
</tr>
|
|
1714
|
+
`;
|
|
1715
|
+
})}
|
|
1716
|
+
</tbody>
|
|
1717
|
+
</table>
|
|
1718
|
+
</div>
|
|
1719
|
+
`}
|
|
1733
1720
|
${!isKanban && !visible.length &&
|
|
1734
1721
|
html`
|
|
1735
1722
|
<${EmptyState}
|