symbiot 0.28.0 → 0.28.1
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/index.mjs +1 -463
- package/package.json +2 -1
- package/ui.mjs +469 -0
package/index.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { createInterface } from "node:readline";
|
|
|
26
26
|
import { createServer } from "node:http";
|
|
27
27
|
import { randomBytes } from "node:crypto";
|
|
28
28
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
29
|
+
import { EMBEDDED_UI } from "./ui.mjs";
|
|
29
30
|
|
|
30
31
|
const HERE = fileURLToPath(new URL(".", import.meta.url));
|
|
31
32
|
let VERSION = "0"; try { VERSION = JSON.parse(readFileSync(join(HERE, "package.json"), "utf8")).version; } catch {}
|
|
@@ -1464,469 +1465,6 @@ function cmdWhoami() {
|
|
|
1464
1465
|
// ---- `symbiot app` : the same UI in a chrome-less browser window ----------
|
|
1465
1466
|
// Self-contained HTML served at / — no backticks or ${} inside (it lives in a
|
|
1466
1467
|
// template literal). Talks to the local API with the per-launch token.
|
|
1467
|
-
const EMBEDDED_UI = `<!doctype html><html><head><meta charset="utf8">
|
|
1468
|
-
<meta name="viewport" content="width=device-width,initial-scale=1"><title>Symbiot</title>
|
|
1469
|
-
<style>
|
|
1470
|
-
:root{--ink:#0E1A1F;--ink2:#15262C;--ink3:#1D333A;--line:#24404A;--bone:#F4F1EA;--text:#B7C9C4;--faint:#7E9690;--green:#3DDC97;--amber:#F2A541;--green-dim:#16322D;--sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
|
|
1471
|
-
*{box-sizing:border-box}html,body{margin:0;height:100%}
|
|
1472
|
-
body{background:var(--ink);color:var(--text);font-family:var(--sans);font-size:14px;display:flex;flex-direction:column}
|
|
1473
|
-
header{padding:16px 18px 10px;display:flex;align-items:center;gap:10px}
|
|
1474
|
-
.dot{width:10px;height:10px;border-radius:50%;background:var(--green);box-shadow:0 0 12px var(--green)}
|
|
1475
|
-
.brand{font-weight:700;color:var(--bone);font-size:16px}
|
|
1476
|
-
.status{margin-left:auto;font-size:12px;color:var(--faint);text-align:right;max-width:52%}
|
|
1477
|
-
.tabs{display:flex;gap:6px;padding:0 14px;border-bottom:1px solid var(--line)}
|
|
1478
|
-
.tab{padding:9px 14px;border:0;background:none;color:var(--faint);font:inherit;font-weight:600;cursor:pointer;border-bottom:2px solid transparent}
|
|
1479
|
-
.tab.active{color:var(--bone);border-bottom-color:var(--green)}
|
|
1480
|
-
main{flex:1;overflow:auto;padding:16px 18px}
|
|
1481
|
-
.row{display:flex;gap:10px;align-items:center;margin-bottom:12px;flex-wrap:wrap}
|
|
1482
|
-
button.act{background:var(--green);color:var(--ink);border:0;border-radius:10px;padding:10px 16px;font:inherit;font-weight:700;cursor:pointer}
|
|
1483
|
-
button.act:hover{filter:brightness(1.08)}
|
|
1484
|
-
button.ghost{background:var(--ink3);color:var(--bone);border:1px solid var(--line);border-radius:10px;padding:9px 14px;font:inherit;font-weight:600;cursor:pointer}
|
|
1485
|
-
button.ghost:hover{border-color:var(--faint)}
|
|
1486
|
-
.out{white-space:pre-wrap;background:var(--ink2);border:1px solid var(--line);border-radius:12px;padding:16px;min-height:180px;color:var(--bone);line-height:1.6}
|
|
1487
|
-
.muted{color:var(--faint)}
|
|
1488
|
-
label{display:block;font-size:12px;color:var(--faint);margin:14px 0 5px}
|
|
1489
|
-
select,input{width:100%;background:var(--ink);border:1px solid var(--line);border-radius:9px;padding:10px 12px;color:var(--bone);font:inherit}
|
|
1490
|
-
select:focus,input:focus{outline:none;border-color:var(--green)}
|
|
1491
|
-
a{color:var(--green);cursor:pointer}.hidden{display:none}
|
|
1492
|
-
.note{font-size:12px;margin-top:10px}.ok{color:var(--green)}.err{color:var(--amber)}
|
|
1493
|
-
.updatebar{display:none;align-items:center;gap:12px;padding:9px 16px;font-size:13px;font-weight:600;background:var(--green);color:var(--ink)}
|
|
1494
|
-
.updatebar.show{display:flex}
|
|
1495
|
-
.updatebar.reconnect{background:var(--amber)}
|
|
1496
|
-
.updatebar button{font:inherit;font-weight:700;border:0;border-radius:8px;padding:5px 12px;background:var(--ink);color:var(--bone);cursor:pointer}
|
|
1497
|
-
.ver{font-size:11px;color:var(--faint);background:var(--ink3);border:1px solid var(--line);border-radius:999px;padding:2px 8px;margin-left:2px;font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
1498
|
-
footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
1499
|
-
.profile{font-size:13px;margin-bottom:8px;line-height:1.5}
|
|
1500
|
-
.profile b{color:var(--bone)}
|
|
1501
|
-
.maprow{display:flex;gap:12px;align-items:stretch}
|
|
1502
|
-
#graph{flex:1;width:100%;height:62vh;min-height:340px;background:var(--ink2);border:1px solid var(--line);border-radius:12px;touch-action:none;cursor:grab}
|
|
1503
|
-
#graph:active{cursor:grabbing}
|
|
1504
|
-
#graph text{font-family:var(--sans);fill:var(--text);font-size:11px;pointer-events:none}
|
|
1505
|
-
#graph .lbl-me{fill:var(--bone);font-weight:700;font-size:13px}
|
|
1506
|
-
.detail{width:290px;flex:none;background:var(--ink2);border:1px solid var(--line);border-radius:12px;padding:14px;overflow:auto;max-height:62vh}
|
|
1507
|
-
.detail h3{margin:0 0 4px;color:var(--bone);font-size:15px}
|
|
1508
|
-
.detail .k{font-size:12px;color:var(--faint);margin-top:8px}
|
|
1509
|
-
.detail ul{margin:6px 0 0;padding-left:18px}.detail li{margin:2px 0}
|
|
1510
|
-
.detail .chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
|
|
1511
|
-
.tag{display:inline-flex;font-size:11px;padding:4px 9px;border-radius:999px;background:var(--green-dim);border:1px solid #2a6b52;color:var(--green)}
|
|
1512
|
-
.mapbar{margin-top:8px;font-size:12.5px;color:var(--text);background:var(--ink3);border:1px solid var(--line);border-radius:9px;padding:9px 12px;min-height:18px;font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
1513
|
-
.mapbar b{color:var(--bone)}
|
|
1514
|
-
.review{margin-top:12px;background:var(--ink2);border:1px solid var(--line);border-radius:12px;padding:16px;min-height:120px}
|
|
1515
|
-
.review h4{margin:0 0 8px;color:var(--green);font-size:11px;letter-spacing:.15em;text-transform:uppercase;font-weight:700}
|
|
1516
|
-
.review .body{color:var(--text);line-height:1.65;font-size:14px;white-space:pre-wrap}
|
|
1517
|
-
.review .rname{color:var(--bone);font-weight:600}
|
|
1518
|
-
.rfoot{margin-top:12px;font-size:11px;color:var(--faint);font-family:ui-monospace,Menlo,Consolas,monospace;border-top:1px solid var(--line);padding-top:8px;line-height:1.5}
|
|
1519
|
-
.verdict{margin-top:12px;padding:10px 12px;border-left:3px solid var(--amber);background:var(--ink3);border-radius:0 8px 8px 0;color:var(--bone);font-size:13.5px;line-height:1.5}
|
|
1520
|
-
.ideas{margin-top:14px}
|
|
1521
|
-
.ideas h4{margin:0 0 6px;color:var(--amber);font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:700}
|
|
1522
|
-
.idea{display:flex;align-items:flex-start;gap:9px;padding:5px 0;font-size:13.5px;color:var(--text)}
|
|
1523
|
-
.idea input{margin-top:3px;flex:none;width:15px;height:15px;cursor:pointer}
|
|
1524
|
-
.task{display:flex;align-items:center;gap:10px;padding:9px 11px;border:1px solid var(--line);border-radius:9px;margin-top:8px;background:var(--ink2)}
|
|
1525
|
-
.task input[type=checkbox]{width:16px;height:16px;flex:none;cursor:pointer}
|
|
1526
|
-
.task .t{flex:1}.task.done .t{color:var(--faint);text-decoration:line-through}
|
|
1527
|
-
.task .rp{font-size:11px;color:var(--faint);background:var(--ink3);border:1px solid var(--line);border-radius:999px;padding:2px 8px}
|
|
1528
|
-
.task .rm{background:none;border:0;color:var(--faint);cursor:pointer;font-size:18px;line-height:1}
|
|
1529
|
-
.task .rm:hover{color:var(--amber)}
|
|
1530
|
-
.tgroup{margin:14px 0 4px;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--green);font-weight:700}
|
|
1531
|
-
.tcount{color:var(--faint);font-weight:400}
|
|
1532
|
-
.taskfilter{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:10px 0 2px}
|
|
1533
|
-
.fl{font-size:10px;color:var(--faint);text-transform:uppercase;letter-spacing:.12em;margin:0 2px 0 8px}
|
|
1534
|
-
.fchip{font-size:12px;padding:4px 10px;border-radius:999px;border:1px solid var(--line);background:var(--ink3);color:var(--text);cursor:pointer;font:inherit}
|
|
1535
|
-
.fchip.on{background:var(--green-dim);border-color:#2a6b52;color:var(--green)}
|
|
1536
|
-
.task .rm.restore{color:var(--green);font-size:15px}
|
|
1537
|
-
.rcard{border:1px solid var(--amber);border-radius:11px;padding:10px 12px;margin-top:8px}
|
|
1538
|
-
.rcard .rhead{display:flex;gap:8px;align-items:baseline;flex-wrap:wrap}.rcard .rhead .muted{font-size:12px}
|
|
1539
|
-
.rcard .row{margin-top:8px}.rcard .task{margin-top:6px}
|
|
1540
|
-
.rdiff{max-height:360px;overflow:auto;font-size:11px;line-height:1.45;background:var(--ink2);border:1px solid var(--line);border-radius:8px;padding:8px;margin-top:8px;white-space:pre}
|
|
1541
|
-
.drift{border:1px solid var(--line);border-radius:10px;margin-top:10px;padding:12px 14px;background:var(--ink2)}
|
|
1542
|
-
.drift .dh{display:flex;gap:8px;align-items:center}
|
|
1543
|
-
.drift .dn{color:var(--bone);font-weight:600}
|
|
1544
|
-
.drift .dd{color:var(--faint);font-size:12px}
|
|
1545
|
-
.drift ul{margin:8px 0 0;padding:0;list-style:none}
|
|
1546
|
-
.drift li{padding:3px 0;font-size:13.5px}
|
|
1547
|
-
.drift li.warn{color:var(--amber)}
|
|
1548
|
-
.drift .ev{color:var(--faint);font-family:ui-monospace,Menlo,monospace;font-size:11px}
|
|
1549
|
-
.dot-w{width:9px;height:9px;border-radius:50%;background:var(--amber);display:inline-block}
|
|
1550
|
-
.dot-c{width:9px;height:9px;border-radius:50%;background:var(--green);display:inline-block}
|
|
1551
|
-
.agent{border:1px solid var(--line);border-radius:10px;margin-top:10px;padding:12px 14px;background:var(--ink2)}
|
|
1552
|
-
.adot{width:10px;height:10px;border-radius:50%;display:inline-block;flex:none}
|
|
1553
|
-
.adot.run{background:var(--amber);animation:pulse 1s ease-in-out infinite}
|
|
1554
|
-
.adot.ok{background:var(--green)} .adot.fail{background:#E5695B}
|
|
1555
|
-
.changed{margin-top:8px;font-size:12.5px;color:var(--bone);background:var(--green-dim);border:1px solid #2a6b52;border-radius:8px;padding:7px 10px}
|
|
1556
|
-
.changed.muted{color:var(--faint);background:var(--ink3);border-color:var(--line)}
|
|
1557
|
-
.changed b{color:var(--green)}
|
|
1558
|
-
.commits{margin-top:6px;font-size:12px;color:var(--text);font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
1559
|
-
.commits code{color:var(--amber);margin-right:6px}
|
|
1560
|
-
@keyframes pulse{0%,100%{opacity:1;box-shadow:0 0 0 0 rgba(242,165,65,.5)}50%{opacity:.4;box-shadow:0 0 0 5px rgba(242,165,65,0)}}
|
|
1561
|
-
.alogout{white-space:pre-wrap;background:var(--ink);border:1px solid var(--line);border-radius:8px;padding:10px;margin-top:10px;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px;line-height:1.5;color:var(--text);max-height:260px;overflow:auto}
|
|
1562
|
-
.bar{height:3px;background:var(--green-dim);border-radius:2px;overflow:hidden;margin-top:8px}
|
|
1563
|
-
.bar > i{display:block;height:100%;width:35%;background:var(--green);border-radius:2px;animation:slide 1.3s ease-in-out infinite}
|
|
1564
|
-
@keyframes slide{0%{margin-left:-35%}100%{margin-left:100%}}
|
|
1565
|
-
.out2{white-space:pre-wrap;background:var(--ink);border:1px solid var(--line);border-radius:9px;padding:10px;margin-top:10px;font-size:13px;line-height:1.55;color:var(--bone)}
|
|
1566
|
-
.legend{display:flex;gap:14px;align-items:center;margin-top:10px;font-size:12px;color:var(--faint);flex-wrap:wrap}
|
|
1567
|
-
.lg{display:inline-flex;gap:6px;align-items:center}
|
|
1568
|
-
.lg i{width:10px;height:10px;border-radius:50%;display:inline-block}
|
|
1569
|
-
@media(max-width:760px){.maprow{flex-direction:column}.detail{width:auto;max-height:none}}
|
|
1570
|
-
</style></head><body>
|
|
1571
|
-
<div id="updatebar" class="updatebar"></div>
|
|
1572
|
-
<header><span class="dot"></span><span class="brand">Symbiot</span><span class="ver" id="ver"></span><span class="status" id="status">...</span></header>
|
|
1573
|
-
<div class="tabs">
|
|
1574
|
-
<button class="tab active" data-tab="map">Map</button>
|
|
1575
|
-
<button class="tab" data-tab="drift">Drift</button>
|
|
1576
|
-
<button class="tab" data-tab="week">Week</button>
|
|
1577
|
-
<button class="tab" data-tab="standup">Standup</button>
|
|
1578
|
-
<button class="tab" data-tab="todo">Todo</button>
|
|
1579
|
-
<button class="tab" data-tab="tasks">Tasks</button>
|
|
1580
|
-
<button class="tab" data-tab="agents">Agents</button>
|
|
1581
|
-
<button class="tab" data-tab="settings">Settings</button>
|
|
1582
|
-
</div>
|
|
1583
|
-
<main>
|
|
1584
|
-
<section id="panel-map">
|
|
1585
|
-
<div class="profile muted" id="profile">Mapping your work…</div>
|
|
1586
|
-
<div class="maprow">
|
|
1587
|
-
<svg id="graph" viewBox="0 0 960 620" preserveAspectRatio="xMidYMid meet"></svg>
|
|
1588
|
-
<aside id="detail" class="detail hidden"></aside>
|
|
1589
|
-
</div>
|
|
1590
|
-
<div id="mapbar" class="mapbar">Hover a node for a summary · click for details · drag to move</div>
|
|
1591
|
-
<div class="legend">
|
|
1592
|
-
<span class="lg"><i style="background:var(--green)"></i>you</span>
|
|
1593
|
-
<span class="lg"><i style="background:var(--bone)"></i>repos</span>
|
|
1594
|
-
<span class="lg"><i style="background:var(--amber)"></i>languages</span>
|
|
1595
|
-
<span class="lg"><i style="background:#6bb3ff"></i>tools</span>
|
|
1596
|
-
<span class="lg"><i style="background:#c58af9"></i>agents</span>
|
|
1597
|
-
<span class="lg"><i style="background:#5fe3b0"></i>AI</span>
|
|
1598
|
-
<span class="lg"><i style="background:#b7a98c"></i>folders (no git)</span>
|
|
1599
|
-
<span class="muted" style="margin-left:8px">scroll to zoom · drag to pan · click a node</span>
|
|
1600
|
-
<button class="ghost" id="remap" style="margin-left:auto">Rescan</button>
|
|
1601
|
-
</div>
|
|
1602
|
-
<div id="review" class="review hidden"></div>
|
|
1603
|
-
</section>
|
|
1604
|
-
<section id="panel-run" class="hidden">
|
|
1605
|
-
<div class="row"><button class="act" id="write">Write my <span id="what">week</span></button>
|
|
1606
|
-
<button class="ghost hidden" id="copy">Copy</button>
|
|
1607
|
-
<span class="muted">Reads your local git and writes it up.</span></div>
|
|
1608
|
-
<div class="out muted" id="out">Nothing yet - hit the button.</div>
|
|
1609
|
-
<div class="rfoot" id="outfoot" style="display:none"></div>
|
|
1610
|
-
</section>
|
|
1611
|
-
<section id="panel-tasks" class="hidden">
|
|
1612
|
-
<div class="row"><input id="newtask" placeholder="Add a task..." style="flex:1"><button class="act" id="addtask">Add</button><button class="ghost" id="pushtasks" title="Write .symbiot/TASKS.md into each repo for your coding agent">Send to repos</button></div>
|
|
1613
|
-
<div class="note muted" style="margin-top:2px">To give tasks to your agent, use <b>Send to repos</b> — filter by tag below to choose which. As the agent finishes each one it lands in <b>Awaiting your review</b>: <b>Approve</b> commits it on a branch and opens a PR, <b>↶</b> sends it back. Ticking a task yourself marks it done (it auto-archives).</div>
|
|
1614
|
-
<div id="pushout"></div>
|
|
1615
|
-
<div id="reviewout"></div>
|
|
1616
|
-
<div id="reviewlist"></div>
|
|
1617
|
-
<div id="taskfilter" class="taskfilter"></div>
|
|
1618
|
-
<div id="tasklist"></div>
|
|
1619
|
-
</section>
|
|
1620
|
-
<section id="panel-agents" class="hidden">
|
|
1621
|
-
<div class="row"><span class="muted">Agents Symbiot has handed work to — live status and output.</span><button class="ghost" id="agentsrefresh" style="margin-left:auto">Refresh</button></div>
|
|
1622
|
-
<div id="agentslist"></div>
|
|
1623
|
-
</section>
|
|
1624
|
-
<section id="panel-drift" class="hidden">
|
|
1625
|
-
<div class="row"><span class="muted">What's out of sync, stuck or at risk across your repos — local git facts.</span>
|
|
1626
|
-
<label class="muted" style="margin-left:auto"><input type="checkbox" id="driftfetch"> fetch latest</label>
|
|
1627
|
-
<label class="muted"><input type="checkbox" id="driftci"> check CI (needs gh)</label>
|
|
1628
|
-
<button class="ghost" id="driftrun">Rescan</button></div>
|
|
1629
|
-
<div id="driftout"></div>
|
|
1630
|
-
</section>
|
|
1631
|
-
<section id="panel-settings" class="hidden">
|
|
1632
|
-
<label>Which AI should Symbiot write with?</label>
|
|
1633
|
-
<select id="provider">
|
|
1634
|
-
<option value="anthropic">Claude (Anthropic)</option>
|
|
1635
|
-
<option value="openai">OpenAI (GPT)</option>
|
|
1636
|
-
<option value="gemini">Gemini (Google)</option>
|
|
1637
|
-
<option value="ollama">Local model (Ollama) - free, no key</option>
|
|
1638
|
-
</select>
|
|
1639
|
-
<div class="note muted">The Map above needs no key. For the write-ups, a hosted model needs an API key - or run <b>Ollama</b> locally for a free, private option (nothing leaves your machine).</div>
|
|
1640
|
-
<div id="keyWrap"><label>API key</label><input id="key" type="password" placeholder="paste your key">
|
|
1641
|
-
<div class="note"><a id="getkey">Where do I get a key?</a></div></div>
|
|
1642
|
-
<div id="baseWrap" class="hidden"><label>Ollama URL</label><input id="baseUrl" type="text" value="http://localhost:11434"></div>
|
|
1643
|
-
<label>Model <span class="muted" id="modelHint"></span></label>
|
|
1644
|
-
<input id="model" type="text" placeholder="(blank = default)">
|
|
1645
|
-
<div class="row" style="margin-top:16px"><button class="act" id="save">Save & connect</button>
|
|
1646
|
-
<span class="note" id="saveMsg"></span></div>
|
|
1647
|
-
<div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
|
|
1648
|
-
<label>Folders to scan for repos</label>
|
|
1649
|
-
<div id="scanroots"></div>
|
|
1650
|
-
<div class="row" style="margin-top:6px"><input id="newroot" placeholder="/path/to/folder (or ~/work) — where your projects live" style="flex:1"><button class="ghost" id="addroot">Add folder</button></div>
|
|
1651
|
-
<div class="note muted" id="scanrootnote">Point Symbiot at where your work lives — inside or outside your home folder. Defaults to your home folder.</div>
|
|
1652
|
-
</div>
|
|
1653
|
-
<div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
|
|
1654
|
-
<label>Hand off to your agent when you "Send to repos"</label>
|
|
1655
|
-
<input id="agentcmd" type="text" placeholder="e.g. claude -p "{prompt}" · code {dir} · leave blank to just write the file">
|
|
1656
|
-
<div class="note muted">Runs in each repo after tasks are written. Use <b>{dir}</b> = repo path, <b>{prompt}</b> = the task instruction. Works with any agent or editor — it's your command.</div>
|
|
1657
|
-
<div id="agentpresets" style="margin-top:8px"></div>
|
|
1658
|
-
</div>
|
|
1659
|
-
<div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
|
|
1660
|
-
<button class="ghost" id="recbtn">Recommend models for my machine</button>
|
|
1661
|
-
<button class="ghost" id="setuplocal" style="margin-left:8px">Set up a free local model</button>
|
|
1662
|
-
<div id="setupout" class="note muted" style="margin-top:10px"></div>
|
|
1663
|
-
<div id="recout" style="margin-top:12px"></div>
|
|
1664
|
-
</div>
|
|
1665
|
-
</section>
|
|
1666
|
-
</main>
|
|
1667
|
-
<footer><button class="ghost" id="quit" style="margin-left:auto">Quit</button></footer>
|
|
1668
|
-
<script>
|
|
1669
|
-
var T=new URLSearchParams(window.location.search).get('t')||'';
|
|
1670
|
-
function api(path,body){return fetch(path,{method:body?'POST':'GET',headers:{'x-symbiot-token':T,'content-type':'application/json'},body:body?JSON.stringify(body):undefined}).then(function(r){return r.json();});}
|
|
1671
|
-
var KEYURL={anthropic:'https://console.anthropic.com/settings/keys',openai:'https://platform.openai.com/api-keys',gemini:'https://aistudio.google.com/apikey',ollama:'https://ollama.com'};
|
|
1672
|
-
var DEFMODEL={anthropic:'claude-opus-5-5',openai:'gpt-4o-mini',gemini:'gemini-1.5-flash',ollama:'llama3.1'};
|
|
1673
|
-
function $(id){return document.getElementById(id);}
|
|
1674
|
-
var current='map';var mapLoaded=false;var driftLoaded=false;
|
|
1675
|
-
var COLORS={person:'#3DDC97',repo:'#F4F1EA',lang:'#F2A541',tool:'#6bb3ff',agent:'#c58af9',ai:'#5fe3b0',folder:'#b7a98c'};
|
|
1676
|
-
function tabs(){return document.querySelectorAll('.tab');}
|
|
1677
|
-
function setTab(tab){current=tab;tabs().forEach(function(t){t.classList.toggle('active',t.dataset.tab===tab);});
|
|
1678
|
-
var isMap=tab==='map',isSet=tab==='settings',isTasks=tab==='tasks',isDrift=tab==='drift',isAgents=tab==='agents',isRun=(tab==='week'||tab==='standup'||tab==='todo');
|
|
1679
|
-
$('panel-map').classList.toggle('hidden',!isMap);
|
|
1680
|
-
$('panel-run').classList.toggle('hidden',!isRun);
|
|
1681
|
-
$('panel-settings').classList.toggle('hidden',!isSet);
|
|
1682
|
-
$('panel-tasks').classList.toggle('hidden',!isTasks);
|
|
1683
|
-
$('panel-drift').classList.toggle('hidden',!isDrift);
|
|
1684
|
-
$('panel-agents').classList.toggle('hidden',!isAgents);
|
|
1685
|
-
if(isRun){$('what').textContent=tab;$('out').textContent='Nothing yet - hit the button.';$('out').classList.add('muted');$('copy').classList.add('hidden');}
|
|
1686
|
-
if(isMap&&!mapLoaded)loadMap();
|
|
1687
|
-
if(isTasks)loadTasks();
|
|
1688
|
-
if(isDrift&&!driftLoaded)loadDrift();
|
|
1689
|
-
if(isAgents)loadAgents(); else stopAgentsPoll();}
|
|
1690
|
-
tabs().forEach(function(t){t.addEventListener('click',function(){setTab(t.dataset.tab);});});
|
|
1691
|
-
function refresh(){api('/api/status').then(function(s){$('status').textContent=s.connected?s.line:'Not connected - open Settings';});}
|
|
1692
|
-
$('write').addEventListener('click',function(){$('out').textContent='Writing...';$('out').classList.add('muted');$('copy').classList.add('hidden');
|
|
1693
|
-
api('/api/run',{cmd:current}).then(function(r){var f=$('outfoot');if(r.error==='not-connected'){$('out').textContent='Not connected yet - open Settings and pick an AI.';f.style.display='none';return;}
|
|
1694
|
-
$('out').textContent=r.text||'(no output)';$('out').classList.remove('muted');$('copy').classList.remove('hidden');if(r.footer){f.textContent=r.footer;f.style.display='block';}else{f.style.display='none';}});});
|
|
1695
|
-
$('copy').addEventListener('click',function(){navigator.clipboard.writeText($('out').textContent);$('copy').textContent='Copied';setTimeout(function(){$('copy').textContent='Copy';},1400);});
|
|
1696
|
-
function syncP(){var p=$('provider').value;var local=p==='ollama';$('keyWrap').classList.toggle('hidden',local);$('baseWrap').classList.toggle('hidden',!local);
|
|
1697
|
-
$('getkey').textContent=local?'About Ollama':'Where do I get a key?';$('modelHint').textContent='(default '+DEFMODEL[p]+')';$('model').placeholder='(blank = '+DEFMODEL[p]+')';}
|
|
1698
|
-
$('provider').addEventListener('change',syncP);
|
|
1699
|
-
$('getkey').addEventListener('click',function(){window.open(KEYURL[$('provider').value],'_blank');});
|
|
1700
|
-
$('save').addEventListener('click',function(){var p=$('provider').value;$('saveMsg').textContent='checking...';$('saveMsg').className='note muted';
|
|
1701
|
-
var cfg={provider:p,model:$('model').value.trim()};if(p==='ollama')cfg.baseUrl=$('baseUrl').value.trim();else cfg.key=$('key').value.trim();
|
|
1702
|
-
api('/api/connect',cfg).then(function(r){$('saveMsg').textContent=r.message||(r.ok?'Connected.':'Could not connect.');$('saveMsg').className='note '+(r.ok?'ok':'err');if(r.ok){$('key').value='';refresh();}});});
|
|
1703
|
-
$('quit').addEventListener('click',function(){api('/api/quit');document.body.innerHTML='<div style=\\'padding:40px;color:#7E9690;font-family:sans-serif\\'>Symbiot stopped. You can close this window.</div>';});
|
|
1704
|
-
var GRAPH=null,sel=null,view={k:1,x:0,y:0},GW=960,GH=620;
|
|
1705
|
-
function esc(s){return String(s).replace(/[&<>]/g,function(ch){return ch==='&'?'&':ch==='<'?'<':'>';});}
|
|
1706
|
-
function layout(nodes,edges){var idx={};nodes.forEach(function(n){n.x=GW/2+(Math.random()-0.5)*GW*0.8;n.y=GH/2+(Math.random()-0.5)*GH*0.8;n.vx=0;n.vy=0;idx[n.id]=n;});
|
|
1707
|
-
for(var it=0;it<340;it++){for(var i=0;i<nodes.length;i++)for(var j=i+1;j<nodes.length;j++){var a=nodes[i],b=nodes[j];var dx=a.x-b.x,dy=a.y-b.y;var d2=dx*dx+dy*dy+0.01;var d=Math.sqrt(d2);var f=4600/d2;a.vx+=f*dx/d;a.vy+=f*dy/d;b.vx-=f*dx/d;b.vy-=f*dy/d;}
|
|
1708
|
-
edges.forEach(function(e){var a=idx[e.source],b=idx[e.target];if(!a||!b)return;var dx=b.x-a.x,dy=b.y-a.y;var d=Math.sqrt(dx*dx+dy*dy)+0.01;var f=(d-115)*0.03;a.vx+=f*dx/d;a.vy+=f*dy/d;b.vx-=f*dx/d;b.vy-=f*dy/d;});
|
|
1709
|
-
nodes.forEach(function(n){n.vx+=(GW/2-n.x)*0.002;n.vy+=(GH/2-n.y)*0.002;n.x+=Math.max(-9,Math.min(9,n.vx));n.y+=Math.max(-9,Math.min(9,n.vy));n.vx*=0.86;n.vy*=0.86;n.x=Math.max(30,Math.min(GW-30,n.x));n.y=Math.max(24,Math.min(GH-28,n.y));});}}
|
|
1710
|
-
function nbrs(id){var s={};s[id]=1;GRAPH.edges.forEach(function(e){if(e.source===id)s[e.target]=1;if(e.target===id)s[e.source]=1;});return s;}
|
|
1711
|
-
function render(){if(!GRAPH)return;var idx={};GRAPH.nodes.forEach(function(n){idx[n.id]=n;});var nb=sel?nbrs(sel):null;
|
|
1712
|
-
var s="<g id='vp' transform='translate("+view.x.toFixed(1)+","+view.y.toFixed(1)+") scale("+view.k.toFixed(3)+")'>";
|
|
1713
|
-
GRAPH.edges.forEach(function(e){var a=idx[e.source],b=idx[e.target];if(!a||!b)return;var op=nb?((nb[e.source]&&nb[e.target])?0.75:0.06):0.45;s+="<line x1='"+a.x.toFixed(1)+"' y1='"+a.y.toFixed(1)+"' x2='"+b.x.toFixed(1)+"' y2='"+b.y.toFixed(1)+"' stroke='#24404A' stroke-width='1' opacity='"+op+"'/>";});
|
|
1714
|
-
GRAPH.nodes.forEach(function(n){var r=Math.max(5,Math.sqrt(n.weight)*2);var col=COLORS[n.type]||"#888";var op=nb?(nb[n.id]?1:0.14):0.95;var st=(n.id===sel)?" stroke='#F4F1EA' stroke-width='2'":"";var title=esc(n.label)+(n.meta?(" - "+(n.meta.commits||0)+" commits"):"");
|
|
1715
|
-
s+="<g class='node' data-id='"+esc(n.id)+"' opacity='"+op+"'><circle cx='"+n.x.toFixed(1)+"' cy='"+n.y.toFixed(1)+"' r='"+r.toFixed(1)+"' fill='"+col+"'"+st+"><title>"+title+"</title></circle>";
|
|
1716
|
-
var cls=n.type==="person"?"lbl-me":"";s+="<text x='"+n.x.toFixed(1)+"' y='"+(n.y+r+12).toFixed(1)+"' text-anchor='middle' class='"+cls+"'>"+esc(n.label)+"</text></g>";});
|
|
1717
|
-
document.getElementById("graph").innerHTML=s+"</g>";}
|
|
1718
|
-
function profileLine(g){var repos=g.nodes.filter(function(n){return n.type==="repo";});var base=g.stats.base||"your home folder";if(!repos.length)return "No git repositories found under "+esc(base)+" yet.";var langs=g.nodes.filter(function(n){return n.type==="lang";}).map(function(n){return n.label;});var top=repos.slice().sort(function(a,b){return (b.meta.commits||0)-(a.meta.commits||0);})[0];var s="Scanned <b>"+esc(base)+"</b> · <b>"+g.stats.repos+"</b> repos · <b>"+g.stats.commits+"</b> of your commits · <b>"+(g.stats.files||0)+"</b> files · ";s+=langs.length?("mostly <b>"+esc(langs.slice(0,3).join(", "))+"</b>"):"no languages detected";if(top)s+=" · most active: <b>"+esc(top.label)+"</b>";return s;}
|
|
1719
|
-
function nodeById(id){for(var i=0;i<GRAPH.nodes.length;i++)if(GRAPH.nodes[i].id===id)return GRAPH.nodes[i];return null;}
|
|
1720
|
-
function summarize(n){if(!n)return "";if(n.type==='repo'){var m=n.meta||{};var bits=[m.commits+' commits'];if(m.branch)bits.push('branch '+m.branch);if(m.last)bits.push('last '+m.last);if(typeof m.files==='number')bits.push(m.files+' files');if(m.langs&&m.langs.length)bits.push(m.langs.join(', '));if(m.tools&&m.tools.length)bits.push(m.tools.join(', '));return "<b>"+esc(n.label)+"</b> "+esc(m.path||'')+" — "+esc(bits.join(' · '));}if(n.type==='lang')return "<b>"+esc(n.label)+"</b> — language (click to see repos)";if(n.type==='tool')return "<b>"+esc(n.label)+"</b> — tool (click to see repos)";if(n.type==='person')return "<b>"+esc(n.label)+"</b> — you";return "<b>"+esc(n.label)+"</b>";}
|
|
1721
|
-
function updateBar(id){var bar=document.getElementById('mapbar');if(!bar)return;var n=id?nodeById(id):(sel?nodeById(sel):null);bar.innerHTML=n?summarize(n):"Hover a node for a summary · click for details · drag to move";}
|
|
1722
|
-
function hideDetail(){document.getElementById('detail').classList.add('hidden');}
|
|
1723
|
-
function showDetail(d){var el=document.getElementById('detail');el.classList.remove('hidden');
|
|
1724
|
-
if(d.error){el.innerHTML="<h3>—</h3><div class='k'>"+esc(d.error)+"</div>";return;}
|
|
1725
|
-
if(d.type==='repo'){var chips="";['branch: '+(d.branch||'?'),d.commits+' commits',(d.dirty?d.dirty+' uncommitted':'clean'),(d.last?'last '+d.last:'')].concat(d.langs||[]).concat(d.tools||[]).forEach(function(x){if(x)chips+="<span class='tag'>"+esc(x)+"</span>";});
|
|
1726
|
-
el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='chips'>"+chips+"</div><button class='act' id='suggest' style='margin-top:12px'>Suggest next steps</button><div id='sugout'></div>";
|
|
1727
|
-
document.getElementById('suggest').addEventListener('click',function(){var o=document.getElementById('sugout');o.innerHTML="<div class='out2'>Thinking...</div>";api('/api/suggest',{path:d.path}).then(function(r){if(r.error==='not-connected'){o.innerHTML="<div class='out2'>Connect a model in Settings to get suggestions - Ollama is free and runs locally.</div>";return;}o.innerHTML="<div class='out2'>"+esc(r.text||'(no output)')+"</div>"+(r.footer?"<div class='rfoot'>"+esc(r.footer)+"</div>":"");});});return;}
|
|
1728
|
-
if(d.type==='lang'||d.type==='tool'){var lis=(d.repos||[]).map(function(r){return "<li>"+esc(r)+"</li>";}).join("");el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>Used in "+((d.repos||[]).length)+" repos</div><ul>"+lis+"</ul>";return;}
|
|
1729
|
-
if(d.type==='agent'){var m=d.meta||{};el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>"+(m.kind==='agent'?'AI coding agent you have installed':'editor you have installed')+"</div>"+(m.cmd?"<div class='out2' style='font-size:11px'>"+esc(m.cmd)+"</div>":"");return;}
|
|
1730
|
-
if(d.type==='ai'){var m=d.meta||{};el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>Currently powering Symbiot · model <b>"+esc(m.model||'?')+"</b>"+(m.source?" · via "+esc(m.source):"")+"</div>";return;}
|
|
1731
|
-
if(d.type==='folder'){var chips='';(d.langs||[]).concat(d.tools||[]).forEach(function(x){chips+="<span class='tag'>"+esc(x)+"</span>";});el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>project folder · not a git repo · "+(d.files||0)+" files</div><div class='chips'>"+chips+"</div><div class='k' style='font-family:ui-monospace,monospace'>"+esc(d.path||'')+"</div><div class='k'>Tasks can be sent here too — the agent runs in this folder.</div>";return;}
|
|
1732
|
-
if(d.type==='person'){var st=d.stats||{};el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>"+st.repos+" repos · "+st.commits+" commits · "+st.languages+" languages · "+st.tools+" tools"+(st.agents?" · "+st.agents+" agents/editors":"")+"</div>";return;}}
|
|
1733
|
-
var reviewCache={};var IDEAS=[];var IREPO="";
|
|
1734
|
-
function hideReview(){var el=document.getElementById('review');el.classList.add('hidden');el.innerHTML='';}
|
|
1735
|
-
function reviewHtml(name,body,verdict,ideas,tasks,footer){IDEAS=ideas||[];IREPO=name;
|
|
1736
|
-
var h="<h4>AI review · <span class='rname'>"+esc(name)+"</span></h4><div class='body'>"+esc(body)+"</div>";
|
|
1737
|
-
if(verdict)h+="<div class='verdict'>⚖️ "+esc(verdict)+"</div>";
|
|
1738
|
-
if(ideas&&ideas.length){h+="<div class='ideas'><h4>Ideas worth doing · tick to add to Tasks</h4>";
|
|
1739
|
-
ideas.forEach(function(idea,i){var t=(tasks||[]).filter(function(x){return x.text===idea&&x.repo===name;})[0];var tid=t?t.id:"";
|
|
1740
|
-
h+="<label class='idea'><input type='checkbox' class='ideachk' data-idx='"+i+"' data-tid='"+esc(tid)+"'"+(t?" checked":"")+"><span>"+esc(idea)+"</span></label>";});
|
|
1741
|
-
h+="</div>";}
|
|
1742
|
-
else h+="<div class='muted' style='margin-top:10px'>No new features suggested — the verdict above is the call.</div>";
|
|
1743
|
-
if(footer)h+="<div class='rfoot'>"+esc(footer)+"</div>";
|
|
1744
|
-
return h;}
|
|
1745
|
-
function wireIdeas(){document.querySelectorAll('.ideachk').forEach(function(cb){cb.addEventListener('change',function(){
|
|
1746
|
-
var idea=IDEAS[+cb.getAttribute('data-idx')];if(idea==null)return;
|
|
1747
|
-
if(cb.checked){api('/api/tasks/add',{text:idea,repo:IREPO}).then(function(it){if(it&&it.id)cb.setAttribute('data-tid',it.id);});}
|
|
1748
|
-
else{var id=cb.getAttribute('data-tid');if(id){api('/api/tasks/remove',{id:id}).then(function(){cb.setAttribute('data-tid','');});}}});});}
|
|
1749
|
-
function loadReview(name,path){var el=document.getElementById('review');el.classList.remove('hidden');
|
|
1750
|
-
if(reviewCache[path]){var c=reviewCache[path];api('/api/tasks').then(function(tasks){el.innerHTML=reviewHtml(name,c.text,c.verdict,c.ideas,tasks,c.footer);wireIdeas();});return;}
|
|
1751
|
-
el.innerHTML="<h4>AI review · <span class='rname'>"+esc(name)+"</span></h4><div class='body'>Reading the project…</div>";
|
|
1752
|
-
Promise.all([api('/api/review',{path:path}),api('/api/tasks')]).then(function(res){var r=res[0]||{},tasks=res[1]||[];
|
|
1753
|
-
if(r.error==='not-connected'){el.innerHTML="<h4>AI review · <span class='rname'>"+esc(name)+"</span></h4><div class='body'>Connect a model in Settings to get a review - Ollama is free and runs locally.</div>"+(r.footer?"<div class='rfoot'>"+esc(r.footer)+"</div>":"");return;}
|
|
1754
|
-
var body=r.text||'(no output)';reviewCache[path]={text:body,verdict:r.verdict||"",ideas:r.ideas||[],footer:r.footer};el.innerHTML=reviewHtml(name,body,r.verdict||"",r.ideas||[],tasks,r.footer);wireIdeas();});}
|
|
1755
|
-
var ALLTASKS=[];var TFILTER={type:'',repo:''};var TARCH=false;
|
|
1756
|
-
var TORDER=['Fixes','Tests & CI','Security','Performance','Refactor','UI/UX','Docs','Features & other'];
|
|
1757
|
-
function taskRow(t,arch){return "<div class='task"+(t.done?" done":"")+"' data-id='"+esc(t.id)+"'>"+(arch?"":"<input type='checkbox' class='taskchk' title='mark done (auto-archives on sync)'"+(t.done?" checked":"")+">")+"<span class='t'>"+esc(t.text)+"</span>"+(t.repo?"<span class='rp'>"+esc(t.repo)+"</span>":"")+(arch?"<button class='rm restore' title='restore to active'>↶</button>":"<button class='rm' title='remove'>×</button>")+"</div>";}
|
|
1758
|
-
function wireTaskRows(el){el.querySelectorAll('.task').forEach(function(row){var id=row.getAttribute('data-id');
|
|
1759
|
-
var cb=row.querySelector('.taskchk');if(cb)cb.addEventListener('change',function(){api('/api/tasks/toggle',{id:id}).then(function(){row.classList.toggle('done');});});
|
|
1760
|
-
var rm=row.querySelector('.rm');if(rm&&rm.classList.contains('restore'))rm.addEventListener('click',function(){api('/api/tasks/restore',{id:id}).then(loadArchived);});
|
|
1761
|
-
else if(rm)rm.addEventListener('click',function(){api('/api/tasks/remove',{id:id}).then(function(){row.remove();});});});}
|
|
1762
|
-
function fchip(dim,val,label){var on=(TFILTER[dim]||'')===val;return "<button class='fchip"+(on?' on':'')+"' data-dim='"+dim+"' data-val='"+esc(val)+"'>"+esc(label)+"</button>";}
|
|
1763
|
-
function renderFilter(){var box=document.getElementById('taskfilter');
|
|
1764
|
-
if(TARCH){box.innerHTML="<button class='fchip on' id='archtoggle'>↩ back to active</button>";document.getElementById('archtoggle').addEventListener('click',function(){loadTasks();});return;}
|
|
1765
|
-
var types={},repos={};ALLTASKS.forEach(function(t){types[t.type||'Features & other']=1;if(t.repo)repos[t.repo]=1;});
|
|
1766
|
-
var fb="<span class='fl'>Type</span>"+fchip('type','','All')+Object.keys(types).sort(function(a,b){return TORDER.indexOf(a)-TORDER.indexOf(b);}).map(function(t){return fchip('type',t,t);}).join('');
|
|
1767
|
-
fb+="<span class='fl'>Repo</span>"+fchip('repo','','All')+Object.keys(repos).sort().map(function(r){return fchip('repo',r,r);}).join('');
|
|
1768
|
-
fb+="<button class='fchip' id='archtoggle' style='margin-left:auto'>🗃 archived</button>";
|
|
1769
|
-
box.innerHTML=fb;
|
|
1770
|
-
box.querySelectorAll('.fchip[data-dim]').forEach(function(c){c.addEventListener('click',function(){TFILTER[c.getAttribute('data-dim')]=c.getAttribute('data-val');renderTasks();});});
|
|
1771
|
-
document.getElementById('archtoggle').addEventListener('click',loadArchived);}
|
|
1772
|
-
function renderTasks(){var el=document.getElementById('tasklist');TARCH=false;renderFilter();
|
|
1773
|
-
var list=ALLTASKS.filter(function(t){return !t.review&&(!TFILTER.type||(t.type||'Features & other')===TFILTER.type)&&(!TFILTER.repo||t.repo===TFILTER.repo);});
|
|
1774
|
-
if(!list.length){el.innerHTML="<div class='muted' style='margin-top:12px'>"+(ALLTASKS.length?"Nothing matches this filter.":"No tasks yet. Tick ideas in a repo review, or add one above.")+"</div>";return;}
|
|
1775
|
-
var groups={};list.forEach(function(t){var ty=t.type||'Features & other';(groups[ty]=groups[ty]||[]).push(t);});
|
|
1776
|
-
var keys=Object.keys(groups).sort(function(a,b){var ia=TORDER.indexOf(a),ib=TORDER.indexOf(b);return (ia<0?99:ia)-(ib<0?99:ib);});
|
|
1777
|
-
var h="";keys.forEach(function(ty){h+="<div class='tgroup'>"+esc(ty)+" <span class='tcount'>"+groups[ty].length+"</span></div>";groups[ty].forEach(function(t){h+=taskRow(t,false);});});
|
|
1778
|
-
el.innerHTML=h;wireTaskRows(el);}
|
|
1779
|
-
function loadTasks(){TARCH=false;api('/api/tasks/sync',{}).then(function(){api('/api/tasks').then(function(list){ALLTASKS=list;renderTasks();});loadPending();});}
|
|
1780
|
-
function loadPending(){api('/api/pending').then(function(list){var el=document.getElementById('reviewlist');if(!list||!list.length){el.innerHTML='';return;}
|
|
1781
|
-
var n=0;list.forEach(function(r){n+=r.tasks.length;});
|
|
1782
|
-
var h="<div class='tgroup' style='color:var(--amber)'>Awaiting your review <span class='tcount'>"+n+"</span></div>";
|
|
1783
|
-
list.forEach(function(r){var ch=r.files.length?(r.files.length+" file"+(r.files.length>1?"s":"")+" changed"+(r.stat?" · "+esc(r.stat):"")):"no uncommitted changes";
|
|
1784
|
-
h+="<div class='rcard' data-repo='"+esc(r.repo)+"'><div class='rhead'><b>"+esc(r.repo)+"</b><span class='muted'>"+(r.path?"on "+esc(r.branch||'?')+" · "+ch:"repo not found on disk")+"</span></div>";
|
|
1785
|
-
r.tasks.forEach(function(t){h+="<div class='task' data-id='"+esc(t.id)+"'><span class='t'>"+esc(t.text)+"</span><button class='rm sendback' title='not right - send back to the agent (unticks it)'>↶</button></div>";});
|
|
1786
|
-
h+="<div class='row'><button class='act approve'"+(r.path?"":" disabled")+" title='commit on a branch, push, open a PR, then archive'>Approve → "+(r.files.length?"PR":"archive")+"</button>"+(r.files.length?"<button class='ghost showdiff'>Show diff</button>":"")+"</div><div class='rdiff hidden'></div></div>";});
|
|
1787
|
-
el.innerHTML=h;
|
|
1788
|
-
el.querySelectorAll('.rcard').forEach(function(card){var repo=card.getAttribute('data-repo');
|
|
1789
|
-
card.querySelectorAll('.sendback').forEach(function(b){b.addEventListener('click',function(){api('/api/pending/sendback',{id:b.closest('.task').getAttribute('data-id')}).then(loadTasks);});});
|
|
1790
|
-
var sd=card.querySelector('.showdiff'),pre=card.querySelector('.rdiff');
|
|
1791
|
-
if(sd)sd.addEventListener('click',function(){if(!pre.classList.contains('hidden')){pre.classList.add('hidden');sd.textContent='Show diff';return;}
|
|
1792
|
-
pre.textContent='Loading...';pre.classList.remove('hidden');sd.textContent='Hide diff';api('/api/pending/diff?repo='+encodeURIComponent(repo)).then(function(d){pre.textContent=(d&&d.diff)||'(no changes)';});});
|
|
1793
|
-
var ap=card.querySelector('.approve');if(ap)ap.addEventListener('click',function(){ap.disabled=true;ap.textContent='Committing & pushing...';
|
|
1794
|
-
api('/api/pending/approve',{repo:repo}).then(function(r){var o=document.getElementById('reviewout');
|
|
1795
|
-
if(!r||r.error){ap.disabled=false;ap.textContent='Approve - retry';o.innerHTML="<div class='note err'>"+esc(repo)+": "+esc((r&&r.error)||'failed')+"</div>";return;}
|
|
1796
|
-
var m="✓ <b>"+esc(repo)+"</b>: approved "+r.approved+" task"+(r.approved>1?"s":"");
|
|
1797
|
-
if(r.commit)m+=" · committed <code>"+esc(r.commit)+"</code> on <code>"+esc(r.branch)+"</code>";
|
|
1798
|
-
if(r.pr)m+=" · <a href='"+esc(r.pr)+"' target='_blank' rel='noopener'>open PR</a>";
|
|
1799
|
-
if(r.note)m+="<div class='muted'>"+esc(r.note)+"</div>";
|
|
1800
|
-
o.innerHTML="<div class='note ok'>"+m+"</div>";loadTasks();});});});});}
|
|
1801
|
-
function loadArchived(){TARCH=true;api('/api/tasks?archived=1').then(function(list){ALLTASKS=list;renderFilter();var el=document.getElementById('tasklist');
|
|
1802
|
-
if(!list.length){el.innerHTML="<div class='muted' style='margin-top:12px'>Nothing archived yet. Completed tasks land here.</div>";return;}
|
|
1803
|
-
var h="<div class='tgroup'>Archived <span class='tcount'>"+list.length+"</span></div>";list.forEach(function(t){h+=taskRow(t,true);});el.innerHTML=h;wireTaskRows(el);});}
|
|
1804
|
-
var agentsTimer=null;
|
|
1805
|
-
function stopAgentsPoll(){if(agentsTimer){clearTimeout(agentsTimer);agentsTimer=null;}}
|
|
1806
|
-
function fmtE(ms){var s=Math.floor(ms/1000);if(s<60)return s+'s';var m=Math.floor(s/60);return m+'m '+(s%60)+'s';}
|
|
1807
|
-
function loadAgents(){api('/api/agents').then(function(list){var el=document.getElementById('agentslist');
|
|
1808
|
-
if(!list||!list.length){el.innerHTML="<div class='muted' style='margin-top:12px'>No agents yet. In <b>Tasks</b>, tick ideas and hit <b>Send to repos</b> (with an agent command set in Settings) — you'll watch it work here.</div>";stopAgentsPoll();return;}
|
|
1809
|
-
el.innerHTML=list.map(function(a){var cls=a.status==='running'?'run':(a.status==='done'?'ok':'fail');
|
|
1810
|
-
var st=a.status==='running'?('working · '+fmtE(a.elapsed)):(esc(a.status)+' · '+fmtE(a.elapsed)+(a.exitCode!=null?' · exit '+a.exitCode:''));
|
|
1811
|
-
var b="<div class='dh'><span class='adot "+cls+"'></span><span class='dn'>"+esc(a.name)+"</span><span class='dd'>"+st+"</span></div>";
|
|
1812
|
-
if(a.status==='running')b+="<div class='bar'><i></i></div>";
|
|
1813
|
-
var ch=a.changed;
|
|
1814
|
-
if(ch&&(ch.dirty||ch.stat||(ch.commits&&ch.commits.length))){
|
|
1815
|
-
var parts=[];
|
|
1816
|
-
if(ch.dirty)parts.push('<b>'+ch.dirty+'</b> file'+(ch.dirty>1?'s':'')+' changed');
|
|
1817
|
-
if(ch.stat)parts.push(esc(ch.stat));
|
|
1818
|
-
if(parts.length)b+="<div class='changed'>What it did · "+parts.join(' · ')+"</div>";
|
|
1819
|
-
if(ch.commits&&ch.commits.length)b+="<div class='commits'>"+ch.commits.map(function(c){return "<div><code>"+esc(c.hash)+"</code> "+esc(c.msg)+"</div>";}).join("")+"</div>";
|
|
1820
|
-
}else if(a.status==='done'){
|
|
1821
|
-
b+="<div class='changed muted'>No file changes detected (the agent may have only planned or asked).</div>";
|
|
1822
|
-
}
|
|
1823
|
-
b+="<pre class='alogout'>"+esc((a.tail&&a.tail.trim())||'(waiting for output…)')+"</pre>";
|
|
1824
|
-
return "<div class='agent'>"+b+"</div>";}).join("");
|
|
1825
|
-
el.querySelectorAll('.alogout').forEach(function(p){p.scrollTop=p.scrollHeight;});
|
|
1826
|
-
var anyRunning=list.some(function(a){return a.status==='running';});
|
|
1827
|
-
stopAgentsPoll();if(anyRunning&¤t==='agents')agentsTimer=setTimeout(loadAgents,2000);});}
|
|
1828
|
-
function loadDrift(){var out=document.getElementById('driftout');out.innerHTML="<div class='muted' style='margin-top:12px'>Reading your repos…</div>";
|
|
1829
|
-
var ci=document.getElementById('driftci').checked?'1':'0';var ft=document.getElementById('driftfetch').checked?'1':'0';
|
|
1830
|
-
api('/api/drift?ci='+ci+'&fetch='+ft).then(function(d){driftLoaded=true;var repos=d.repos||[];var risky=repos.filter(function(r){return r.flags.some(function(f){return f.level==='warn';});});
|
|
1831
|
-
var h="<div class='k' style='margin:10px 0'><b>"+repos.length+"</b> repos · <b>"+risky.length+"</b> with risks"+(d.partial?" · <span class='err'>partial — the scan hit its time limit</span>":"")+"</div>";
|
|
1832
|
-
repos.forEach(function(r){if(!r.flags.length)return;var warn=r.flags.some(function(f){return f.level==='warn';});
|
|
1833
|
-
h+="<div class='drift'><div class='dh'><span class='"+(warn?'dot-w':'dot-c')+"'></span><span class='dn'>"+esc(r.name)+"</span><span class='dd'>"+esc(r.def)+(r.fetchAgeDays!=null&&r.fetchAgeDays>3?" · fetch "+r.fetchAgeDays+"d old":"")+"</span></div><ul>";
|
|
1834
|
-
r.flags.forEach(function(f){h+="<li class='"+esc(f.level)+"'>"+(f.level==='warn'?'⚠ ':'· ')+esc(f.text)+(f.evidence?" <span class='ev'>["+esc(f.evidence)+"]</span>":"")+"</li>";});
|
|
1835
|
-
h+="</ul></div>";});
|
|
1836
|
-
var clean=repos.filter(function(r){return !r.flags.length;}).map(function(r){return r.name;});
|
|
1837
|
-
if(clean.length)h+="<div class='muted' style='margin-top:10px'>clean: "+esc(clean.join(", "))+"</div>";
|
|
1838
|
-
if(!repos.length)h+="<div class='muted'>No repos found under your home folder.</div>";
|
|
1839
|
-
out.innerHTML=h;});}
|
|
1840
|
-
function addTaskUI(){var i=document.getElementById('newtask');var v=(i.value||'').trim();if(!v)return;api('/api/tasks/add',{text:v,repo:''}).then(function(){i.value='';loadTasks();});}
|
|
1841
|
-
function pushTasksUI(){var o=document.getElementById('pushout');var btn=document.getElementById('pushtasks');btn.disabled=true;
|
|
1842
|
-
o.innerHTML="<div class='muted' style='margin-top:10px'><span class='dot-c' style='background:var(--amber)'></span> Writing .symbiot/TASKS.md into your repos…</div>";
|
|
1843
|
-
api('/api/tasks/push',{type:TFILTER.type||'',repo:TFILTER.repo||''}).then(function(r){btn.disabled=false;
|
|
1844
|
-
if(r.empty){o.innerHTML="<div class='muted' style='margin-top:10px'>No open tasks to send. Tick ideas in a repo review, or add tasks above.</div>";return;}
|
|
1845
|
-
var n=(r.written||[]).length;var h="<div class='drift' style='margin-top:10px'><div class='dh'><span class='dot-c'></span><span class='dn'>Done — wrote "+n+" file"+(n===1?"":"s")+"</span></div>";
|
|
1846
|
-
if(n){h+="<ul>";r.written.forEach(function(w){h+="<li class='info'>✓ <b>"+esc(w.name)+"</b> <span class='ev'>"+esc(w.file)+" ("+w.count+" task"+(w.count===1?"":"s")+")</span></li>";});h+="</ul>";}
|
|
1847
|
-
if(r.unresolved&&r.unresolved.length){h+="<div class='dd' style='margin-top:6px'>⚠ not written (repo not found under your home folder): "+esc(r.unresolved.map(function(u){return u.name;}).join(", "))+"</div>";}
|
|
1848
|
-
if(r.handoff&&r.written&&r.written.length){r.written.forEach(function(w){api('/api/open',{path:w.path});});h+="<div class='dd' style='margin-top:8px'>🤖 Handed "+n+" repo(s) to your agent — opening the <b>Agents</b> tab to watch it work…</div>";setTimeout(function(){setTab('agents');},500);}
|
|
1849
|
-
else{h+="<div class='dd' style='margin-top:8px'>Set an <b>agent command</b> in Settings to auto-run it on send (and watch it in the Agents tab). For now, tell your agent: <b>“Read .symbiot/TASKS.md and implement the unchecked items.”</b></div>";}
|
|
1850
|
-
h+="</div>";
|
|
1851
|
-
o.innerHTML=h;}).catch(function(e){btn.disabled=false;o.innerHTML="<div class='err' style='margin-top:10px'>Couldn't write tasks: "+esc(String((e&&e.message)||e))+"</div>";});}
|
|
1852
|
-
function loadAgentCfg(){api('/api/agentcfg').then(function(d){document.getElementById('agentcmd').value=d.cmd||'';
|
|
1853
|
-
var chips=[];(d.agents||[]).forEach(function(a){chips.push(a);});(d.editors||[]).forEach(function(e){chips.push(e);});
|
|
1854
|
-
var box=document.getElementById('agentpresets');
|
|
1855
|
-
if(!chips.length){box.innerHTML="<span class='muted' style='font-size:12px'>Nothing detected on PATH — type your own command above.</span>";return;}
|
|
1856
|
-
box.innerHTML="<span class='muted' style='font-size:12px'>Detected — click to use:</span><br>"+chips.map(function(c,i){return "<button class='ghost preset' data-i='"+i+"' style='padding:4px 10px;font-size:12px;margin:5px 5px 0 0'>"+esc(c.label)+"</button>";}).join("");
|
|
1857
|
-
box.querySelectorAll('.preset').forEach(function(btn){btn.addEventListener('click',function(){document.getElementById('agentcmd').value=chips[+btn.getAttribute('data-i')].tmpl;saveAgent();});});});}
|
|
1858
|
-
function saveAgent(){api('/api/agentcmd',{cmd:document.getElementById('agentcmd').value});}
|
|
1859
|
-
function loadScanRoots(){api('/api/scanroots').then(function(d){var box=document.getElementById('scanroots');var roots=d.effective||[];
|
|
1860
|
-
box.innerHTML=roots.map(function(r){var custom=(d.roots||[]).indexOf(r)>=0;return "<div class='task' data-p='"+esc(r)+"'><span class='t' style='font-family:ui-monospace,monospace;font-size:12px'>"+esc(r)+"</span>"+(r===d.home?"<span class='rp'>home</span>":"")+(custom?"<button class='rm rmroot' title='remove'>×</button>":"")+"</div>";}).join("");
|
|
1861
|
-
box.querySelectorAll('.rmroot').forEach(function(btn){btn.addEventListener('click',function(){api('/api/scanroots/remove',{path:btn.closest('.task').getAttribute('data-p')}).then(function(){loadScanRoots();mapLoaded=false;driftLoaded=false;});});});});}
|
|
1862
|
-
function addRootUI(){var i=document.getElementById('newroot');var v=(i.value||'').trim();if(!v)return;var n=document.getElementById('scanrootnote');
|
|
1863
|
-
api('/api/scanroots/add',{path:v}).then(function(r){if(r.error){n.innerHTML="<span class='err'>"+esc(r.error)+"</span>";return;}i.value='';n.textContent='Added — the Map/Drift will rescan.';loadScanRoots();mapLoaded=false;driftLoaded=false;});}
|
|
1864
|
-
function setupLocalUI(){var o=document.getElementById('setupout');o.innerHTML="Setting up a local model…";
|
|
1865
|
-
api('/api/setup-local',{}).then(function(r){if(r.error==='not-installed'){var i=r.install||{};o.innerHTML="Ollama isn't installed. Run this once, then click again:<div class='out2'>"+esc(i.cmd||'')+"</div><div class='muted' style='font-size:11px'>or download: "+esc(i.alt||'https://ollama.com/download')+"</div>";return;}
|
|
1866
|
-
o.innerHTML="Pulling <b>"+esc(r.model)+"</b> — watch it in the <b>Agents</b> tab. Symbiot switches to it automatically when the download finishes.";setTimeout(function(){setTab('agents');},700);}).catch(function(e){o.innerHTML="<span class='err'>Setup failed: "+esc(String((e&&e.message)||e))+"</span>";});}
|
|
1867
|
-
function selectNode(id){sel=id;render();updateBar(id);var n=nodeById(id);api('/api/node?id='+encodeURIComponent(id)).then(showDetail);
|
|
1868
|
-
if(n&&n.type==='repo'&&n.meta&&n.meta.path){loadReview(n.label,n.meta.path);}else{hideReview();}}
|
|
1869
|
-
function screenToGraph(el,ev){var rc=el.getBoundingClientRect();var mx=(ev.clientX-rc.left)/rc.width*GW;var my=(ev.clientY-rc.top)/rc.height*GH;return {x:(mx-view.x)/view.k,y:(my-view.y)/view.k};}
|
|
1870
|
-
function initGraphEvents(){var el=document.getElementById('graph');var mode=null,moved=0,sx=0,sy=0,ox=0,oy=0,downId=null,dnode=null;
|
|
1871
|
-
function nodeAt(ev){var t=ev.target;var g=t&&t.closest?t.closest('.node'):null;return g?g.getAttribute('data-id'):null;}
|
|
1872
|
-
el.addEventListener('wheel',function(ev){ev.preventDefault();if(!GRAPH)return;var rc=el.getBoundingClientRect();var mx=(ev.clientX-rc.left)/rc.width*GW;var my=(ev.clientY-rc.top)/rc.height*GH;var nk=Math.max(0.3,Math.min(4,view.k*(ev.deltaY<0?1.12:0.89)));view.x=mx-(mx-view.x)*(nk/view.k);view.y=my-(my-view.y)*(nk/view.k);view.k=nk;render();},{passive:false});
|
|
1873
|
-
el.addEventListener('pointerdown',function(ev){if(!GRAPH)return;moved=0;sx=ev.clientX;sy=ev.clientY;ox=view.x;oy=view.y;downId=nodeAt(ev);if(downId){mode='node';dnode=nodeById(downId);}else{mode='pan';}try{el.setPointerCapture(ev.pointerId);}catch(e){}});
|
|
1874
|
-
el.addEventListener('pointermove',function(ev){
|
|
1875
|
-
if(!mode){updateBar(nodeAt(ev));return;}
|
|
1876
|
-
var dx=ev.clientX-sx,dy=ev.clientY-sy;moved+=Math.abs(dx)+Math.abs(dy);if(moved<4)return;
|
|
1877
|
-
if(mode==='node'&&dnode){var p=screenToGraph(el,ev);dnode.x=p.x;dnode.y=p.y;render();}
|
|
1878
|
-
else if(mode==='pan'){var rc=el.getBoundingClientRect();view.x=ox+dx/rc.width*GW;view.y=oy+dy/rc.height*GH;render();}});
|
|
1879
|
-
el.addEventListener('pointerup',function(ev){var id=downId,wasClick=moved<6;mode=null;dnode=null;downId=null;
|
|
1880
|
-
if(id){selectNode(id);}else if(wasClick){sel=null;hideDetail();hideReview();render();updateBar(null);}});
|
|
1881
|
-
el.addEventListener('mouseleave',function(){if(!mode)updateBar(null);}); }
|
|
1882
|
-
function scanLine(s){return 'Mapping your work… '+s.phase+(s.total?' '+s.done+'/'+s.total:'')+(s.item?' · '+s.item:'')+' · '+Math.round((s.elapsed||0)/1000)+'s';}
|
|
1883
|
-
function loadMap(){var p=document.getElementById("profile");p.textContent="Mapping your work...";document.getElementById("graph").innerHTML="";sel=null;hideDetail();hideReview();var done=false;
|
|
1884
|
-
function poll(){if(done)return;api('/api/scan').then(function(s){if(done)return;if(s&&s.active&&s.phase)p.textContent=scanLine(s);setTimeout(poll,600);}).catch(function(){});}
|
|
1885
|
-
setTimeout(poll,400);
|
|
1886
|
-
api("/api/map").then(function(g){done=true;mapLoaded=true;if(!g.nodes||!g.nodes.length){p.textContent="No git repositories found under your home folder.";return;}GRAPH=g;layout(g.nodes,g.edges);view={k:1,x:0,y:0};p.innerHTML=profileLine(g)+(g.stats&&g.stats.partial?" · <span class='err'>partial — the scan hit its time limit</span>":"");render();});}
|
|
1887
|
-
function fitBadge(m){return m.fits?"<span class='tag'>fits your RAM</span>":"<span class='tag' style='background:#3a2a12;border-color:#6b4a1f;color:#F2A541'>needs more RAM</span>";}
|
|
1888
|
-
function loadRec(){var out=document.getElementById('recout');out.innerHTML="<div class='muted'>Reading your hardware...</div>";
|
|
1889
|
-
api('/api/models').then(function(d){var hw=d.hardware,rec=d.rec;
|
|
1890
|
-
var h="<div class='k'><b>"+hw.ramGB+" GB</b> RAM · "+hw.cpuCount+"-core · "+esc(hw.platform)+"/"+esc(hw.arch)+(hw.gpu?" · GPU: "+esc(hw.gpu.name)+(hw.gpu.vramGB?" ("+hw.gpu.vramGB+"GB)":""):"")+"</div>";
|
|
1891
|
-
h+="<div class='ideas'><h4>Local models · free via Ollama</h4>";
|
|
1892
|
-
rec.local.forEach(function(m){h+="<div class='idea' style='justify-content:space-between'><span><b>"+esc(m.tier)+"</b> · <code>ollama pull "+esc(m.model)+"</code> <span class='muted'>~"+m.needGB+"GB · "+esc(m.note)+"</span></span> "+fitBadge(m)+"</div>";});
|
|
1893
|
-
h+="<div class='muted' style='margin-top:6px'>Best fit: <b>"+esc(rec.best)+"</b> — pull it, then pick <b>Local model (Ollama)</b> above.</div></div>";
|
|
1894
|
-
h+="<div class='ideas'><h4>Paid models · bring an API key</h4>";
|
|
1895
|
-
rec.paid.forEach(function(p){h+="<div class='idea'><span><b>"+esc(p.provider)+"</b> · "+esc(p.tier)+" · <code>"+esc(p.model)+"</code> <span class='muted'>· "+esc(p.note)+"</span></span></div>";});
|
|
1896
|
-
h+="</div>";out.innerHTML=h;});}
|
|
1897
|
-
document.getElementById('remap').addEventListener('click',loadMap);
|
|
1898
|
-
document.getElementById('recbtn').addEventListener('click',loadRec);
|
|
1899
|
-
document.getElementById('setuplocal').addEventListener('click',setupLocalUI);
|
|
1900
|
-
document.getElementById('addroot').addEventListener('click',addRootUI);
|
|
1901
|
-
document.getElementById('newroot').addEventListener('keydown',function(e){if(e.key==='Enter')addRootUI();});
|
|
1902
|
-
document.getElementById('driftrun').addEventListener('click',function(){driftLoaded=false;loadDrift();});
|
|
1903
|
-
document.getElementById('agentsrefresh').addEventListener('click',loadAgents);
|
|
1904
|
-
document.getElementById('addtask').addEventListener('click',addTaskUI);
|
|
1905
|
-
document.getElementById('pushtasks').addEventListener('click',pushTasksUI);
|
|
1906
|
-
document.getElementById('agentcmd').addEventListener('change',saveAgent);
|
|
1907
|
-
document.getElementById('newtask').addEventListener('keydown',function(e){if(e.key==='Enter')addTaskUI();});
|
|
1908
|
-
var SRV_STARTED=null,srvDown=false,updBusy=false;
|
|
1909
|
-
function ubar(){return document.getElementById('updatebar');}
|
|
1910
|
-
function heartbeat(fresh){
|
|
1911
|
-
api('/api/ping'+(fresh?'?fresh=1':'')).then(function(p){
|
|
1912
|
-
var b=ubar();
|
|
1913
|
-
var ve=document.getElementById('ver');if(ve&&p.version)ve.textContent='v'+p.version;
|
|
1914
|
-
if(SRV_STARTED===null){SRV_STARTED=p.started;}
|
|
1915
|
-
else if(p.started!==SRV_STARTED){location.reload();return;}
|
|
1916
|
-
if(srvDown){location.reload();return;}
|
|
1917
|
-
if(updBusy)return;
|
|
1918
|
-
if(p.latest&&p.newer){
|
|
1919
|
-
b.className='updatebar show';
|
|
1920
|
-
b.innerHTML="A new Symbiot ("+esc(p.latest)+") is available — you're on "+esc(p.version)+". <button id='doupd'>Update & restart</button>";
|
|
1921
|
-
var btn=document.getElementById('doupd');if(btn)btn.onclick=doUpdate;
|
|
1922
|
-
}else{b.className='updatebar';}
|
|
1923
|
-
}).catch(function(){srvDown=true;var b=ubar();b.className='updatebar reconnect show';b.textContent='Reconnecting to Symbiot…';});
|
|
1924
|
-
}
|
|
1925
|
-
function doUpdate(){updBusy=true;var b=ubar();b.className='updatebar show';b.textContent='Updating & restarting… this page will reload itself when it is back.';api('/api/update',{});}
|
|
1926
|
-
setInterval(heartbeat,4000);heartbeat(true);
|
|
1927
|
-
window.addEventListener('focus',function(){heartbeat(true);}); // re-check for updates when you come back to the window
|
|
1928
|
-
initGraphEvents();syncP();refresh();loadMap();loadAgentCfg();loadScanRoots();
|
|
1929
|
-
</script></body></html>`;
|
|
1930
1468
|
|
|
1931
1469
|
function readBody(req) {
|
|
1932
1470
|
return new Promise((resolve) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice — Claude, OpenAI, Gemini, or a local model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"index.mjs",
|
|
11
|
+
"ui.mjs",
|
|
11
12
|
"README.md",
|
|
12
13
|
"LICENSE"
|
|
13
14
|
],
|
package/ui.mjs
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
// symbiot — the `symbiot app` page, served at / by index.mjs (cmdApp).
|
|
2
|
+
//
|
|
3
|
+
// Self-contained HTML served at / — no backticks or ${} inside (it lives in a
|
|
4
|
+
// template literal). Talks to the local API with the per-launch token.
|
|
5
|
+
// Kept apart from the CLI/server so UI edits can't break the backend (and vice
|
|
6
|
+
// versa); test/smoke.mjs boots the real page and exercises every handler.
|
|
7
|
+
export const EMBEDDED_UI = `<!doctype html><html><head><meta charset="utf8">
|
|
8
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"><title>Symbiot</title>
|
|
9
|
+
<style>
|
|
10
|
+
:root{--ink:#0E1A1F;--ink2:#15262C;--ink3:#1D333A;--line:#24404A;--bone:#F4F1EA;--text:#B7C9C4;--faint:#7E9690;--green:#3DDC97;--amber:#F2A541;--green-dim:#16322D;--sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
|
|
11
|
+
*{box-sizing:border-box}html,body{margin:0;height:100%}
|
|
12
|
+
body{background:var(--ink);color:var(--text);font-family:var(--sans);font-size:14px;display:flex;flex-direction:column}
|
|
13
|
+
header{padding:16px 18px 10px;display:flex;align-items:center;gap:10px}
|
|
14
|
+
.dot{width:10px;height:10px;border-radius:50%;background:var(--green);box-shadow:0 0 12px var(--green)}
|
|
15
|
+
.brand{font-weight:700;color:var(--bone);font-size:16px}
|
|
16
|
+
.status{margin-left:auto;font-size:12px;color:var(--faint);text-align:right;max-width:52%}
|
|
17
|
+
.tabs{display:flex;gap:6px;padding:0 14px;border-bottom:1px solid var(--line)}
|
|
18
|
+
.tab{padding:9px 14px;border:0;background:none;color:var(--faint);font:inherit;font-weight:600;cursor:pointer;border-bottom:2px solid transparent}
|
|
19
|
+
.tab.active{color:var(--bone);border-bottom-color:var(--green)}
|
|
20
|
+
main{flex:1;overflow:auto;padding:16px 18px}
|
|
21
|
+
.row{display:flex;gap:10px;align-items:center;margin-bottom:12px;flex-wrap:wrap}
|
|
22
|
+
button.act{background:var(--green);color:var(--ink);border:0;border-radius:10px;padding:10px 16px;font:inherit;font-weight:700;cursor:pointer}
|
|
23
|
+
button.act:hover{filter:brightness(1.08)}
|
|
24
|
+
button.ghost{background:var(--ink3);color:var(--bone);border:1px solid var(--line);border-radius:10px;padding:9px 14px;font:inherit;font-weight:600;cursor:pointer}
|
|
25
|
+
button.ghost:hover{border-color:var(--faint)}
|
|
26
|
+
.out{white-space:pre-wrap;background:var(--ink2);border:1px solid var(--line);border-radius:12px;padding:16px;min-height:180px;color:var(--bone);line-height:1.6}
|
|
27
|
+
.muted{color:var(--faint)}
|
|
28
|
+
label{display:block;font-size:12px;color:var(--faint);margin:14px 0 5px}
|
|
29
|
+
select,input{width:100%;background:var(--ink);border:1px solid var(--line);border-radius:9px;padding:10px 12px;color:var(--bone);font:inherit}
|
|
30
|
+
select:focus,input:focus{outline:none;border-color:var(--green)}
|
|
31
|
+
a{color:var(--green);cursor:pointer}.hidden{display:none}
|
|
32
|
+
.note{font-size:12px;margin-top:10px}.ok{color:var(--green)}.err{color:var(--amber)}
|
|
33
|
+
.updatebar{display:none;align-items:center;gap:12px;padding:9px 16px;font-size:13px;font-weight:600;background:var(--green);color:var(--ink)}
|
|
34
|
+
.updatebar.show{display:flex}
|
|
35
|
+
.updatebar.reconnect{background:var(--amber)}
|
|
36
|
+
.updatebar button{font:inherit;font-weight:700;border:0;border-radius:8px;padding:5px 12px;background:var(--ink);color:var(--bone);cursor:pointer}
|
|
37
|
+
.ver{font-size:11px;color:var(--faint);background:var(--ink3);border:1px solid var(--line);border-radius:999px;padding:2px 8px;margin-left:2px;font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
38
|
+
footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
39
|
+
.profile{font-size:13px;margin-bottom:8px;line-height:1.5}
|
|
40
|
+
.profile b{color:var(--bone)}
|
|
41
|
+
.maprow{display:flex;gap:12px;align-items:stretch}
|
|
42
|
+
#graph{flex:1;width:100%;height:62vh;min-height:340px;background:var(--ink2);border:1px solid var(--line);border-radius:12px;touch-action:none;cursor:grab}
|
|
43
|
+
#graph:active{cursor:grabbing}
|
|
44
|
+
#graph text{font-family:var(--sans);fill:var(--text);font-size:11px;pointer-events:none}
|
|
45
|
+
#graph .lbl-me{fill:var(--bone);font-weight:700;font-size:13px}
|
|
46
|
+
.detail{width:290px;flex:none;background:var(--ink2);border:1px solid var(--line);border-radius:12px;padding:14px;overflow:auto;max-height:62vh}
|
|
47
|
+
.detail h3{margin:0 0 4px;color:var(--bone);font-size:15px}
|
|
48
|
+
.detail .k{font-size:12px;color:var(--faint);margin-top:8px}
|
|
49
|
+
.detail ul{margin:6px 0 0;padding-left:18px}.detail li{margin:2px 0}
|
|
50
|
+
.detail .chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
|
|
51
|
+
.tag{display:inline-flex;font-size:11px;padding:4px 9px;border-radius:999px;background:var(--green-dim);border:1px solid #2a6b52;color:var(--green)}
|
|
52
|
+
.mapbar{margin-top:8px;font-size:12.5px;color:var(--text);background:var(--ink3);border:1px solid var(--line);border-radius:9px;padding:9px 12px;min-height:18px;font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
53
|
+
.mapbar b{color:var(--bone)}
|
|
54
|
+
.review{margin-top:12px;background:var(--ink2);border:1px solid var(--line);border-radius:12px;padding:16px;min-height:120px}
|
|
55
|
+
.review h4{margin:0 0 8px;color:var(--green);font-size:11px;letter-spacing:.15em;text-transform:uppercase;font-weight:700}
|
|
56
|
+
.review .body{color:var(--text);line-height:1.65;font-size:14px;white-space:pre-wrap}
|
|
57
|
+
.review .rname{color:var(--bone);font-weight:600}
|
|
58
|
+
.rfoot{margin-top:12px;font-size:11px;color:var(--faint);font-family:ui-monospace,Menlo,Consolas,monospace;border-top:1px solid var(--line);padding-top:8px;line-height:1.5}
|
|
59
|
+
.verdict{margin-top:12px;padding:10px 12px;border-left:3px solid var(--amber);background:var(--ink3);border-radius:0 8px 8px 0;color:var(--bone);font-size:13.5px;line-height:1.5}
|
|
60
|
+
.ideas{margin-top:14px}
|
|
61
|
+
.ideas h4{margin:0 0 6px;color:var(--amber);font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:700}
|
|
62
|
+
.idea{display:flex;align-items:flex-start;gap:9px;padding:5px 0;font-size:13.5px;color:var(--text)}
|
|
63
|
+
.idea input{margin-top:3px;flex:none;width:15px;height:15px;cursor:pointer}
|
|
64
|
+
.task{display:flex;align-items:center;gap:10px;padding:9px 11px;border:1px solid var(--line);border-radius:9px;margin-top:8px;background:var(--ink2)}
|
|
65
|
+
.task input[type=checkbox]{width:16px;height:16px;flex:none;cursor:pointer}
|
|
66
|
+
.task .t{flex:1}.task.done .t{color:var(--faint);text-decoration:line-through}
|
|
67
|
+
.task .rp{font-size:11px;color:var(--faint);background:var(--ink3);border:1px solid var(--line);border-radius:999px;padding:2px 8px}
|
|
68
|
+
.task .rm{background:none;border:0;color:var(--faint);cursor:pointer;font-size:18px;line-height:1}
|
|
69
|
+
.task .rm:hover{color:var(--amber)}
|
|
70
|
+
.tgroup{margin:14px 0 4px;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--green);font-weight:700}
|
|
71
|
+
.tcount{color:var(--faint);font-weight:400}
|
|
72
|
+
.taskfilter{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:10px 0 2px}
|
|
73
|
+
.fl{font-size:10px;color:var(--faint);text-transform:uppercase;letter-spacing:.12em;margin:0 2px 0 8px}
|
|
74
|
+
.fchip{font-size:12px;padding:4px 10px;border-radius:999px;border:1px solid var(--line);background:var(--ink3);color:var(--text);cursor:pointer;font:inherit}
|
|
75
|
+
.fchip.on{background:var(--green-dim);border-color:#2a6b52;color:var(--green)}
|
|
76
|
+
.task .rm.restore{color:var(--green);font-size:15px}
|
|
77
|
+
.rcard{border:1px solid var(--amber);border-radius:11px;padding:10px 12px;margin-top:8px}
|
|
78
|
+
.rcard .rhead{display:flex;gap:8px;align-items:baseline;flex-wrap:wrap}.rcard .rhead .muted{font-size:12px}
|
|
79
|
+
.rcard .row{margin-top:8px}.rcard .task{margin-top:6px}
|
|
80
|
+
.rdiff{max-height:360px;overflow:auto;font-size:11px;line-height:1.45;background:var(--ink2);border:1px solid var(--line);border-radius:8px;padding:8px;margin-top:8px;white-space:pre}
|
|
81
|
+
.drift{border:1px solid var(--line);border-radius:10px;margin-top:10px;padding:12px 14px;background:var(--ink2)}
|
|
82
|
+
.drift .dh{display:flex;gap:8px;align-items:center}
|
|
83
|
+
.drift .dn{color:var(--bone);font-weight:600}
|
|
84
|
+
.drift .dd{color:var(--faint);font-size:12px}
|
|
85
|
+
.drift ul{margin:8px 0 0;padding:0;list-style:none}
|
|
86
|
+
.drift li{padding:3px 0;font-size:13.5px}
|
|
87
|
+
.drift li.warn{color:var(--amber)}
|
|
88
|
+
.drift .ev{color:var(--faint);font-family:ui-monospace,Menlo,monospace;font-size:11px}
|
|
89
|
+
.dot-w{width:9px;height:9px;border-radius:50%;background:var(--amber);display:inline-block}
|
|
90
|
+
.dot-c{width:9px;height:9px;border-radius:50%;background:var(--green);display:inline-block}
|
|
91
|
+
.agent{border:1px solid var(--line);border-radius:10px;margin-top:10px;padding:12px 14px;background:var(--ink2)}
|
|
92
|
+
.adot{width:10px;height:10px;border-radius:50%;display:inline-block;flex:none}
|
|
93
|
+
.adot.run{background:var(--amber);animation:pulse 1s ease-in-out infinite}
|
|
94
|
+
.adot.ok{background:var(--green)} .adot.fail{background:#E5695B}
|
|
95
|
+
.changed{margin-top:8px;font-size:12.5px;color:var(--bone);background:var(--green-dim);border:1px solid #2a6b52;border-radius:8px;padding:7px 10px}
|
|
96
|
+
.changed.muted{color:var(--faint);background:var(--ink3);border-color:var(--line)}
|
|
97
|
+
.changed b{color:var(--green)}
|
|
98
|
+
.commits{margin-top:6px;font-size:12px;color:var(--text);font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
99
|
+
.commits code{color:var(--amber);margin-right:6px}
|
|
100
|
+
@keyframes pulse{0%,100%{opacity:1;box-shadow:0 0 0 0 rgba(242,165,65,.5)}50%{opacity:.4;box-shadow:0 0 0 5px rgba(242,165,65,0)}}
|
|
101
|
+
.alogout{white-space:pre-wrap;background:var(--ink);border:1px solid var(--line);border-radius:8px;padding:10px;margin-top:10px;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px;line-height:1.5;color:var(--text);max-height:260px;overflow:auto}
|
|
102
|
+
.bar{height:3px;background:var(--green-dim);border-radius:2px;overflow:hidden;margin-top:8px}
|
|
103
|
+
.bar > i{display:block;height:100%;width:35%;background:var(--green);border-radius:2px;animation:slide 1.3s ease-in-out infinite}
|
|
104
|
+
@keyframes slide{0%{margin-left:-35%}100%{margin-left:100%}}
|
|
105
|
+
.out2{white-space:pre-wrap;background:var(--ink);border:1px solid var(--line);border-radius:9px;padding:10px;margin-top:10px;font-size:13px;line-height:1.55;color:var(--bone)}
|
|
106
|
+
.legend{display:flex;gap:14px;align-items:center;margin-top:10px;font-size:12px;color:var(--faint);flex-wrap:wrap}
|
|
107
|
+
.lg{display:inline-flex;gap:6px;align-items:center}
|
|
108
|
+
.lg i{width:10px;height:10px;border-radius:50%;display:inline-block}
|
|
109
|
+
@media(max-width:760px){.maprow{flex-direction:column}.detail{width:auto;max-height:none}}
|
|
110
|
+
</style></head><body>
|
|
111
|
+
<div id="updatebar" class="updatebar"></div>
|
|
112
|
+
<header><span class="dot"></span><span class="brand">Symbiot</span><span class="ver" id="ver"></span><span class="status" id="status">...</span></header>
|
|
113
|
+
<div class="tabs">
|
|
114
|
+
<button class="tab active" data-tab="map">Map</button>
|
|
115
|
+
<button class="tab" data-tab="drift">Drift</button>
|
|
116
|
+
<button class="tab" data-tab="week">Week</button>
|
|
117
|
+
<button class="tab" data-tab="standup">Standup</button>
|
|
118
|
+
<button class="tab" data-tab="todo">Todo</button>
|
|
119
|
+
<button class="tab" data-tab="tasks">Tasks</button>
|
|
120
|
+
<button class="tab" data-tab="agents">Agents</button>
|
|
121
|
+
<button class="tab" data-tab="settings">Settings</button>
|
|
122
|
+
</div>
|
|
123
|
+
<main>
|
|
124
|
+
<section id="panel-map">
|
|
125
|
+
<div class="profile muted" id="profile">Mapping your work…</div>
|
|
126
|
+
<div class="maprow">
|
|
127
|
+
<svg id="graph" viewBox="0 0 960 620" preserveAspectRatio="xMidYMid meet"></svg>
|
|
128
|
+
<aside id="detail" class="detail hidden"></aside>
|
|
129
|
+
</div>
|
|
130
|
+
<div id="mapbar" class="mapbar">Hover a node for a summary · click for details · drag to move</div>
|
|
131
|
+
<div class="legend">
|
|
132
|
+
<span class="lg"><i style="background:var(--green)"></i>you</span>
|
|
133
|
+
<span class="lg"><i style="background:var(--bone)"></i>repos</span>
|
|
134
|
+
<span class="lg"><i style="background:var(--amber)"></i>languages</span>
|
|
135
|
+
<span class="lg"><i style="background:#6bb3ff"></i>tools</span>
|
|
136
|
+
<span class="lg"><i style="background:#c58af9"></i>agents</span>
|
|
137
|
+
<span class="lg"><i style="background:#5fe3b0"></i>AI</span>
|
|
138
|
+
<span class="lg"><i style="background:#b7a98c"></i>folders (no git)</span>
|
|
139
|
+
<span class="muted" style="margin-left:8px">scroll to zoom · drag to pan · click a node</span>
|
|
140
|
+
<button class="ghost" id="remap" style="margin-left:auto">Rescan</button>
|
|
141
|
+
</div>
|
|
142
|
+
<div id="review" class="review hidden"></div>
|
|
143
|
+
</section>
|
|
144
|
+
<section id="panel-run" class="hidden">
|
|
145
|
+
<div class="row"><button class="act" id="write">Write my <span id="what">week</span></button>
|
|
146
|
+
<button class="ghost hidden" id="copy">Copy</button>
|
|
147
|
+
<span class="muted">Reads your local git and writes it up.</span></div>
|
|
148
|
+
<div class="out muted" id="out">Nothing yet - hit the button.</div>
|
|
149
|
+
<div class="rfoot" id="outfoot" style="display:none"></div>
|
|
150
|
+
</section>
|
|
151
|
+
<section id="panel-tasks" class="hidden">
|
|
152
|
+
<div class="row"><input id="newtask" placeholder="Add a task..." style="flex:1"><button class="act" id="addtask">Add</button><button class="ghost" id="pushtasks" title="Write .symbiot/TASKS.md into each repo for your coding agent">Send to repos</button></div>
|
|
153
|
+
<div class="note muted" style="margin-top:2px">To give tasks to your agent, use <b>Send to repos</b> — filter by tag below to choose which. As the agent finishes each one it lands in <b>Awaiting your review</b>: <b>Approve</b> commits it on a branch and opens a PR, <b>↶</b> sends it back. Ticking a task yourself marks it done (it auto-archives).</div>
|
|
154
|
+
<div id="pushout"></div>
|
|
155
|
+
<div id="reviewout"></div>
|
|
156
|
+
<div id="reviewlist"></div>
|
|
157
|
+
<div id="taskfilter" class="taskfilter"></div>
|
|
158
|
+
<div id="tasklist"></div>
|
|
159
|
+
</section>
|
|
160
|
+
<section id="panel-agents" class="hidden">
|
|
161
|
+
<div class="row"><span class="muted">Agents Symbiot has handed work to — live status and output.</span><button class="ghost" id="agentsrefresh" style="margin-left:auto">Refresh</button></div>
|
|
162
|
+
<div id="agentslist"></div>
|
|
163
|
+
</section>
|
|
164
|
+
<section id="panel-drift" class="hidden">
|
|
165
|
+
<div class="row"><span class="muted">What's out of sync, stuck or at risk across your repos — local git facts.</span>
|
|
166
|
+
<label class="muted" style="margin-left:auto"><input type="checkbox" id="driftfetch"> fetch latest</label>
|
|
167
|
+
<label class="muted"><input type="checkbox" id="driftci"> check CI (needs gh)</label>
|
|
168
|
+
<button class="ghost" id="driftrun">Rescan</button></div>
|
|
169
|
+
<div id="driftout"></div>
|
|
170
|
+
</section>
|
|
171
|
+
<section id="panel-settings" class="hidden">
|
|
172
|
+
<label>Which AI should Symbiot write with?</label>
|
|
173
|
+
<select id="provider">
|
|
174
|
+
<option value="anthropic">Claude (Anthropic)</option>
|
|
175
|
+
<option value="openai">OpenAI (GPT)</option>
|
|
176
|
+
<option value="gemini">Gemini (Google)</option>
|
|
177
|
+
<option value="ollama">Local model (Ollama) - free, no key</option>
|
|
178
|
+
</select>
|
|
179
|
+
<div class="note muted">The Map above needs no key. For the write-ups, a hosted model needs an API key - or run <b>Ollama</b> locally for a free, private option (nothing leaves your machine).</div>
|
|
180
|
+
<div id="keyWrap"><label>API key</label><input id="key" type="password" placeholder="paste your key">
|
|
181
|
+
<div class="note"><a id="getkey">Where do I get a key?</a></div></div>
|
|
182
|
+
<div id="baseWrap" class="hidden"><label>Ollama URL</label><input id="baseUrl" type="text" value="http://localhost:11434"></div>
|
|
183
|
+
<label>Model <span class="muted" id="modelHint"></span></label>
|
|
184
|
+
<input id="model" type="text" placeholder="(blank = default)">
|
|
185
|
+
<div class="row" style="margin-top:16px"><button class="act" id="save">Save & connect</button>
|
|
186
|
+
<span class="note" id="saveMsg"></span></div>
|
|
187
|
+
<div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
|
|
188
|
+
<label>Folders to scan for repos</label>
|
|
189
|
+
<div id="scanroots"></div>
|
|
190
|
+
<div class="row" style="margin-top:6px"><input id="newroot" placeholder="/path/to/folder (or ~/work) — where your projects live" style="flex:1"><button class="ghost" id="addroot">Add folder</button></div>
|
|
191
|
+
<div class="note muted" id="scanrootnote">Point Symbiot at where your work lives — inside or outside your home folder. Defaults to your home folder.</div>
|
|
192
|
+
</div>
|
|
193
|
+
<div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
|
|
194
|
+
<label>Hand off to your agent when you "Send to repos"</label>
|
|
195
|
+
<input id="agentcmd" type="text" placeholder="e.g. claude -p "{prompt}" · code {dir} · leave blank to just write the file">
|
|
196
|
+
<div class="note muted">Runs in each repo after tasks are written. Use <b>{dir}</b> = repo path, <b>{prompt}</b> = the task instruction. Works with any agent or editor — it's your command.</div>
|
|
197
|
+
<div id="agentpresets" style="margin-top:8px"></div>
|
|
198
|
+
</div>
|
|
199
|
+
<div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
|
|
200
|
+
<button class="ghost" id="recbtn">Recommend models for my machine</button>
|
|
201
|
+
<button class="ghost" id="setuplocal" style="margin-left:8px">Set up a free local model</button>
|
|
202
|
+
<div id="setupout" class="note muted" style="margin-top:10px"></div>
|
|
203
|
+
<div id="recout" style="margin-top:12px"></div>
|
|
204
|
+
</div>
|
|
205
|
+
</section>
|
|
206
|
+
</main>
|
|
207
|
+
<footer><button class="ghost" id="quit" style="margin-left:auto">Quit</button></footer>
|
|
208
|
+
<script>
|
|
209
|
+
var T=new URLSearchParams(window.location.search).get('t')||'';
|
|
210
|
+
function api(path,body){return fetch(path,{method:body?'POST':'GET',headers:{'x-symbiot-token':T,'content-type':'application/json'},body:body?JSON.stringify(body):undefined}).then(function(r){return r.json();});}
|
|
211
|
+
var KEYURL={anthropic:'https://console.anthropic.com/settings/keys',openai:'https://platform.openai.com/api-keys',gemini:'https://aistudio.google.com/apikey',ollama:'https://ollama.com'};
|
|
212
|
+
var DEFMODEL={anthropic:'claude-opus-5-5',openai:'gpt-4o-mini',gemini:'gemini-1.5-flash',ollama:'llama3.1'};
|
|
213
|
+
function $(id){return document.getElementById(id);}
|
|
214
|
+
var current='map';var mapLoaded=false;var driftLoaded=false;
|
|
215
|
+
var COLORS={person:'#3DDC97',repo:'#F4F1EA',lang:'#F2A541',tool:'#6bb3ff',agent:'#c58af9',ai:'#5fe3b0',folder:'#b7a98c'};
|
|
216
|
+
function tabs(){return document.querySelectorAll('.tab');}
|
|
217
|
+
function setTab(tab){current=tab;tabs().forEach(function(t){t.classList.toggle('active',t.dataset.tab===tab);});
|
|
218
|
+
var isMap=tab==='map',isSet=tab==='settings',isTasks=tab==='tasks',isDrift=tab==='drift',isAgents=tab==='agents',isRun=(tab==='week'||tab==='standup'||tab==='todo');
|
|
219
|
+
$('panel-map').classList.toggle('hidden',!isMap);
|
|
220
|
+
$('panel-run').classList.toggle('hidden',!isRun);
|
|
221
|
+
$('panel-settings').classList.toggle('hidden',!isSet);
|
|
222
|
+
$('panel-tasks').classList.toggle('hidden',!isTasks);
|
|
223
|
+
$('panel-drift').classList.toggle('hidden',!isDrift);
|
|
224
|
+
$('panel-agents').classList.toggle('hidden',!isAgents);
|
|
225
|
+
if(isRun){$('what').textContent=tab;$('out').textContent='Nothing yet - hit the button.';$('out').classList.add('muted');$('copy').classList.add('hidden');}
|
|
226
|
+
if(isMap&&!mapLoaded)loadMap();
|
|
227
|
+
if(isTasks)loadTasks();
|
|
228
|
+
if(isDrift&&!driftLoaded)loadDrift();
|
|
229
|
+
if(isAgents)loadAgents(); else stopAgentsPoll();}
|
|
230
|
+
tabs().forEach(function(t){t.addEventListener('click',function(){setTab(t.dataset.tab);});});
|
|
231
|
+
function refresh(){api('/api/status').then(function(s){$('status').textContent=s.connected?s.line:'Not connected - open Settings';});}
|
|
232
|
+
$('write').addEventListener('click',function(){$('out').textContent='Writing...';$('out').classList.add('muted');$('copy').classList.add('hidden');
|
|
233
|
+
api('/api/run',{cmd:current}).then(function(r){var f=$('outfoot');if(r.error==='not-connected'){$('out').textContent='Not connected yet - open Settings and pick an AI.';f.style.display='none';return;}
|
|
234
|
+
$('out').textContent=r.text||'(no output)';$('out').classList.remove('muted');$('copy').classList.remove('hidden');if(r.footer){f.textContent=r.footer;f.style.display='block';}else{f.style.display='none';}});});
|
|
235
|
+
$('copy').addEventListener('click',function(){navigator.clipboard.writeText($('out').textContent);$('copy').textContent='Copied';setTimeout(function(){$('copy').textContent='Copy';},1400);});
|
|
236
|
+
function syncP(){var p=$('provider').value;var local=p==='ollama';$('keyWrap').classList.toggle('hidden',local);$('baseWrap').classList.toggle('hidden',!local);
|
|
237
|
+
$('getkey').textContent=local?'About Ollama':'Where do I get a key?';$('modelHint').textContent='(default '+DEFMODEL[p]+')';$('model').placeholder='(blank = '+DEFMODEL[p]+')';}
|
|
238
|
+
$('provider').addEventListener('change',syncP);
|
|
239
|
+
$('getkey').addEventListener('click',function(){window.open(KEYURL[$('provider').value],'_blank');});
|
|
240
|
+
$('save').addEventListener('click',function(){var p=$('provider').value;$('saveMsg').textContent='checking...';$('saveMsg').className='note muted';
|
|
241
|
+
var cfg={provider:p,model:$('model').value.trim()};if(p==='ollama')cfg.baseUrl=$('baseUrl').value.trim();else cfg.key=$('key').value.trim();
|
|
242
|
+
api('/api/connect',cfg).then(function(r){$('saveMsg').textContent=r.message||(r.ok?'Connected.':'Could not connect.');$('saveMsg').className='note '+(r.ok?'ok':'err');if(r.ok){$('key').value='';refresh();}});});
|
|
243
|
+
$('quit').addEventListener('click',function(){api('/api/quit');document.body.innerHTML='<div style=\\'padding:40px;color:#7E9690;font-family:sans-serif\\'>Symbiot stopped. You can close this window.</div>';});
|
|
244
|
+
var GRAPH=null,sel=null,view={k:1,x:0,y:0},GW=960,GH=620;
|
|
245
|
+
function esc(s){return String(s).replace(/[&<>]/g,function(ch){return ch==='&'?'&':ch==='<'?'<':'>';});}
|
|
246
|
+
function layout(nodes,edges){var idx={};nodes.forEach(function(n){n.x=GW/2+(Math.random()-0.5)*GW*0.8;n.y=GH/2+(Math.random()-0.5)*GH*0.8;n.vx=0;n.vy=0;idx[n.id]=n;});
|
|
247
|
+
for(var it=0;it<340;it++){for(var i=0;i<nodes.length;i++)for(var j=i+1;j<nodes.length;j++){var a=nodes[i],b=nodes[j];var dx=a.x-b.x,dy=a.y-b.y;var d2=dx*dx+dy*dy+0.01;var d=Math.sqrt(d2);var f=4600/d2;a.vx+=f*dx/d;a.vy+=f*dy/d;b.vx-=f*dx/d;b.vy-=f*dy/d;}
|
|
248
|
+
edges.forEach(function(e){var a=idx[e.source],b=idx[e.target];if(!a||!b)return;var dx=b.x-a.x,dy=b.y-a.y;var d=Math.sqrt(dx*dx+dy*dy)+0.01;var f=(d-115)*0.03;a.vx+=f*dx/d;a.vy+=f*dy/d;b.vx-=f*dx/d;b.vy-=f*dy/d;});
|
|
249
|
+
nodes.forEach(function(n){n.vx+=(GW/2-n.x)*0.002;n.vy+=(GH/2-n.y)*0.002;n.x+=Math.max(-9,Math.min(9,n.vx));n.y+=Math.max(-9,Math.min(9,n.vy));n.vx*=0.86;n.vy*=0.86;n.x=Math.max(30,Math.min(GW-30,n.x));n.y=Math.max(24,Math.min(GH-28,n.y));});}}
|
|
250
|
+
function nbrs(id){var s={};s[id]=1;GRAPH.edges.forEach(function(e){if(e.source===id)s[e.target]=1;if(e.target===id)s[e.source]=1;});return s;}
|
|
251
|
+
function render(){if(!GRAPH)return;var idx={};GRAPH.nodes.forEach(function(n){idx[n.id]=n;});var nb=sel?nbrs(sel):null;
|
|
252
|
+
var s="<g id='vp' transform='translate("+view.x.toFixed(1)+","+view.y.toFixed(1)+") scale("+view.k.toFixed(3)+")'>";
|
|
253
|
+
GRAPH.edges.forEach(function(e){var a=idx[e.source],b=idx[e.target];if(!a||!b)return;var op=nb?((nb[e.source]&&nb[e.target])?0.75:0.06):0.45;s+="<line x1='"+a.x.toFixed(1)+"' y1='"+a.y.toFixed(1)+"' x2='"+b.x.toFixed(1)+"' y2='"+b.y.toFixed(1)+"' stroke='#24404A' stroke-width='1' opacity='"+op+"'/>";});
|
|
254
|
+
GRAPH.nodes.forEach(function(n){var r=Math.max(5,Math.sqrt(n.weight)*2);var col=COLORS[n.type]||"#888";var op=nb?(nb[n.id]?1:0.14):0.95;var st=(n.id===sel)?" stroke='#F4F1EA' stroke-width='2'":"";var title=esc(n.label)+(n.meta?(" - "+(n.meta.commits||0)+" commits"):"");
|
|
255
|
+
s+="<g class='node' data-id='"+esc(n.id)+"' opacity='"+op+"'><circle cx='"+n.x.toFixed(1)+"' cy='"+n.y.toFixed(1)+"' r='"+r.toFixed(1)+"' fill='"+col+"'"+st+"><title>"+title+"</title></circle>";
|
|
256
|
+
var cls=n.type==="person"?"lbl-me":"";s+="<text x='"+n.x.toFixed(1)+"' y='"+(n.y+r+12).toFixed(1)+"' text-anchor='middle' class='"+cls+"'>"+esc(n.label)+"</text></g>";});
|
|
257
|
+
document.getElementById("graph").innerHTML=s+"</g>";}
|
|
258
|
+
function profileLine(g){var repos=g.nodes.filter(function(n){return n.type==="repo";});var base=g.stats.base||"your home folder";if(!repos.length)return "No git repositories found under "+esc(base)+" yet.";var langs=g.nodes.filter(function(n){return n.type==="lang";}).map(function(n){return n.label;});var top=repos.slice().sort(function(a,b){return (b.meta.commits||0)-(a.meta.commits||0);})[0];var s="Scanned <b>"+esc(base)+"</b> · <b>"+g.stats.repos+"</b> repos · <b>"+g.stats.commits+"</b> of your commits · <b>"+(g.stats.files||0)+"</b> files · ";s+=langs.length?("mostly <b>"+esc(langs.slice(0,3).join(", "))+"</b>"):"no languages detected";if(top)s+=" · most active: <b>"+esc(top.label)+"</b>";return s;}
|
|
259
|
+
function nodeById(id){for(var i=0;i<GRAPH.nodes.length;i++)if(GRAPH.nodes[i].id===id)return GRAPH.nodes[i];return null;}
|
|
260
|
+
function summarize(n){if(!n)return "";if(n.type==='repo'){var m=n.meta||{};var bits=[m.commits+' commits'];if(m.branch)bits.push('branch '+m.branch);if(m.last)bits.push('last '+m.last);if(typeof m.files==='number')bits.push(m.files+' files');if(m.langs&&m.langs.length)bits.push(m.langs.join(', '));if(m.tools&&m.tools.length)bits.push(m.tools.join(', '));return "<b>"+esc(n.label)+"</b> "+esc(m.path||'')+" — "+esc(bits.join(' · '));}if(n.type==='lang')return "<b>"+esc(n.label)+"</b> — language (click to see repos)";if(n.type==='tool')return "<b>"+esc(n.label)+"</b> — tool (click to see repos)";if(n.type==='person')return "<b>"+esc(n.label)+"</b> — you";return "<b>"+esc(n.label)+"</b>";}
|
|
261
|
+
function updateBar(id){var bar=document.getElementById('mapbar');if(!bar)return;var n=id?nodeById(id):(sel?nodeById(sel):null);bar.innerHTML=n?summarize(n):"Hover a node for a summary · click for details · drag to move";}
|
|
262
|
+
function hideDetail(){document.getElementById('detail').classList.add('hidden');}
|
|
263
|
+
function showDetail(d){var el=document.getElementById('detail');el.classList.remove('hidden');
|
|
264
|
+
if(d.error){el.innerHTML="<h3>—</h3><div class='k'>"+esc(d.error)+"</div>";return;}
|
|
265
|
+
if(d.type==='repo'){var chips="";['branch: '+(d.branch||'?'),d.commits+' commits',(d.dirty?d.dirty+' uncommitted':'clean'),(d.last?'last '+d.last:'')].concat(d.langs||[]).concat(d.tools||[]).forEach(function(x){if(x)chips+="<span class='tag'>"+esc(x)+"</span>";});
|
|
266
|
+
el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='chips'>"+chips+"</div><button class='act' id='suggest' style='margin-top:12px'>Suggest next steps</button><div id='sugout'></div>";
|
|
267
|
+
document.getElementById('suggest').addEventListener('click',function(){var o=document.getElementById('sugout');o.innerHTML="<div class='out2'>Thinking...</div>";api('/api/suggest',{path:d.path}).then(function(r){if(r.error==='not-connected'){o.innerHTML="<div class='out2'>Connect a model in Settings to get suggestions - Ollama is free and runs locally.</div>";return;}o.innerHTML="<div class='out2'>"+esc(r.text||'(no output)')+"</div>"+(r.footer?"<div class='rfoot'>"+esc(r.footer)+"</div>":"");});});return;}
|
|
268
|
+
if(d.type==='lang'||d.type==='tool'){var lis=(d.repos||[]).map(function(r){return "<li>"+esc(r)+"</li>";}).join("");el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>Used in "+((d.repos||[]).length)+" repos</div><ul>"+lis+"</ul>";return;}
|
|
269
|
+
if(d.type==='agent'){var m=d.meta||{};el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>"+(m.kind==='agent'?'AI coding agent you have installed':'editor you have installed')+"</div>"+(m.cmd?"<div class='out2' style='font-size:11px'>"+esc(m.cmd)+"</div>":"");return;}
|
|
270
|
+
if(d.type==='ai'){var m=d.meta||{};el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>Currently powering Symbiot · model <b>"+esc(m.model||'?')+"</b>"+(m.source?" · via "+esc(m.source):"")+"</div>";return;}
|
|
271
|
+
if(d.type==='folder'){var chips='';(d.langs||[]).concat(d.tools||[]).forEach(function(x){chips+="<span class='tag'>"+esc(x)+"</span>";});el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>project folder · not a git repo · "+(d.files||0)+" files</div><div class='chips'>"+chips+"</div><div class='k' style='font-family:ui-monospace,monospace'>"+esc(d.path||'')+"</div><div class='k'>Tasks can be sent here too — the agent runs in this folder.</div>";return;}
|
|
272
|
+
if(d.type==='person'){var st=d.stats||{};el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>"+st.repos+" repos · "+st.commits+" commits · "+st.languages+" languages · "+st.tools+" tools"+(st.agents?" · "+st.agents+" agents/editors":"")+"</div>";return;}}
|
|
273
|
+
var reviewCache={};var IDEAS=[];var IREPO="";
|
|
274
|
+
function hideReview(){var el=document.getElementById('review');el.classList.add('hidden');el.innerHTML='';}
|
|
275
|
+
function reviewHtml(name,body,verdict,ideas,tasks,footer){IDEAS=ideas||[];IREPO=name;
|
|
276
|
+
var h="<h4>AI review · <span class='rname'>"+esc(name)+"</span></h4><div class='body'>"+esc(body)+"</div>";
|
|
277
|
+
if(verdict)h+="<div class='verdict'>⚖️ "+esc(verdict)+"</div>";
|
|
278
|
+
if(ideas&&ideas.length){h+="<div class='ideas'><h4>Ideas worth doing · tick to add to Tasks</h4>";
|
|
279
|
+
ideas.forEach(function(idea,i){var t=(tasks||[]).filter(function(x){return x.text===idea&&x.repo===name;})[0];var tid=t?t.id:"";
|
|
280
|
+
h+="<label class='idea'><input type='checkbox' class='ideachk' data-idx='"+i+"' data-tid='"+esc(tid)+"'"+(t?" checked":"")+"><span>"+esc(idea)+"</span></label>";});
|
|
281
|
+
h+="</div>";}
|
|
282
|
+
else h+="<div class='muted' style='margin-top:10px'>No new features suggested — the verdict above is the call.</div>";
|
|
283
|
+
if(footer)h+="<div class='rfoot'>"+esc(footer)+"</div>";
|
|
284
|
+
return h;}
|
|
285
|
+
function wireIdeas(){document.querySelectorAll('.ideachk').forEach(function(cb){cb.addEventListener('change',function(){
|
|
286
|
+
var idea=IDEAS[+cb.getAttribute('data-idx')];if(idea==null)return;
|
|
287
|
+
if(cb.checked){api('/api/tasks/add',{text:idea,repo:IREPO}).then(function(it){if(it&&it.id)cb.setAttribute('data-tid',it.id);});}
|
|
288
|
+
else{var id=cb.getAttribute('data-tid');if(id){api('/api/tasks/remove',{id:id}).then(function(){cb.setAttribute('data-tid','');});}}});});}
|
|
289
|
+
function loadReview(name,path){var el=document.getElementById('review');el.classList.remove('hidden');
|
|
290
|
+
if(reviewCache[path]){var c=reviewCache[path];api('/api/tasks').then(function(tasks){el.innerHTML=reviewHtml(name,c.text,c.verdict,c.ideas,tasks,c.footer);wireIdeas();});return;}
|
|
291
|
+
el.innerHTML="<h4>AI review · <span class='rname'>"+esc(name)+"</span></h4><div class='body'>Reading the project…</div>";
|
|
292
|
+
Promise.all([api('/api/review',{path:path}),api('/api/tasks')]).then(function(res){var r=res[0]||{},tasks=res[1]||[];
|
|
293
|
+
if(r.error==='not-connected'){el.innerHTML="<h4>AI review · <span class='rname'>"+esc(name)+"</span></h4><div class='body'>Connect a model in Settings to get a review - Ollama is free and runs locally.</div>"+(r.footer?"<div class='rfoot'>"+esc(r.footer)+"</div>":"");return;}
|
|
294
|
+
var body=r.text||'(no output)';reviewCache[path]={text:body,verdict:r.verdict||"",ideas:r.ideas||[],footer:r.footer};el.innerHTML=reviewHtml(name,body,r.verdict||"",r.ideas||[],tasks,r.footer);wireIdeas();});}
|
|
295
|
+
var ALLTASKS=[];var TFILTER={type:'',repo:''};var TARCH=false;
|
|
296
|
+
var TORDER=['Fixes','Tests & CI','Security','Performance','Refactor','UI/UX','Docs','Features & other'];
|
|
297
|
+
function taskRow(t,arch){return "<div class='task"+(t.done?" done":"")+"' data-id='"+esc(t.id)+"'>"+(arch?"":"<input type='checkbox' class='taskchk' title='mark done (auto-archives on sync)'"+(t.done?" checked":"")+">")+"<span class='t'>"+esc(t.text)+"</span>"+(t.repo?"<span class='rp'>"+esc(t.repo)+"</span>":"")+(arch?"<button class='rm restore' title='restore to active'>↶</button>":"<button class='rm' title='remove'>×</button>")+"</div>";}
|
|
298
|
+
function wireTaskRows(el){el.querySelectorAll('.task').forEach(function(row){var id=row.getAttribute('data-id');
|
|
299
|
+
var cb=row.querySelector('.taskchk');if(cb)cb.addEventListener('change',function(){api('/api/tasks/toggle',{id:id}).then(function(){row.classList.toggle('done');});});
|
|
300
|
+
var rm=row.querySelector('.rm');if(rm&&rm.classList.contains('restore'))rm.addEventListener('click',function(){api('/api/tasks/restore',{id:id}).then(loadArchived);});
|
|
301
|
+
else if(rm)rm.addEventListener('click',function(){api('/api/tasks/remove',{id:id}).then(function(){row.remove();});});});}
|
|
302
|
+
function fchip(dim,val,label){var on=(TFILTER[dim]||'')===val;return "<button class='fchip"+(on?' on':'')+"' data-dim='"+dim+"' data-val='"+esc(val)+"'>"+esc(label)+"</button>";}
|
|
303
|
+
function renderFilter(){var box=document.getElementById('taskfilter');
|
|
304
|
+
if(TARCH){box.innerHTML="<button class='fchip on' id='archtoggle'>↩ back to active</button>";document.getElementById('archtoggle').addEventListener('click',function(){loadTasks();});return;}
|
|
305
|
+
var types={},repos={};ALLTASKS.forEach(function(t){types[t.type||'Features & other']=1;if(t.repo)repos[t.repo]=1;});
|
|
306
|
+
var fb="<span class='fl'>Type</span>"+fchip('type','','All')+Object.keys(types).sort(function(a,b){return TORDER.indexOf(a)-TORDER.indexOf(b);}).map(function(t){return fchip('type',t,t);}).join('');
|
|
307
|
+
fb+="<span class='fl'>Repo</span>"+fchip('repo','','All')+Object.keys(repos).sort().map(function(r){return fchip('repo',r,r);}).join('');
|
|
308
|
+
fb+="<button class='fchip' id='archtoggle' style='margin-left:auto'>🗃 archived</button>";
|
|
309
|
+
box.innerHTML=fb;
|
|
310
|
+
box.querySelectorAll('.fchip[data-dim]').forEach(function(c){c.addEventListener('click',function(){TFILTER[c.getAttribute('data-dim')]=c.getAttribute('data-val');renderTasks();});});
|
|
311
|
+
document.getElementById('archtoggle').addEventListener('click',loadArchived);}
|
|
312
|
+
function renderTasks(){var el=document.getElementById('tasklist');TARCH=false;renderFilter();
|
|
313
|
+
var list=ALLTASKS.filter(function(t){return !t.review&&(!TFILTER.type||(t.type||'Features & other')===TFILTER.type)&&(!TFILTER.repo||t.repo===TFILTER.repo);});
|
|
314
|
+
if(!list.length){el.innerHTML="<div class='muted' style='margin-top:12px'>"+(ALLTASKS.length?"Nothing matches this filter.":"No tasks yet. Tick ideas in a repo review, or add one above.")+"</div>";return;}
|
|
315
|
+
var groups={};list.forEach(function(t){var ty=t.type||'Features & other';(groups[ty]=groups[ty]||[]).push(t);});
|
|
316
|
+
var keys=Object.keys(groups).sort(function(a,b){var ia=TORDER.indexOf(a),ib=TORDER.indexOf(b);return (ia<0?99:ia)-(ib<0?99:ib);});
|
|
317
|
+
var h="";keys.forEach(function(ty){h+="<div class='tgroup'>"+esc(ty)+" <span class='tcount'>"+groups[ty].length+"</span></div>";groups[ty].forEach(function(t){h+=taskRow(t,false);});});
|
|
318
|
+
el.innerHTML=h;wireTaskRows(el);}
|
|
319
|
+
function loadTasks(){TARCH=false;api('/api/tasks/sync',{}).then(function(){api('/api/tasks').then(function(list){ALLTASKS=list;renderTasks();});loadPending();});}
|
|
320
|
+
function loadPending(){api('/api/pending').then(function(list){var el=document.getElementById('reviewlist');if(!list||!list.length){el.innerHTML='';return;}
|
|
321
|
+
var n=0;list.forEach(function(r){n+=r.tasks.length;});
|
|
322
|
+
var h="<div class='tgroup' style='color:var(--amber)'>Awaiting your review <span class='tcount'>"+n+"</span></div>";
|
|
323
|
+
list.forEach(function(r){var ch=r.files.length?(r.files.length+" file"+(r.files.length>1?"s":"")+" changed"+(r.stat?" · "+esc(r.stat):"")):"no uncommitted changes";
|
|
324
|
+
h+="<div class='rcard' data-repo='"+esc(r.repo)+"'><div class='rhead'><b>"+esc(r.repo)+"</b><span class='muted'>"+(r.path?"on "+esc(r.branch||'?')+" · "+ch:"repo not found on disk")+"</span></div>";
|
|
325
|
+
r.tasks.forEach(function(t){h+="<div class='task' data-id='"+esc(t.id)+"'><span class='t'>"+esc(t.text)+"</span><button class='rm sendback' title='not right - send back to the agent (unticks it)'>↶</button></div>";});
|
|
326
|
+
h+="<div class='row'><button class='act approve'"+(r.path?"":" disabled")+" title='commit on a branch, push, open a PR, then archive'>Approve → "+(r.files.length?"PR":"archive")+"</button>"+(r.files.length?"<button class='ghost showdiff'>Show diff</button>":"")+"</div><div class='rdiff hidden'></div></div>";});
|
|
327
|
+
el.innerHTML=h;
|
|
328
|
+
el.querySelectorAll('.rcard').forEach(function(card){var repo=card.getAttribute('data-repo');
|
|
329
|
+
card.querySelectorAll('.sendback').forEach(function(b){b.addEventListener('click',function(){api('/api/pending/sendback',{id:b.closest('.task').getAttribute('data-id')}).then(loadTasks);});});
|
|
330
|
+
var sd=card.querySelector('.showdiff'),pre=card.querySelector('.rdiff');
|
|
331
|
+
if(sd)sd.addEventListener('click',function(){if(!pre.classList.contains('hidden')){pre.classList.add('hidden');sd.textContent='Show diff';return;}
|
|
332
|
+
pre.textContent='Loading...';pre.classList.remove('hidden');sd.textContent='Hide diff';api('/api/pending/diff?repo='+encodeURIComponent(repo)).then(function(d){pre.textContent=(d&&d.diff)||'(no changes)';});});
|
|
333
|
+
var ap=card.querySelector('.approve');if(ap)ap.addEventListener('click',function(){ap.disabled=true;ap.textContent='Committing & pushing...';
|
|
334
|
+
api('/api/pending/approve',{repo:repo}).then(function(r){var o=document.getElementById('reviewout');
|
|
335
|
+
if(!r||r.error){ap.disabled=false;ap.textContent='Approve - retry';o.innerHTML="<div class='note err'>"+esc(repo)+": "+esc((r&&r.error)||'failed')+"</div>";return;}
|
|
336
|
+
var m="✓ <b>"+esc(repo)+"</b>: approved "+r.approved+" task"+(r.approved>1?"s":"");
|
|
337
|
+
if(r.commit)m+=" · committed <code>"+esc(r.commit)+"</code> on <code>"+esc(r.branch)+"</code>";
|
|
338
|
+
if(r.pr)m+=" · <a href='"+esc(r.pr)+"' target='_blank' rel='noopener'>open PR</a>";
|
|
339
|
+
if(r.note)m+="<div class='muted'>"+esc(r.note)+"</div>";
|
|
340
|
+
o.innerHTML="<div class='note ok'>"+m+"</div>";loadTasks();});});});});}
|
|
341
|
+
function loadArchived(){TARCH=true;api('/api/tasks?archived=1').then(function(list){ALLTASKS=list;renderFilter();var el=document.getElementById('tasklist');
|
|
342
|
+
if(!list.length){el.innerHTML="<div class='muted' style='margin-top:12px'>Nothing archived yet. Completed tasks land here.</div>";return;}
|
|
343
|
+
var h="<div class='tgroup'>Archived <span class='tcount'>"+list.length+"</span></div>";list.forEach(function(t){h+=taskRow(t,true);});el.innerHTML=h;wireTaskRows(el);});}
|
|
344
|
+
var agentsTimer=null;
|
|
345
|
+
function stopAgentsPoll(){if(agentsTimer){clearTimeout(agentsTimer);agentsTimer=null;}}
|
|
346
|
+
function fmtE(ms){var s=Math.floor(ms/1000);if(s<60)return s+'s';var m=Math.floor(s/60);return m+'m '+(s%60)+'s';}
|
|
347
|
+
function loadAgents(){api('/api/agents').then(function(list){var el=document.getElementById('agentslist');
|
|
348
|
+
if(!list||!list.length){el.innerHTML="<div class='muted' style='margin-top:12px'>No agents yet. In <b>Tasks</b>, tick ideas and hit <b>Send to repos</b> (with an agent command set in Settings) — you'll watch it work here.</div>";stopAgentsPoll();return;}
|
|
349
|
+
el.innerHTML=list.map(function(a){var cls=a.status==='running'?'run':(a.status==='done'?'ok':'fail');
|
|
350
|
+
var st=a.status==='running'?('working · '+fmtE(a.elapsed)):(esc(a.status)+' · '+fmtE(a.elapsed)+(a.exitCode!=null?' · exit '+a.exitCode:''));
|
|
351
|
+
var b="<div class='dh'><span class='adot "+cls+"'></span><span class='dn'>"+esc(a.name)+"</span><span class='dd'>"+st+"</span></div>";
|
|
352
|
+
if(a.status==='running')b+="<div class='bar'><i></i></div>";
|
|
353
|
+
var ch=a.changed;
|
|
354
|
+
if(ch&&(ch.dirty||ch.stat||(ch.commits&&ch.commits.length))){
|
|
355
|
+
var parts=[];
|
|
356
|
+
if(ch.dirty)parts.push('<b>'+ch.dirty+'</b> file'+(ch.dirty>1?'s':'')+' changed');
|
|
357
|
+
if(ch.stat)parts.push(esc(ch.stat));
|
|
358
|
+
if(parts.length)b+="<div class='changed'>What it did · "+parts.join(' · ')+"</div>";
|
|
359
|
+
if(ch.commits&&ch.commits.length)b+="<div class='commits'>"+ch.commits.map(function(c){return "<div><code>"+esc(c.hash)+"</code> "+esc(c.msg)+"</div>";}).join("")+"</div>";
|
|
360
|
+
}else if(a.status==='done'){
|
|
361
|
+
b+="<div class='changed muted'>No file changes detected (the agent may have only planned or asked).</div>";
|
|
362
|
+
}
|
|
363
|
+
b+="<pre class='alogout'>"+esc((a.tail&&a.tail.trim())||'(waiting for output…)')+"</pre>";
|
|
364
|
+
return "<div class='agent'>"+b+"</div>";}).join("");
|
|
365
|
+
el.querySelectorAll('.alogout').forEach(function(p){p.scrollTop=p.scrollHeight;});
|
|
366
|
+
var anyRunning=list.some(function(a){return a.status==='running';});
|
|
367
|
+
stopAgentsPoll();if(anyRunning&¤t==='agents')agentsTimer=setTimeout(loadAgents,2000);});}
|
|
368
|
+
function loadDrift(){var out=document.getElementById('driftout');out.innerHTML="<div class='muted' style='margin-top:12px'>Reading your repos…</div>";
|
|
369
|
+
var ci=document.getElementById('driftci').checked?'1':'0';var ft=document.getElementById('driftfetch').checked?'1':'0';
|
|
370
|
+
api('/api/drift?ci='+ci+'&fetch='+ft).then(function(d){driftLoaded=true;var repos=d.repos||[];var risky=repos.filter(function(r){return r.flags.some(function(f){return f.level==='warn';});});
|
|
371
|
+
var h="<div class='k' style='margin:10px 0'><b>"+repos.length+"</b> repos · <b>"+risky.length+"</b> with risks"+(d.partial?" · <span class='err'>partial — the scan hit its time limit</span>":"")+"</div>";
|
|
372
|
+
repos.forEach(function(r){if(!r.flags.length)return;var warn=r.flags.some(function(f){return f.level==='warn';});
|
|
373
|
+
h+="<div class='drift'><div class='dh'><span class='"+(warn?'dot-w':'dot-c')+"'></span><span class='dn'>"+esc(r.name)+"</span><span class='dd'>"+esc(r.def)+(r.fetchAgeDays!=null&&r.fetchAgeDays>3?" · fetch "+r.fetchAgeDays+"d old":"")+"</span></div><ul>";
|
|
374
|
+
r.flags.forEach(function(f){h+="<li class='"+esc(f.level)+"'>"+(f.level==='warn'?'⚠ ':'· ')+esc(f.text)+(f.evidence?" <span class='ev'>["+esc(f.evidence)+"]</span>":"")+"</li>";});
|
|
375
|
+
h+="</ul></div>";});
|
|
376
|
+
var clean=repos.filter(function(r){return !r.flags.length;}).map(function(r){return r.name;});
|
|
377
|
+
if(clean.length)h+="<div class='muted' style='margin-top:10px'>clean: "+esc(clean.join(", "))+"</div>";
|
|
378
|
+
if(!repos.length)h+="<div class='muted'>No repos found under your home folder.</div>";
|
|
379
|
+
out.innerHTML=h;});}
|
|
380
|
+
function addTaskUI(){var i=document.getElementById('newtask');var v=(i.value||'').trim();if(!v)return;api('/api/tasks/add',{text:v,repo:''}).then(function(){i.value='';loadTasks();});}
|
|
381
|
+
function pushTasksUI(){var o=document.getElementById('pushout');var btn=document.getElementById('pushtasks');btn.disabled=true;
|
|
382
|
+
o.innerHTML="<div class='muted' style='margin-top:10px'><span class='dot-c' style='background:var(--amber)'></span> Writing .symbiot/TASKS.md into your repos…</div>";
|
|
383
|
+
api('/api/tasks/push',{type:TFILTER.type||'',repo:TFILTER.repo||''}).then(function(r){btn.disabled=false;
|
|
384
|
+
if(r.empty){o.innerHTML="<div class='muted' style='margin-top:10px'>No open tasks to send. Tick ideas in a repo review, or add tasks above.</div>";return;}
|
|
385
|
+
var n=(r.written||[]).length;var h="<div class='drift' style='margin-top:10px'><div class='dh'><span class='dot-c'></span><span class='dn'>Done — wrote "+n+" file"+(n===1?"":"s")+"</span></div>";
|
|
386
|
+
if(n){h+="<ul>";r.written.forEach(function(w){h+="<li class='info'>✓ <b>"+esc(w.name)+"</b> <span class='ev'>"+esc(w.file)+" ("+w.count+" task"+(w.count===1?"":"s")+")</span></li>";});h+="</ul>";}
|
|
387
|
+
if(r.unresolved&&r.unresolved.length){h+="<div class='dd' style='margin-top:6px'>⚠ not written (repo not found under your home folder): "+esc(r.unresolved.map(function(u){return u.name;}).join(", "))+"</div>";}
|
|
388
|
+
if(r.handoff&&r.written&&r.written.length){r.written.forEach(function(w){api('/api/open',{path:w.path});});h+="<div class='dd' style='margin-top:8px'>🤖 Handed "+n+" repo(s) to your agent — opening the <b>Agents</b> tab to watch it work…</div>";setTimeout(function(){setTab('agents');},500);}
|
|
389
|
+
else{h+="<div class='dd' style='margin-top:8px'>Set an <b>agent command</b> in Settings to auto-run it on send (and watch it in the Agents tab). For now, tell your agent: <b>“Read .symbiot/TASKS.md and implement the unchecked items.”</b></div>";}
|
|
390
|
+
h+="</div>";
|
|
391
|
+
o.innerHTML=h;}).catch(function(e){btn.disabled=false;o.innerHTML="<div class='err' style='margin-top:10px'>Couldn't write tasks: "+esc(String((e&&e.message)||e))+"</div>";});}
|
|
392
|
+
function loadAgentCfg(){api('/api/agentcfg').then(function(d){document.getElementById('agentcmd').value=d.cmd||'';
|
|
393
|
+
var chips=[];(d.agents||[]).forEach(function(a){chips.push(a);});(d.editors||[]).forEach(function(e){chips.push(e);});
|
|
394
|
+
var box=document.getElementById('agentpresets');
|
|
395
|
+
if(!chips.length){box.innerHTML="<span class='muted' style='font-size:12px'>Nothing detected on PATH — type your own command above.</span>";return;}
|
|
396
|
+
box.innerHTML="<span class='muted' style='font-size:12px'>Detected — click to use:</span><br>"+chips.map(function(c,i){return "<button class='ghost preset' data-i='"+i+"' style='padding:4px 10px;font-size:12px;margin:5px 5px 0 0'>"+esc(c.label)+"</button>";}).join("");
|
|
397
|
+
box.querySelectorAll('.preset').forEach(function(btn){btn.addEventListener('click',function(){document.getElementById('agentcmd').value=chips[+btn.getAttribute('data-i')].tmpl;saveAgent();});});});}
|
|
398
|
+
function saveAgent(){api('/api/agentcmd',{cmd:document.getElementById('agentcmd').value});}
|
|
399
|
+
function loadScanRoots(){api('/api/scanroots').then(function(d){var box=document.getElementById('scanroots');var roots=d.effective||[];
|
|
400
|
+
box.innerHTML=roots.map(function(r){var custom=(d.roots||[]).indexOf(r)>=0;return "<div class='task' data-p='"+esc(r)+"'><span class='t' style='font-family:ui-monospace,monospace;font-size:12px'>"+esc(r)+"</span>"+(r===d.home?"<span class='rp'>home</span>":"")+(custom?"<button class='rm rmroot' title='remove'>×</button>":"")+"</div>";}).join("");
|
|
401
|
+
box.querySelectorAll('.rmroot').forEach(function(btn){btn.addEventListener('click',function(){api('/api/scanroots/remove',{path:btn.closest('.task').getAttribute('data-p')}).then(function(){loadScanRoots();mapLoaded=false;driftLoaded=false;});});});});}
|
|
402
|
+
function addRootUI(){var i=document.getElementById('newroot');var v=(i.value||'').trim();if(!v)return;var n=document.getElementById('scanrootnote');
|
|
403
|
+
api('/api/scanroots/add',{path:v}).then(function(r){if(r.error){n.innerHTML="<span class='err'>"+esc(r.error)+"</span>";return;}i.value='';n.textContent='Added — the Map/Drift will rescan.';loadScanRoots();mapLoaded=false;driftLoaded=false;});}
|
|
404
|
+
function setupLocalUI(){var o=document.getElementById('setupout');o.innerHTML="Setting up a local model…";
|
|
405
|
+
api('/api/setup-local',{}).then(function(r){if(r.error==='not-installed'){var i=r.install||{};o.innerHTML="Ollama isn't installed. Run this once, then click again:<div class='out2'>"+esc(i.cmd||'')+"</div><div class='muted' style='font-size:11px'>or download: "+esc(i.alt||'https://ollama.com/download')+"</div>";return;}
|
|
406
|
+
o.innerHTML="Pulling <b>"+esc(r.model)+"</b> — watch it in the <b>Agents</b> tab. Symbiot switches to it automatically when the download finishes.";setTimeout(function(){setTab('agents');},700);}).catch(function(e){o.innerHTML="<span class='err'>Setup failed: "+esc(String((e&&e.message)||e))+"</span>";});}
|
|
407
|
+
function selectNode(id){sel=id;render();updateBar(id);var n=nodeById(id);api('/api/node?id='+encodeURIComponent(id)).then(showDetail);
|
|
408
|
+
if(n&&n.type==='repo'&&n.meta&&n.meta.path){loadReview(n.label,n.meta.path);}else{hideReview();}}
|
|
409
|
+
function screenToGraph(el,ev){var rc=el.getBoundingClientRect();var mx=(ev.clientX-rc.left)/rc.width*GW;var my=(ev.clientY-rc.top)/rc.height*GH;return {x:(mx-view.x)/view.k,y:(my-view.y)/view.k};}
|
|
410
|
+
function initGraphEvents(){var el=document.getElementById('graph');var mode=null,moved=0,sx=0,sy=0,ox=0,oy=0,downId=null,dnode=null;
|
|
411
|
+
function nodeAt(ev){var t=ev.target;var g=t&&t.closest?t.closest('.node'):null;return g?g.getAttribute('data-id'):null;}
|
|
412
|
+
el.addEventListener('wheel',function(ev){ev.preventDefault();if(!GRAPH)return;var rc=el.getBoundingClientRect();var mx=(ev.clientX-rc.left)/rc.width*GW;var my=(ev.clientY-rc.top)/rc.height*GH;var nk=Math.max(0.3,Math.min(4,view.k*(ev.deltaY<0?1.12:0.89)));view.x=mx-(mx-view.x)*(nk/view.k);view.y=my-(my-view.y)*(nk/view.k);view.k=nk;render();},{passive:false});
|
|
413
|
+
el.addEventListener('pointerdown',function(ev){if(!GRAPH)return;moved=0;sx=ev.clientX;sy=ev.clientY;ox=view.x;oy=view.y;downId=nodeAt(ev);if(downId){mode='node';dnode=nodeById(downId);}else{mode='pan';}try{el.setPointerCapture(ev.pointerId);}catch(e){}});
|
|
414
|
+
el.addEventListener('pointermove',function(ev){
|
|
415
|
+
if(!mode){updateBar(nodeAt(ev));return;}
|
|
416
|
+
var dx=ev.clientX-sx,dy=ev.clientY-sy;moved+=Math.abs(dx)+Math.abs(dy);if(moved<4)return;
|
|
417
|
+
if(mode==='node'&&dnode){var p=screenToGraph(el,ev);dnode.x=p.x;dnode.y=p.y;render();}
|
|
418
|
+
else if(mode==='pan'){var rc=el.getBoundingClientRect();view.x=ox+dx/rc.width*GW;view.y=oy+dy/rc.height*GH;render();}});
|
|
419
|
+
el.addEventListener('pointerup',function(ev){var id=downId,wasClick=moved<6;mode=null;dnode=null;downId=null;
|
|
420
|
+
if(id){selectNode(id);}else if(wasClick){sel=null;hideDetail();hideReview();render();updateBar(null);}});
|
|
421
|
+
el.addEventListener('mouseleave',function(){if(!mode)updateBar(null);}); }
|
|
422
|
+
function scanLine(s){return 'Mapping your work… '+s.phase+(s.total?' '+s.done+'/'+s.total:'')+(s.item?' · '+s.item:'')+' · '+Math.round((s.elapsed||0)/1000)+'s';}
|
|
423
|
+
function loadMap(){var p=document.getElementById("profile");p.textContent="Mapping your work...";document.getElementById("graph").innerHTML="";sel=null;hideDetail();hideReview();var done=false;
|
|
424
|
+
function poll(){if(done)return;api('/api/scan').then(function(s){if(done)return;if(s&&s.active&&s.phase)p.textContent=scanLine(s);setTimeout(poll,600);}).catch(function(){});}
|
|
425
|
+
setTimeout(poll,400);
|
|
426
|
+
api("/api/map").then(function(g){done=true;mapLoaded=true;if(!g.nodes||!g.nodes.length){p.textContent="No git repositories found under your home folder.";return;}GRAPH=g;layout(g.nodes,g.edges);view={k:1,x:0,y:0};p.innerHTML=profileLine(g)+(g.stats&&g.stats.partial?" · <span class='err'>partial — the scan hit its time limit</span>":"");render();});}
|
|
427
|
+
function fitBadge(m){return m.fits?"<span class='tag'>fits your RAM</span>":"<span class='tag' style='background:#3a2a12;border-color:#6b4a1f;color:#F2A541'>needs more RAM</span>";}
|
|
428
|
+
function loadRec(){var out=document.getElementById('recout');out.innerHTML="<div class='muted'>Reading your hardware...</div>";
|
|
429
|
+
api('/api/models').then(function(d){var hw=d.hardware,rec=d.rec;
|
|
430
|
+
var h="<div class='k'><b>"+hw.ramGB+" GB</b> RAM · "+hw.cpuCount+"-core · "+esc(hw.platform)+"/"+esc(hw.arch)+(hw.gpu?" · GPU: "+esc(hw.gpu.name)+(hw.gpu.vramGB?" ("+hw.gpu.vramGB+"GB)":""):"")+"</div>";
|
|
431
|
+
h+="<div class='ideas'><h4>Local models · free via Ollama</h4>";
|
|
432
|
+
rec.local.forEach(function(m){h+="<div class='idea' style='justify-content:space-between'><span><b>"+esc(m.tier)+"</b> · <code>ollama pull "+esc(m.model)+"</code> <span class='muted'>~"+m.needGB+"GB · "+esc(m.note)+"</span></span> "+fitBadge(m)+"</div>";});
|
|
433
|
+
h+="<div class='muted' style='margin-top:6px'>Best fit: <b>"+esc(rec.best)+"</b> — pull it, then pick <b>Local model (Ollama)</b> above.</div></div>";
|
|
434
|
+
h+="<div class='ideas'><h4>Paid models · bring an API key</h4>";
|
|
435
|
+
rec.paid.forEach(function(p){h+="<div class='idea'><span><b>"+esc(p.provider)+"</b> · "+esc(p.tier)+" · <code>"+esc(p.model)+"</code> <span class='muted'>· "+esc(p.note)+"</span></span></div>";});
|
|
436
|
+
h+="</div>";out.innerHTML=h;});}
|
|
437
|
+
document.getElementById('remap').addEventListener('click',loadMap);
|
|
438
|
+
document.getElementById('recbtn').addEventListener('click',loadRec);
|
|
439
|
+
document.getElementById('setuplocal').addEventListener('click',setupLocalUI);
|
|
440
|
+
document.getElementById('addroot').addEventListener('click',addRootUI);
|
|
441
|
+
document.getElementById('newroot').addEventListener('keydown',function(e){if(e.key==='Enter')addRootUI();});
|
|
442
|
+
document.getElementById('driftrun').addEventListener('click',function(){driftLoaded=false;loadDrift();});
|
|
443
|
+
document.getElementById('agentsrefresh').addEventListener('click',loadAgents);
|
|
444
|
+
document.getElementById('addtask').addEventListener('click',addTaskUI);
|
|
445
|
+
document.getElementById('pushtasks').addEventListener('click',pushTasksUI);
|
|
446
|
+
document.getElementById('agentcmd').addEventListener('change',saveAgent);
|
|
447
|
+
document.getElementById('newtask').addEventListener('keydown',function(e){if(e.key==='Enter')addTaskUI();});
|
|
448
|
+
var SRV_STARTED=null,srvDown=false,updBusy=false;
|
|
449
|
+
function ubar(){return document.getElementById('updatebar');}
|
|
450
|
+
function heartbeat(fresh){
|
|
451
|
+
api('/api/ping'+(fresh?'?fresh=1':'')).then(function(p){
|
|
452
|
+
var b=ubar();
|
|
453
|
+
var ve=document.getElementById('ver');if(ve&&p.version)ve.textContent='v'+p.version;
|
|
454
|
+
if(SRV_STARTED===null){SRV_STARTED=p.started;}
|
|
455
|
+
else if(p.started!==SRV_STARTED){location.reload();return;}
|
|
456
|
+
if(srvDown){location.reload();return;}
|
|
457
|
+
if(updBusy)return;
|
|
458
|
+
if(p.latest&&p.newer){
|
|
459
|
+
b.className='updatebar show';
|
|
460
|
+
b.innerHTML="A new Symbiot ("+esc(p.latest)+") is available — you're on "+esc(p.version)+". <button id='doupd'>Update & restart</button>";
|
|
461
|
+
var btn=document.getElementById('doupd');if(btn)btn.onclick=doUpdate;
|
|
462
|
+
}else{b.className='updatebar';}
|
|
463
|
+
}).catch(function(){srvDown=true;var b=ubar();b.className='updatebar reconnect show';b.textContent='Reconnecting to Symbiot…';});
|
|
464
|
+
}
|
|
465
|
+
function doUpdate(){updBusy=true;var b=ubar();b.className='updatebar show';b.textContent='Updating & restarting… this page will reload itself when it is back.';api('/api/update',{});}
|
|
466
|
+
setInterval(heartbeat,4000);heartbeat(true);
|
|
467
|
+
window.addEventListener('focus',function(){heartbeat(true);}); // re-check for updates when you come back to the window
|
|
468
|
+
initGraphEvents();syncP();refresh();loadMap();loadAgentCfg();loadScanRoots();
|
|
469
|
+
</script></body></html>`;
|